[update] Better UI
This commit is contained in:
@@ -46,6 +46,9 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.fetch2)
|
||||
implementation(libs.fetch2okhttp)
|
||||
|
||||
implementation(libs.hilt.android)
|
||||
implementation(libs.hilt.navigation.compose)
|
||||
ksp(libs.hilt.android.compiler)
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.Intent
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import com.acitelight.aether.service.AbyssTunnelProxy
|
||||
import com.acitelight.aether.service.FetchManager
|
||||
import com.acitelight.aether.service.SettingsDataStoreManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -21,6 +22,8 @@ import javax.inject.Inject
|
||||
class AbyssService: Service() {
|
||||
@Inject
|
||||
lateinit var proxy: AbyssTunnelProxy
|
||||
@Inject
|
||||
lateinit var downloader: FetchManager
|
||||
|
||||
private val binder = AbyssServiceBinder()
|
||||
private val _isInitialized = MutableStateFlow(false)
|
||||
|
||||
@@ -8,4 +8,17 @@ import com.acitelight.aether.model.Video
|
||||
object Global {
|
||||
var loggedIn by mutableStateOf(false)
|
||||
var sameClassVideos: List<Video>? = null
|
||||
private set
|
||||
|
||||
fun updateRelate(v: List<Video>, s: Video)
|
||||
{
|
||||
sameClassVideos = if (v.contains(s)) {
|
||||
val index = v.indexOf(s)
|
||||
val afterS = v.subList(index, v.size)
|
||||
val beforeS = v.subList(0, index)
|
||||
afterS + beforeS
|
||||
} else {
|
||||
v
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,13 +13,22 @@ import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.automirrored.filled.CompareArrows
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardColors
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
@@ -32,6 +41,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -55,6 +65,7 @@ import com.acitelight.aether.view.ComicPageView
|
||||
import com.acitelight.aether.view.ComicScreen
|
||||
import com.acitelight.aether.view.HomeScreen
|
||||
import com.acitelight.aether.view.MeScreen
|
||||
import com.acitelight.aether.view.TransmissionScreen
|
||||
import com.acitelight.aether.view.VideoPlayer
|
||||
import com.acitelight.aether.view.VideoScreen
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@@ -147,21 +158,29 @@ fun AppNavigation() {
|
||||
) { innerPadding ->
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Screen.Home.route,
|
||||
startDestination = Screen.Me.route,
|
||||
modifier = if(shouldShowBottomBar)Modifier.padding(innerPadding) else Modifier.padding(0.dp)
|
||||
) {
|
||||
composable(Screen.Home.route) {
|
||||
HomeScreen(navController = navController)
|
||||
CardPage(title = "Home") {
|
||||
HomeScreen(navController = navController)
|
||||
}
|
||||
}
|
||||
composable(Screen.Video.route) {
|
||||
VideoScreen(navController = navController)
|
||||
CardPage(title = "Videos") {
|
||||
VideoScreen(navController = navController)
|
||||
}
|
||||
}
|
||||
composable(Screen.Comic.route) {
|
||||
ComicScreen(navController = navController)
|
||||
CardPage(title = "Comic") {
|
||||
ComicScreen(navController = navController)
|
||||
}
|
||||
}
|
||||
|
||||
composable(Screen.Transmission.route) {
|
||||
// ComicScreen()
|
||||
CardPage(title = "Tasks") {
|
||||
TransmissionScreen()
|
||||
}
|
||||
}
|
||||
composable(Screen.Me.route) {
|
||||
MeScreen();
|
||||
@@ -217,8 +236,6 @@ fun BottomNavigationBar(navController: NavController) {
|
||||
Screen.Transmission,
|
||||
Screen.Me
|
||||
) else listOf(
|
||||
Screen.Home,
|
||||
Screen.Transmission,
|
||||
Screen.Me
|
||||
)
|
||||
|
||||
@@ -242,6 +259,37 @@ fun BottomNavigationBar(navController: NavController) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CardPage(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Box(Modifier.background(if (isSystemInDarkTheme()) {
|
||||
Color.Black
|
||||
} else {
|
||||
Color.White
|
||||
}).fillMaxSize())
|
||||
{
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
elevation = CardDefaults.cardElevation(4.dp),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = colorScheme.background)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Screen(val route: String, val icon: ImageVector, val title: String) {
|
||||
data object Home : Screen("home_route", Icons.Filled.Home, "Home")
|
||||
data object Video : Screen("video_route", Icons.Filled.VideoLibrary, "Video")
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.acitelight.aether.model
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.tonyodev.fetch2.Status
|
||||
|
||||
class DownloadItemState(
|
||||
val id: Int,
|
||||
fileName: String,
|
||||
filePath: String,
|
||||
url: String,
|
||||
progress: Int,
|
||||
status: Status,
|
||||
downloadedBytes: Long,
|
||||
totalBytes: Long
|
||||
) {
|
||||
var fileName by mutableStateOf(fileName)
|
||||
var filePath by mutableStateOf(filePath)
|
||||
var url by mutableStateOf(url)
|
||||
var progress by mutableStateOf(progress)
|
||||
var status by mutableStateOf(status)
|
||||
var downloadedBytes by mutableStateOf(downloadedBytes)
|
||||
var totalBytes by mutableStateOf(totalBytes)
|
||||
}
|
||||
@@ -12,8 +12,10 @@ import java.net.InetAddress
|
||||
import java.net.ServerSocket
|
||||
import java.net.Socket
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
@Singleton
|
||||
class AbyssTunnelProxy @Inject constructor(
|
||||
private val settingsDataStoreManager: SettingsDataStoreManager
|
||||
) {
|
||||
|
||||
@@ -221,6 +221,7 @@ object ApiClient {
|
||||
withContext(Dispatchers.IO)
|
||||
{
|
||||
(context as AetherApp).abyssService?.proxy?.config(base.toUri().host!!, 4096)
|
||||
context.abyssService?.downloader?.init()
|
||||
}
|
||||
|
||||
api = createRetrofit().create(ApiInterface::class.java)
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.acitelight.aether.service
|
||||
|
||||
import android.content.Context
|
||||
import com.acitelight.aether.service.ApiClient.createOkHttp
|
||||
import com.tonyodev.fetch2.Download
|
||||
import com.tonyodev.fetch2.Fetch
|
||||
import com.tonyodev.fetch2.FetchConfiguration
|
||||
import com.tonyodev.fetch2.FetchListener
|
||||
import com.tonyodev.fetch2.Request
|
||||
import com.tonyodev.fetch2.Status
|
||||
import com.tonyodev.fetch2okhttp.OkHttpDownloader
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class FetchManager @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
private var fetch: Fetch? = null
|
||||
private var listener: FetchListener? = null
|
||||
|
||||
fun init()
|
||||
{
|
||||
val fetchConfiguration = FetchConfiguration.Builder(context)
|
||||
.setDownloadConcurrentLimit(8)
|
||||
.setHttpDownloader(OkHttpDownloader(createOkHttp()))
|
||||
.build()
|
||||
|
||||
fetch = Fetch.Impl.getInstance(fetchConfiguration)
|
||||
}
|
||||
|
||||
// listener management
|
||||
fun setListener(l: FetchListener) {
|
||||
if (fetch == null)
|
||||
return
|
||||
listener?.let { fetch?.removeListener(it) }
|
||||
listener = l
|
||||
fetch?.addListener(l)
|
||||
}
|
||||
|
||||
fun removeListener() {
|
||||
listener?.let {
|
||||
fetch?.removeListener(it)
|
||||
}
|
||||
listener = null
|
||||
}
|
||||
|
||||
// query downloads
|
||||
fun getAllDownloads(callback: (List<Download>) -> Unit) {
|
||||
if (fetch == null) init()
|
||||
fetch?.getDownloads { list -> callback(list) } ?: callback(emptyList())
|
||||
}
|
||||
|
||||
fun getDownloadsByStatus(status: Status, callback: (List<Download>) -> Unit) {
|
||||
if (fetch == null) init()
|
||||
fetch?.getDownloadsWithStatus(status) { list -> callback(list) } ?: callback(emptyList())
|
||||
}
|
||||
|
||||
// operations
|
||||
fun pause(id: Int) {
|
||||
fetch?.pause(id)
|
||||
}
|
||||
|
||||
fun resume(id: Int) {
|
||||
fetch?.resume(id)
|
||||
}
|
||||
|
||||
fun cancel(id: Int) {
|
||||
fetch?.cancel(id)
|
||||
}
|
||||
|
||||
fun delete(id: Int, callback: (() -> Unit)? = null) {
|
||||
fetch?.delete(id) {
|
||||
callback?.invoke()
|
||||
} ?: callback?.invoke()
|
||||
}
|
||||
|
||||
|
||||
// enqueue helper if needed
|
||||
fun enqueue(request: Request, onEnqueued: ((Request) -> Unit)? = null, onError: ((com.tonyodev.fetch2.Error) -> Unit)? = null) {
|
||||
if (fetch == null) init()
|
||||
fetch?.enqueue(request, { r -> onEnqueued?.invoke(r) }, { e -> onError?.invoke(e) })
|
||||
}
|
||||
}
|
||||
@@ -3,35 +3,135 @@ package com.acitelight.aether.ui.theme
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.lerp
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
fun generateColorScheme(primaryColor: Color, isDarkMode: Boolean): ColorScheme {
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
val background = if (isDarkMode) Color(0xFF121212) else Color(0xFFFFFFFF)
|
||||
val surface = if (isDarkMode) Color(0xFF1E1E1E) else Color(0xFFFFFFFF)
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
val surfaceContainer = if (isDarkMode) Color(0xFF232323) else Color(0xFFFDFDFD)
|
||||
val surfaceContainerLow = if (isDarkMode) Color(0xFF1A1A1A) else Color(0xFFF5F5F5)
|
||||
val surfaceContainerHigh = if (isDarkMode) Color(0xFF2A2A2A) else Color(0xFFFAFAFA)
|
||||
val surfaceContainerHighest = if (isDarkMode) Color(0xFF333333) else Color(0xFFFFFFFF)
|
||||
val surfaceContainerLowest = if (isDarkMode) Color(0xFF0F0F0F) else Color(0xFFF0F0F0)
|
||||
val surfaceBright = if (isDarkMode) Color(0xFF2C2C2C) else Color(0xFFFFFFFF)
|
||||
val surfaceDim = if (isDarkMode) Color(0xFF141414) else Color(0xFFF8F8F8)
|
||||
|
||||
fun tint(surface: Color, factor: Float) = lerp(surface, primaryColor, factor)
|
||||
|
||||
return if (isDarkMode) {
|
||||
darkColorScheme(
|
||||
primary = primaryColor,
|
||||
onPrimary = Color.White,
|
||||
primaryContainer = tint(primaryColor, 0.2f),
|
||||
onPrimaryContainer = Color.White,
|
||||
inversePrimary = tint(primaryColor, 0.6f),
|
||||
|
||||
secondary = tint(primaryColor, 0.4f),
|
||||
onSecondary = Color.White,
|
||||
secondaryContainer = tint(primaryColor, 0.2f),
|
||||
onSecondaryContainer = Color.White,
|
||||
|
||||
tertiary = tint(primaryColor, 0.5f),
|
||||
onTertiary = Color.White,
|
||||
tertiaryContainer = tint(primaryColor, 0.2f),
|
||||
onTertiaryContainer = Color.White,
|
||||
|
||||
background = background,
|
||||
onBackground = Color.White,
|
||||
|
||||
surface = surface,
|
||||
onSurface = Color.White,
|
||||
|
||||
surfaceVariant = tint(surface, 0.1f),
|
||||
onSurfaceVariant = Color(0xFFE0E0E0),
|
||||
|
||||
surfaceTint = primaryColor,
|
||||
|
||||
inverseSurface = Color.White,
|
||||
inverseOnSurface = Color(0xFF121212),
|
||||
|
||||
error = Color(0xFFCF6679),
|
||||
onError = Color.Black,
|
||||
errorContainer = Color(0xFFB00020),
|
||||
onErrorContainer = Color.White,
|
||||
|
||||
outline = Color(0xFF757575),
|
||||
outlineVariant = Color(0xFF494949),
|
||||
scrim = Color.Black,
|
||||
|
||||
surfaceBright = tint(surfaceBright, 0.1f),
|
||||
surfaceContainer = tint(surfaceContainer, 0.1f),
|
||||
surfaceContainerHigh = tint(surfaceContainerHigh, 0.12f),
|
||||
surfaceContainerHighest = tint(surfaceContainerHighest, 0.15f),
|
||||
surfaceContainerLow = tint(surfaceContainerLow, 0.08f),
|
||||
surfaceContainerLowest = tint(surfaceContainerLowest, 0.05f),
|
||||
surfaceDim = tint(surfaceDim, 0.1f)
|
||||
)
|
||||
} else {
|
||||
lightColorScheme(
|
||||
primary = primaryColor,
|
||||
onPrimary = Color.White,
|
||||
primaryContainer = tint(primaryColor, 0.3f),
|
||||
onPrimaryContainer = Color.Black,
|
||||
inversePrimary = tint(primaryColor, 0.6f),
|
||||
|
||||
secondary = tint(primaryColor, 0.4f),
|
||||
onSecondary = Color.Black,
|
||||
secondaryContainer = tint(primaryColor, 0.2f),
|
||||
onSecondaryContainer = Color.Black,
|
||||
|
||||
tertiary = tint(primaryColor, 0.5f),
|
||||
onTertiary = Color.Black,
|
||||
tertiaryContainer = tint(primaryColor, 0.3f),
|
||||
onTertiaryContainer = Color.Black,
|
||||
|
||||
background = background,
|
||||
onBackground = Color.Black,
|
||||
|
||||
surface = surface,
|
||||
onSurface = Color.Black,
|
||||
|
||||
surfaceVariant = tint(surface, 0.1f),
|
||||
onSurfaceVariant = Color(0xFF49454F),
|
||||
|
||||
surfaceTint = primaryColor,
|
||||
|
||||
inverseSurface = Color(0xFF121212),
|
||||
inverseOnSurface = Color.White,
|
||||
|
||||
error = Color(0xFFB00020),
|
||||
onError = Color.White,
|
||||
errorContainer = Color(0xFFFFDAD6),
|
||||
onErrorContainer = Color.Black,
|
||||
|
||||
outline = Color(0xFF737373),
|
||||
outlineVariant = Color(0xFFD0C4C9),
|
||||
scrim = Color.Black,
|
||||
|
||||
surfaceBright = tint(surfaceBright, 0.1f),
|
||||
surfaceContainer = tint(surfaceContainer, 0.1f),
|
||||
surfaceContainerHigh = tint(surfaceContainerHigh, 0.12f),
|
||||
surfaceContainerHighest = tint(surfaceContainerHighest, 0.15f),
|
||||
surfaceContainerLow = tint(surfaceContainerLow, 0.08f),
|
||||
surfaceContainerLowest = tint(surfaceContainerLowest, 0.05f),
|
||||
surfaceDim = tint(surfaceDim, 0.05f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val DarkColorScheme = generateColorScheme(Color(0xFFFF4081), isDarkMode = true)
|
||||
private val LightColorScheme = generateColorScheme(Color(0xFFFF4081), isDarkMode = false)
|
||||
|
||||
@Composable
|
||||
fun AetherTheme(
|
||||
@@ -41,11 +141,6 @@ fun AetherTheme(
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -43,6 +44,7 @@ fun ComicGridView(comicId: String, navController: NavHostController, comicGridVi
|
||||
comicGridViewModel.resolve(comicId.hexToString())
|
||||
comicGridViewModel.updateProcess(comicId.hexToString()){}
|
||||
ToggleFullScreen(false)
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
|
||||
val context = LocalContext.current
|
||||
val comic by comicGridViewModel.comic
|
||||
@@ -53,28 +55,25 @@ fun ComicGridView(comicId: String, navController: NavHostController, comicGridVi
|
||||
Box(
|
||||
Modifier
|
||||
.padding(horizontal = 16.dp).padding(top = 36.dp)
|
||||
.background(Color.White.copy(alpha = 0.65f), shape = RoundedCornerShape(12.dp))
|
||||
.background(colorScheme.surfaceContainerHighest, shape = RoundedCornerShape(12.dp))
|
||||
)
|
||||
{
|
||||
Text(
|
||||
text = comic!!.comic.comic_name,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.Black,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.padding(4.dp)
|
||||
)
|
||||
modifier = Modifier.padding(4.dp))
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
.padding(horizontal = 16.dp).padding(top = 4.dp)
|
||||
.background(Color.White.copy(alpha = 0.65f), shape = RoundedCornerShape(12.dp))
|
||||
.background(colorScheme.surfaceContainerHighest, shape = RoundedCornerShape(12.dp))
|
||||
) {
|
||||
Text(
|
||||
text = comic!!.comic.author,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.Black,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.padding(4.dp)
|
||||
)
|
||||
@@ -83,13 +82,12 @@ fun ComicGridView(comicId: String, navController: NavHostController, comicGridVi
|
||||
Box(
|
||||
Modifier
|
||||
.padding(horizontal = 16.dp).padding(top = 4.dp)
|
||||
.background(Color.White.copy(alpha = 0.65f), shape = RoundedCornerShape(12.dp))
|
||||
.background(colorScheme.surfaceContainerHighest, shape = RoundedCornerShape(12.dp))
|
||||
) {
|
||||
Text(
|
||||
text = "Tags : ${comic!!.comic.tags.joinToString(", ")}",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.Black,
|
||||
maxLines = 5,
|
||||
modifier = Modifier.padding(4.dp)
|
||||
)
|
||||
@@ -105,7 +103,7 @@ fun ComicGridView(comicId: String, navController: NavHostController, comicGridVi
|
||||
Box(
|
||||
Modifier
|
||||
.padding(horizontal = 16.dp).padding(top = 6.dp).padding(bottom = 20.dp).heightIn(min = 42.dp)
|
||||
.background(Color.White.copy(alpha = 0.65f), shape = RoundedCornerShape(12.dp))
|
||||
.background(colorScheme.surfaceContainerHighest, shape = RoundedCornerShape(12.dp))
|
||||
.clickable{
|
||||
comicGridViewModel.updateProcess(comicId.hexToString())
|
||||
{
|
||||
@@ -133,7 +131,6 @@ fun ComicGridView(comicId: String, navController: NavHostController, comicGridVi
|
||||
text = "Last Read Position: ${k.first.name} ${k.second}/${comic!!.getChapterLength(k.first.page)}",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.Black,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.padding(4.dp).weight(1f)
|
||||
)
|
||||
@@ -142,7 +139,6 @@ fun ComicGridView(comicId: String, navController: NavHostController, comicGridVi
|
||||
text = "Read from scratch",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.Black,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.padding(4.dp).weight(1f)
|
||||
)
|
||||
|
||||
@@ -26,6 +26,7 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -139,6 +140,7 @@ fun ComicScreen(
|
||||
comicScreenViewModel.SetupClient()
|
||||
val included = comicScreenViewModel.included
|
||||
val state = rememberLazyGridState()
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
|
||||
Column {
|
||||
|
||||
@@ -154,9 +156,7 @@ fun ComicScreen(
|
||||
Box(
|
||||
Modifier
|
||||
.background(
|
||||
if (included.contains(i)) Color.Green.copy(alpha = 0.65f) else Color.White.copy(
|
||||
alpha = 0.65f
|
||||
),
|
||||
if (included.contains(i)) Color.Green.copy(alpha = 0.65f) else colorScheme.surfaceContainerHighest,
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
)
|
||||
.height(32.dp).widthIn(max = 72.dp)
|
||||
@@ -174,8 +174,7 @@ fun ComicScreen(
|
||||
maxLines = 1,
|
||||
modifier = Modifier
|
||||
.padding(2.dp)
|
||||
.align(Alignment.Center),
|
||||
color = Color.Black
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,12 +26,13 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import com.acitelight.aether.Global
|
||||
import com.acitelight.aether.Global.updateRelate
|
||||
import com.acitelight.aether.service.MediaManager
|
||||
import com.acitelight.aether.service.RecentManager
|
||||
import com.acitelight.aether.viewModel.HomeScreenViewModel
|
||||
|
||||
@Composable
|
||||
fun HomeScreen(homeScreenViewModel: HomeScreenViewModel = hiltViewModel(), navController: NavController)
|
||||
fun HomeScreen(homeScreenViewModel: HomeScreenViewModel = androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel<HomeScreenViewModel>(), navController: NavController)
|
||||
{
|
||||
if(Global.loggedIn)
|
||||
homeScreenViewModel.Init()
|
||||
@@ -42,9 +43,9 @@ fun HomeScreen(homeScreenViewModel: HomeScreenViewModel = hiltViewModel(), navCo
|
||||
{
|
||||
Column {
|
||||
Text(
|
||||
text = "Recent",
|
||||
text = "Videos",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(16.dp).align(Alignment.Start)
|
||||
modifier = Modifier.padding(8.dp).align(Alignment.Start)
|
||||
)
|
||||
|
||||
HorizontalDivider(Modifier.padding(8.dp), 2.dp, DividerDefaults.color)
|
||||
@@ -56,7 +57,7 @@ fun HomeScreen(homeScreenViewModel: HomeScreenViewModel = hiltViewModel(), navCo
|
||||
.padding(horizontal = 12.dp),
|
||||
i,
|
||||
{
|
||||
Global.sameClassVideos = RecentManager.recent
|
||||
updateRelate(RecentManager.recent, i)
|
||||
val route = "video_player_route/${ "${i.klass}/${i.id}".toHex() }"
|
||||
navController.navigate(route)
|
||||
}, homeScreenViewModel.imageLoader!!)
|
||||
|
||||
@@ -46,7 +46,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
fun MeScreen(meScreenViewModel: MeScreenViewModel = hiltViewModel()) {
|
||||
fun MeScreen(meScreenViewModel: MeScreenViewModel = androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel<MeScreenViewModel>()) {
|
||||
val context = LocalContext.current
|
||||
var username by meScreenViewModel.username;
|
||||
var privateKey by meScreenViewModel.privateKey;
|
||||
@@ -200,7 +200,7 @@ fun MeScreen(meScreenViewModel: MeScreenViewModel = hiltViewModel()) {
|
||||
onClick = {
|
||||
meScreenViewModel.updateServer(url, cert, context)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(0.5f)
|
||||
modifier = Modifier.weight(0.5f)
|
||||
) {
|
||||
Text("Save")
|
||||
}
|
||||
@@ -213,7 +213,7 @@ fun MeScreen(meScreenViewModel: MeScreenViewModel = hiltViewModel()) {
|
||||
Log.i("Delay Analyze", "Abyss Hello: ${h.string()}")
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(0.5f)
|
||||
modifier = Modifier.weight(0.5f)
|
||||
) {
|
||||
Text("Ping")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.acitelight.aether.view
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.Stop
|
||||
import androidx.compose.material.icons.*
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CardElevation
|
||||
import androidx.compose.material3.DividerDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProgressIndicatorDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import com.acitelight.aether.model.DownloadItemState
|
||||
import com.acitelight.aether.viewModel.TransmissionScreenViewModel
|
||||
import com.tonyodev.fetch2.Download
|
||||
import com.tonyodev.fetch2.FetchListener
|
||||
import com.tonyodev.fetch2.Status
|
||||
import com.tonyodev.fetch2core.DownloadBlock
|
||||
|
||||
@Composable
|
||||
fun TransmissionScreen(transmissionScreenViewModel: TransmissionScreenViewModel = hiltViewModel<TransmissionScreenViewModel>())
|
||||
{
|
||||
val downloads = transmissionScreenViewModel.downloads
|
||||
|
||||
Surface(modifier = Modifier.fillMaxWidth()) {
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
items(downloads, key = { it.id }) { item ->
|
||||
DownloadCard(
|
||||
model = item,
|
||||
onPause = { transmissionScreenViewModel.pause(item.id) },
|
||||
onResume = { transmissionScreenViewModel.resume(item.id) },
|
||||
onCancel = { transmissionScreenViewModel.cancel(item.id) },
|
||||
onDelete = { transmissionScreenViewModel.delete(item.id, true) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun DownloadCard(
|
||||
model: DownloadItemState,
|
||||
onPause: () -> Unit,
|
||||
onResume: () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
elevation = CardDefaults.cardElevation(4.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Column(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(text = model.fileName, style = MaterialTheme.typography.titleMedium)
|
||||
Text(text = model.url, style = MaterialTheme.typography.displayMedium, modifier = Modifier.padding(top = 4.dp))
|
||||
}
|
||||
|
||||
// progress percentage
|
||||
Text(text = "${model.progress}%", modifier = Modifier.padding(start = 8.dp))
|
||||
}
|
||||
|
||||
// progress bar
|
||||
LinearProgressIndicator(
|
||||
progress = { model.progress.coerceIn(0, 100) / 100f },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp, bottom = 8.dp),
|
||||
color = ProgressIndicatorDefaults.linearColor,
|
||||
trackColor = ProgressIndicatorDefaults.linearTrackColor,
|
||||
strokeCap = ProgressIndicatorDefaults.LinearStrokeCap,
|
||||
)
|
||||
|
||||
// action buttons
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
when (model.status) {
|
||||
Status.DOWNLOADING -> {
|
||||
Button(onClick = onPause) {
|
||||
Icon(imageVector = Icons.Default.Pause, contentDescription = "Pause")
|
||||
Text(text = " Pause", modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
Button(onClick = onCancel) {
|
||||
Icon(imageVector = Icons.Default.Stop, contentDescription = "Cancel")
|
||||
Text(text = " Cancel", modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
}
|
||||
Status.PAUSED, Status.QUEUED -> {
|
||||
Button(onClick = onResume) {
|
||||
Icon(imageVector = Icons.Default.PlayArrow, contentDescription = "Resume")
|
||||
Text(text = " Resume", modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
Button(onClick = onCancel) {
|
||||
Icon(imageVector = Icons.Default.Stop, contentDescription = "Cancel")
|
||||
Text(text = " Cancel", modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
}
|
||||
Status.COMPLETED -> {
|
||||
Button(onClick = onDelete) {
|
||||
Icon(imageVector = Icons.Default.Delete, contentDescription = "Delete")
|
||||
Text(text = " Delete", modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// for FAILED, CANCELLED, REMOVED etc.
|
||||
Button(onClick = onResume) {
|
||||
Icon(imageVector = Icons.Default.PlayArrow, contentDescription = "Retry")
|
||||
Text(text = " Retry", modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
Button(onClick = onDelete) {
|
||||
Icon(imageVector = Icons.Default.Delete, contentDescription = "Delete")
|
||||
Text(text = " Delete", modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -625,7 +625,7 @@ fun VideoPlayerPortal(videoPlayerViewModel: VideoPlayerViewModel, navController:
|
||||
{
|
||||
TabRow (
|
||||
selectedTabIndex = videoPlayerViewModel.tabIndex,
|
||||
modifier = Modifier.height(38.dp).fillMaxWidth(0.6f)
|
||||
modifier = Modifier.height(38.dp)
|
||||
) {
|
||||
Tab(
|
||||
selected = videoPlayerViewModel.tabIndex == 0,
|
||||
@@ -645,8 +645,6 @@ fun VideoPlayerPortal(videoPlayerViewModel: VideoPlayerViewModel, navController:
|
||||
|
||||
LazyColumn(state = listState, modifier = Modifier.fillMaxWidth()) {
|
||||
item{
|
||||
HorizontalDivider(Modifier, 2.dp, DividerDefaults.color)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Start).padding(horizontal = 12.dp).padding(top = 12.dp),
|
||||
text = videoPlayerViewModel.video?.video?.name ?: "",
|
||||
|
||||
@@ -19,6 +19,7 @@ import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -43,6 +44,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.navigation.NavHostController
|
||||
import coil3.request.ImageRequest
|
||||
import com.acitelight.aether.Global
|
||||
import com.acitelight.aether.Global.updateRelate
|
||||
import java.nio.charset.Charset
|
||||
|
||||
fun String.toHex(): String {
|
||||
@@ -93,10 +95,10 @@ fun VideoScreen(videoScreenViewModel: VideoScreenViewModel = viewModel(), navCon
|
||||
fun TopRow(videoScreenViewModel: VideoScreenViewModel)
|
||||
{
|
||||
val tabIndex by videoScreenViewModel.tabIndex;
|
||||
|
||||
if(videoScreenViewModel.classes.isEmpty()) return
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
|
||||
ScrollableTabRow (selectedTabIndex = tabIndex) {
|
||||
ScrollableTabRow (selectedTabIndex = tabIndex, modifier = Modifier.background(colorScheme.surface)) {
|
||||
videoScreenViewModel.classes.forEachIndexed { index, title ->
|
||||
Tab(
|
||||
selected = tabIndex == index,
|
||||
@@ -116,7 +118,7 @@ fun VideoCard(video: Video, navController: NavHostController, videoScreenViewMod
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
onClick = {
|
||||
Global.sameClassVideos = videoScreenViewModel.classesMap[videoScreenViewModel.classes[tabIndex]] ?: mutableStateListOf()
|
||||
updateRelate(videoScreenViewModel.classesMap[videoScreenViewModel.classes[tabIndex]] ?: mutableStateListOf(), video)
|
||||
val route = "video_player_route/${ "${video.klass}/${video.id}".toHex() }"
|
||||
navController.navigate(route)
|
||||
}
|
||||
@@ -168,7 +170,7 @@ fun VideoCard(video: Video, navController: NavHostController, videoScreenViewMod
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text("Class", fontSize = 12.sp)
|
||||
Text("${video.klass}", fontSize = 12.sp)
|
||||
Text(video.klass, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,13 +40,11 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
|
||||
@HiltViewModel
|
||||
class HomeScreenViewModel @Inject constructor(
|
||||
private val settingsDataStoreManager: SettingsDataStoreManager,
|
||||
@ApplicationContext private val context: Context
|
||||
|
||||
) : ViewModel()
|
||||
{
|
||||
var _init = false
|
||||
var imageLoader: ImageLoader? = null;
|
||||
val uss = settingsDataStoreManager.useSelfSignedFlow
|
||||
|
||||
@Composable
|
||||
fun Init(){
|
||||
@@ -65,31 +63,4 @@ class HomeScreenViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
val u = settingsDataStoreManager.userNameFlow.first()
|
||||
val p = settingsDataStoreManager.privateKeyFlow.first()
|
||||
val ur = settingsDataStoreManager.urlFlow.first()
|
||||
val c = settingsDataStoreManager.certFlow.first()
|
||||
|
||||
if(u=="" || p=="" || ur=="") return@launch
|
||||
|
||||
try{
|
||||
val usedUrl = ApiClient.apply(context, ur, if(uss.first()) c else "")
|
||||
|
||||
if (MediaManager.token == "null")
|
||||
MediaManager.token = AuthManager.fetchToken(
|
||||
u,
|
||||
p
|
||||
)!!
|
||||
|
||||
Global.loggedIn = true
|
||||
}catch(e: Exception)
|
||||
{
|
||||
Global.loggedIn = false
|
||||
print(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,24 @@ class MeScreenViewModel @Inject constructor(
|
||||
privateKey.value = if (settingsDataStoreManager.privateKeyFlow.first() == "") "" else "******"
|
||||
url.value = settingsDataStoreManager.urlFlow.first()
|
||||
cert.value = settingsDataStoreManager.certFlow.first()
|
||||
|
||||
if(username.value=="" || privateKey.value=="" || url.value=="") return@launch
|
||||
|
||||
try{
|
||||
val usedUrl = ApiClient.apply(context, url.value, if(uss.first()) cert.value else "")
|
||||
|
||||
if (MediaManager.token == "null")
|
||||
MediaManager.token = AuthManager.fetchToken(
|
||||
username.value,
|
||||
settingsDataStoreManager.privateKeyFlow.first()
|
||||
)!!
|
||||
|
||||
Global.loggedIn = true
|
||||
}catch(e: Exception)
|
||||
{
|
||||
Global.loggedIn = false
|
||||
print(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.acitelight.aether.viewModel
|
||||
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.acitelight.aether.model.DownloadItemState
|
||||
import com.acitelight.aether.service.FetchManager
|
||||
import com.tonyodev.fetch2.Download
|
||||
import com.tonyodev.fetch2.FetchListener
|
||||
import com.tonyodev.fetch2core.DownloadBlock
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class TransmissionScreenViewModel @Inject constructor(
|
||||
private val fetchManager: FetchManager
|
||||
) : ViewModel() {
|
||||
private val _downloads: SnapshotStateList<DownloadItemState> = mutableStateListOf()
|
||||
val downloads: SnapshotStateList<DownloadItemState> = _downloads
|
||||
|
||||
// map id -> state object reference (no index bookkeeping)
|
||||
private val idToState: MutableMap<Int, DownloadItemState> = mutableMapOf()
|
||||
|
||||
private val fetchListener = object : FetchListener {
|
||||
override fun onAdded(download: Download) { handleUpsert(download) }
|
||||
override fun onQueued(download: Download, waitingOnNetwork: Boolean) { handleUpsert(download) }
|
||||
override fun onWaitingNetwork(download: Download) {
|
||||
|
||||
}
|
||||
|
||||
override fun onProgress(download: Download, etaInMilliSeconds: Long, downloadedBytesPerSecond: Long) { handleUpsert(download) }
|
||||
override fun onPaused(download: Download) { handleUpsert(download) }
|
||||
override fun onResumed(download: Download) { handleUpsert(download) }
|
||||
override fun onCompleted(download: Download) { handleUpsert(download) }
|
||||
override fun onCancelled(download: Download) { handleUpsert(download) }
|
||||
override fun onRemoved(download: Download) { handleRemove(download.id) }
|
||||
override fun onDeleted(download: Download) { handleRemove(download.id) }
|
||||
override fun onDownloadBlockUpdated(download: Download, downloadBlock: DownloadBlock, blockCount: Int) { handleUpsert(download) }
|
||||
override fun onStarted(
|
||||
download: Download,
|
||||
downloadBlocks: List<DownloadBlock>,
|
||||
totalBlocks: Int
|
||||
) {
|
||||
handleUpsert(download)
|
||||
}
|
||||
|
||||
override fun onError(download: Download, error: com.tonyodev.fetch2.Error, throwable: Throwable?) { handleUpsert(download) }
|
||||
}
|
||||
|
||||
private fun handleUpsert(download: Download) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
upsertOnMain(download)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRemove(id: Int) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
removeOnMain(id)
|
||||
}
|
||||
}
|
||||
|
||||
private fun upsertOnMain(download: Download) {
|
||||
val existing = idToState[download.id]
|
||||
if (existing != null) {
|
||||
// update fields in-place -> minimal recomposition
|
||||
existing.filePath = download.file ?: existing.filePath
|
||||
existing.fileName = try { File(existing.filePath).name } catch (_: Exception) { existing.fileName }
|
||||
existing.url = download.url
|
||||
existing.progress = download.progress
|
||||
existing.status = download.status
|
||||
existing.downloadedBytes = download.downloaded
|
||||
existing.totalBytes = download.total
|
||||
} else {
|
||||
// new item: add to head (or tail depending on preference)
|
||||
val newState = downloadToState(download)
|
||||
_downloads.add(0, newState)
|
||||
idToState[newState.id] = newState
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeOnMain(id: Int) {
|
||||
val state = idToState.remove(id)
|
||||
if (state != null) {
|
||||
_downloads.remove(state)
|
||||
} else {
|
||||
val idx = _downloads.indexOfFirst { it.id == id }
|
||||
if (idx >= 0) {
|
||||
val removed = _downloads.removeAt(idx)
|
||||
idToState.remove(removed.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun downloadToState(download: Download): DownloadItemState {
|
||||
val filePath = download.file ?: ""
|
||||
val fileName = try { File(filePath).name } catch (_: Exception) { filePath }
|
||||
return DownloadItemState(
|
||||
id = download.id,
|
||||
fileName = fileName,
|
||||
filePath = filePath,
|
||||
url = download.url,
|
||||
progress = download.progress,
|
||||
status = download.status,
|
||||
downloadedBytes = download.downloaded,
|
||||
totalBytes = download.total
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// UI actions delegated to FetchManager
|
||||
fun pause(id: Int) = fetchManager.pause(id)
|
||||
fun resume(id: Int) = fetchManager.resume(id)
|
||||
fun cancel(id: Int) = fetchManager.cancel(id)
|
||||
fun delete(id: Int, deleteFile: Boolean = true) {
|
||||
fetchManager.delete(id) {
|
||||
viewModelScope.launch(Dispatchers.Main) { removeOnMain(id) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
fetchManager.removeListener()
|
||||
}
|
||||
|
||||
init {
|
||||
fetchManager.setListener(fetchListener)
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
fetchManager.getAllDownloads { list ->
|
||||
_downloads.clear()
|
||||
idToState.clear()
|
||||
list.sortedByDescending { it.id }.forEach { d ->
|
||||
val s = downloadToState(d)
|
||||
_downloads.add(s)
|
||||
idToState[s.id] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
[versions]
|
||||
agp = "8.13.0"
|
||||
ariaCompiler = "latest"
|
||||
bcprovJdk15on = "1.70"
|
||||
bcprovJdk18on = "1.81"
|
||||
coilCompose = "3.3.0"
|
||||
@@ -7,6 +8,8 @@ coilNetworkOkhttp = "3.3.0"
|
||||
converterGson = "3.0.0"
|
||||
datastorePreferences = "1.1.7"
|
||||
exoplayerplus = "0.2.0"
|
||||
fetch2 = "3.4.1"
|
||||
fetch2okhttp = "3.4.1"
|
||||
gson = "2.13.1"
|
||||
kotlin = "2.2.20"
|
||||
coreKtx = "1.17.0"
|
||||
@@ -15,23 +18,23 @@ junitVersion = "1.3.0"
|
||||
espressoCore = "3.7.0"
|
||||
kotlinxSerializationJson = "1.9.0"
|
||||
lifecycleRuntimeKtx = "2.9.3"
|
||||
activityCompose = "1.10.1"
|
||||
composeBom = "2025.08.01"
|
||||
activityCompose = "1.11.0"
|
||||
composeBom = "2025.09.00"
|
||||
media3Common = "1.8.0"
|
||||
media3Exoplayer = "1.8.0"
|
||||
media3Ui = "1.8.0"
|
||||
navigationCompose = "2.9.3"
|
||||
navigationCompose = "2.9.4"
|
||||
okhttp = "5.1.0"
|
||||
retrofit = "3.0.0"
|
||||
retrofit2KotlinxSerializationConverter = "1.0.0"
|
||||
media3DatasourceOkhttp = "1.8.0"
|
||||
roomCompiler = "2.7.2"
|
||||
roomKtx = "2.7.2"
|
||||
roomRuntime = "2.7.2"
|
||||
roomCompiler = "2.8.0"
|
||||
roomKtx = "2.8.0"
|
||||
roomRuntime = "2.8.0"
|
||||
|
||||
ksp = "2.1.21-2.0.2"
|
||||
hilt = "2.57.1"
|
||||
hilt-navigation-compose = "1.2.0"
|
||||
hilt-navigation-compose = "1.3.0"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
@@ -44,10 +47,13 @@ androidx-navigation-compose = { module = "androidx.navigation:navigation-compose
|
||||
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomCompiler" }
|
||||
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomKtx" }
|
||||
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" }
|
||||
aria-compiler = { module = "com.arialyy.aria:aria-compiler", version.ref = "ariaCompiler" }
|
||||
bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk15on", version.ref = "bcprovJdk15on" }
|
||||
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" }
|
||||
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coilNetworkOkhttp" }
|
||||
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
|
||||
fetch2 = { module = "com.github.tonyofrancis.Fetch:fetch2", version.ref = "fetch2" }
|
||||
fetch2okhttp = { module = "com.github.tonyofrancis.Fetch:fetch2okhttp", version.ref = "fetch2okhttp" }
|
||||
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
|
||||
@@ -16,6 +16,9 @@ dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = uri("https://jitpack.io")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user