[feat&optimize] Group Batch Download, Optimize download logic

This commit is contained in:
acite
2025-09-27 00:37:57 +08:00
parent a89f892306
commit 5b770a965d
7 changed files with 304 additions and 146 deletions

View File

@@ -5,7 +5,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import com.tonyodev.fetch2.Status import com.tonyodev.fetch2.Status
class DownloadItemState( class VideoDownloadItemState(
val id: Int, val id: Int,
fileName: String, fileName: String,
filePath: String, filePath: String,
@@ -15,7 +15,8 @@ class DownloadItemState(
downloadedBytes: Long, downloadedBytes: Long,
totalBytes: Long, totalBytes: Long,
klass: String, klass: String,
vid: String vid: String,
val type: String
) { ) {
var fileName by mutableStateOf(fileName) var fileName by mutableStateOf(fileName)
var filePath by mutableStateOf(filePath) var filePath by mutableStateOf(filePath)

View File

@@ -38,8 +38,7 @@ class FetchManager @Inject constructor(
private var client: OkHttpClient? = null private var client: OkHttpClient? = null
val configured = MutableStateFlow(false) val configured = MutableStateFlow(false)
fun init() fun init() {
{
client = createOkHttp() client = createOkHttp()
val fetchConfiguration = FetchConfiguration.Builder(context) val fetchConfiguration = FetchConfiguration.Builder(context)
.setDownloadConcurrentLimit(8) .setDownloadConcurrentLimit(8)
@@ -72,8 +71,7 @@ class FetchManager @Inject constructor(
fetch?.getDownloads { list -> callback(list) } ?: callback(emptyList()) fetch?.getDownloads { list -> callback(list) } ?: callback(emptyList())
} }
suspend fun getAllDownloadsAsync(): List<Download> suspend fun getAllDownloadsAsync(): List<Download> {
{
configured.filter { it }.first() configured.filter { it }.first()
val completed = MutableStateFlow(false) val completed = MutableStateFlow(false)
var r = listOf<Download>() var r = listOf<Download>()
@@ -106,71 +104,88 @@ class FetchManager @Inject constructor(
} ?: callback?.invoke() } ?: callback?.invoke()
} }
private suspend fun enqueue(request: Request, onEnqueued: ((Request) -> Unit)? = null, onError: ((com.tonyodev.fetch2.Error) -> Unit)? = null) { private suspend fun enqueue(
request: Request,
onEnqueued: ((Request) -> Unit)? = null,
onError: ((com.tonyodev.fetch2.Error) -> Unit)? = null
) {
configured.filter { it }.first() configured.filter { it }.first()
fetch?.enqueue(request, { r -> onEnqueued?.invoke(r) }, { e -> onError?.invoke(e) }) fetch?.enqueue(request, { r -> onEnqueued?.invoke(r) }, { e -> onError?.invoke(e) })
} }
private fun getVideosDirectory() { private fun makeFolder(video: Video) {
val appFilesDir = context.getExternalFilesDir(null) val appFilesDir = context.getExternalFilesDir(null)
val videosDir = File(appFilesDir, "videos") val videosDir = File(appFilesDir, "videos/${video.klass}/${video.id}/gallery")
videosDir.mkdirs()
if (!videosDir.exists()) {
val created = videosDir.mkdirs()
}
} }
suspend fun downloadFile( suspend fun startVideoDownload(video: Video) {
client: OkHttpClient, makeFolder(video)
url: String, File(
destFile: File context.getExternalFilesDir(null),
): Result<Unit> = withContext(Dispatchers.IO) { "videos/${video.klass}/${video.id}/summary.json"
try { ).writeText(Json.encodeToString(video))
val request = okhttp3.Request.Builder().url(url).build()
client.newCall(request).execute().use { response -> val videoPath =
if (!response.isSuccessful) { File(context.getExternalFilesDir(null), "videos/${video.klass}/${video.id}/video.mp4")
return@withContext Result.failure(IOException("Unexpected code $response")) val coverPath =
File(context.getExternalFilesDir(null), "videos/${video.klass}/${video.id}/cover.jpg")
val subtitlePath = File(
context.getExternalFilesDir(null),
"videos/${video.klass}/${video.id}/subtitle.vtt"
)
val requests = mutableListOf(
Request(video.getVideo(), videoPath.path).apply {
extras = Extras(
mapOf(
"name" to video.video.name,
"id" to video.id,
"class" to video.klass,
"type" to "main"
)
)
},
Request(video.getCover(), coverPath.path).apply {
extras = Extras(
mapOf(
"name" to video.video.name,
"id" to video.id,
"class" to video.klass,
"type" to "cover"
)
)
},
Request(video.getSubtitle(), subtitlePath.path).apply {
extras = Extras(
mapOf(
"name" to video.video.name,
"id" to video.id,
"class" to video.klass,
"type" to "subtitle"
)
)
},
)
for (p in video.getGallery()) {
requests.add(
Request(p.url, File(
context.getExternalFilesDir(null),
"videos/${video.klass}/${video.id}/gallery/${p.name}"
).path).apply {
extras = Extras(
mapOf(
"name" to video.video.name,
"id" to video.id,
"class" to video.klass,
"type" to "gallery"
)
)
} }
)
destFile.parentFile?.mkdirs()
response.body.byteStream().use { input ->
destFile.outputStream().use { output ->
input.copyTo(output)
}
}
}
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun startVideoDownload(video: Video)
{
val path = File(context.getExternalFilesDir(null), "videos/${video.klass}/${video.id}/video.mp4")
val request = Request(video.getVideo(), path.path).apply {
extras = Extras(mapOf("name" to video.video.name, "id" to video.id, "class" to video.klass))
} }
downloadFile( for (i in requests)
client!!, enqueue(i)
video.getCover(),
File(context.getExternalFilesDir(null), "videos/${video.klass}/${video.id}/cover.jpg"))
downloadFile(
client!!,
video.getSubtitle(),
File(context.getExternalFilesDir(null), "videos/${video.klass}/${video.id}/subtitle.vtt"))
enqueue(request)
File(context.getExternalFilesDir(null), "videos/${video.klass}/${video.id}/summary.json").writeText(Json.encodeToString(video))
for(p in video.getGallery())
{
downloadFile(
client!!,
p.url,
File(context.getExternalFilesDir(null), "videos/${video.klass}/${video.id}/gallery/${p.name}"))
}
} }
} }

View File

@@ -81,7 +81,16 @@ fun HomeScreen(
i, i,
{ {
updateRelate(homeScreenViewModel.recentManager.recentVideo, i) updateRelate(homeScreenViewModel.recentManager.recentVideo, i)
val route = "video_player_route/${ "${i.klass}/${i.id}".toHex() }"
val playList = mutableListOf("${i.klass}/${i.id}")
val fv = homeScreenViewModel.videoLibrary.classesMap.map { it.value }.flatten()
val group = fv.filter { it.klass == i.klass && it.video.group == i.video.group }
for (i in group) {
playList.add("${i.klass}/${i.id}")
}
val route = "video_player_route/${playList.joinToString(",").toHex()}"
navController.navigate(route) navController.navigate(route)
}, homeScreenViewModel.imageLoader!!) }, homeScreenViewModel.imageLoader!!)
HorizontalDivider(Modifier.padding(vertical = 8.dp).alpha(0.4f), 1.dp, DividerDefaults.color) HorizontalDivider(Modifier.padding(vertical = 8.dp).alpha(0.4f), 1.dp, DividerDefaults.color)

View File

@@ -6,8 +6,8 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@@ -16,24 +16,17 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Pause import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.Stop import androidx.compose.material.icons.filled.Stop
import androidx.compose.material.icons.*
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CardElevation
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProgressIndicatorDefaults import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -45,17 +38,13 @@ import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.Navigator
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import coil3.request.ImageRequest import coil3.request.ImageRequest
import com.acitelight.aether.Global.updateRelate import com.acitelight.aether.Global.updateRelate
import com.acitelight.aether.model.DownloadItemState import com.acitelight.aether.model.VideoDownloadItemState
import com.acitelight.aether.model.Video import com.acitelight.aether.model.Video
import com.acitelight.aether.viewModel.TransmissionScreenViewModel import com.acitelight.aether.viewModel.TransmissionScreenViewModel
import com.tonyodev.fetch2.Download
import com.tonyodev.fetch2.FetchListener
import com.tonyodev.fetch2.Status import com.tonyodev.fetch2.Status
import com.tonyodev.fetch2core.DownloadBlock
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -63,21 +52,45 @@ import kotlinx.serialization.json.Json
import java.io.File import java.io.File
@Composable @Composable
fun TransmissionScreen(navigator: NavHostController, transmissionScreenViewModel: TransmissionScreenViewModel = hiltViewModel<TransmissionScreenViewModel>()) { fun TransmissionScreen(
navigator: NavHostController,
transmissionScreenViewModel: TransmissionScreenViewModel = hiltViewModel<TransmissionScreenViewModel>()
) {
val downloads = transmissionScreenViewModel.downloads val downloads = transmissionScreenViewModel.downloads
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
items(downloads, key = { it.id }) { item -> items(downloads.filter { it.type == "main" }, key = { it.id }) { item ->
DownloadCard( VideoDownloadCard(
navigator = navigator, navigator = navigator,
viewModel = transmissionScreenViewModel, viewModel = transmissionScreenViewModel,
model = item, model = item,
onPause = { transmissionScreenViewModel.pause(item.id) }, onPause = {
onResume = { transmissionScreenViewModel.resume(item.id) }, for (i in downloadToGroup(
onCancel = { transmissionScreenViewModel.cancel(item.id) }, item,
onDelete = { transmissionScreenViewModel.delete(item.id, true) } downloads
)) transmissionScreenViewModel.pause(i.id)
},
onResume = {
for (i in downloadToGroup(
item,
downloads
)) transmissionScreenViewModel.resume(i.id)
},
onCancel = {
for (i in downloadToGroup(
item,
downloads
)) transmissionScreenViewModel.cancel(i.id)
},
onDelete = {
for (i in downloadToGroup(
item,
downloads
)) transmissionScreenViewModel.delete(i.id, true)
}
) )
} }
} }
@@ -85,10 +98,10 @@ fun TransmissionScreen(navigator: NavHostController, transmissionScreenViewModel
@Composable @Composable
private fun DownloadCard( private fun VideoDownloadCard(
navigator: NavHostController, navigator: NavHostController,
viewModel: TransmissionScreenViewModel, viewModel: TransmissionScreenViewModel,
model: DownloadItemState, model: VideoDownloadItemState,
onPause: () -> Unit, onPause: () -> Unit,
onResume: () -> Unit, onResume: () -> Unit,
onCancel: () -> Unit, onCancel: () -> Unit,
@@ -102,24 +115,50 @@ private fun DownloadCard(
.padding(8.dp) .padding(8.dp)
.background(Color.Transparent) .background(Color.Transparent)
.clickable(onClick = { .clickable(onClick = {
if(model.status == Status.COMPLETED) if (model.status == Status.COMPLETED) {
{
viewModel.viewModelScope.launch(Dispatchers.IO) viewModel.viewModelScope.launch(Dispatchers.IO)
{ {
val downloaded = viewModel.fetchManager.getAllDownloadsAsync().filter { val downloaded = viewModel.fetchManager.getAllDownloadsAsync().filter {
it.status == Status.COMPLETED && it.extras.getString("isComic", "") != "true" it.status == Status.COMPLETED && it.extras.getString(
"isComic",
""
) != "true"
} }
val jsonQuery = downloaded.map{ File( val jsonQuery = downloaded.map {
viewModel.context.getExternalFilesDir(null), File(
"videos/${it.extras.getString("class", "")}/${it.extras.getString("id", "")}/summary.json").readText() } viewModel.context.getExternalFilesDir(null),
.map { Json.decodeFromString<Video>(it).toLocal(viewModel.context.getExternalFilesDir(null)!!.path) } "videos/${
it.extras.getString(
"class",
""
)
}/${it.extras.getString("id", "")}/summary.json"
).readText()
}
.map {
Json.decodeFromString<Video>(it)
.toLocal(viewModel.context.getExternalFilesDir(null)!!.path)
}
updateRelate( updateRelate(
jsonQuery, jsonQuery.first { it.id == model.vid && it.klass == model.klass } jsonQuery,
jsonQuery.first { it.id == model.vid && it.klass == model.klass }
) )
val route = "video_player_route/${"${model.klass}/${model.vid}".toHex()}"
withContext(Dispatchers.Main){ val playList = mutableListOf("${model.klass}/${model.vid}")
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) {
playList.add("${i.klass}/${i.id}")
}
}
val route = "video_player_route/${playList.joinToString(",").toHex()}"
withContext(Dispatchers.Main) {
navigator.navigate(route) navigator.navigate(route)
} }
} }
@@ -137,32 +176,52 @@ private fun DownloadCard(
) { ) {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text(text = model.fileName, style = MaterialTheme.typography.titleMedium) Text(text = model.fileName, style = MaterialTheme.typography.titleMedium)
// Text(text = model.filePath, style = MaterialTheme.typography.titleSmall)
} }
} }
Box(Modifier Box(
.fillMaxWidth() Modifier
.padding(top = 5.dp)) .fillMaxWidth()
.padding(top = 5.dp)
)
{ {
Card( Card(
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
modifier = Modifier.align(Alignment.CenterStart) modifier = Modifier.align(Alignment.CenterStart)
) { ) {
AsyncImage( val video = viewModel.modelToVideo(model)
model = ImageRequest.Builder(LocalContext.current)
.data( if (video == null)
File( AsyncImage(
viewModel.context.getExternalFilesDir(null), model = ImageRequest.Builder(LocalContext.current)
"videos/${model.klass}/${model.vid}/cover.jpg" .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") .diskCacheKey("${model.klass}/${model.vid}/cover")
.build(), .build(),
contentDescription = null, contentDescription = null,
modifier = Modifier.heightIn(max = 100.dp), modifier = Modifier.height(100.dp),
contentScale = ContentScale.Fit contentScale = ContentScale.Fit
) )
else {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(video.getCover())
.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)) { Column(Modifier.align(Alignment.BottomEnd)) {
@@ -258,3 +317,10 @@ private fun DownloadCard(
} }
} }
} }
fun downloadToGroup(
i: VideoDownloadItemState,
downloads: List<VideoDownloadItemState>
): List<VideoDownloadItemState> {
return downloads.filter { it.vid == i.vid && it.klass == i.klass }
}

View File

@@ -7,6 +7,7 @@ import coil3.ImageLoader
import coil3.network.okhttp.OkHttpNetworkFetcherFactory import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import com.acitelight.aether.service.ApiClient.createOkHttp import com.acitelight.aether.service.ApiClient.createOkHttp
import com.acitelight.aether.service.RecentManager import com.acitelight.aether.service.RecentManager
import com.acitelight.aether.service.VideoLibrary
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@@ -16,7 +17,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext
@HiltViewModel @HiltViewModel
class HomeScreenViewModel @Inject constructor( class HomeScreenViewModel @Inject constructor(
val recentManager: RecentManager, val recentManager: RecentManager,
@ApplicationContext val context: Context @ApplicationContext val context: Context,
val videoLibrary: VideoLibrary,
) : ViewModel() ) : ViewModel()
{ {
var imageLoader: ImageLoader? = null var imageLoader: ImageLoader? = null

View File

@@ -5,7 +5,11 @@ 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 com.acitelight.aether.model.DownloadItemState import coil3.ImageLoader
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import com.acitelight.aether.model.Video
import com.acitelight.aether.model.VideoDownloadItemState
import com.acitelight.aether.service.ApiClient.createOkHttp
import com.acitelight.aether.service.FetchManager import com.acitelight.aether.service.FetchManager
import com.acitelight.aether.service.VideoLibrary import com.acitelight.aether.service.VideoLibrary
import com.tonyodev.fetch2.Download import com.tonyodev.fetch2.Download
@@ -22,36 +26,82 @@ import javax.inject.Inject
class TransmissionScreenViewModel @Inject constructor( class TransmissionScreenViewModel @Inject constructor(
val fetchManager: FetchManager, val fetchManager: FetchManager,
@ApplicationContext val context: Context, @ApplicationContext val context: Context,
private val videoLibrary: VideoLibrary val videoLibrary: VideoLibrary,
) : ViewModel() { ) : ViewModel() {
private val _downloads: SnapshotStateList<DownloadItemState> = mutableStateListOf() var imageLoader: ImageLoader? = null
val downloads: SnapshotStateList<DownloadItemState> = _downloads val downloads: SnapshotStateList<VideoDownloadItemState> = mutableStateListOf()
// map id -> state object reference (no index bookkeeping) // map id -> state object reference (no index bookkeeping)
private val idToState: MutableMap<Int, DownloadItemState> = mutableMapOf() 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 }
}
private val fetchListener = object : FetchListener { private val fetchListener = object : FetchListener {
override fun onAdded(download: Download) { handleUpsert(download) } override fun onAdded(download: Download) {
override fun onQueued(download: Download, waitingOnNetwork: Boolean) { handleUpsert(download) } handleUpsert(download)
}
override fun onQueued(download: Download, waitingOnNetwork: Boolean) {
handleUpsert(download)
}
override fun onWaitingNetwork(download: Download) { override fun onWaitingNetwork(download: Download) {
} }
override fun onProgress(download: Download, etaInMilliSeconds: Long, downloadedBytesPerSecond: Long) { handleUpsert(download) } override fun onProgress(
override fun onPaused(download: Download) { handleUpsert(download) } download: Download,
override fun onResumed(download: Download) { handleUpsert(download) } etaInMilliSeconds: Long,
override fun onCompleted(download: Download) { downloadedBytesPerSecond: Long
val ii = videoLibrary.classesMap[download.extras.getString("class", "")] ) {
?.indexOfFirst { it.id == download.extras.getString("id", "") }!!
val newi = videoLibrary.classesMap[download.extras.getString("class", "")]!![ii]
videoLibrary.classesMap[download.extras.getString("class", "")]!![ii] = newi.toLocal(context.getExternalFilesDir(null)!!.path)
handleUpsert(download) handleUpsert(download)
} }
override fun onCancelled(download: Download) { handleUpsert(download) }
override fun onRemoved(download: Download) { handleRemove(download.id) } override fun onPaused(download: Download) {
override fun onDeleted(download: Download) { handleRemove(download.id) } handleUpsert(download)
override fun onDownloadBlockUpdated(download: Download, downloadBlock: DownloadBlock, totalBlocks: Int) { handleUpsert(download) } }
override fun onResumed(download: Download) {
handleUpsert(download)
}
override fun onCompleted(download: Download) {
handleUpsert(download)
if (download.extras.getString("type", "") == "main") {
val ii = videoLibrary.classesMap[download.extras.getString("class", "")]
?.indexOfFirst { it.id == download.extras.getString("id", "") }!!
val newi = videoLibrary.classesMap[download.extras.getString("class", "")]!![ii]
videoLibrary.classesMap[download.extras.getString("class", "")]!![ii] =
newi.toLocal(context.getExternalFilesDir(null)!!.path)
}
}
override fun onCancelled(download: Download) {
handleUpsert(download)
}
override fun onRemoved(download: Download) {
handleRemove(download.id)
}
override fun onDeleted(download: Download) {
handleRemove(download.id)
}
override fun onDownloadBlockUpdated(
download: Download,
downloadBlock: DownloadBlock,
totalBlocks: Int
) {
handleUpsert(download)
}
override fun onStarted( override fun onStarted(
download: Download, download: Download,
downloadBlocks: List<DownloadBlock>, downloadBlocks: List<DownloadBlock>,
@@ -60,7 +110,13 @@ class TransmissionScreenViewModel @Inject constructor(
handleUpsert(download) handleUpsert(download)
} }
override fun onError(download: Download, error: com.tonyodev.fetch2.Error, throwable: Throwable?) { handleUpsert(download) } override fun onError(
download: Download,
error: com.tonyodev.fetch2.Error,
throwable: Throwable?
) {
handleUpsert(download)
}
} }
private fun handleUpsert(download: Download) { private fun handleUpsert(download: Download) {
@@ -89,7 +145,7 @@ class TransmissionScreenViewModel @Inject constructor(
} else { } else {
// new item: add to head (or tail depending on preference) // new item: add to head (or tail depending on preference)
val newState = downloadToState(download) val newState = downloadToState(download)
_downloads.add(0, newState) downloads.add(0, newState)
idToState[newState.id] = newState idToState[newState.id] = newState
} }
} }
@@ -97,19 +153,20 @@ class TransmissionScreenViewModel @Inject constructor(
private fun removeOnMain(id: Int) { private fun removeOnMain(id: Int) {
val state = idToState.remove(id) val state = idToState.remove(id)
if (state != null) { if (state != null) {
_downloads.remove(state) downloads.remove(state)
} else { } else {
val idx = _downloads.indexOfFirst { it.id == id } val idx = downloads.indexOfFirst { it.id == id }
if (idx >= 0) { if (idx >= 0) {
val removed = _downloads.removeAt(idx) val removed = downloads.removeAt(idx)
idToState.remove(removed.id) idToState.remove(removed.id)
} }
} }
} }
private fun downloadToState(download: Download): DownloadItemState {
private fun downloadToState(download: Download): VideoDownloadItemState {
val filePath = download.file val filePath = download.file
return DownloadItemState( return VideoDownloadItemState(
id = download.id, id = download.id,
fileName = download.request.extras.getString("name", ""), fileName = download.request.extras.getString("name", ""),
filePath = filePath, filePath = filePath,
@@ -119,7 +176,8 @@ class TransmissionScreenViewModel @Inject constructor(
downloadedBytes = download.downloaded, downloadedBytes = download.downloaded,
totalBytes = download.total, totalBytes = download.total,
klass = download.extras.getString("class", ""), klass = download.extras.getString("class", ""),
vid = download.extras.getString("id", "") vid = download.extras.getString("id", ""),
type = download.extras.getString("type", "")
) )
} }
@@ -140,19 +198,25 @@ class TransmissionScreenViewModel @Inject constructor(
} }
init { init {
imageLoader = ImageLoader.Builder(context)
.components {
add(OkHttpNetworkFetcherFactory(createOkHttp()))
}
.build()
viewModelScope.launch { viewModelScope.launch {
fetchManager.setListener(fetchListener) fetchManager.setListener(fetchListener)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
fetchManager.getAllDownloads { list -> fetchManager.getAllDownloads { list ->
_downloads.clear() downloads.clear()
idToState.clear() idToState.clear()
list.sortedBy { it.extras.getString("name", "") }.forEach { d -> list.sortedBy { it.extras.getString("name", "") }.forEach { d ->
val s = downloadToState(d) val s = downloadToState(d)
_downloads.add(s) downloads.add(s)
idToState[s.id] = s idToState[s.id] = s
} }
} }
} }
} }
} }
} }

View File

@@ -47,6 +47,7 @@ import androidx.core.net.toUri
import androidx.media3.common.Tracks import androidx.media3.common.Tracks
import androidx.media3.datasource.DefaultDataSource import androidx.media3.datasource.DefaultDataSource
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import com.acitelight.aether.Global
import com.acitelight.aether.model.KeyImage import com.acitelight.aether.model.KeyImage
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -99,7 +100,7 @@ class VideoPlayerViewModel @Inject constructor(
return return
_init = true _init = true
val vs = videoId.hexToString().split(",").map { it.split("/") } val vs = videoId.hexToString().split(",").map { it.split("/") }.toMutableList()
imageLoader = ImageLoader.Builder(context) imageLoader = ImageLoader.Builder(context)
.components { .components {
add(OkHttpNetworkFetcherFactory(createOkHttp())) add(OkHttpNetworkFetcherFactory(createOkHttp()))
@@ -107,11 +108,11 @@ class VideoPlayerViewModel @Inject constructor(
.build() .build()
viewModelScope.launch { viewModelScope.launch {
videos = mediaManager.queryVideoBulk(vs.first()[0], vs.map { it[1] })!!
val ii = database.userDao().getAll().first() val ii = database.userDao().getAll().first()
val ix = ii.filter { it.id in videos.map{ m -> m.id } }.maxByOrNull { it.time } val ix = ii.filter { it.id in videos.map{ m -> m.id } }.maxByOrNull { it.time }
videos = mediaManager.queryVideoBulk(vs.first()[0], vs.map { it[1] })!!
startPlay( startPlay(
if (ix != null) if (ix != null)
videos.first { it.id == ix.id } videos.first { it.id == ix.id }