Compare commits

2 Commits

8 changed files with 166 additions and 66 deletions

View File

@@ -4,6 +4,7 @@ import android.content.Context
import com.acitelight.aether.model.BookMark import com.acitelight.aether.model.BookMark
import com.acitelight.aether.model.Comic import com.acitelight.aether.model.Comic
import com.acitelight.aether.model.Video import com.acitelight.aether.model.Video
import com.acitelight.aether.model.VideoDownloadItemState
import com.tonyodev.fetch2.Status import com.tonyodev.fetch2.Status
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@@ -44,15 +45,34 @@ class MediaManager @Inject constructor(
} }
} }
suspend fun queryVideo(klass: String, id: String, model: VideoDownloadItemState): Video?
{
if(model.status == Status.COMPLETED)
{
val jsonString = File(
context.getExternalFilesDir(null),
"videos/$klass/$id/summary.json"
).readText()
return Json.decodeFromString<Video>(jsonString).toLocal(context.getExternalFilesDir(null)?.path!!)
}
try {
val j = ApiClient.api!!.queryVideo(klass, id, token)
return Video(klass = klass, id = id, token=token, isLocal = false, localBase = "", video = j)
}catch (_: Exception)
{
return null
}
}
suspend fun queryVideo(klass: String, id: String): Video? suspend fun queryVideo(klass: String, id: String): Video?
{ {
val downloaded = fetchManager.getAllDownloadsAsync().filter { val downloaded = fetchManager.getAllDownloadsAsync().filter {
it.status == Status.COMPLETED &&
it.extras.getString("id", "") == id && it.extras.getString("id", "") == id &&
it.extras.getString("class", "") == klass it.extras.getString("class", "") == klass
} }
if(!downloaded.isEmpty()) if(downloaded.all{ it.status == Status.COMPLETED })
{ {
val jsonString = File( val jsonString = File(
context.getExternalFilesDir(null), context.getExternalFilesDir(null),
@@ -72,16 +92,16 @@ class MediaManager @Inject constructor(
suspend fun queryVideoBulk(klass: String, id: List<String>): List<Video>? { suspend fun queryVideoBulk(klass: String, id: List<String>): List<Video>? {
return try { return try {
val completedDownloads = fetchManager.getAllDownloadsAsync() val downloads = fetchManager.getAllDownloadsAsync()
.filter { it.status == Status.COMPLETED }
val localIds = mutableSetOf<String>() val localIds = mutableSetOf<String>()
val remoteIds = mutableListOf<String>() val remoteIds = mutableListOf<String>()
for (videoId in id) { for (videoId in id) {
if (completedDownloads.any { if (downloads.filter {
it.extras.getString("id", "") == videoId && it.extras.getString("id", "") == videoId &&
it.extras.getString("class", "") == klass it.extras.getString("class", "") == klass
}) { }.all{ it.status == Status.COMPLETED} ) {
localIds.add(videoId) localIds.add(videoId)
} else { } else {
remoteIds.add(videoId) remoteIds.add(videoId)

View File

@@ -82,7 +82,7 @@ fun HomeScreen(
{ {
updateRelate(homeScreenViewModel.recentManager.recentVideo, i) updateRelate(homeScreenViewModel.recentManager.recentVideo, i)
val playList = mutableListOf("${i.klass}/${i.id}") val playList = mutableListOf<String>()
val fv = homeScreenViewModel.videoLibrary.classesMap.map { it.value }.flatten() val fv = homeScreenViewModel.videoLibrary.classesMap.map { it.value }.flatten()
val group = fv.filter { it.klass == i.klass && it.video.group == i.video.group } val group = fv.filter { it.klass == i.klass && it.video.group == i.video.group }
@@ -90,7 +90,7 @@ fun HomeScreen(
playList.add("${i.klass}/${i.id}") playList.add("${i.klass}/${i.id}")
} }
val route = "video_player_route/${playList.joinToString(",").toHex()}" val route = "video_player_route/${(playList.joinToString(",") + "|${i.id}").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

@@ -12,6 +12,8 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -28,7 +30,9 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun PlaylistPanel(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewModel) fun PlaylistPanel(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewModel)
{ {
val colorScheme = MaterialTheme.colorScheme
val name by videoPlayerViewModel.currentName val name by videoPlayerViewModel.currentName
val id by videoPlayerViewModel.currentId
LazyRow( LazyRow(
modifier = modifier modifier = modifier
@@ -48,7 +52,13 @@ fun PlaylistPanel(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewModel
videoPlayerViewModel.viewModelScope.launch { videoPlayerViewModel.viewModelScope.launch {
videoPlayerViewModel.startPlay(it) videoPlayerViewModel.startPlay(it)
} }
} },
colors =
if (it.id == id)
CardDefaults.cardColors(containerColor = colorScheme.primary)
else
CardDefaults.cardColors()
) { ) {
Box(Modifier.padding(8.dp).fillMaxSize()) Box(Modifier.padding(8.dp).fillMaxSize())
{ {

View File

@@ -50,6 +50,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import kotlin.collections.sortedWith
@Composable @Composable
fun TransmissionScreen( fun TransmissionScreen(
@@ -120,9 +121,12 @@ private fun VideoDownloadCard(
{ {
val downloaded = viewModel.fetchManager.getAllDownloadsAsync().filter { val downloaded = viewModel.fetchManager.getAllDownloadsAsync().filter {
it.status == Status.COMPLETED && it.extras.getString( it.status == Status.COMPLETED && it.extras.getString(
"isComic", "class",
"" ""
) != "true" ) != "comic" && it.extras.getString(
"type",
""
) == "main"
} }
val jsonQuery = downloaded.map { val jsonQuery = downloaded.map {
@@ -146,18 +150,18 @@ private fun VideoDownloadCard(
jsonQuery.first { it.id == model.vid && it.klass == model.klass } jsonQuery.first { it.id == model.vid && it.klass == model.klass }
) )
val playList = mutableListOf("${model.klass}/${model.vid}") val playList = mutableListOf<String>()
val fv = viewModel.videoLibrary.classesMap.map { it.value }.flatten() val fv = viewModel.videoLibrary.classesMap.map { it.value }.flatten()
val video = fv.firstOrNull { it.klass == model.klass && it.id == model.vid } val video = fv.firstOrNull { it.klass == model.klass && it.id == model.vid }
if (video != null) { if (video != null) {
val group = fv.filter { it.klass == video.klass && it.video.group == video.video.group } val group = fv.filter { it.klass == video.klass && it.video.group == video.video.group }
for (i in group) { for (i in group.sortedWith(compareBy(naturalOrder()) { it.video.name })) {
playList.add("${i.klass}/${i.id}") playList.add("${i.klass}/${i.id}")
} }
} }
val route = "video_player_route/${playList.joinToString(",").toHex()}" val route = "video_player_route/${(playList.joinToString(",") + "|${model.vid}").toHex()}"
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
navigator.navigate(route) navigator.navigate(route)
} }

View File

@@ -47,6 +47,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.acitelight.aether.Global import com.acitelight.aether.Global
import com.acitelight.aether.Global.updateRelate
import com.acitelight.aether.ToggleFullScreen import com.acitelight.aether.ToggleFullScreen
import com.acitelight.aether.viewModel.VideoPlayerViewModel import com.acitelight.aether.viewModel.VideoPlayerViewModel
@@ -108,9 +109,11 @@ fun VideoPlayerPortal(
val duration by videoPlayerViewModel.currentDuration val duration by videoPlayerViewModel.currentDuration
ToggleFullScreen(false) ToggleFullScreen(false)
Column(Modifier Column(
Modifier
.nestedScroll(nestedScrollConnection) .nestedScroll(nestedScrollConnection)
.fillMaxHeight()) .fillMaxHeight()
)
{ {
Box { Box {
PortalCorePlayer( PortalCorePlayer(
@@ -194,13 +197,15 @@ fun VideoPlayerPortal(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
) )
Row(Modifier Row(
Modifier
.align(Alignment.Start) .align(Alignment.Start)
.padding(horizontal = 4.dp) .padding(horizontal = 4.dp)
.alpha(0.5f)) { .alpha(0.5f)
) {
Text( Text(
modifier = Modifier.padding(horizontal = 8.dp), modifier = Modifier.padding(horizontal = 8.dp),
text = klass, text = "$klass.$id",
fontSize = 14.sp, fontSize = 14.sp,
maxLines = 1, maxLines = 1,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@@ -217,12 +222,18 @@ fun VideoPlayerPortal(
HorizontalDivider(Modifier.padding(vertical = 8.dp), 1.dp, DividerDefaults.color) HorizontalDivider(Modifier.padding(vertical = 8.dp), 1.dp, DividerDefaults.color)
if (videoPlayerViewModel.videos.size > 1) {
PlaylistPanel( PlaylistPanel(
Modifier, Modifier,
videoPlayerViewModel = videoPlayerViewModel videoPlayerViewModel = videoPlayerViewModel
) )
HorizontalDivider(Modifier.padding(vertical = 8.dp), 1.dp, DividerDefaults.color) HorizontalDivider(
Modifier.padding(vertical = 8.dp),
1.dp,
DividerDefaults.color
)
}
HorizontalGallery(videoPlayerViewModel) HorizontalGallery(videoPlayerViewModel)
HorizontalDivider(Modifier.padding(vertical = 8.dp), 1.dp, DividerDefaults.color) HorizontalDivider(Modifier.padding(vertical = 8.dp), 1.dp, DividerDefaults.color)
@@ -237,7 +248,16 @@ fun VideoPlayerPortal(
{ {
videoPlayerViewModel.isPlaying = false videoPlayerViewModel.isPlaying = false
videoPlayerViewModel.player?.pause() videoPlayerViewModel.player?.pause()
val route = "video_player_route/${"${i.klass}/${i.id}".toHex()}"
val playList = mutableListOf<String>()
val fv = videoPlayerViewModel.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)
}, videoPlayerViewModel.imageLoader!! }, videoPlayerViewModel.imageLoader!!
) )

View File

@@ -11,6 +11,7 @@ import com.acitelight.aether.model.Video
import com.acitelight.aether.model.VideoDownloadItemState import com.acitelight.aether.model.VideoDownloadItemState
import com.acitelight.aether.service.ApiClient.createOkHttp import com.acitelight.aether.service.ApiClient.createOkHttp
import com.acitelight.aether.service.FetchManager import com.acitelight.aether.service.FetchManager
import com.acitelight.aether.service.MediaManager
import com.acitelight.aether.service.VideoLibrary import com.acitelight.aether.service.VideoLibrary
import com.tonyodev.fetch2.Download import com.tonyodev.fetch2.Download
import com.tonyodev.fetch2.FetchListener import com.tonyodev.fetch2.FetchListener
@@ -27,6 +28,7 @@ class TransmissionScreenViewModel @Inject constructor(
val fetchManager: FetchManager, val fetchManager: FetchManager,
@ApplicationContext val context: Context, @ApplicationContext val context: Context,
val videoLibrary: VideoLibrary, val videoLibrary: VideoLibrary,
val mediaManager: MediaManager,
) : ViewModel() { ) : ViewModel() {
var imageLoader: ImageLoader? = null var imageLoader: ImageLoader? = null
val downloads: SnapshotStateList<VideoDownloadItemState> = mutableStateListOf() val downloads: SnapshotStateList<VideoDownloadItemState> = mutableStateListOf()
@@ -34,8 +36,7 @@ class TransmissionScreenViewModel @Inject constructor(
// map id -> state object reference (no index bookkeeping) // map id -> state object reference (no index bookkeeping)
private val idToState: MutableMap<Int, VideoDownloadItemState> = mutableMapOf() private val idToState: MutableMap<Int, VideoDownloadItemState> = mutableMapOf()
fun modelToVideo(model: VideoDownloadItemState): Video? fun modelToVideo(model: VideoDownloadItemState): Video? {
{
val fv = videoLibrary.classesMap.map { it.value }.flatten() val fv = videoLibrary.classesMap.map { it.value }.flatten()
return fv.firstOrNull { it.klass == model.klass && it.id == model.vid } return fv.firstOrNull { it.klass == model.klass && it.id == model.vid }
} }
@@ -54,9 +55,7 @@ class TransmissionScreenViewModel @Inject constructor(
} }
override fun onProgress( override fun onProgress(
download: Download, download: Download, etaInMilliSeconds: Long, downloadedBytesPerSecond: Long
etaInMilliSeconds: Long,
downloadedBytesPerSecond: Long
) { ) {
handleUpsert(download) handleUpsert(download)
} }
@@ -73,12 +72,21 @@ class TransmissionScreenViewModel @Inject constructor(
handleUpsert(download) handleUpsert(download)
if (download.extras.getString("type", "") == "main") { if (download.extras.getString("type", "") == "main") {
val ii = videoLibrary.classesMap[download.extras.getString("class", "")] val ii = videoLibrary.classesMap[download.extras.getString(
?.indexOfFirst { it.id == download.extras.getString("id", "") }!! "class",
""
)]?.indexOfFirst { it.id == download.extras.getString("id", "") }
val newi = videoLibrary.classesMap[download.extras.getString("class", "")]!![ii] if (ii != null) {
videoLibrary.classesMap[download.extras.getString("class", "")]!![ii] = val newi =
newi.toLocal(context.getExternalFilesDir(null)!!.path) videoLibrary.classesMap[download.extras.getString("class", "")]?.get(ii)
if (newi != null) videoLibrary.classesMap[download.extras.getString(
"class",
""
)]?.set(
ii, newi.toLocal(context.getExternalFilesDir(null)!!.path)
)
}
} }
} }
@@ -95,25 +103,19 @@ class TransmissionScreenViewModel @Inject constructor(
} }
override fun onDownloadBlockUpdated( override fun onDownloadBlockUpdated(
download: Download, download: Download, downloadBlock: DownloadBlock, totalBlocks: Int
downloadBlock: DownloadBlock,
totalBlocks: Int
) { ) {
handleUpsert(download) handleUpsert(download)
} }
override fun onStarted( override fun onStarted(
download: Download, download: Download, downloadBlocks: List<DownloadBlock>, totalBlocks: Int
downloadBlocks: List<DownloadBlock>,
totalBlocks: Int
) { ) {
handleUpsert(download) handleUpsert(download)
} }
override fun onError( override fun onError(
download: Download, download: Download, error: com.tonyodev.fetch2.Error, throwable: Throwable?
error: com.tonyodev.fetch2.Error,
throwable: Throwable?
) { ) {
handleUpsert(download) handleUpsert(download)
} }
@@ -123,6 +125,23 @@ class TransmissionScreenViewModel @Inject constructor(
viewModelScope.launch(Dispatchers.Main) { viewModelScope.launch(Dispatchers.Main) {
upsertOnMain(download) upsertOnMain(download)
} }
val state = downloadToState(download)
if (videoLibrary.classes.contains(state.klass)) videoLibrary.classes.add(state.klass)
if (!videoLibrary.classesMap.containsKey(state.klass)) videoLibrary.classesMap[state.klass] =
mutableStateListOf()
if (videoLibrary.classesMap[state.klass]?.any { it.id == state.vid } != true) {
viewModelScope.launch(Dispatchers.IO) {
val v = mediaManager.queryVideo(state.klass, state.vid, state)
if (v != null) {
videoLibrary.classesMap[state.klass]?.add(v)
}
}
}
} }
private fun handleRemove(id: Int) { private fun handleRemove(id: Int) {
@@ -198,22 +217,31 @@ class TransmissionScreenViewModel @Inject constructor(
} }
init { init {
imageLoader = ImageLoader.Builder(context) imageLoader = ImageLoader.Builder(context).components {
.components {
add(OkHttpNetworkFetcherFactory(createOkHttp())) add(OkHttpNetworkFetcherFactory(createOkHttp()))
} }.build()
.build()
viewModelScope.launch { viewModelScope.launch {
fetchManager.setListener(fetchListener) fetchManager.setListener(fetchListener)
withContext(Dispatchers.Main) { val downloaded = fetchManager.getAllDownloadsAsync()
fetchManager.getAllDownloads { list ->
downloads.clear() downloads.clear()
idToState.clear() idToState.clear()
list.sortedBy { it.extras.getString("name", "") }.forEach { d -> downloaded.sortedWith(compareBy(naturalOrder()) { 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
if (videoLibrary.classes.contains(s.klass)) videoLibrary.classes.add(s.klass)
if (!videoLibrary.classesMap.containsKey(s.klass)) videoLibrary.classesMap[s.klass] =
mutableStateListOf()
if (videoLibrary.classesMap[s.klass]?.any { it.id == s.vid } != true) {
val v = mediaManager.queryVideo(s.klass, s.vid, s)
if (v != null) {
videoLibrary.classesMap[s.klass]?.add(v)
} }
} }
} }

View File

@@ -49,6 +49,7 @@ 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.Global
import com.acitelight.aether.model.KeyImage import com.acitelight.aether.model.KeyImage
import com.acitelight.aether.service.VideoLibrary
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -56,7 +57,8 @@ import kotlinx.coroutines.flow.first
class VideoPlayerViewModel @Inject constructor( class VideoPlayerViewModel @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
val mediaManager: MediaManager, val mediaManager: MediaManager,
val recentManager: RecentManager val recentManager: RecentManager,
val videoLibrary: VideoLibrary,
) : ViewModel() { ) : ViewModel() {
var tabIndex by mutableIntStateOf(0) var tabIndex by mutableIntStateOf(0)
var isPlaying by mutableStateOf(true) var isPlaying by mutableStateOf(true)
@@ -100,7 +102,16 @@ class VideoPlayerViewModel @Inject constructor(
return return
_init = true _init = true
val vs = videoId.hexToString().split(",").map { it.split("/") }.toMutableList() val oId = videoId.hexToString()
var spec = "-1"
var vs = mutableListOf<List<String>>()
if(oId.contains("|"))
{
vs = oId.split("|")[0].split(",").map { it.split("/") }.toMutableList()
spec = oId.split("|")[1]
}
imageLoader = ImageLoader.Builder(context) imageLoader = ImageLoader.Builder(context)
.components { .components {
add(OkHttpNetworkFetcherFactory(createOkHttp())) add(OkHttpNetworkFetcherFactory(createOkHttp()))
@@ -114,7 +125,9 @@ class VideoPlayerViewModel @Inject constructor(
videos = mediaManager.queryVideoBulk(vs.first()[0], vs.map { it[1] })!! videos = mediaManager.queryVideoBulk(vs.first()[0], vs.map { it[1] })!!
startPlay( startPlay(
if (ix != null) if(spec != "-1")
videos.first { it.id == spec}
else if (ix != null)
videos.first { it.id == ix.id } videos.first { it.id == ix.id }
else videos.first() else videos.first()
) )

View File

@@ -54,13 +54,16 @@ class VideoScreenViewModel @Inject constructor(
if (Global.loggedIn) { if (Global.loggedIn) {
videoLibrary.classes.addAll(mediaManager.listVideoKlasses()) videoLibrary.classes.addAll(mediaManager.listVideoKlasses())
videoLibrary.classes.distinct()
if(videoLibrary.classes.isEmpty()) if(videoLibrary.classes.isEmpty())
return return
var i = 0 var i = 0
for (it in videoLibrary.classes) { for (it in videoLibrary.classes) {
videoLibrary.updatingMap[i++] = false videoLibrary.updatingMap[i++] = false
videoLibrary.classesMap[it] = mutableStateListOf<Video>() if(!videoLibrary.classesMap.containsKey(it))
videoLibrary.classesMap[it] = mutableStateListOf()
} }
videoLibrary.updatingMap[0] = true videoLibrary.updatingMap[0] = true
val vl = val vl =
@@ -69,6 +72,7 @@ class VideoScreenViewModel @Inject constructor(
if (vl != null) { if (vl != null) {
val r = vl.sortedWith(compareBy(naturalOrder()) { it.video.name }) val r = vl.sortedWith(compareBy(naturalOrder()) { it.video.name })
videoLibrary.classesMap[videoLibrary.classes[0]]?.addAll(r) videoLibrary.classesMap[videoLibrary.classes[0]]?.addAll(r)
videoLibrary.classesMap[videoLibrary.classes[0]]?.distinctBy { it.id }
} }
} }
else { else {
@@ -77,7 +81,7 @@ class VideoScreenViewModel @Inject constructor(
videoLibrary.classesMap["Offline"] = mutableStateListOf<Video>() videoLibrary.classesMap["Offline"] = mutableStateListOf<Video>()
val downloaded = fetchManager.getAllDownloadsAsync().filter { val downloaded = fetchManager.getAllDownloadsAsync().filter {
it.status == Status.COMPLETED && it.extras.getString("isComic", "") != "true" it.status == Status.COMPLETED && it.extras.getString("class", "") != "comic"
} }
val jsonQuery = downloaded.map{ File( val jsonQuery = downloaded.map{ File(
@@ -107,6 +111,7 @@ class VideoScreenViewModel @Inject constructor(
if (vl != null) { if (vl != null) {
val r = vl.sortedWith(compareBy(naturalOrder()) { it.video.name }) val r = vl.sortedWith(compareBy(naturalOrder()) { it.video.name })
videoLibrary.classesMap[videoLibrary.classes[index]]?.addAll(r) videoLibrary.classesMap[videoLibrary.classes[index]]?.addAll(r)
videoLibrary.classesMap[videoLibrary.classes[index]]?.distinctBy { it.id }
} }
} }
} }