[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 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)
|
||||||
@@ -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}"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
Reference in New Issue
Block a user