8 Commits

Author SHA1 Message Date
rootacite
7bd92f91f4 [update] ui patch 3 2025-11-01 22:03:44 +08:00
rootacite
78d6564964 [update] ui patch 2 2025-11-01 21:53:20 +08:00
rootacite
512da7be1c [update] ui patch 2025-11-01 03:28:13 +08:00
rootacite
9efbcdfe8a [feat] Optional sort, tags folder 2025-10-29 23:53:14 +08:00
rootacite
c3e0a23ed1 [update] Comic sort policy 2025-10-29 20:14:07 +08:00
acite
7be18dd517 [feat] Playlist remember 2025-10-09 11:33:34 +08:00
acite
a13ddbdd87 [feat] Live framework. 2025-10-06 22:54:08 +08:00
acite
390094b8b0 [fix&optimize] Fix the issue of element teleportation in ComicScreen pages 2025-10-02 12:24:26 +08:00
22 changed files with 825 additions and 535 deletions

View File

@@ -56,6 +56,7 @@ import com.acitelight.aether.view.pages.ComicGridView
import com.acitelight.aether.view.pages.ComicPageView import com.acitelight.aether.view.pages.ComicPageView
import com.acitelight.aether.view.pages.ComicScreen import com.acitelight.aether.view.pages.ComicScreen
import com.acitelight.aether.view.pages.HomeScreen import com.acitelight.aether.view.pages.HomeScreen
import com.acitelight.aether.view.pages.LiveScreen
import com.acitelight.aether.view.pages.MeScreen import com.acitelight.aether.view.pages.MeScreen
import com.acitelight.aether.view.pages.TransmissionScreen import com.acitelight.aether.view.pages.TransmissionScreen
import com.acitelight.aether.view.pages.VideoPlayer import com.acitelight.aether.view.pages.VideoPlayer
@@ -179,12 +180,21 @@ fun AppNavigation() {
TransmissionScreen(navigator = navController) TransmissionScreen(navigator = navController)
} }
} }
composable(Screen.Live.route,
enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
popEnterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) },
popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) }) {
LiveScreen()
}
composable(Screen.Me.route, composable(Screen.Me.route,
enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) }, enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) }, exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
popEnterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) }, popEnterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) },
popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) }) { popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) }) {
MeScreen(); MeScreen()
} }
composable( composable(
@@ -246,6 +256,7 @@ fun BottomNavigationBar(navController: NavController) {
Screen.Video, Screen.Video,
Screen.Comic, Screen.Comic,
Screen.Transmission, Screen.Transmission,
Screen.Live,
Screen.Me Screen.Me
) else listOf( ) else listOf(
Screen.Video, Screen.Video,
@@ -310,6 +321,8 @@ sealed class Screen(val route: String, val icon: ImageVector, val title: String)
data object Comic : Screen("comic_route", Icons.Filled.Image, "Comic") data object Comic : Screen("comic_route", Icons.Filled.Image, "Comic")
data object Transmission : Screen("transmission_route", data object Transmission : Screen("transmission_route",
Icons.AutoMirrored.Filled.CompareArrows, "Transmission") Icons.AutoMirrored.Filled.CompareArrows, "Transmission")
data object Live : Screen("live_route",
Icons.Filled.LiveTv, "Live")
data object Me : Screen("me_route", Icons.Filled.AccountCircle, "me") data object Me : Screen("me_route", Icons.Filled.AccountCircle, "me")
data object VideoPlayer : Screen("video_player_route/{videoId}", Icons.Filled.PlayArrow, "VideoPlayer") data object VideoPlayer : Screen("video_player_route/{videoId}", Icons.Filled.PlayArrow, "VideoPlayer")
data object ComicGrid : Screen("comic_grid_route/{comicId}", Icons.Filled.PlayArrow, "ComicGrid") data object ComicGrid : Screen("comic_grid_route/{comicId}", Icons.Filled.PlayArrow, "ComicGrid")

View File

@@ -13,6 +13,12 @@ class Video(
val id: String, val id: String,
val video: VideoResponse val video: VideoResponse
) { ) {
companion object {
fun getCoverStatic(api: ApiClient, klass: String, id: String): String
{
return "${api.getBase()}api/video/$klass/$id/cover"
}
}
fun getCover(api: ApiClient): String { fun getCover(api: ApiClient): String {
return if (isLocal) return if (isLocal)
"$localBase/videos/$klass/$id/cover.jpg" "$localBase/videos/$klass/$id/cover.jpg"

View File

@@ -16,7 +16,8 @@ class VideoDownloadItemState(
totalBytes: Long, totalBytes: Long,
klass: String, klass: String,
vid: String, vid: String,
val type: String val type: String,
val group: String
) { ) {
var fileName by mutableStateOf(fileName) var fileName by mutableStateOf(fileName)
var filePath by mutableStateOf(filePath) var filePath by mutableStateOf(filePath)

View File

@@ -46,6 +46,9 @@ class ApiClient @Inject constructor(
fun getBase(): String{ fun getBase(): String{
return replaceAbyssProtocol(base) return replaceAbyssProtocol(base)
} }
fun getDomain(): String = domain
private var base: String = "" private var base: String = ""
private var domain: String = "" private var domain: String = ""
private var cert: String = "" private var cert: String = ""
@@ -236,7 +239,7 @@ class ApiClient @Inject constructor(
throw Exception("No reachable URL found") throw Exception("No reachable URL found")
} }
domain = selectedUrl.toHttpUrlOrNull()?.host ?: "" domain = replaceAbyssProtocol(selectedUrl).toHttpUrlOrNull()?.host ?: ""
cert = crt cert = crt
base = selectedUrl base = selectedUrl
withContext(Dispatchers.IO) withContext(Dispatchers.IO)

View File

@@ -6,6 +6,7 @@ import com.tonyodev.fetch2.Download
import com.tonyodev.fetch2.Fetch import com.tonyodev.fetch2.Fetch
import com.tonyodev.fetch2.FetchConfiguration import com.tonyodev.fetch2.FetchConfiguration
import com.tonyodev.fetch2.FetchListener import com.tonyodev.fetch2.FetchListener
import com.tonyodev.fetch2.Priority
import com.tonyodev.fetch2.Request import com.tonyodev.fetch2.Request
import com.tonyodev.fetch2core.Extras import com.tonyodev.fetch2core.Extras
import com.tonyodev.fetch2okhttp.OkHttpDownloader import com.tonyodev.fetch2okhttp.OkHttpDownloader
@@ -128,21 +129,25 @@ class FetchManager @Inject constructor(
val requests = mutableListOf( val requests = mutableListOf(
Request(video.getVideo(apiClient), videoPath.path).apply { Request(video.getVideo(apiClient), videoPath.path).apply {
priority = Priority.LOW
extras = Extras( extras = Extras(
mapOf( mapOf(
"name" to video.video.name, "name" to video.video.name,
"id" to video.id, "id" to video.id,
"class" to video.klass, "class" to video.klass,
"group" to (video.video.group ?: ""),
"type" to "main" "type" to "main"
) )
) )
}, },
Request(video.getCover(apiClient), coverPath.path).apply { Request(video.getCover(apiClient), coverPath.path).apply {
priority = Priority.HIGH
extras = Extras( extras = Extras(
mapOf( mapOf(
"name" to video.video.name, "name" to video.video.name,
"id" to video.id, "id" to video.id,
"class" to video.klass, "class" to video.klass,
"group" to (video.video.group ?: ""),
"type" to "cover" "type" to "cover"
) )
) )
@@ -153,6 +158,7 @@ class FetchManager @Inject constructor(
"name" to video.video.name, "name" to video.video.name,
"id" to video.id, "id" to video.id,
"class" to video.klass, "class" to video.klass,
"group" to (video.video.group ?: ""),
"type" to "subtitle" "type" to "subtitle"
) )
) )
@@ -169,6 +175,7 @@ class FetchManager @Inject constructor(
"name" to video.video.name, "name" to video.video.name,
"id" to video.id, "id" to video.id,
"class" to video.klass, "class" to video.klass,
"group" to (video.video.group ?: ""),
"type" to "gallery" "type" to "gallery"
) )
) )

View File

@@ -156,7 +156,7 @@ class MediaManager @Inject constructor(
{ {
try{ try{
val j = apiClient.api!!.getComics() val j = apiClient.api!!.getComics()
return j return j.sorted()
}catch (_: Exception) }catch (_: Exception)
{ {
return listOf() return listOf()

View File

@@ -64,11 +64,11 @@ fun ComicCard(
.diskCacheKey("${comic.id}/cover") .diskCacheKey("${comic.id}/cover")
.build(), .build(),
contentDescription = null, contentDescription = null,
imageLoader = comicScreenViewModel.imageLoader!!,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.clip(RoundedCornerShape(8.dp)), .clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.Crop, contentScale = ContentScale.Fit,
imageLoader = comicScreenViewModel.imageLoader!!,
) )
Box( Box(
@@ -100,22 +100,21 @@ fun ComicCard(
} }
Text( Text(
text = comic.comic.comic_name, text = comic.comic.comic_name,
fontSize = 14.sp, fontSize = 12.sp,
lineHeight = 17.sp, lineHeight = 14.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
maxLines = 2, maxLines = 2,
modifier = Modifier
.padding(4.dp)
.heightIn(min = 14.dp)
)
Text(
text = "Id: ${comic.id}",
fontSize = 10.sp,
lineHeight = 12.sp,
maxLines = 1,
modifier = Modifier.padding(4.dp) modifier = Modifier.padding(4.dp)
) )
Box(Modifier.padding(4.dp).fillMaxWidth()){
Text(
text = "Id: ${comic.id}",
fontSize = 12.sp,
lineHeight = 14.sp,
maxLines = 1,
modifier = Modifier.align(Alignment.CenterStart)
)
}
} }
} }
} }

View File

@@ -1,271 +1,293 @@
package com.acitelight.aether.view.components package com.acitelight.aether.view.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Stop
import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProgressIndicatorDefaults import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import coil3.request.ImageRequest import coil3.request.ImageRequest
import com.acitelight.aether.Global.updateRelate
import com.acitelight.aether.model.Video import com.acitelight.aether.model.Video
import com.acitelight.aether.model.VideoDownloadItemState import com.acitelight.aether.model.VideoDownloadItemState
import com.acitelight.aether.view.pages.toHex import com.acitelight.aether.view.pages.downloadToGroup
import com.acitelight.aether.viewModel.TransmissionScreenViewModel import com.acitelight.aether.viewModel.TransmissionScreenViewModel
import com.tonyodev.fetch2.Status import com.tonyodev.fetch2.Status
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import kotlin.math.abs import kotlin.math.abs
@Composable
private fun VideoDownloadCardMiniPack(
navigator: NavHostController,
viewModel: TransmissionScreenViewModel,
model: VideoDownloadItemState,
imageHeight: Dp = 85.dp,
imageMaxWidth: Dp = 140.dp
) {
val downloads = viewModel.downloads
VideoDownloadCardMini(
navigator = navigator,
viewModel = viewModel,
model = model,
onPause = {
for (i in downloadToGroup(
model,
downloads
)) viewModel.pause(i.id)
},
onResume = {
for (i in downloadToGroup(
model,
downloads
)) viewModel.resume(i.id)
},
onRetry = {
for (i in downloadToGroup(
model,
downloads
)) viewModel.retry(i.id)
},
imageHeight,
imageMaxWidth
)
}
@Composable @Composable
fun VideoDownloadCard( fun VideoDownloadCard(
navigator: NavHostController, navigator: NavHostController,
viewModel: TransmissionScreenViewModel, viewModel: TransmissionScreenViewModel,
model: VideoDownloadItemState, models: List<VideoDownloadItemState>
onPause: () -> Unit,
onResume: () -> Unit,
onCancel: () -> Unit,
onDelete: () -> Unit,
onRetry: () -> Unit
) { ) {
Card( val item = models.first()
shape = RoundedCornerShape(8.dp), var mutiSelection by viewModel.mutiSelection
elevation = CardDefaults.cardElevation(4.dp), val colorScheme = MaterialTheme.colorScheme
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.background(Color.Transparent)
.clickable(onClick = {
if (model.status == Status.COMPLETED) {
viewModel.viewModelScope.launch(Dispatchers.IO)
{
val downloaded = viewModel.fetchManager.getAllDownloadsAsync().filter {
it.status == Status.COMPLETED && it.extras.getString(
"class",
""
) != "comic" && it.extras.getString(
"type",
""
) == "main"
}
val jsonQuery = downloaded.map { if (models.size == 1) {
File( VideoDownloadCardMiniPack(
viewModel.context.getExternalFilesDir(null), navigator = navigator,
"videos/${ viewModel = viewModel,
it.extras.getString( model = item
"class", )
"" } else if (models.size > 1) {
) val imageModel = if (item.status == Status.COMPLETED) {
}/${it.extras.getString("id", "")}/summary.json" ImageRequest.Builder(LocalContext.current)
).readText() .data(
} File(
.map { viewModel.context.getExternalFilesDir(null),
Json.decodeFromString<Video>(it) "videos/${item.klass}/${item.vid}/cover.jpg"
.toLocal(viewModel.context.getExternalFilesDir(null)!!.path) )
} )
.memoryCacheKey("${item.klass}/${item.vid}/cover")
.diskCacheKey("${item.klass}/${item.vid}/cover")
.build()
} else {
ImageRequest.Builder(LocalContext.current)
.data(Video.getCoverStatic(viewModel.apiClient, item.klass, item.vid))
.memoryCacheKey("${item.klass}/${item.vid}/cover")
.diskCacheKey("${item.klass}/${item.vid}/cover")
.build()
}
updateRelate( Card(
jsonQuery, colors = CardDefaults.cardColors(containerColor = Color.Transparent),
jsonQuery.first { it.id == model.vid && it.klass == model.klass } shape = RoundedCornerShape(8.dp),
)
val playList = mutableListOf<String>()
val fv = viewModel.videoLibrary.classesMap.map { it.value }.flatten()
val video = fv.firstOrNull { it.klass == model.klass && it.id == model.vid }
if (video != null) {
val group = fv.filter { it.klass == video.klass && it.video.group == video.video.group }
for (i in group.sortedWith(compareBy(naturalOrder()) { it.video.name })) {
playList.add("${i.klass}/${i.id}")
}
}
val route = "video_player_route/${(playList.joinToString(",") + "|${model.vid}").toHex()}"
withContext(Dispatchers.Main) {
navigator.navigate(route)
}
}
}
})
) {
Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(12.dp) .padding(horizontal = 4.dp)
.background(Color.Transparent)
.combinedClickable(
onClick = {
if (!mutiSelection)
viewModel.groupExpandMap[item.group] = !viewModel.groupExpandMap.getOrDefault(item.group, false)
else {
if(!models.all { "${it.klass}/${it.vid}" in viewModel.mutiSelectionList })
viewModel.mutiSelectionList.addAll(
models.map { "${it.klass}/${it.vid}" }.filter { it !in viewModel.mutiSelectionList }
)
else
viewModel.mutiSelectionList.removeAll(models.map { "${it.klass}/${it.vid}" })
}
},
onLongClick = {
mutiSelection = !mutiSelection
}
)
.height(85.dp)
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize()
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.weight(1f)) {
Text(text = model.fileName, style = MaterialTheme.typography.titleMedium, maxLines = 2)
// Text(text = model.filePath, style = MaterialTheme.typography.titleSmall)
}
}
Box(
Modifier
.fillMaxWidth()
.padding(top = 5.dp)
) )
{ {
Card( Box(
shape = RoundedCornerShape(8.dp), Modifier
modifier = Modifier.align(Alignment.CenterStart) .fillMaxHeight()
) { )
val video = viewModel.modelToVideo(model) {
AsyncImage(
model = imageModel,
contentDescription = null,
modifier = Modifier
.height(85.dp)
.clip(RoundedCornerShape(8.dp))
.widthIn(max = 140.dp)
.background(Color.Black),
contentScale = ContentScale.Crop
)
if (video == null) Box(Modifier
AsyncImage( .padding(4.dp)
model = ImageRequest.Builder(LocalContext.current) .background(Color.Black.copy(0.4f), shape = RoundedCornerShape(6.dp))
.data( .align(Alignment.BottomEnd)
File( .padding(2.dp))
viewModel.context.getExternalFilesDir(null), {
"videos/${model.klass}/${model.vid}/cover.jpg" Text(text = "${models.size} Videos",
) fontWeight = FontWeight.Bold,
) fontSize = 12.sp,
.memoryCacheKey("${model.klass}/${model.vid}/cover") lineHeight = 13.5.sp)
.diskCacheKey("${model.klass}/${model.vid}/cover")
.build(),
contentDescription = null,
modifier = Modifier.height(100.dp),
contentScale = ContentScale.Fit
)
else {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(video.getCover(viewModel.apiClient))
.memoryCacheKey("${model.klass}/${model.vid}/cover")
.diskCacheKey("${model.klass}/${model.vid}/cover")
.build(),
contentDescription = null,
modifier = Modifier.height(100.dp),
contentScale = ContentScale.Fit,
imageLoader = viewModel.imageLoader!!
)
} }
} }
Column(Modifier.align(Alignment.BottomEnd)) { Box(
Modifier
.fillMaxSize()
.padding(all = 4.dp)
.padding(end = 4.dp)
)
{
Text( Text(
text = "${model.progress.coerceIn(0, 100)}%", text = models.first().group,
modifier = Modifier lineHeight = 14.sp,
.padding(start = 8.dp) fontSize = 12.sp,
.align(Alignment.End) fontWeight = FontWeight.Bold,
maxLines = 2,
modifier = Modifier.align(Alignment.TopEnd)
) )
Text( Column(Modifier.align(Alignment.BottomEnd)) {
modifier = Modifier
.padding(start = 8.dp) Row(
.align(Alignment.End), Modifier
text = "%.2f MB/%.2f MB".format( .align(Alignment.End)
model.downloadedBytes / (1024.0 * 1024.0), .padding(vertical = 2.dp)
model.totalBytes / (1024.0 * 1024.0) ) {
), Text(
fontSize = 10.sp, modifier = Modifier,
fontWeight = FontWeight.Bold, text = "%.2f MB/%.2f MB".format(
maxLines = 1, models.sumOf { it.downloadedBytes } / (1024.0 * 1024.0),
) models.sumOf { it.totalBytes } / (1024.0 * 1024.0)
),
fontSize = 10.sp,
lineHeight = 11.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
)
Spacer(Modifier.width(12.dp))
Text(
text = "${
models.map { it.progress }.average().toInt().coerceIn(0, 100)
}%",
modifier = Modifier,
fontSize = 10.sp,
lineHeight = 11.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
)
}
BiliMiniSlider(
value = abs(models.map { it.progress }.average().toInt()).coerceIn(
0,
100
) / 100f,
modifier = Modifier
.height(6.dp)
.align(Alignment.End)
.fillMaxWidth(),
onValueChange = {
},
colors = SliderDefaults.colors(
thumbColor = Color(0xFFFFFFFF),
activeTrackColor = MaterialTheme.colorScheme.primary,
inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f)
)
)
}
} }
} }
}
AnimatedVisibility(
// progress bar visible = viewModel.groupExpandMap.getOrDefault(item.group, false) || mutiSelection,
LinearProgressIndicator( enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
progress = { abs(model.progress).coerceIn(0, 100) / 100f }, exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut()
modifier = Modifier )
{
Column(
Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 8.dp, bottom = 8.dp), .heightIn(max = 300.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) { HorizontalDivider(
Status.DOWNLOADING -> { Modifier.padding(horizontal = 16.dp, vertical = 3.dp),
Button(onClick = onPause) { 2.dp,
Icon(imageVector = Icons.Default.Pause, contentDescription = "Pause") DividerDefaults.color
Text(text = " Pause", modifier = Modifier.padding(start = 6.dp)) )
} LazyColumn(
Button(onClick = onCancel) { Modifier
Icon(imageVector = Icons.Default.Stop, contentDescription = "Cancel") .padding(horizontal = 16.dp)
Text(text = " Cancel", modifier = Modifier.padding(start = 6.dp)) .background(colorScheme.surface, shape = RoundedCornerShape(6.dp))
} )
} {
items(
Status.PAUSED, Status.QUEUED -> { items = models,
Button(onClick = onResume) { key = { it.id }
Icon( ) { single ->
imageVector = Icons.Default.PlayArrow, Box(Modifier.padding(vertical = 4.dp))
contentDescription = "Resume" {
VideoDownloadCardMiniPack(
navigator = navigator,
viewModel = viewModel,
model = single,
imageHeight = 75.dp,
imageMaxWidth = 120.dp
) )
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 = onRetry) {
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

@@ -1,7 +1,12 @@
package com.acitelight.aether.view.components package com.acitelight.aether.view.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -11,20 +16,21 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Checkbox
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SliderDefaults import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -32,12 +38,14 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import coil3.request.ImageRequest import coil3.request.ImageRequest
import com.acitelight.aether.model.Video
import com.acitelight.aether.model.VideoDownloadItemState import com.acitelight.aether.model.VideoDownloadItemState
import com.acitelight.aether.viewModel.TransmissionScreenViewModel import com.acitelight.aether.viewModel.TransmissionScreenViewModel
import com.tonyodev.fetch2.Status import com.tonyodev.fetch2.Status
@@ -53,29 +61,31 @@ fun VideoDownloadCardMini(
model: VideoDownloadItemState, model: VideoDownloadItemState,
onPause: () -> Unit, onPause: () -> Unit,
onResume: () -> Unit, onResume: () -> Unit,
onCancel: () -> Unit, onRetry: () -> Unit,
onDelete: () -> Unit, imageHeight: Dp = 85.dp,
onRetry: () -> Unit imageMaxWidth: Dp = 140.dp
) { ) {
val video = viewModel.modelToVideo(model) var mutiSelection by viewModel.mutiSelection
val imageModel = val imageModel = if (model.status == Status.COMPLETED)
if (video == null) {
ImageRequest.Builder(LocalContext.current) ImageRequest.Builder(LocalContext.current)
.data( .data(
File( File(
viewModel.context.getExternalFilesDir(null), viewModel.context.getExternalFilesDir(null),
"videos/${model.klass}/${model.vid}/cover.jpg" "videos/${model.klass}/${model.vid}/cover.jpg"
)
) )
.memoryCacheKey("${model.klass}/${model.vid}/cover") )
.diskCacheKey("${model.klass}/${model.vid}/cover") .memoryCacheKey("${model.klass}/${model.vid}/cover")
.build() .diskCacheKey("${model.klass}/${model.vid}/cover")
else .build()
ImageRequest.Builder(LocalContext.current) } else
.data(video.getCover(viewModel.apiClient)) {
.memoryCacheKey("${model.klass}/${model.vid}/cover") ImageRequest.Builder(LocalContext.current)
.diskCacheKey("${model.klass}/${model.vid}/cover") .data(Video.getCoverStatic(viewModel.apiClient, model.klass, model.vid))
.build() .memoryCacheKey("${model.klass}/${model.vid}/cover")
.diskCacheKey("${model.klass}/${model.vid}/cover")
.build()
}
Card( Card(
colors = CardDefaults.cardColors(containerColor = Color.Transparent), colors = CardDefaults.cardColors(containerColor = Color.Transparent),
@@ -84,54 +94,70 @@ fun VideoDownloadCardMini(
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 4.dp) .padding(horizontal = 4.dp)
.background(Color.Transparent) .background(Color.Transparent)
.clickable(onClick = { .combinedClickable(
when (model.status) { onClick = {
Status.COMPLETED -> viewModel.viewModelScope.launch(Dispatchers.IO) if (!mutiSelection)
{ when (model.status) {
viewModel.playStart(model, navigator) Status.COMPLETED -> viewModel.viewModelScope.launch(Dispatchers.IO)
{
viewModel.playStart(model, navigator)
}
Status.DOWNLOADING -> onPause()
Status.PAUSED -> onResume()
Status.ADDED, Status.FAILED, Status.CANCELLED -> onRetry()
else -> {}
}
else {
if ("${model.klass}/${model.vid}" !in viewModel.mutiSelectionList)
viewModel.mutiSelectionList.add("${model.klass}/${model.vid}")
else
viewModel.mutiSelectionList.remove("${model.klass}/${model.vid}")
} }
Status.DOWNLOADING -> onPause() },
Status.PAUSED -> onResume() onLongClick = {
Status.ADDED, Status.FAILED, Status.CANCELLED -> onRetry() mutiSelection = !mutiSelection
else -> {}
} }
}) )
.height(100.dp) .height(imageHeight)
) { ) {
Row( Row(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
{ {
Box(Modifier Row(
.fillMaxHeight()) Modifier.fillMaxHeight()
)
{ {
AnimatedVisibility(
modifier = Modifier.align(Alignment.CenterVertically),
visible = mutiSelection,
enter = expandHorizontally(expandFrom = Alignment.Start) + fadeIn(),
exit = shrinkHorizontally(shrinkTowards = Alignment.Start) + fadeOut()
)
{
Checkbox(
checked = "${model.klass}/${model.vid}" in viewModel.mutiSelectionList,
onCheckedChange = { state ->
if (state)
viewModel.mutiSelectionList.add("${model.klass}/${model.vid}")
else
viewModel.mutiSelectionList.remove("${model.klass}/${model.vid}")
}
)
}
AsyncImage( AsyncImage(
model = imageModel, model = imageModel,
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.height(100.dp) .height(imageHeight)
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(8.dp))
.widthIn(max = 150.dp) .widthIn(max = imageMaxWidth)
.background(Color.Black), .background(Color.Black),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop,
imageLoader = viewModel.imageLoader!!
) )
IconButton(
onClick = onDelete,
Modifier
.padding(2.dp)
.size(24.dp)
.align(Alignment.TopStart)
.background(MaterialTheme.colorScheme.error, RoundedCornerShape(4.dp))
.clip(RoundedCornerShape(4.dp))
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete",
tint = Color.White,
modifier = Modifier.size(20.dp)
)
}
} }
Box( Box(
@@ -165,9 +191,11 @@ fun VideoDownloadCardMini(
maxLines = 1, maxLines = 1,
) )
Row(Modifier Row(
.align(Alignment.End) Modifier
.padding(vertical = 2.dp)) { .align(Alignment.End)
.padding(vertical = 2.dp)
) {
Text( Text(
modifier = Modifier, modifier = Modifier,
text = "%.2f MB/%.2f MB".format( text = "%.2f MB/%.2f MB".format(
@@ -198,23 +226,25 @@ fun VideoDownloadCardMini(
onValueChange = { onValueChange = {
}, },
colors = when(model.status) colors = when (model.status) {
{
Status.DOWNLOADING, Status.QUEUED, Status.ADDED -> SliderDefaults.colors( Status.DOWNLOADING, Status.QUEUED, Status.ADDED -> SliderDefaults.colors(
thumbColor = Color(0xFFFFFFFF), thumbColor = Color(0xFFFFFFFF),
activeTrackColor = MaterialTheme.colorScheme.primary, activeTrackColor = MaterialTheme.colorScheme.primary,
inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f) inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f)
) )
Status.PAUSED -> SliderDefaults.colors( Status.PAUSED -> SliderDefaults.colors(
thumbColor = Color(0xFFFFFFFF), thumbColor = Color(0xFFFFFFFF),
activeTrackColor = Color(0xFFFFA500), activeTrackColor = Color(0xFFFFA500),
inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f) inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f)
) )
Status.COMPLETED -> SliderDefaults.colors( Status.COMPLETED -> SliderDefaults.colors(
thumbColor = Color(0xFFFFFFFF), thumbColor = Color(0xFFFFFFFF),
activeTrackColor = MaterialTheme.colorScheme.primary, activeTrackColor = MaterialTheme.colorScheme.primary,
inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f) inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f)
) )
else -> SliderDefaults.colors( else -> SliderDefaults.colors(
thumbColor = Color(0xFFFFFFFF), thumbColor = Color(0xFFFFFFFF),
activeTrackColor = MaterialTheme.colorScheme.error, activeTrackColor = MaterialTheme.colorScheme.error,

View File

@@ -31,6 +31,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
@@ -52,6 +53,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -87,6 +89,18 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
val activity = (context as? Activity)!! val activity = (context as? Activity)!!
val exoPlayer: ExoPlayer = videoPlayerViewModel.player!! val exoPlayer: ExoPlayer = videoPlayerViewModel.player!!
val name by videoPlayerViewModel.currentName
val id by videoPlayerViewModel.currentId
val listState = rememberLazyListState()
val videos = videoPlayerViewModel.videos
LaunchedEffect(id, videos) {
val targetIndex = videos.indexOfFirst { it.id == id }
if (targetIndex >= 0) {
listState.scrollToItem(targetIndex)
}
}
val audioManager = remember { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager } val audioManager = remember { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager }
val maxVolume = remember { audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) } val maxVolume = remember { audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) }
var volFactor by remember { var volFactor by remember {
@@ -95,9 +109,6 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
) )
} }
val name by videoPlayerViewModel.currentName
val id by videoPlayerViewModel.currentId
fun setVolume(value: Int) { fun setVolume(value: Int) {
audioManager.setStreamVolume( audioManager.setStreamVolume(
AudioManager.STREAM_MUSIC, AudioManager.STREAM_MUSIC,
@@ -229,7 +240,9 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
} }
} }
}, },
modifier = Modifier.fillMaxWidth() modifier = Modifier
.align(Alignment.Center)
.fillMaxWidth()
) )
} }
@@ -588,7 +601,7 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
colors = CardDefaults.cardColors(containerColor = colorScheme.surface.copy(0.75f)) colors = CardDefaults.cardColors(containerColor = colorScheme.surface.copy(0.75f))
) )
{ {
LazyColumn(contentPadding = PaddingValues(vertical = 4.dp)) { LazyColumn(state = listState, contentPadding = PaddingValues(vertical = 4.dp)) {
items(videoPlayerViewModel.videos) { item -> items(videoPlayerViewModel.videos) { item ->
MiniPlaylistCard(Modifier.padding(4.dp), video = item, imageLoader = videoPlayerViewModel.imageLoader!!, MiniPlaylistCard(Modifier.padding(4.dp), video = item, imageLoader = videoPlayerViewModel.imageLoader!!,
selected = id == item.id, apiClient = videoPlayerViewModel.apiClient) selected = id == item.id, apiClient = videoPlayerViewModel.apiClient)

View File

@@ -260,7 +260,7 @@ fun VideoPlayerPortal(
playList.add("${i.klass}/${i.id}") playList.add("${i.klass}/${i.id}")
} }
val route = "video_player_route/${playList.joinToString(",").toHex()}" val route = "video_player_route/${(playList.joinToString(",") + "|${i.id}").toHex()}"
navController.navigate(route) navController.navigate(route)
} }
HorizontalDivider( HorizontalDivider(

View File

@@ -96,7 +96,7 @@ fun ComicGridView(
} }
LaunchedEffect(comicGridViewModel) { LaunchedEffect(comicGridViewModel) {
comicGridViewModel.coverHeight = screenHeight * 0.4f comicGridViewModel.coverHeight = screenHeight * 0.3f
if(comicGridViewModel.maxHeight == 0.dp) if(comicGridViewModel.maxHeight == 0.dp)
comicGridViewModel.maxHeight = screenHeight * 0.8f comicGridViewModel.maxHeight = screenHeight * 0.8f
} }
@@ -252,7 +252,7 @@ fun ComicGridView(
fontSize = 11.sp, fontSize = 11.sp,
lineHeight = 15.sp, lineHeight = 15.sp,
maxLines = 3, maxLines = 3,
modifier = Modifier.padding(horizontal = 16.dp).padding(bottom = 4.dp) modifier = Modifier.padding(horizontal = 16.dp).padding(bottom = 4.dp).align(Alignment.CenterStart)
) )
Button(onClick = { Button(onClick = {
@@ -379,10 +379,10 @@ fun ChapterCard(
{ {
Text( Text(
text = chapter.name, text = chapter.name,
fontSize = 14.sp, fontSize = 16.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
maxLines = 2, maxLines = 2,
lineHeight = 16.sp, lineHeight = 18.sp,
modifier = Modifier modifier = Modifier
.padding(horizontal = 8.dp).padding(vertical = 4.dp) .padding(horizontal = 8.dp).padding(vertical = 4.dp)
.background(Color.Transparent) .background(Color.Transparent)
@@ -391,7 +391,6 @@ fun ChapterCard(
text = "${comic.getChapterLength(chapter.page)} Pages", text = "${comic.getChapterLength(chapter.page)} Pages",
fontSize = 14.sp, fontSize = 14.sp,
lineHeight = 16.sp, lineHeight = 16.sp,
fontWeight = FontWeight.Bold,
maxLines = 1, maxLines = 1,
modifier = Modifier modifier = Modifier
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)

View File

@@ -243,7 +243,15 @@ fun ComicPageView(
{ {
val k = it.getPageChapterIndex(pagerState.currentPage) val k = it.getPageChapterIndex(pagerState.currentPage)
Column(Modifier Column(Modifier
.padding(bottom = 24.dp)) { .background(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Black.copy(alpha = 0.9f),
)
)
)) {
Spacer(Modifier.height(42.dp))
LazyRow( LazyRow(
horizontalArrangement = Arrangement.spacedBy(5.dp), horizontalArrangement = Arrangement.spacedBy(5.dp),
state = comicPageViewModel.listState!!, modifier = Modifier state = comicPageViewModel.listState!!, modifier = Modifier
@@ -266,7 +274,7 @@ fun ComicPageView(
pagerState.requestScrollToPage(page = r) pagerState.requestScrollToPage(page = r)
} }
) { ) {
Box(Modifier.padding(1.dp)) Box(Modifier.padding(0.dp))
{ {
AsyncImage( AsyncImage(
model = ImageRequest.Builder(LocalContext.current) model = ImageRequest.Builder(LocalContext.current)
@@ -321,6 +329,9 @@ fun ComicPageView(
} }
) )
Spacer(Modifier.height(24.dp))
} }
} }
} }

View File

@@ -1,5 +1,11 @@
package com.acitelight.aether.view.pages package com.acitelight.aether.view.pages
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -27,13 +33,21 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -122,6 +136,7 @@ fun VariableGrid(
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ComicScreen( fun ComicScreen(
navController: NavHostController, navController: NavHostController,
@@ -131,11 +146,17 @@ fun ComicScreen(
val state = rememberLazyStaggeredGridState() val state = rememberLazyStaggeredGridState()
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
var searchFilter by comicScreenViewModel.searchFilter var searchFilter by comicScreenViewModel.searchFilter
var isTagsVisible by remember { mutableStateOf(false) }
var sortType by remember { mutableIntStateOf(0) }
Column { Column(
modifier = Modifier.animateContentSize()
) {
Row(Modifier Row(Modifier
.padding(4.dp) .padding(horizontal = 8.dp).padding(top = 4.dp)
.align(Alignment.CenterHorizontally)) { .align(Alignment.CenterHorizontally)
)
{
Text( Text(
text = "Comics", text = "Comics",
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
@@ -149,7 +170,7 @@ fun ComicScreen(
.align(Alignment.CenterVertically) .align(Alignment.CenterVertically)
.height(36.dp) .height(36.dp)
.widthIn(max = 240.dp) .widthIn(max = 240.dp)
.background(colorScheme.primary, RoundedCornerShape(8.dp)) .background(colorScheme.surface, RoundedCornerShape(8.dp))
.padding(horizontal = 6.dp) .padding(horizontal = 6.dp)
) { ) {
Icon( Icon(
@@ -174,44 +195,115 @@ fun ComicScreen(
} }
} }
VariableGrid( Row(Modifier
modifier = Modifier .padding(horizontal = 8.dp)
.heightIn(max = 88.dp) .align(Alignment.CenterHorizontally)
.padding(4.dp),
rowHeight = 32.dp
) )
{ {
for (i in comicScreenViewModel.tags) { Text(
text = "Sorted by: ",
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
modifier = Modifier.padding(horizontal = 6.dp).align(Alignment.CenterVertically)
)
Box( RadioButton(
Modifier selected = (sortType == 0),
.background( onClick = { sortType = 0 },
if (included.contains(i)) Color.Green.copy(alpha = 0.65f) else colorScheme.surface, modifier = Modifier.align(Alignment.CenterVertically).size(24.dp)
shape = RoundedCornerShape(4.dp) )
) Text(
.height(32.dp).widthIn(max = 72.dp) text = "Id",
.clickable { fontWeight = FontWeight.Bold,
if (included.contains(i)) fontSize = 16.sp,
included.remove(i) modifier = Modifier.align(Alignment.CenterVertically).padding(3.dp)
else )
included.add(i) Spacer(modifier = Modifier.width(12.dp))
}
) { RadioButton(
Text( selected = (sortType == 1),
text = i, onClick = { sortType = 1 },
fontWeight = FontWeight.Bold, modifier = Modifier.align(Alignment.CenterVertically).size(24.dp)
fontSize = 16.sp, )
maxLines = 1, Text(
modifier = Modifier text = "Name",
.padding(2.dp) fontWeight = FontWeight.Bold,
.align(Alignment.Center) fontSize = 16.sp,
) modifier = Modifier.align(Alignment.CenterVertically).padding(3.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Spacer(Modifier.weight(1f))
Card(
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(containerColor = colorScheme.surface),
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(horizontal = 4.dp)
.padding(vertical = 4.dp)
.height(32.dp)
.width(64.dp),
onClick = {
isTagsVisible = !isTagsVisible
})
{
Row(Modifier.fillMaxSize())
{
Text(text = "Tags", fontWeight = FontWeight.Bold, fontSize = 16.sp, modifier = Modifier.align(Alignment.CenterVertically).padding(start = 5.dp))
ExposedDropdownMenuDefaults.TrailingIcon(expanded = isTagsVisible, modifier = Modifier.align(Alignment.CenterVertically).padding(end = 5.dp))
} }
} }
} }
HorizontalDivider(Modifier.padding(1.dp), thickness = 1.5.dp) HorizontalDivider(Modifier.padding(1.dp), thickness = 1.5.dp)
AnimatedVisibility(
visible = isTagsVisible,
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut()
) {
Column {
VariableGrid(
modifier = Modifier
.heightIn(max = 80.dp)
.padding(3.dp),
rowHeight = 30.dp
)
{
for (i in comicScreenViewModel.tags) {
Box(
Modifier
.background(
if (included.contains(i)) Color.Green.copy(alpha = 0.65f) else colorScheme.surface,
shape = RoundedCornerShape(4.dp)
)
.height(32.dp)
.widthIn(max = 72.dp)
.clickable {
if (included.contains(i))
included.remove(i)
else
included.add(i)
}
) {
Text(
text = i,
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
maxLines = 1,
modifier = Modifier
.padding(2.dp)
.align(Alignment.Center)
)
}
}
}
HorizontalDivider(Modifier.padding(1.dp), thickness = 1.5.dp)
}
}
LazyVerticalStaggeredGrid( LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Adaptive(120.dp), columns = StaggeredGridCells.Adaptive(120.dp),
contentPadding = PaddingValues(4.dp), contentPadding = PaddingValues(4.dp),
@@ -221,14 +313,25 @@ fun ComicScreen(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
items( items(
items = comicScreenViewModel.comics.filter { searchFilter.isEmpty() || searchFilter in it.comic.comic_name }.filter { x -> items = comicScreenViewModel.comics
included.all { y -> y in x.comic.tags } || included.isEmpty() .filter { searchFilter.isEmpty() || searchFilter in it.comic.comic_name }
}, .filter { x ->
included.all { y -> y in x.comic.tags } || included.isEmpty()
}
.sortedByDescending {
when(sortType)
{
0 -> it.id.toInt().toString().padStart(10, '0')
1 -> it.comic.comic_name
else -> it.id
}
},
key = { it.id } key = { it.id }
) { comic -> ) { comic ->
Box(modifier = Modifier Box(
.fillMaxWidth() modifier = Modifier
.wrapContentHeight() .fillMaxWidth()
.wrapContentHeight()
) { ) {
ComicCard(comic, navController, comicScreenViewModel) ComicCard(comic, navController, comicScreenViewModel)
} }

View File

@@ -0,0 +1,13 @@
package com.acitelight.aether.view.pages
import androidx.compose.runtime.Composable
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.acitelight.aether.viewModel.LiveScreenViewModel
@Composable
fun LiveScreen(
liveScreenViewModel: LiveScreenViewModel = hiltViewModel<LiveScreenViewModel>()
)
{
}

View File

@@ -15,6 +15,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Key import androidx.compose.material.icons.filled.Key
import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Textsms
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
@@ -41,6 +42,7 @@ fun MeScreen(meScreenViewModel: MeScreenViewModel = hiltViewModel<MeScreenViewMo
var privateKey by meScreenViewModel.privateKey var privateKey by meScreenViewModel.privateKey
var url by meScreenViewModel.url var url by meScreenViewModel.url
var cert by meScreenViewModel.cert var cert by meScreenViewModel.cert
var pak by meScreenViewModel.pak
val uss by meScreenViewModel.uss.collectAsState(initial = false) val uss by meScreenViewModel.uss.collectAsState(initial = false)
@@ -50,7 +52,8 @@ fun MeScreen(meScreenViewModel: MeScreenViewModel = hiltViewModel<MeScreenViewMo
.padding(8.dp), .padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top verticalArrangement = Arrangement.Top
) { )
{
// Card component for a clean, contained UI block // Card component for a clean, contained UI block
item{ item{
Card( Card(
@@ -196,6 +199,54 @@ fun MeScreen(meScreenViewModel: MeScreenViewModel = hiltViewModel<MeScreenViewMo
} }
} }
} }
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
)
{
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Toolbox",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier
.padding(bottom = 16.dp)
.align(Alignment.Start)
)
Spacer(modifier = Modifier.width(8.dp))
OutlinedTextField(
value = pak,
onValueChange = { pak = it },
label = { Text("Packet") },
leadingIcon = {
Icon(Icons.Default.Textsms, contentDescription = "Packet")
},
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
Row{
Button(
onClick = {
meScreenViewModel.sendPacket(pak)
},
modifier = Modifier.weight(0.5f).padding(8.dp)
) {
Text("Send")
}
}
}
}
} }
} }
} }

View File

@@ -1,36 +1,50 @@
package com.acitelight.aether.view.pages package com.acitelight.aether.view.pages
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.DividerDefaults import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.acitelight.aether.model.VideoDownloadItemState import com.acitelight.aether.model.VideoDownloadItemState
import com.acitelight.aether.view.components.BiliMiniSlider import com.acitelight.aether.view.components.BiliMiniSlider
import com.acitelight.aether.view.components.VideoDownloadCardMini import com.acitelight.aether.view.components.VideoDownloadCard
import com.acitelight.aether.viewModel.TransmissionScreenViewModel import com.acitelight.aether.viewModel.TransmissionScreenViewModel
import com.tonyodev.fetch2.Status import com.tonyodev.fetch2.Status
import kotlin.collections.sortedWith import java.io.File
import kotlin.comparisons.compareBy
import kotlin.comparisons.naturalOrder
@Composable @Composable
fun TransmissionScreen( fun TransmissionScreen(
navigator: NavHostController, navigator: NavHostController,
transmissionScreenViewModel: TransmissionScreenViewModel = hiltViewModel<TransmissionScreenViewModel>() transmissionScreenViewModel: TransmissionScreenViewModel = hiltViewModel<TransmissionScreenViewModel>()
) { ) {
var mutiSelection by transmissionScreenViewModel.mutiSelection
val downloads = transmissionScreenViewModel.downloads val downloads = transmissionScreenViewModel.downloads
Column()
Column(modifier = Modifier.animateContentSize())
{ {
Text( Text(
text = "Video Tasks", text = "Video Tasks",
@@ -73,55 +87,57 @@ fun TransmissionScreen(
} }
) )
HorizontalDivider(Modifier.padding(8.dp), 2.dp, DividerDefaults.color) AnimatedVisibility(
visible = mutiSelection,
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut()
){
Column(Modifier.fillMaxWidth()) {
Button(onClick = {
mutiSelection = false
transmissionScreenViewModel.mutiSelectionList.forEach {
item ->
val klass = item.split("/").first()
val id = item.split("/").last()
for (i in downloadToGroup(
klass, id,
downloads
)) transmissionScreenViewModel.delete(i.id)
File(
transmissionScreenViewModel.context.getExternalFilesDir(null),
"videos/${klass}/${id}/summary.json"
).delete()
}
}, modifier = Modifier.align(Alignment.End).padding(horizontal = 8.dp))
{
Text(text = "Delete", fontWeight = FontWeight.Bold)
}
}
}
HorizontalDivider(Modifier.padding(horizontal = 8.dp).padding(vertical = 4.dp), 2.dp, DividerDefaults.color)
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
{ {
items( items(
downloads items = downloads
.filter { it.type == "main" } .filter { it.type == "main" }
.sortedWith(compareBy(naturalOrder()) { it.fileName }) .sortedBy { it.status == Status.COMPLETED }
.sortedBy { it.status == Status.COMPLETED }, key = { it.id }) .groupBy { it.group }.map { it.value }
, key = { it.first().id }
)
{ item -> { item ->
VideoDownloadCardMini( VideoDownloadCard(
navigator = navigator, navigator = navigator,
viewModel = transmissionScreenViewModel, viewModel = transmissionScreenViewModel,
model = item, models = item.sortedWith(compareBy(naturalOrder()) { it.fileName })
onPause = {
for (i in downloadToGroup(
item,
downloads
)) transmissionScreenViewModel.pause(i.id)
},
onResume = {
for (i in downloadToGroup(
item,
downloads
)) transmissionScreenViewModel.resume(i.id)
},
onCancel = {
for (i in downloadToGroup(
item,
downloads
)) transmissionScreenViewModel.delete(i.id)
},
onDelete = {
for (i in downloadToGroup(
item,
downloads
)) transmissionScreenViewModel.delete(i.id)
},
onRetry = {
for (i in downloadToGroup(
item,
downloads
)) transmissionScreenViewModel.retry(i.id)
}
) )
HorizontalDivider( HorizontalDivider(
Modifier.padding(horizontal = 16.dp, vertical = 6.dp), Modifier.padding(horizontal = 16.dp, vertical = 3.dp),
2.dp, 2.dp,
DividerDefaults.color DividerDefaults.color
) )
@@ -136,3 +152,10 @@ fun downloadToGroup(
): List<VideoDownloadItemState> { ): List<VideoDownloadItemState> {
return downloads.filter { it.vid == i.vid && it.klass == i.klass } return downloads.filter { it.vid == i.vid && it.klass == i.klass }
} }
fun downloadToGroup(
klass: String, id: String,
downloads: List<VideoDownloadItemState>
): List<VideoDownloadItemState> {
return downloads.filter { it.vid == id && it.klass == klass }
}

View File

@@ -1,19 +1,13 @@
package com.acitelight.aether.view.pages package com.acitelight.aether.view.pages
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -23,8 +17,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.items import androidx.compose.foundation.lazy.staggeredgrid.items
@@ -32,48 +24,41 @@ import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridS
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SecondaryScrollableTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.acitelight.aether.model.Video import com.acitelight.aether.model.Video
import com.acitelight.aether.viewModel.VideoScreenViewModel import com.acitelight.aether.viewModel.VideoScreenViewModel
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import coil3.request.ImageRequest
import com.acitelight.aether.CardPage import com.acitelight.aether.CardPage
import com.acitelight.aether.Global.updateRelate
import com.acitelight.aether.view.components.VideoCard import com.acitelight.aether.view.components.VideoCard
import kotlinx.coroutines.launch
import java.nio.charset.Charset import java.nio.charset.Charset
import kotlin.collections.sortedWith import kotlin.collections.sortedWith
fun videoToView(v: List<Video>): Map<String?, List<Video>> fun videoToGroup(v: List<Video>): Map<String?, List<Video>> {
{ return v.map {
return v.map { if(it.video.group != null) it else Video(id=it.id, isLocal = it.isLocal, localBase = it.localBase, if (it.video.group != null) it else Video(
klass = it.klass, video = it.video.copy(group = it.video.name)) }.groupBy { it.video.group } id = it.id, isLocal = it.isLocal, localBase = it.localBase,
klass = it.klass, video = it.video.copy(group = it.video.name)
)
}.groupBy { it.video.group }
} }
fun String.toHex(): String { fun String.toHex(): String {
@@ -102,12 +87,13 @@ fun VideoScreen(
var menuVisibility by videoScreenViewModel.menuVisibility var menuVisibility by videoScreenViewModel.menuVisibility
var searchFilter by videoScreenViewModel.searchFilter var searchFilter by videoScreenViewModel.searchFilter
var doneInit by videoScreenViewModel.doneInit var doneInit by videoScreenViewModel.doneInit
val vb = videoToView(videoScreenViewModel.videoLibrary.classesMap.getOrDefault( val vb = videoToGroup(
videoScreenViewModel.videoLibrary.classes.getOrNull( videoScreenViewModel.videoLibrary.classesMap.getOrDefault(
tabIndex videoScreenViewModel.videoLibrary.classes.getOrNull(
), listOf() tabIndex
).filter { it.video.name.contains(searchFilter) }).filter { it.key != null } ), listOf()
.map{ i -> Pair(i.key!!, i.value.sortedWith(compareBy(naturalOrder()) { it.video.name }) ) } ).filter { it.video.name.contains(searchFilter) }).filter { it.key != null }
.map { i -> Pair(i.key!!, i.value.sortedWith(compareBy(naturalOrder()) { it.video.name })) }
.toList() .toList()
if (doneInit) if (doneInit)
@@ -117,75 +103,31 @@ fun VideoScreen(
Column( Column(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
Text( Row(
text = "Videos", Modifier
style = MaterialTheme.typography.headlineMedium, .padding(horizontal = 8.dp)
modifier = Modifier .padding(top = 4.dp)
.padding(horizontal = 8.dp) .align(Alignment.CenterHorizontally)
.align(Alignment.Start)
) )
// TopRow(videoScreenViewModel);
Row(Modifier.padding(bottom = 4.dp).padding(start = 8.dp))
{ {
Card( Text(
shape = RoundedCornerShape(8.dp), text = "Videos",
colors = CardDefaults.cardColors(containerColor = colorScheme.primary), style = MaterialTheme.typography.headlineMedium,
modifier = Modifier modifier = Modifier
.padding(horizontal = 8.dp)
.align(Alignment.CenterVertically) .align(Alignment.CenterVertically)
.padding(horizontal = 1.dp) )
.size(36.dp),
onClick = {
menuVisibility = !menuVisibility
})
{
Box(Modifier.fillMaxSize())
{
Icon(
modifier = Modifier
.size(30.dp)
.align(Alignment.Center),
imageVector = Icons.Default.Menu,
contentDescription = "Catalogue"
)
}
}
Card( Spacer(Modifier.weight(1f))
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(containerColor = colorScheme.primary),
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(horizontal = 1.dp)
.height(36.dp),
onClick = {
menuVisibility = !menuVisibility
})
{
Box(Modifier.fillMaxHeight())
{
Text(
text = videoScreenViewModel.videoLibrary.classes.getOrNull(
tabIndex
)
?: "",
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier
.align(Alignment.CenterStart)
.padding(horizontal = 8.dp),
maxLines = 1
)
}
}
Row( Row(
modifier = Modifier modifier = Modifier
.height(36.dp) .height(36.dp)
.widthIn(max = 240.dp) .widthIn(max = 240.dp)
.background(colorScheme.primary, RoundedCornerShape(8.dp)) .background(colorScheme.surface, RoundedCornerShape(8.dp))
.padding(horizontal = 6.dp) .padding(horizontal = 6.dp)
) { )
{
Icon( Icon(
modifier = Modifier modifier = Modifier
.size(30.dp) .size(30.dp)
@@ -207,11 +149,9 @@ fun VideoScreen(
) )
} }
} }
HorizontalDivider(
Modifier.padding(4.dp), TopRow(videoScreenViewModel)
2.dp,
DividerDefaults.color
)
LazyVerticalStaggeredGrid( LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Adaptive(160.dp), columns = StaggeredGridCells.Adaptive(160.dp),
contentPadding = PaddingValues(8.dp), contentPadding = PaddingValues(8.dp),
@@ -231,52 +171,47 @@ fun VideoScreen(
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight()
) { ) {
if(video.second.isNotEmpty()) if (video.second.isNotEmpty())
VideoCard(video.second, navController, videoScreenViewModel) VideoCard(video.second, navController, videoScreenViewModel)
} }
} }
} }
} }
AnimatedVisibility(
visible = menuVisibility,
enter = slideInHorizontally(initialOffsetX = { full -> full }),
exit = slideOutHorizontally(targetOffsetX = { full -> full }),
modifier = Modifier.align(Alignment.CenterEnd)
) {
Card(
Modifier
.fillMaxHeight()
.width(250.dp)
.align(Alignment.CenterEnd),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(containerColor = colorScheme.surface)
)
{
LazyColumn {
items(videoScreenViewModel.videoLibrary.classes) { item ->
CatalogueItemRow(
item = Pair(
videoScreenViewModel.videoLibrary.classes.indexOf(item),
item
),
onItemClick = {
menuVisibility = false
videoScreenViewModel.setTabIndex(
videoScreenViewModel.videoLibrary.classes.indexOf(
item
)
)
}
)
}
}
}
}
} }
} }
} }
@Composable
fun TopRow(videoScreenViewModel: VideoScreenViewModel) {
val tabIndex by videoScreenViewModel.tabIndex;
if (videoScreenViewModel.videoLibrary.classes.isEmpty()) return
val colorScheme = MaterialTheme.colorScheme
SecondaryScrollableTabRow(
selectedTabIndex = tabIndex,
modifier = Modifier
.background(Color.Transparent)
.padding(vertical = 4.dp)
) {
videoScreenViewModel.videoLibrary.classes.forEachIndexed { index, title ->
Tab(
modifier = Modifier.height(42.dp),
selected = tabIndex == index,
onClick = { videoScreenViewModel.setTabIndex(index) },
text = {
Text(
text = title,
maxLines = 1,
fontWeight = FontWeight.Bold,
lineHeight = 16.sp,
fontSize = 14.sp
)
},
)
}
}
}
@Composable @Composable
fun CatalogueItemRow( fun CatalogueItemRow(
item: Pair<Int, String>, item: Pair<Int, String>,

View File

@@ -61,7 +61,7 @@ class ComicScreenViewModel @Inject constructor(
val m = mediaManager.queryComicInfoBulk(l) val m = mediaManager.queryComicInfoBulk(l)
if(m != null) { if(m != null) {
comics.addAll(m.sortedWith(compareBy(naturalOrder()) { it.comic.comic_name })) comics.addAll(m.sortedBy { it.id.toInt() }.reversed())
tags.addAll(m.flatMap { it.comic.tags }.groupingBy { it }.eachCount() tags.addAll(m.flatMap { it.comic.tags }.groupingBy { it }.eachCount()
.entries.sortedByDescending { it.value } .entries.sortedByDescending { it.value }
.map { it.key }) .map { it.key })

View File

@@ -0,0 +1,14 @@
package com.acitelight.aether.viewModel
import androidx.lifecycle.ViewModel
import com.acitelight.aether.service.ApiClient
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class LiveScreenViewModel @Inject constructor(
val apiClient: ApiClient
) : ViewModel(){
}

View File

@@ -18,6 +18,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetAddress
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@@ -33,6 +36,7 @@ class MeScreenViewModel @Inject constructor(
val privateKey = mutableStateOf("") val privateKey = mutableStateOf("")
val url = mutableStateOf("") val url = mutableStateOf("")
val cert = mutableStateOf("") val cert = mutableStateOf("")
val pak = mutableStateOf("")
val uss = settingsDataStoreManager.useSelfSignedFlow val uss = settingsDataStoreManager.useSelfSignedFlow
@@ -108,7 +112,8 @@ class MeScreenViewModel @Inject constructor(
} }
} }
fun updateAccount(u: String, p: String) { fun updateAccount(u: String, p: String)
{
viewModelScope.launch { viewModelScope.launch {
settingsDataStoreManager.saveUserName(u) settingsDataStoreManager.saveUserName(u)
settingsDataStoreManager.savePrivateKey(p) settingsDataStoreManager.savePrivateKey(p)
@@ -142,4 +147,22 @@ class MeScreenViewModel @Inject constructor(
} }
} }
} }
fun sendPacket(p: String)
{
val b = (p + "\r\n").toByteArray(Charsets.UTF_8)
viewModelScope.launch {
withContext(Dispatchers.IO) {
val addr = InetAddress.getByName(apiClient.getDomain())
val socket = DatagramSocket()
val packet = DatagramPacket(
b, b.size, addr, 4096
)
socket.send(packet)
}
}
}
} }

View File

@@ -1,7 +1,10 @@
package com.acitelight.aether.viewModel package com.acitelight.aether.viewModel
import android.content.Context import android.content.Context
import android.widget.Toast
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@@ -42,11 +45,9 @@ class TransmissionScreenViewModel @Inject constructor(
// map id -> state object reference (no index bookkeeping) // map id -> state object reference (no index bookkeeping)
private val idToState: MutableMap<Int, VideoDownloadItemState> = mutableMapOf() private val idToState: MutableMap<Int, VideoDownloadItemState> = mutableMapOf()
var mutiSelection = mutableStateOf(false)
fun modelToVideo(model: VideoDownloadItemState): Video? { var mutiSelectionList = mutableStateListOf<String>()
val fv = videoLibrary.classesMap.map { it.value }.flatten() var groupExpandMap = mutableStateMapOf<String, Boolean>()
return fv.firstOrNull { it.klass == model.klass && it.id == model.vid }
}
private val fetchListener = object : FetchListener { private val fetchListener = object : FetchListener {
override fun onAdded(download: Download) { override fun onAdded(download: Download) {
@@ -79,18 +80,18 @@ class TransmissionScreenViewModel @Inject constructor(
handleUpsert(download) handleUpsert(download)
if (download.extras.getString("type", "") == "main") { if (download.extras.getString("type", "") == "main") {
val ii = videoLibrary.classesMap[download.extras.getString( val klass = download.extras.getString("class", "")
"class", val ii = videoLibrary.classesMap[klass]?.indexOfFirst {
"" it.id == download.extras.getString(
)]?.indexOfFirst { it.id == download.extras.getString("id", "") } "id",
""
)
}
if (ii != null) { if (ii != null) {
val newi = val newi =
videoLibrary.classesMap[download.extras.getString("class", "")]?.get(ii) videoLibrary.classesMap[klass]?.get(ii)
if (newi != null) videoLibrary.classesMap[download.extras.getString( if (newi != null) videoLibrary.classesMap[klass]?.set(
"class",
""
)]?.set(
ii, newi.toLocal(context.getExternalFilesDir(null)!!.path) ii, newi.toLocal(context.getExternalFilesDir(null)!!.path)
) )
} }
@@ -107,6 +108,28 @@ class TransmissionScreenViewModel @Inject constructor(
override fun onDeleted(download: Download) { override fun onDeleted(download: Download) {
handleRemove(download.id) handleRemove(download.id)
if (download.extras.getString("type", "") == "main") {
viewModelScope.launch {
val klass = download.extras.getString("class", "")
val ii = videoLibrary.classesMap[klass]?.indexOfFirst {
it.id == download.extras.getString(
"id",
""
)
}
if (ii != null) {
val v = mediaManager.queryVideo(klass, download.extras.getString("id", ""))
if (v != null) {
val newi = videoLibrary.classesMap[klass]?.get(ii)
if (newi != null) videoLibrary.classesMap[klass]?.set(
ii, v
)
}
}
}
}
} }
override fun onDownloadBlockUpdated( override fun onDownloadBlockUpdated(
@@ -203,7 +226,8 @@ class TransmissionScreenViewModel @Inject constructor(
totalBytes = download.total, totalBytes = download.total,
klass = download.extras.getString("class", ""), klass = download.extras.getString("class", ""),
vid = download.extras.getString("id", ""), vid = download.extras.getString("id", ""),
type = download.extras.getString("type", "") type = download.extras.getString("type", ""),
group = download.extras.getString("group", "")
) )
} }
@@ -223,8 +247,7 @@ class TransmissionScreenViewModel @Inject constructor(
fetchManager.removeListener() fetchManager.removeListener()
} }
suspend fun playStart(model: VideoDownloadItemState, navigator: NavHostController) suspend fun playStart(model: VideoDownloadItemState, navigator: NavHostController) {
{
val downloaded = fetchManager.getAllDownloadsAsync().filter { val downloaded = fetchManager.getAllDownloadsAsync().filter {
it.status == Status.COMPLETED && it.extras.getString( it.status == Status.COMPLETED && it.extras.getString(
"class", "class",
@@ -261,7 +284,8 @@ class TransmissionScreenViewModel @Inject constructor(
val video = fv.firstOrNull { it.klass == model.klass && it.id == model.vid } val video = fv.firstOrNull { it.klass == model.klass && it.id == model.vid }
if (video != null) { if (video != null) {
val group = fv.filter { it.klass == video.klass && it.video.group == video.video.group && it.video.group != "null" } val group =
fv.filter { it.klass == video.klass && it.video.group == video.video.group && it.video.group != "null" }
for (i in group.sortedWith(compareBy(naturalOrder()) { it.video.name })) { for (i in group.sortedWith(compareBy(naturalOrder()) { it.video.name })) {
playList.add("${i.klass}/${i.id}") playList.add("${i.klass}/${i.id}")
} }