[feat&optimize] Group Batch Download, Optimize download logic
This commit is contained in:
@@ -5,7 +5,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.tonyodev.fetch2.Status
|
||||
|
||||
class DownloadItemState(
|
||||
class VideoDownloadItemState(
|
||||
val id: Int,
|
||||
fileName: String,
|
||||
filePath: String,
|
||||
@@ -15,7 +15,8 @@ class DownloadItemState(
|
||||
downloadedBytes: Long,
|
||||
totalBytes: Long,
|
||||
klass: String,
|
||||
vid: String
|
||||
vid: String,
|
||||
val type: String
|
||||
) {
|
||||
var fileName by mutableStateOf(fileName)
|
||||
var filePath by mutableStateOf(filePath)
|
||||
@@ -38,8 +38,7 @@ class FetchManager @Inject constructor(
|
||||
private var client: OkHttpClient? = null
|
||||
val configured = MutableStateFlow(false)
|
||||
|
||||
fun init()
|
||||
{
|
||||
fun init() {
|
||||
client = createOkHttp()
|
||||
val fetchConfiguration = FetchConfiguration.Builder(context)
|
||||
.setDownloadConcurrentLimit(8)
|
||||
@@ -72,8 +71,7 @@ class FetchManager @Inject constructor(
|
||||
fetch?.getDownloads { list -> callback(list) } ?: callback(emptyList())
|
||||
}
|
||||
|
||||
suspend fun getAllDownloadsAsync(): List<Download>
|
||||
{
|
||||
suspend fun getAllDownloadsAsync(): List<Download> {
|
||||
configured.filter { it }.first()
|
||||
val completed = MutableStateFlow(false)
|
||||
var r = listOf<Download>()
|
||||
@@ -106,71 +104,88 @@ class FetchManager @Inject constructor(
|
||||
} ?: 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()
|
||||
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 videosDir = File(appFilesDir, "videos")
|
||||
|
||||
if (!videosDir.exists()) {
|
||||
val created = videosDir.mkdirs()
|
||||
}
|
||||
val videosDir = File(appFilesDir, "videos/${video.klass}/${video.id}/gallery")
|
||||
videosDir.mkdirs()
|
||||
}
|
||||
|
||||
suspend fun downloadFile(
|
||||
client: OkHttpClient,
|
||||
url: String,
|
||||
destFile: File
|
||||
): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val request = okhttp3.Request.Builder().url(url).build()
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
return@withContext Result.failure(IOException("Unexpected code $response"))
|
||||
suspend fun startVideoDownload(video: Video) {
|
||||
makeFolder(video)
|
||||
File(
|
||||
context.getExternalFilesDir(null),
|
||||
"videos/${video.klass}/${video.id}/summary.json"
|
||||
).writeText(Json.encodeToString(video))
|
||||
|
||||
val videoPath =
|
||||
File(context.getExternalFilesDir(null), "videos/${video.klass}/${video.id}/video.mp4")
|
||||
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(
|
||||
client!!,
|
||||
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}"))
|
||||
}
|
||||
for (i in requests)
|
||||
enqueue(i)
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,16 @@ fun HomeScreen(
|
||||
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)
|
||||
}, homeScreenViewModel.imageLoader!!)
|
||||
HorizontalDivider(Modifier.padding(vertical = 8.dp).alpha(0.4f), 1.dp, DividerDefaults.color)
|
||||
|
||||
@@ -6,8 +6,8 @@ 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.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.lazy.LazyColumn
|
||||
@@ -16,24 +16,17 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
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.PlayArrow
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
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.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProgressIndicatorDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
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.Modifier
|
||||
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.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.Navigator
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.request.ImageRequest
|
||||
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.viewModel.TransmissionScreenViewModel
|
||||
import com.tonyodev.fetch2.Download
|
||||
import com.tonyodev.fetch2.FetchListener
|
||||
import com.tonyodev.fetch2.Status
|
||||
import com.tonyodev.fetch2core.DownloadBlock
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -63,21 +52,45 @@ import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
fun TransmissionScreen(navigator: NavHostController, transmissionScreenViewModel: TransmissionScreenViewModel = hiltViewModel<TransmissionScreenViewModel>()) {
|
||||
fun TransmissionScreen(
|
||||
navigator: NavHostController,
|
||||
transmissionScreenViewModel: TransmissionScreenViewModel = hiltViewModel<TransmissionScreenViewModel>()
|
||||
) {
|
||||
val downloads = transmissionScreenViewModel.downloads
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(downloads, key = { it.id }) { item ->
|
||||
DownloadCard(
|
||||
items(downloads.filter { it.type == "main" }, key = { it.id }) { item ->
|
||||
VideoDownloadCard(
|
||||
navigator = navigator,
|
||||
viewModel = transmissionScreenViewModel,
|
||||
model = item,
|
||||
onPause = { transmissionScreenViewModel.pause(item.id) },
|
||||
onResume = { transmissionScreenViewModel.resume(item.id) },
|
||||
onCancel = { transmissionScreenViewModel.cancel(item.id) },
|
||||
onDelete = { transmissionScreenViewModel.delete(item.id, true) }
|
||||
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.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
|
||||
private fun DownloadCard(
|
||||
private fun VideoDownloadCard(
|
||||
navigator: NavHostController,
|
||||
viewModel: TransmissionScreenViewModel,
|
||||
model: DownloadItemState,
|
||||
model: VideoDownloadItemState,
|
||||
onPause: () -> Unit,
|
||||
onResume: () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
@@ -102,24 +115,50 @@ private fun DownloadCard(
|
||||
.padding(8.dp)
|
||||
.background(Color.Transparent)
|
||||
.clickable(onClick = {
|
||||
if(model.status == Status.COMPLETED)
|
||||
{
|
||||
if (model.status == Status.COMPLETED) {
|
||||
viewModel.viewModelScope.launch(Dispatchers.IO)
|
||||
{
|
||||
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(
|
||||
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) }
|
||||
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 }
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -137,32 +176,52 @@ private fun DownloadCard(
|
||||
) {
|
||||
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))
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 5.dp)
|
||||
)
|
||||
{
|
||||
Card(
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
modifier = Modifier.align(Alignment.CenterStart)
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(
|
||||
File(
|
||||
viewModel.context.getExternalFilesDir(null),
|
||||
"videos/${model.klass}/${model.vid}/cover.jpg"
|
||||
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"
|
||||
)
|
||||
)
|
||||
)
|
||||
.diskCacheKey("${model.klass}/${model.vid}/cover")
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.heightIn(max = 100.dp),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
.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())
|
||||
.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)) {
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import coil3.ImageLoader
|
||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||
import com.acitelight.aether.service.ApiClient.createOkHttp
|
||||
import com.acitelight.aether.service.RecentManager
|
||||
import com.acitelight.aether.service.VideoLibrary
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -16,7 +17,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
@HiltViewModel
|
||||
class HomeScreenViewModel @Inject constructor(
|
||||
val recentManager: RecentManager,
|
||||
@ApplicationContext val context: Context
|
||||
@ApplicationContext val context: Context,
|
||||
val videoLibrary: VideoLibrary,
|
||||
) : ViewModel()
|
||||
{
|
||||
var imageLoader: ImageLoader? = null
|
||||
|
||||
@@ -5,7 +5,11 @@ import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.lifecycle.ViewModel
|
||||
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.VideoLibrary
|
||||
import com.tonyodev.fetch2.Download
|
||||
@@ -22,36 +26,82 @@ import javax.inject.Inject
|
||||
class TransmissionScreenViewModel @Inject constructor(
|
||||
val fetchManager: FetchManager,
|
||||
@ApplicationContext val context: Context,
|
||||
private val videoLibrary: VideoLibrary
|
||||
val videoLibrary: VideoLibrary,
|
||||
) : ViewModel() {
|
||||
private val _downloads: SnapshotStateList<DownloadItemState> = mutableStateListOf()
|
||||
val downloads: SnapshotStateList<DownloadItemState> = _downloads
|
||||
var imageLoader: ImageLoader? = null
|
||||
val downloads: SnapshotStateList<VideoDownloadItemState> = mutableStateListOf()
|
||||
|
||||
// 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 {
|
||||
override fun onAdded(download: Download) { handleUpsert(download) }
|
||||
override fun onQueued(download: Download, waitingOnNetwork: Boolean) { handleUpsert(download) }
|
||||
override fun onAdded(download: Download) {
|
||||
handleUpsert(download)
|
||||
}
|
||||
|
||||
override fun onQueued(download: Download, waitingOnNetwork: Boolean) {
|
||||
handleUpsert(download)
|
||||
}
|
||||
|
||||
override fun onWaitingNetwork(download: Download) {
|
||||
|
||||
}
|
||||
|
||||
override fun onProgress(download: Download, etaInMilliSeconds: Long, downloadedBytesPerSecond: Long) { handleUpsert(download) }
|
||||
override fun onPaused(download: Download) { handleUpsert(download) }
|
||||
override fun onResumed(download: Download) { handleUpsert(download) }
|
||||
override fun onCompleted(download: Download) {
|
||||
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 onProgress(
|
||||
download: Download,
|
||||
etaInMilliSeconds: Long,
|
||||
downloadedBytesPerSecond: Long
|
||||
) {
|
||||
handleUpsert(download)
|
||||
}
|
||||
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 onPaused(download: Download) {
|
||||
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(
|
||||
download: Download,
|
||||
downloadBlocks: List<DownloadBlock>,
|
||||
@@ -60,7 +110,13 @@ class TransmissionScreenViewModel @Inject constructor(
|
||||
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) {
|
||||
@@ -89,7 +145,7 @@ class TransmissionScreenViewModel @Inject constructor(
|
||||
} else {
|
||||
// new item: add to head (or tail depending on preference)
|
||||
val newState = downloadToState(download)
|
||||
_downloads.add(0, newState)
|
||||
downloads.add(0, newState)
|
||||
idToState[newState.id] = newState
|
||||
}
|
||||
}
|
||||
@@ -97,19 +153,20 @@ class TransmissionScreenViewModel @Inject constructor(
|
||||
private fun removeOnMain(id: Int) {
|
||||
val state = idToState.remove(id)
|
||||
if (state != null) {
|
||||
_downloads.remove(state)
|
||||
downloads.remove(state)
|
||||
} else {
|
||||
val idx = _downloads.indexOfFirst { it.id == id }
|
||||
val idx = downloads.indexOfFirst { it.id == id }
|
||||
if (idx >= 0) {
|
||||
val removed = _downloads.removeAt(idx)
|
||||
val removed = downloads.removeAt(idx)
|
||||
idToState.remove(removed.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun downloadToState(download: Download): DownloadItemState {
|
||||
|
||||
private fun downloadToState(download: Download): VideoDownloadItemState {
|
||||
val filePath = download.file
|
||||
|
||||
return DownloadItemState(
|
||||
return VideoDownloadItemState(
|
||||
id = download.id,
|
||||
fileName = download.request.extras.getString("name", ""),
|
||||
filePath = filePath,
|
||||
@@ -119,7 +176,8 @@ class TransmissionScreenViewModel @Inject constructor(
|
||||
downloadedBytes = download.downloaded,
|
||||
totalBytes = download.total,
|
||||
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 {
|
||||
imageLoader = ImageLoader.Builder(context)
|
||||
.components {
|
||||
add(OkHttpNetworkFetcherFactory(createOkHttp()))
|
||||
}
|
||||
.build()
|
||||
|
||||
viewModelScope.launch {
|
||||
fetchManager.setListener(fetchListener)
|
||||
withContext(Dispatchers.Main) {
|
||||
fetchManager.getAllDownloads { list ->
|
||||
_downloads.clear()
|
||||
downloads.clear()
|
||||
idToState.clear()
|
||||
list.sortedBy { it.extras.getString("name", "") }.forEach { d ->
|
||||
val s = downloadToState(d)
|
||||
_downloads.add(s)
|
||||
downloads.add(s)
|
||||
idToState[s.id] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ import androidx.core.net.toUri
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||
import com.acitelight.aether.Global
|
||||
import com.acitelight.aether.model.KeyImage
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -99,7 +100,7 @@ class VideoPlayerViewModel @Inject constructor(
|
||||
return
|
||||
_init = true
|
||||
|
||||
val vs = videoId.hexToString().split(",").map { it.split("/") }
|
||||
val vs = videoId.hexToString().split(",").map { it.split("/") }.toMutableList()
|
||||
imageLoader = ImageLoader.Builder(context)
|
||||
.components {
|
||||
add(OkHttpNetworkFetcherFactory(createOkHttp()))
|
||||
@@ -107,11 +108,11 @@ class VideoPlayerViewModel @Inject constructor(
|
||||
.build()
|
||||
|
||||
viewModelScope.launch {
|
||||
videos = mediaManager.queryVideoBulk(vs.first()[0], vs.map { it[1] })!!
|
||||
|
||||
val ii = database.userDao().getAll().first()
|
||||
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(
|
||||
if (ix != null)
|
||||
videos.first { it.id == ix.id }
|
||||
|
||||
Reference in New Issue
Block a user