[update] Better UI

This commit is contained in:
acite
2025-09-14 18:26:05 +08:00
parent 9c04d7679c
commit cc540903d3
21 changed files with 661 additions and 98 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
}
}
}

View File

@@ -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")

View File

@@ -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)
}

View File

@@ -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
) {

View File

@@ -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)

View File

@@ -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) })
}
}

View File

@@ -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
}

View File

@@ -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)
)

View File

@@ -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)
)
}
}

View File

@@ -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!!)

View File

@@ -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")
}

View File

@@ -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))
}
}
}
}
}
}
}

View File

@@ -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 ?: "",

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}
}
}
}
}

View File

@@ -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" }

View File

@@ -16,6 +16,9 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url = uri("https://jitpack.io")
}
}
}