[update] ui patch 2

This commit is contained in:
rootacite
2025-11-01 21:53:20 +08:00
parent 512da7be1c
commit 78d6564964
6 changed files with 436 additions and 334 deletions

View File

@@ -13,6 +13,12 @@ class Video(
val id: String,
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 {
return if (isLocal)
"$localBase/videos/$klass/$id/cover.jpg"

View File

@@ -6,6 +6,7 @@ import com.tonyodev.fetch2.Download
import com.tonyodev.fetch2.Fetch
import com.tonyodev.fetch2.FetchConfiguration
import com.tonyodev.fetch2.FetchListener
import com.tonyodev.fetch2.Priority
import com.tonyodev.fetch2.Request
import com.tonyodev.fetch2core.Extras
import com.tonyodev.fetch2okhttp.OkHttpDownloader
@@ -128,6 +129,7 @@ class FetchManager @Inject constructor(
val requests = mutableListOf(
Request(video.getVideo(apiClient), videoPath.path).apply {
priority = Priority.LOW
extras = Extras(
mapOf(
"name" to video.video.name,
@@ -139,6 +141,7 @@ class FetchManager @Inject constructor(
)
},
Request(video.getCover(apiClient), coverPath.path).apply {
priority = Priority.HIGH
extras = Extras(
mapOf(
"name" to video.video.name,

View File

@@ -1,271 +1,293 @@
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.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.height
import androidx.compose.foundation.layout.heightIn
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.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.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
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.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import com.acitelight.aether.Global.updateRelate
import com.acitelight.aether.model.Video
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.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 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
fun VideoDownloadCard(
navigator: NavHostController,
viewModel: TransmissionScreenViewModel,
model: VideoDownloadItemState,
onPause: () -> Unit,
onResume: () -> Unit,
onCancel: () -> Unit,
onDelete: () -> Unit,
onRetry: () -> Unit
models: List<VideoDownloadItemState>
) {
Card(
shape = RoundedCornerShape(8.dp),
elevation = CardDefaults.cardElevation(4.dp),
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 item = models.first()
var mutiSelection by viewModel.mutiSelection
val colorScheme = MaterialTheme.colorScheme
val jsonQuery = downloaded.map {
File(
viewModel.context.getExternalFilesDir(null),
"videos/${
it.extras.getString(
"class",
""
)
}/${it.extras.getString("id", "")}/summary.json"
).readText()
}
.map {
Json.decodeFromString<Video>(it)
.toLocal(viewModel.context.getExternalFilesDir(null)!!.path)
}
if (models.size == 1) {
VideoDownloadCardMiniPack(
navigator = navigator,
viewModel = viewModel,
model = item
)
} else if (models.size > 1) {
val imageModel = if (item.status == Status.COMPLETED) {
ImageRequest.Builder(LocalContext.current)
.data(
File(
viewModel.context.getExternalFilesDir(null),
"videos/${item.klass}/${item.vid}/cover.jpg"
)
)
.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(
jsonQuery,
jsonQuery.first { it.id == model.vid && it.klass == model.klass }
)
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(
Card(
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.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(
verticalAlignment = Alignment.CenterVertically,
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)
modifier = Modifier.fillMaxSize()
)
{
Card(
shape = RoundedCornerShape(8.dp),
modifier = Modifier.align(Alignment.CenterStart)
) {
val video = viewModel.modelToVideo(model)
Box(
Modifier
.fillMaxHeight()
)
{
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)
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(
File(
viewModel.context.getExternalFilesDir(null),
"videos/${model.klass}/${model.vid}/cover.jpg"
)
)
.memoryCacheKey("${model.klass}/${model.vid}/cover")
.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!!
)
Box(Modifier
.padding(4.dp)
.background(Color.Black.copy(0.4f), shape = RoundedCornerShape(6.dp))
.align(Alignment.BottomEnd)
.padding(2.dp))
{
Text(text = "${models.size} Videos",
fontWeight = FontWeight.Bold,
fontSize = 12.sp,
lineHeight = 13.5.sp)
}
}
Column(Modifier.align(Alignment.BottomEnd)) {
Box(
Modifier
.fillMaxSize()
.padding(all = 4.dp)
.padding(end = 4.dp)
)
{
Text(
text = "${model.progress.coerceIn(0, 100)}%",
modifier = Modifier
.padding(start = 8.dp)
.align(Alignment.End)
text = models.first().group,
lineHeight = 14.sp,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
maxLines = 2,
modifier = Modifier.align(Alignment.TopEnd)
)
Text(
modifier = Modifier
.padding(start = 8.dp)
.align(Alignment.End),
text = "%.2f MB/%.2f MB".format(
model.downloadedBytes / (1024.0 * 1024.0),
model.totalBytes / (1024.0 * 1024.0)
),
fontSize = 10.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
)
Column(Modifier.align(Alignment.BottomEnd)) {
Row(
Modifier
.align(Alignment.End)
.padding(vertical = 2.dp)
) {
Text(
modifier = Modifier,
text = "%.2f MB/%.2f MB".format(
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)
)
)
}
}
}
}
// progress bar
LinearProgressIndicator(
progress = { abs(model.progress).coerceIn(0, 100) / 100f },
modifier = Modifier
AnimatedVisibility(
visible = viewModel.groupExpandMap.getOrDefault(item.group, false) || mutiSelection,
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut()
)
{
Column(
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()
.heightIn(max = 300.dp)
) {
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"
HorizontalDivider(
Modifier.padding(horizontal = 16.dp, vertical = 3.dp),
2.dp,
DividerDefaults.color
)
LazyColumn(
Modifier
.padding(horizontal = 16.dp)
.background(colorScheme.surface, shape = RoundedCornerShape(6.dp))
)
{
items(
items = models,
key = { it.id }
) { single ->
Box(Modifier.padding(vertical = 4.dp))
{
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
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.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
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.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.platform.LocalContext
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.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import com.acitelight.aether.model.Video
import com.acitelight.aether.model.VideoDownloadItemState
import com.acitelight.aether.viewModel.TransmissionScreenViewModel
import com.tonyodev.fetch2.Status
@@ -53,29 +61,31 @@ fun VideoDownloadCardMini(
model: VideoDownloadItemState,
onPause: () -> Unit,
onResume: () -> Unit,
onCancel: () -> Unit,
onDelete: () -> Unit,
onRetry: () -> Unit
onRetry: () -> Unit,
imageHeight: Dp = 85.dp,
imageMaxWidth: Dp = 140.dp
) {
val video = viewModel.modelToVideo(model)
val imageModel =
if (video == null)
ImageRequest.Builder(LocalContext.current)
.data(
File(
viewModel.context.getExternalFilesDir(null),
"videos/${model.klass}/${model.vid}/cover.jpg"
)
var mutiSelection by viewModel.mutiSelection
val imageModel = if (model.status == Status.COMPLETED)
{
ImageRequest.Builder(LocalContext.current)
.data(
File(
viewModel.context.getExternalFilesDir(null),
"videos/${model.klass}/${model.vid}/cover.jpg"
)
.memoryCacheKey("${model.klass}/${model.vid}/cover")
.diskCacheKey("${model.klass}/${model.vid}/cover")
.build()
else
ImageRequest.Builder(LocalContext.current)
.data(video.getCover(viewModel.apiClient))
.memoryCacheKey("${model.klass}/${model.vid}/cover")
.diskCacheKey("${model.klass}/${model.vid}/cover")
.build()
)
.memoryCacheKey("${model.klass}/${model.vid}/cover")
.diskCacheKey("${model.klass}/${model.vid}/cover")
.build()
} else
{
ImageRequest.Builder(LocalContext.current)
.data(Video.getCoverStatic(viewModel.apiClient, model.klass, model.vid))
.memoryCacheKey("${model.klass}/${model.vid}/cover")
.diskCacheKey("${model.klass}/${model.vid}/cover")
.build()
}
Card(
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
@@ -84,54 +94,70 @@ fun VideoDownloadCardMini(
.fillMaxWidth()
.padding(horizontal = 4.dp)
.background(Color.Transparent)
.clickable(onClick = {
when (model.status) {
Status.COMPLETED -> viewModel.viewModelScope.launch(Dispatchers.IO)
{
viewModel.playStart(model, navigator)
.combinedClickable(
onClick = {
if (!mutiSelection)
when (model.status) {
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()
Status.ADDED, Status.FAILED, Status.CANCELLED -> onRetry()
else -> {}
},
onLongClick = {
mutiSelection = !mutiSelection
}
})
.height(100.dp)
)
.height(imageHeight)
) {
Row(
modifier = Modifier.fillMaxSize()
)
{
Box(Modifier
.fillMaxHeight())
Row(
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(
model = imageModel,
contentDescription = null,
modifier = Modifier
.height(100.dp)
.height(imageHeight)
.clip(RoundedCornerShape(8.dp))
.widthIn(max = 150.dp)
.widthIn(max = imageMaxWidth)
.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(
@@ -165,9 +191,11 @@ fun VideoDownloadCardMini(
maxLines = 1,
)
Row(Modifier
.align(Alignment.End)
.padding(vertical = 2.dp)) {
Row(
Modifier
.align(Alignment.End)
.padding(vertical = 2.dp)
) {
Text(
modifier = Modifier,
text = "%.2f MB/%.2f MB".format(
@@ -198,23 +226,25 @@ fun VideoDownloadCardMini(
onValueChange = {
},
colors = when(model.status)
{
colors = when (model.status) {
Status.DOWNLOADING, Status.QUEUED, Status.ADDED -> SliderDefaults.colors(
thumbColor = Color(0xFFFFFFFF),
activeTrackColor = MaterialTheme.colorScheme.primary,
inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f)
)
Status.PAUSED -> SliderDefaults.colors(
thumbColor = Color(0xFFFFFFFF),
activeTrackColor = Color(0xFFFFA500),
inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f)
)
Status.COMPLETED -> SliderDefaults.colors(
thumbColor = Color(0xFFFFFFFF),
activeTrackColor = MaterialTheme.colorScheme.primary,
inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f)
)
else -> SliderDefaults.colors(
thumbColor = Color(0xFFFFFFFF),
activeTrackColor = MaterialTheme.colorScheme.error,

View File

@@ -1,37 +1,50 @@
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.acitelight.aether.model.VideoDownloadItemState
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.tonyodev.fetch2.Status
import java.io.File
import kotlin.collections.sortedWith
import kotlin.comparisons.compareBy
import kotlin.comparisons.naturalOrder
@Composable
fun TransmissionScreen(
navigator: NavHostController,
transmissionScreenViewModel: TransmissionScreenViewModel = hiltViewModel<TransmissionScreenViewModel>()
) {
var mutiSelection by transmissionScreenViewModel.mutiSelection
val downloads = transmissionScreenViewModel.downloads
Column()
Column(modifier = Modifier.animateContentSize())
{
Text(
text = "Video Tasks",
@@ -74,6 +87,36 @@ fun TransmissionScreen(
}
)
AnimatedVisibility(
visible = mutiSelection,
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut()
){
Column {
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(8.dp), 2.dp, DividerDefaults.color)
LazyColumn(
@@ -81,52 +124,20 @@ fun TransmissionScreen(
)
{
items(
downloads
items = downloads
.filter { it.type == "main" }
.sortedBy { it.status == Status.COMPLETED }, key = { it.id })
.sortedBy { it.status == Status.COMPLETED }
.groupBy { it.group }.map { it.value }
, key = { it.first().id }
)
{ item ->
VideoDownloadCardMini(
VideoDownloadCard(
navigator = navigator,
viewModel = transmissionScreenViewModel,
model = item,
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)
File(
transmissionScreenViewModel.context.getExternalFilesDir(null),
"videos/${item.klass}/${item.vid}/summary.json"
).delete()
},
onRetry = {
for (i in downloadToGroup(
item,
downloads
)) transmissionScreenViewModel.retry(i.id)
}
models = item.sortedWith(compareBy(naturalOrder()) { it.fileName })
)
HorizontalDivider(
Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
Modifier.padding(horizontal = 16.dp, vertical = 3.dp),
2.dp,
DividerDefaults.color
)
@@ -141,3 +152,10 @@ fun downloadToGroup(
): List<VideoDownloadItemState> {
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,7 +1,10 @@
package com.acitelight.aether.viewModel
import android.content.Context
import android.widget.Toast
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -42,11 +45,9 @@ class TransmissionScreenViewModel @Inject constructor(
// map id -> state object reference (no index bookkeeping)
private val idToState: MutableMap<Int, VideoDownloadItemState> = mutableMapOf()
fun modelToVideo(model: VideoDownloadItemState): Video? {
val fv = videoLibrary.classesMap.map { it.value }.flatten()
return fv.firstOrNull { it.klass == model.klass && it.id == model.vid }
}
var mutiSelection = mutableStateOf(false)
var mutiSelectionList = mutableStateListOf<String>()
var groupExpandMap = mutableStateMapOf<String, Boolean>()
private val fetchListener = object : FetchListener {
override fun onAdded(download: Download) {
@@ -79,18 +80,18 @@ class TransmissionScreenViewModel @Inject constructor(
handleUpsert(download)
if (download.extras.getString("type", "") == "main") {
val ii = videoLibrary.classesMap[download.extras.getString(
"class",
""
)]?.indexOfFirst { it.id == download.extras.getString("id", "") }
val klass = download.extras.getString("class", "")
val ii = videoLibrary.classesMap[klass]?.indexOfFirst {
it.id == download.extras.getString(
"id",
""
)
}
if (ii != null) {
val newi =
videoLibrary.classesMap[download.extras.getString("class", "")]?.get(ii)
if (newi != null) videoLibrary.classesMap[download.extras.getString(
"class",
""
)]?.set(
videoLibrary.classesMap[klass]?.get(ii)
if (newi != null) videoLibrary.classesMap[klass]?.set(
ii, newi.toLocal(context.getExternalFilesDir(null)!!.path)
)
}
@@ -107,6 +108,28 @@ class TransmissionScreenViewModel @Inject constructor(
override fun onDeleted(download: Download) {
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(
@@ -224,8 +247,7 @@ class TransmissionScreenViewModel @Inject constructor(
fetchManager.removeListener()
}
suspend fun playStart(model: VideoDownloadItemState, navigator: NavHostController)
{
suspend fun playStart(model: VideoDownloadItemState, navigator: NavHostController) {
val downloaded = fetchManager.getAllDownloadsAsync().filter {
it.status == Status.COMPLETED && it.extras.getString(
"class",
@@ -262,7 +284,8 @@ class TransmissionScreenViewModel @Inject constructor(
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 && 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 })) {
playList.add("${i.klass}/${i.id}")
}