[feat] Complete video caching system

This commit is contained in:
acite
2025-09-15 03:15:43 +08:00
parent ad51c5da2f
commit e94249aa8f
22 changed files with 613 additions and 211 deletions

View File

@@ -1,5 +1,6 @@
package com.acitelight.aether.viewModel
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
@@ -17,11 +18,13 @@ import com.acitelight.aether.service.ApiClient.createOkHttp
import com.acitelight.aether.service.MediaManager
import com.acitelight.aether.service.RecentManager
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class ComicGridViewModel @Inject constructor(
@ApplicationContext val context: Context,
val mediaManager: MediaManager
) : ViewModel()
{
@@ -31,22 +34,17 @@ class ComicGridViewModel @Inject constructor(
var db: ComicRecordDatabase? = null
var record = mutableStateOf<ComicRecord?>(null)
@Composable
fun SetupClient()
{
val context = LocalContext.current
init {
imageLoader = ImageLoader.Builder(context)
.components {
add(OkHttpNetworkFetcherFactory(createOkHttp()))
}
.build()
db = remember {
try{
db = try{
ComicRecordDatabase.getDatabase(context)
}catch (e: Exception) {
print(e.message)
} as ComicRecordDatabase?
}
}
fun resolve(id: String)

View File

@@ -1,5 +1,6 @@
package com.acitelight.aether.viewModel
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.ui.platform.LocalContext
@@ -12,6 +13,7 @@ import com.acitelight.aether.model.ComicResponse
import com.acitelight.aether.service.ApiClient.createOkHttp
import com.acitelight.aether.service.MediaManager
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
@@ -19,6 +21,7 @@ import javax.inject.Inject
@HiltViewModel
class ComicScreenViewModel @Inject constructor(
@ApplicationContext private val context: Context,
val mediaManager: MediaManager
) : ViewModel() {
@@ -48,18 +51,13 @@ class ComicScreenViewModel @Inject constructor(
}
}
@Composable
fun SetupClient()
{
val context = LocalContext.current
init {
imageLoader = ImageLoader.Builder(context)
.components {
add(OkHttpNetworkFetcherFactory(createOkHttp()))
}
.build()
}
init {
viewModelScope.launch {
val l = mediaManager.listComics()
val m = mediaManager.queryComicInfoBulk(l)

View File

@@ -5,11 +5,13 @@ import android.content.Context
import android.widget.Toast
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.LocalContext
import androidx.core.net.toUri
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.acitelight.aether.AetherApp
import com.acitelight.aether.Global
import com.acitelight.aether.dataStore
import com.acitelight.aether.model.Video
@@ -25,6 +27,8 @@ import javax.inject.Inject
import com.acitelight.aether.service.*
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@HiltViewModel
class MeScreenViewModel @Inject constructor(
@@ -59,10 +63,19 @@ class MeScreenViewModel @Inject constructor(
)!!
Global.loggedIn = true
withContext(Dispatchers.IO)
{
(context as AetherApp).abyssService?.proxy?.config(ApiClient.getBase().toUri().host!!, 4096)
context.abyssService?.downloader?.init()
}
}catch(e: Exception)
{
Global.loggedIn = false
print(e.message)
withContext(Dispatchers.IO)
{
(context as AetherApp).abyssService?.downloader?.init()
}
Toast.makeText(context, "Error: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
}
@@ -94,6 +107,11 @@ class MeScreenViewModel @Inject constructor(
)!!
Global.loggedIn = true
withContext(Dispatchers.IO)
{
(context as AetherApp).abyssService?.proxy?.config(ApiClient.getBase().toUri().host!!, 4096)
context.abyssService?.downloader?.init()
}
Toast.makeText(context, "Server Updated, Used Url: $usedUrl", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
print(e.message)
@@ -124,6 +142,11 @@ class MeScreenViewModel @Inject constructor(
)!!
Global.loggedIn = true
withContext(Dispatchers.IO)
{
(context as AetherApp).abyssService?.proxy?.config(ApiClient.getBase().toUri().host!!, 4096)
context.abyssService?.downloader?.init()
}
Toast.makeText(context, "Account Updated", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
print(e.message)

View File

@@ -1,25 +1,28 @@
package com.acitelight.aether.viewModel
import android.content.Context
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 com.acitelight.aether.service.FetchManager
import com.acitelight.aether.service.VideoLibrary
import com.tonyodev.fetch2.Download
import com.tonyodev.fetch2.FetchListener
import com.tonyodev.fetch2core.DownloadBlock
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import java.io.File
import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
class TransmissionScreenViewModel @Inject constructor(
private val fetchManager: FetchManager
val fetchManager: FetchManager,
@ApplicationContext val context: Context,
private val videoLibrary: VideoLibrary
) : ViewModel() {
private val _downloads: SnapshotStateList<DownloadItemState> = mutableStateListOf()
val downloads: SnapshotStateList<DownloadItemState> = _downloads
@@ -37,11 +40,18 @@ class TransmissionScreenViewModel @Inject constructor(
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) { 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)
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, blockCount: Int) { handleUpsert(download) }
override fun onDownloadBlockUpdated(download: Download, downloadBlock: DownloadBlock, totalBlocks: Int) { handleUpsert(download) }
override fun onStarted(
download: Download,
downloadBlocks: List<DownloadBlock>,
@@ -107,7 +117,9 @@ class TransmissionScreenViewModel @Inject constructor(
progress = download.progress,
status = download.status,
downloadedBytes = download.downloaded,
totalBytes = download.total
totalBytes = download.total,
klass = download.extras.getString("class", ""),
vid = download.extras.getString("id", "")
)
}
@@ -128,15 +140,17 @@ class TransmissionScreenViewModel @Inject constructor(
}
init {
fetchManager.setListener(fetchListener)
viewModelScope.launch(Dispatchers.Main) {
fetchManager.getAllDownloads { list ->
_downloads.clear()
idToState.clear()
list.sortedByDescending { it.id }.forEach { d ->
val s = downloadToState(d)
_downloads.add(s)
idToState[s.id] = s
viewModelScope.launch {
fetchManager.setListener(fetchListener)
withContext(Dispatchers.Main) {
fetchManager.getAllDownloads { list ->
_downloads.clear()
idToState.clear()
list.sortedBy { it.extras.getString("name", "") }.forEach { d ->
val s = downloadToState(d)
_downloads.add(s)
idToState[s.id] = s
}
}
}
}

View File

@@ -1,6 +1,7 @@
package com.acitelight.aether.viewModel
import android.app.Activity
import android.net.Uri
import androidx.annotation.OptIn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
@@ -14,8 +15,10 @@ import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.Player.STATE_READY
import androidx.media3.common.util.Log
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.ExoPlayer
@@ -34,16 +37,16 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.File
import javax.inject.Inject
@HiltViewModel
class VideoPlayerViewModel @Inject constructor(
val mediaManager: MediaManager,
val recentManager: RecentManager
) : ViewModel()
{
) : ViewModel() {
var tabIndex by mutableIntStateOf(0)
var isPlaying by mutableStateOf(true)
var isPlaying by mutableStateOf(true)
var playProcess by mutableFloatStateOf(0.0f)
var planeVisibility by mutableStateOf(true)
var isLongPressing by mutableStateOf(false)
@@ -65,17 +68,16 @@ class VideoPlayerViewModel @Inject constructor(
val dataSourceFactory = OkHttpDataSource.Factory(createOkHttp())
var imageLoader: ImageLoader? = null;
var brit by mutableFloatStateOf(0.5f)
var brit by mutableFloatStateOf(0.5f)
@OptIn(UnstableApi::class)
@Composable
fun Init(videoId: String)
{
if(_init) return;
fun Init(videoId: String) {
if (_init) return;
val context = LocalContext.current
val v = videoId.hexToString()
imageLoader = ImageLoader.Builder(context)
imageLoader = ImageLoader.Builder(context)
.components {
add(OkHttpNetworkFetcherFactory(createOkHttp()))
}
@@ -85,29 +87,35 @@ class VideoPlayerViewModel @Inject constructor(
viewModelScope.launch {
video = mediaManager.queryVideo(v.split("/")[0], v.split("/")[1])!!
recentManager.Push(context, VideoQueryIndex(v.split("/")[0], v.split("/")[1]))
_player = ExoPlayer
.Builder(context)
.setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))
_player = (if(video!!.isLocal) ExoPlayer.Builder(context) else ExoPlayer.Builder(context).setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory)))
.build().apply {
val url = video?.getVideo() ?: ""
val mediaItem = MediaItem.fromUri(url)
setMediaItem(mediaItem)
prepare()
playWhenReady = true
val url = video?.getVideo() ?: ""
val mediaItem = if (video!!.isLocal)
MediaItem.fromUri(Uri.fromFile(File(url)))
else
MediaItem.fromUri(url)
addListener(object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
if (playbackState == STATE_READY) {
startPlaying = true
setMediaItem(mediaItem)
prepare()
playWhenReady = true
addListener(object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
if (playbackState == STATE_READY) {
startPlaying = true
}
}
}
override fun onRenderedFirstFrame() {
super.onRenderedFirstFrame()
renderedFirst = true
}
})
}
override fun onRenderedFirstFrame() {
super.onRenderedFirstFrame()
renderedFirst = true
}
override fun onPlayerError(error: PlaybackException) {
Log.e("ExoPlayer", "Playback error: ", error)
}
})
}
startListen()
}
}
@@ -116,8 +124,7 @@ class VideoPlayerViewModel @Inject constructor(
}
@OptIn(UnstableApi::class)
fun startListen()
{
fun startListen() {
CoroutineScope(Dispatchers.Main).launch {
while (_player?.isReleased != true) {
val __player = _player!!;

View File

@@ -13,86 +13,106 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import coil3.ImageLoader
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import com.acitelight.aether.Global
import com.acitelight.aether.model.Video
import com.acitelight.aether.service.ApiClient.createOkHttp
import com.acitelight.aether.service.FetchManager
import com.acitelight.aether.service.MediaManager
import com.acitelight.aether.service.RecentManager
import com.acitelight.aether.service.VideoLibrary
import com.tonyodev.fetch2.Status
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import java.io.File
import javax.inject.Inject
import javax.inject.Singleton
@HiltViewModel
class VideoScreenViewModel @Inject constructor(
private val fetchManager: FetchManager,
@ApplicationContext val context: Context,
val mediaManager: MediaManager,
val recentManager: RecentManager
) : ViewModel()
{
val recentManager: RecentManager,
val videoLibrary: VideoLibrary
) : ViewModel() {
private val _tabIndex = mutableIntStateOf(0)
val tabIndex: State<Int> = _tabIndex
// val videos = mutableStateListOf<Video>()
var classes = mutableStateListOf<String>()
val classesMap = mutableStateMapOf<String, SnapshotStateList<Video>>()
var imageLoader: ImageLoader? = null;
val updatingMap: MutableMap<Int, Boolean> = mutableMapOf()
@Composable
fun SetupClient()
{
val context = LocalContext.current
imageLoader = ImageLoader.Builder(context)
.components {
add(OkHttpNetworkFetcherFactory(createOkHttp()))
}
.build()
}
suspend fun init() {
classes.addAll(mediaManager.listVideoKlasses())
var i = 0
for(it in classes)
{
updatingMap[i++] = false
classesMap[it] = mutableStateListOf<Video>()
}
updatingMap[0] = true
val vl = mediaManager.queryVideoBulk(classes[0], mediaManager.queryVideoKlasses(classes[0]))
fetchManager.configured.filter { it }.first()
if(vl != null){
val r = vl.sortedWith(compareBy(naturalOrder()) { it.video.name })
classesMap[classes[0]]?.addAll(r)
if (Global.loggedIn) {
videoLibrary.classes.addAll(mediaManager.listVideoKlasses())
var i = 0
for (it in videoLibrary.classes) {
videoLibrary.updatingMap[i++] = false
videoLibrary.classesMap[it] = mutableStateListOf<Video>()
}
videoLibrary.updatingMap[0] = true
val vl =
mediaManager.queryVideoBulk(videoLibrary.classes[0], mediaManager.queryVideoKlasses(videoLibrary.classes[0]))
if (vl != null) {
val r = vl.sortedWith(compareBy(naturalOrder()) { it.video.name })
videoLibrary.classesMap[videoLibrary.classes[0]]?.addAll(r)
}
} else {
videoLibrary.classes.add("Offline")
videoLibrary.updatingMap[0] = true
videoLibrary.classesMap["Offline"] = mutableStateListOf<Video>()
val downloaded = fetchManager.getAllDownloadsAsync().filter {
it.status == Status.COMPLETED && it.extras.getString("isComic", "") != "true"
}
val jsonQuery = downloaded.map{ File(
context.getExternalFilesDir(null),
"videos/${it.extras.getString("class", "")}/${it.extras.getString("id", "")}/summary.json").readText() }
.map { Json.decodeFromString<Video>(it).toLocal(context.getExternalFilesDir(null)!!.path) }
videoLibrary.classesMap[videoLibrary.classes[0]]?.addAll(jsonQuery)
}
}
fun setTabIndex(index: Int)
{
fun setTabIndex(index: Int) {
viewModelScope.launch()
{
_tabIndex.intValue = index;
if(updatingMap[index] == true) return@launch
if (videoLibrary.updatingMap[index] == true) return@launch
updatingMap[index] = true
videoLibrary.updatingMap[index] = true
val vl = mediaManager.queryVideoBulk(classes[index], mediaManager.queryVideoKlasses(classes[index]))
val vl = mediaManager.queryVideoBulk(
videoLibrary.classes[index],
mediaManager.queryVideoKlasses(videoLibrary.classes[index])
)
if(vl != null){
if (vl != null) {
val r = vl.sortedWith(compareBy(naturalOrder()) { it.video.name })
classesMap[classes[index]]?.addAll(r)
videoLibrary.classesMap[videoLibrary.classes[index]]?.addAll(r)
}
}
}
fun download(video :Video)
{
suspend fun download(video: Video) {
fetchManager.startVideoDownload(video)
}
init {
viewModelScope.launch {
imageLoader = ImageLoader.Builder(context)
.components {
add(OkHttpNetworkFetcherFactory(createOkHttp()))
}
.build()
viewModelScope.launch(Dispatchers.IO) {
init()
}
}