[feat] Better Transmission UI
This commit is contained in:
@@ -231,6 +231,7 @@ class ApiClient @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (selectedUrl == null) {
|
if (selectedUrl == null) {
|
||||||
|
client = createOkHttp()
|
||||||
throw Exception("No reachable URL found")
|
throw Exception("No reachable URL found")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,11 +243,6 @@ class ApiClient @Inject constructor(
|
|||||||
(context as AetherApp).abyssService?.proxy?.config(getBase().toUri().host!!, 4096)
|
(context as AetherApp).abyssService?.proxy?.config(getBase().toUri().host!!, 4096)
|
||||||
}
|
}
|
||||||
api = createRetrofit().create(ApiInterface::class.java)
|
api = createRetrofit().create(ApiInterface::class.java)
|
||||||
|
|
||||||
Log.i("Delay Analyze", "Start Abyss Hello")
|
|
||||||
val h = api!!.hello()
|
|
||||||
Log.i("Delay Analyze", "Abyss Hello: ${h.string()}")
|
|
||||||
|
|
||||||
return base
|
return base
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
api = null
|
api = null
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ class FetchManager @Inject constructor(
|
|||||||
fetch?.resume(id)
|
fetch?.resume(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun retry(id: Int) {
|
||||||
|
fetch?.retry(id)
|
||||||
|
}
|
||||||
|
|
||||||
fun cancel(id: Int) {
|
fun cancel(id: Int) {
|
||||||
fetch?.cancel(id)
|
fetch?.cancel(id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.SliderColors
|
||||||
import androidx.compose.material3.SliderDefaults
|
import androidx.compose.material3.SliderDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -63,9 +64,13 @@ fun BiliMiniSlider(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
value: Float,
|
value: Float,
|
||||||
onValueChange: (Float) -> Unit,
|
onValueChange: (Float) -> Unit,
|
||||||
valueRange: ClosedFloatingPointRange<Float> = 0f..1f
|
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
|
||||||
|
colors: SliderColors = SliderDefaults.colors(
|
||||||
|
thumbColor = Color(0xFFFFFFFF),
|
||||||
|
activeTrackColor = MaterialTheme.colorScheme.primary,
|
||||||
|
inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
|
||||||
val trackHeight = 3.dp
|
val trackHeight = 3.dp
|
||||||
|
|
||||||
Slider(
|
Slider(
|
||||||
@@ -73,11 +78,8 @@ fun BiliMiniSlider(
|
|||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
valueRange = valueRange,
|
valueRange = valueRange,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
colors = SliderDefaults.colors(
|
colors = colors,
|
||||||
thumbColor = Color(0xFFFFFFFF),
|
enabled = false,
|
||||||
activeTrackColor = colorScheme.primary,
|
|
||||||
inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f)
|
|
||||||
),
|
|
||||||
thumb = {
|
thumb = {
|
||||||
|
|
||||||
},
|
},
|
||||||
@@ -86,14 +88,14 @@ fun BiliMiniSlider(
|
|||||||
Modifier
|
Modifier
|
||||||
.height(trackHeight)
|
.height(trackHeight)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(Color.LightGray.copy(alpha = 0.3f), RoundedCornerShape(50))
|
.background(colors.inactiveTrackColor, RoundedCornerShape(50))
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.align(Alignment.CenterStart)
|
.align(Alignment.CenterStart)
|
||||||
.fillMaxWidth(value)
|
.fillMaxWidth(value)
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.background(colorScheme.primary, RoundedCornerShape(50))
|
.background(colors.activeTrackColor, RoundedCornerShape(50))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,15 +84,27 @@ fun TransmissionScreen(
|
|||||||
maxLines = 1
|
maxLines = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val downloading = downloads.filter { it.status == Status.DOWNLOADING }
|
||||||
|
BiliMiniSlider(
|
||||||
|
value = if (downloading.sumOf { it.totalBytes } == 0L) 1f else downloading.sumOf { it.downloadedBytes } / downloading.sumOf { it.totalBytes }.toFloat(),
|
||||||
|
modifier = Modifier
|
||||||
|
.height(6.dp)
|
||||||
|
.align(Alignment.End)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
onValueChange = {
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
HorizontalDivider(Modifier.padding(8.dp), 2.dp, DividerDefaults.color)
|
HorizontalDivider(Modifier.padding(8.dp), 2.dp, DividerDefaults.color)
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
items(downloads.filter { it.type == "main" }, key = { it.id }) { item ->
|
items(downloads.filter { it.type == "main" }.sortedBy { it.status == Status.COMPLETED }, key = { it.id }) { item ->
|
||||||
VideoDownloadCard(
|
VideoDownloadCardMini(
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
viewModel = transmissionScreenViewModel,
|
viewModel = transmissionScreenViewModel,
|
||||||
model = item,
|
model = item,
|
||||||
@@ -112,13 +124,19 @@ fun TransmissionScreen(
|
|||||||
for (i in downloadToGroup(
|
for (i in downloadToGroup(
|
||||||
item,
|
item,
|
||||||
downloads
|
downloads
|
||||||
)) transmissionScreenViewModel.cancel(i.id)
|
)) transmissionScreenViewModel.delete(i.id)
|
||||||
},
|
},
|
||||||
onDelete = {
|
onDelete = {
|
||||||
for (i in downloadToGroup(
|
for (i in downloadToGroup(
|
||||||
item,
|
item,
|
||||||
downloads
|
downloads
|
||||||
)) transmissionScreenViewModel.delete(i.id)
|
)) transmissionScreenViewModel.delete(i.id)
|
||||||
|
},
|
||||||
|
onRetry = {
|
||||||
|
for (i in downloadToGroup(
|
||||||
|
item,
|
||||||
|
downloads
|
||||||
|
)) transmissionScreenViewModel.retry(i.id)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -126,231 +144,6 @@ fun TransmissionScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun VideoDownloadCard(
|
|
||||||
navigator: NavHostController,
|
|
||||||
viewModel: TransmissionScreenViewModel,
|
|
||||||
model: VideoDownloadItemState,
|
|
||||||
onPause: () -> Unit,
|
|
||||||
onResume: () -> Unit,
|
|
||||||
onCancel: () -> Unit,
|
|
||||||
onDelete: () -> Unit
|
|
||||||
) {
|
|
||||||
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 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(12.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(text = model.fileName, style = MaterialTheme.typography.titleMedium)
|
|
||||||
// Text(text = model.filePath, style = MaterialTheme.typography.titleSmall)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 5.dp)
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Card(
|
|
||||||
shape = RoundedCornerShape(8.dp),
|
|
||||||
modifier = Modifier.align(Alignment.CenterStart)
|
|
||||||
) {
|
|
||||||
val video = viewModel.modelToVideo(model)
|
|
||||||
|
|
||||||
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!!
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(Modifier.align(Alignment.BottomEnd)) {
|
|
||||||
Text(
|
|
||||||
text = "${model.progress}%",
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(start = 8.dp)
|
|
||||||
.align(Alignment.End)
|
|
||||||
)
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// progress bar
|
|
||||||
LinearProgressIndicator(
|
|
||||||
progress = { abs(model.progress).coerceIn(0, 100) / 100f },
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 8.dp, bottom = 8.dp),
|
|
||||||
color = ProgressIndicatorDefaults.linearColor,
|
|
||||||
trackColor = ProgressIndicatorDefaults.linearTrackColor,
|
|
||||||
strokeCap = ProgressIndicatorDefaults.LinearStrokeCap,
|
|
||||||
)
|
|
||||||
|
|
||||||
// action buttons
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
when (model.status) {
|
|
||||||
Status.DOWNLOADING -> {
|
|
||||||
Button(onClick = onPause) {
|
|
||||||
Icon(imageVector = Icons.Default.Pause, contentDescription = "Pause")
|
|
||||||
Text(text = " Pause", modifier = Modifier.padding(start = 6.dp))
|
|
||||||
}
|
|
||||||
Button(onClick = onCancel) {
|
|
||||||
Icon(imageVector = Icons.Default.Stop, contentDescription = "Cancel")
|
|
||||||
Text(text = " Cancel", modifier = Modifier.padding(start = 6.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Status.PAUSED, Status.QUEUED -> {
|
|
||||||
Button(onClick = onResume) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.PlayArrow,
|
|
||||||
contentDescription = "Resume"
|
|
||||||
)
|
|
||||||
Text(text = " Resume", modifier = Modifier.padding(start = 6.dp))
|
|
||||||
}
|
|
||||||
Button(onClick = onCancel) {
|
|
||||||
Icon(imageVector = Icons.Default.Stop, contentDescription = "Cancel")
|
|
||||||
Text(text = " Cancel", modifier = Modifier.padding(start = 6.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Status.COMPLETED -> {
|
|
||||||
Button(onClick = onDelete) {
|
|
||||||
Icon(imageVector = Icons.Default.Delete, contentDescription = "Delete")
|
|
||||||
Text(text = " Delete", modifier = Modifier.padding(start = 6.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
// for FAILED, CANCELLED, REMOVED etc.
|
|
||||||
Button(onClick = onResume) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.PlayArrow,
|
|
||||||
contentDescription = "Retry"
|
|
||||||
)
|
|
||||||
Text(text = " Retry", modifier = Modifier.padding(start = 6.dp))
|
|
||||||
}
|
|
||||||
Button(onClick = onDelete) {
|
|
||||||
Icon(imageVector = Icons.Default.Delete, contentDescription = "Delete")
|
|
||||||
Text(text = " Delete", modifier = Modifier.padding(start = 6.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun downloadToGroup(
|
fun downloadToGroup(
|
||||||
i: VideoDownloadItemState,
|
i: VideoDownloadItemState,
|
||||||
downloads: List<VideoDownloadItemState>
|
downloads: List<VideoDownloadItemState>
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
package com.acitelight.aether.view
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
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.MaterialTheme
|
||||||
|
import androidx.compose.material3.ProgressIndicatorDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
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.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.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
|
||||||
|
fun VideoDownloadCard(
|
||||||
|
navigator: NavHostController,
|
||||||
|
viewModel: TransmissionScreenViewModel,
|
||||||
|
model: VideoDownloadItemState,
|
||||||
|
onPause: () -> Unit,
|
||||||
|
onResume: () -> Unit,
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
onRetry: () -> Unit
|
||||||
|
) {
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(text = model.fileName, style = MaterialTheme.typography.titleMedium, maxLines = 2)
|
||||||
|
// Text(text = model.filePath, style = MaterialTheme.typography.titleSmall)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 5.dp)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
modifier = Modifier.align(Alignment.CenterStart)
|
||||||
|
) {
|
||||||
|
val video = viewModel.modelToVideo(model)
|
||||||
|
|
||||||
|
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!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(Modifier.align(Alignment.BottomEnd)) {
|
||||||
|
Text(
|
||||||
|
text = "${model.progress.coerceIn(0, 100)}%",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.align(Alignment.End)
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// progress bar
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = { abs(model.progress).coerceIn(0, 100) / 100f },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 8.dp, bottom = 8.dp),
|
||||||
|
color = ProgressIndicatorDefaults.linearColor,
|
||||||
|
trackColor = ProgressIndicatorDefaults.linearTrackColor,
|
||||||
|
strokeCap = ProgressIndicatorDefaults.LinearStrokeCap,
|
||||||
|
)
|
||||||
|
|
||||||
|
// action buttons
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
when (model.status) {
|
||||||
|
Status.DOWNLOADING -> {
|
||||||
|
Button(onClick = onPause) {
|
||||||
|
Icon(imageVector = Icons.Default.Pause, contentDescription = "Pause")
|
||||||
|
Text(text = " Pause", modifier = Modifier.padding(start = 6.dp))
|
||||||
|
}
|
||||||
|
Button(onClick = onCancel) {
|
||||||
|
Icon(imageVector = Icons.Default.Stop, contentDescription = "Cancel")
|
||||||
|
Text(text = " Cancel", modifier = Modifier.padding(start = 6.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Status.PAUSED, Status.QUEUED -> {
|
||||||
|
Button(onClick = onResume) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.PlayArrow,
|
||||||
|
contentDescription = "Resume"
|
||||||
|
)
|
||||||
|
Text(text = " Resume", modifier = Modifier.padding(start = 6.dp))
|
||||||
|
}
|
||||||
|
Button(onClick = onCancel) {
|
||||||
|
Icon(imageVector = Icons.Default.Stop, contentDescription = "Cancel")
|
||||||
|
Text(text = " Cancel", modifier = Modifier.padding(start = 6.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Status.COMPLETED -> {
|
||||||
|
Button(onClick = onDelete) {
|
||||||
|
Icon(imageVector = Icons.Default.Delete, contentDescription = "Delete")
|
||||||
|
Text(text = " Delete", modifier = Modifier.padding(start = 6.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
// for FAILED, CANCELLED, REMOVED etc.
|
||||||
|
Button(onClick = 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
package com.acitelight.aether.view
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
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.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.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.IconButton
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
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.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.layout.ModifierLocalBeyondBoundsLayout
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
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.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
|
||||||
|
fun VideoDownloadCardMini(
|
||||||
|
navigator: NavHostController,
|
||||||
|
viewModel: TransmissionScreenViewModel,
|
||||||
|
model: VideoDownloadItemState,
|
||||||
|
onPause: () -> Unit,
|
||||||
|
onResume: () -> Unit,
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
onRetry: () -> Unit
|
||||||
|
) {
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.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()
|
||||||
|
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
elevation = CardDefaults.cardElevation(4.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 4.dp)
|
||||||
|
.background(Color.Transparent)
|
||||||
|
.clickable(onClick = {
|
||||||
|
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 -> {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.height(85.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Box(Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.widthIn(max = 152.dp))
|
||||||
|
{
|
||||||
|
AsyncImage(
|
||||||
|
model = imageModel,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
|
||||||
|
},
|
||||||
|
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(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.padding(end = 4.dp)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
text = model.fileName,
|
||||||
|
lineHeight = 14.sp,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 2,
|
||||||
|
modifier = Modifier.align(Alignment.TopEnd)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(Modifier.align(Alignment.BottomEnd)) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.align(Alignment.End),
|
||||||
|
text = when (model.status) {
|
||||||
|
Status.COMPLETED -> "Completed"
|
||||||
|
Status.PAUSED, Status.QUEUED -> "Paused"
|
||||||
|
Status.DOWNLOADING -> "Downloading"
|
||||||
|
else -> "Error"
|
||||||
|
},
|
||||||
|
fontSize = 10.sp,
|
||||||
|
lineHeight = 11.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(Modifier
|
||||||
|
.align(Alignment.End)
|
||||||
|
.padding(vertical = 2.dp)) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier,
|
||||||
|
text = "%.2f MB/%.2f MB".format(
|
||||||
|
model.downloadedBytes / (1024.0 * 1024.0),
|
||||||
|
model.totalBytes / (1024.0 * 1024.0)
|
||||||
|
),
|
||||||
|
fontSize = 10.sp,
|
||||||
|
lineHeight = 11.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
|
Text(
|
||||||
|
text = "${model.progress.coerceIn(0, 100)}%",
|
||||||
|
modifier = Modifier,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
lineHeight = 11.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
BiliMiniSlider(
|
||||||
|
value = abs(model.progress).coerceIn(0, 100) / 100f,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(6.dp)
|
||||||
|
.align(Alignment.End)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
onValueChange = {
|
||||||
|
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,21 +5,28 @@ import androidx.compose.runtime.mutableStateListOf
|
|||||||
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
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||||
|
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.service.ApiClient
|
import com.acitelight.aether.service.ApiClient
|
||||||
import com.acitelight.aether.service.FetchManager
|
import com.acitelight.aether.service.FetchManager
|
||||||
import com.acitelight.aether.service.MediaManager
|
import com.acitelight.aether.service.MediaManager
|
||||||
import com.acitelight.aether.service.VideoLibrary
|
import com.acitelight.aether.service.VideoLibrary
|
||||||
|
import com.acitelight.aether.view.toHex
|
||||||
import com.tonyodev.fetch2.Download
|
import com.tonyodev.fetch2.Download
|
||||||
import com.tonyodev.fetch2.FetchListener
|
import com.tonyodev.fetch2.FetchListener
|
||||||
|
import com.tonyodev.fetch2.Status
|
||||||
import com.tonyodev.fetch2core.DownloadBlock
|
import com.tonyodev.fetch2core.DownloadBlock
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -128,7 +135,7 @@ class TransmissionScreenViewModel @Inject constructor(
|
|||||||
|
|
||||||
val state = downloadToState(download)
|
val state = downloadToState(download)
|
||||||
|
|
||||||
if (videoLibrary.classes.contains(state.klass)) videoLibrary.classes.add(state.klass)
|
if (!videoLibrary.classes.contains(state.klass)) videoLibrary.classes.add(state.klass)
|
||||||
|
|
||||||
if (!videoLibrary.classesMap.containsKey(state.klass)) videoLibrary.classesMap[state.klass] =
|
if (!videoLibrary.classesMap.containsKey(state.klass)) videoLibrary.classesMap[state.klass] =
|
||||||
mutableStateListOf()
|
mutableStateListOf()
|
||||||
@@ -204,7 +211,7 @@ class TransmissionScreenViewModel @Inject constructor(
|
|||||||
// UI actions delegated to FetchManager
|
// UI actions delegated to FetchManager
|
||||||
fun pause(id: Int) = fetchManager.pause(id)
|
fun pause(id: Int) = fetchManager.pause(id)
|
||||||
fun resume(id: Int) = fetchManager.resume(id)
|
fun resume(id: Int) = fetchManager.resume(id)
|
||||||
fun cancel(id: Int) = fetchManager.cancel(id)
|
fun retry(id: Int) = fetchManager.retry(id)
|
||||||
fun delete(id: Int) {
|
fun delete(id: Int) {
|
||||||
fetchManager.delete(id) {
|
fetchManager.delete(id) {
|
||||||
viewModelScope.launch(Dispatchers.Main) { removeOnMain(id) }
|
viewModelScope.launch(Dispatchers.Main) { removeOnMain(id) }
|
||||||
@@ -216,6 +223,56 @@ class TransmissionScreenViewModel @Inject constructor(
|
|||||||
fetchManager.removeListener()
|
fetchManager.removeListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun playStart(model: VideoDownloadItemState, navigator: NavHostController)
|
||||||
|
{
|
||||||
|
val downloaded = fetchManager.getAllDownloadsAsync().filter {
|
||||||
|
it.status == Status.COMPLETED && it.extras.getString(
|
||||||
|
"class",
|
||||||
|
""
|
||||||
|
) != "comic" && it.extras.getString(
|
||||||
|
"type",
|
||||||
|
""
|
||||||
|
) == "main"
|
||||||
|
}
|
||||||
|
|
||||||
|
val jsonQuery = downloaded.map {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(null),
|
||||||
|
"videos/${
|
||||||
|
it.extras.getString(
|
||||||
|
"class",
|
||||||
|
""
|
||||||
|
)
|
||||||
|
}/${it.extras.getString("id", "")}/summary.json"
|
||||||
|
).readText()
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
Json.decodeFromString<Video>(it)
|
||||||
|
.toLocal(context.getExternalFilesDir(null)!!.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRelate(
|
||||||
|
jsonQuery,
|
||||||
|
jsonQuery.first { it.id == model.vid && it.klass == model.klass }
|
||||||
|
)
|
||||||
|
|
||||||
|
val playList = mutableListOf<String>()
|
||||||
|
val fv = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
imageLoader = ImageLoader.Builder(context).components {
|
imageLoader = ImageLoader.Builder(context).components {
|
||||||
add(OkHttpNetworkFetcherFactory(apiClient.getClient()))
|
add(OkHttpNetworkFetcherFactory(apiClient.getClient()))
|
||||||
|
|||||||
@@ -46,8 +46,9 @@ class VideoScreenViewModel @Inject constructor(
|
|||||||
fetchManager.configured.filter { it }.first()
|
fetchManager.configured.filter { it }.first()
|
||||||
|
|
||||||
if (Global.loggedIn) {
|
if (Global.loggedIn) {
|
||||||
videoLibrary.classes.addAll(mediaManager.listVideoKlasses())
|
videoLibrary.classes.addAll(
|
||||||
videoLibrary.classes.distinct()
|
mediaManager.listVideoKlasses().filter { it !in videoLibrary.classes }
|
||||||
|
)
|
||||||
|
|
||||||
if(videoLibrary.classes.isEmpty())
|
if(videoLibrary.classes.isEmpty())
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user