[update] ui patch 2
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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}")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user