update
This commit is contained in:
@@ -24,6 +24,13 @@ class LocalStorageService(private val context: Context) {
|
||||
|
||||
fun deleteLocalFile(videoId: Int): Boolean = videoFile(videoId).delete()
|
||||
|
||||
fun getLocalVideoIds(): List<Int> {
|
||||
return videosDir().listFiles()
|
||||
?.filter { it.extension == "mp4" }
|
||||
?.mapNotNull { it.nameWithoutExtension.toIntOrNull() }
|
||||
?: emptyList()
|
||||
}
|
||||
|
||||
fun downloadAndSave(videoId: Int): File {
|
||||
val url = "${ApiClient.BASE_URL}videos/$videoId/file"
|
||||
val file = videoFile(videoId)
|
||||
|
||||
@@ -15,8 +15,11 @@ interface VideoApi {
|
||||
@POST("videos/{id}/download")
|
||||
suspend fun triggerDownload(@Path("id") id: Int): Map<String, String>
|
||||
|
||||
@retrofit2.http.DELETE("videos")
|
||||
suspend fun deleteNotDownloaded(@Query("profile_id") profileId: Int): Map<String, Int>
|
||||
@POST("videos/cleanup")
|
||||
suspend fun cleanupVideos(@retrofit2.http.Body body: Map<String, @JvmSuppressWildcards Any>): Map<String, Int>
|
||||
|
||||
@retrofit2.http.DELETE("videos/{id}/file")
|
||||
suspend fun deleteServerFile(@Path("id") id: Int): Map<String, String>
|
||||
|
||||
@GET("profiles")
|
||||
suspend fun getProfiles(): List<Profile>
|
||||
|
||||
@@ -13,8 +13,14 @@ class VideoRepository(private val api: VideoApi = ApiClient.api) {
|
||||
response["status"] ?: "unknown"
|
||||
}
|
||||
|
||||
suspend fun deleteNotDownloaded(profileId: Int): Result<Int> = runCatching {
|
||||
val response = api.deleteNotDownloaded(profileId)
|
||||
suspend fun deleteServerFile(videoId: Int): Result<String> = runCatching {
|
||||
val response = api.deleteServerFile(videoId)
|
||||
response["status"] ?: "unknown"
|
||||
}
|
||||
|
||||
suspend fun cleanupVideos(profileId: Int, excludeIds: List<Int>): Result<Int> = runCatching {
|
||||
val body = mapOf("profile_id" to profileId, "exclude_ids" to excludeIds)
|
||||
val response = api.cleanupVideos(body)
|
||||
response["deleted"] ?: 0
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ fun AllVideosScreen(viewModel: VideoViewModel, onVideoClick: (Int) -> Unit) {
|
||||
}
|
||||
else -> {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(minSize = 250.dp),
|
||||
columns = GridCells.Adaptive(minSize = 160.dp),
|
||||
contentPadding = PaddingValues(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
|
||||
@@ -52,7 +52,7 @@ fun DownloadedScreen(viewModel: VideoViewModel, onVideoClick: (Int) -> Unit) {
|
||||
}
|
||||
else -> {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(minSize = 250.dp),
|
||||
columns = GridCells.Adaptive(minSize = 160.dp),
|
||||
contentPadding = PaddingValues(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
|
||||
@@ -48,7 +48,7 @@ fun VideoDetailScreen(
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val video = viewModel.getVideoById(videoId)
|
||||
val isLocal = viewModel.isLocallyAvailable(videoId)
|
||||
val isLocal = remember(state.downloadStatus) { viewModel.isLocallyAvailable(videoId) }
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
LaunchedEffect(state.downloadStatus) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.youtubeapp.ui.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.youtubeapp.data.ApiClient
|
||||
import com.youtubeapp.data.LocalStorageService
|
||||
import com.youtubeapp.data.Profile
|
||||
import com.youtubeapp.data.Video
|
||||
@@ -12,6 +14,11 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.WebSocket
|
||||
import okhttp3.WebSocketListener
|
||||
|
||||
data class VideoUiState(
|
||||
val allVideos: List<Video> = emptyList(),
|
||||
@@ -28,6 +35,7 @@ class VideoViewModel : ViewModel() {
|
||||
private val repository = VideoRepository()
|
||||
private var localStorage: LocalStorageService? = null
|
||||
private var prefs: android.content.SharedPreferences? = null
|
||||
private var webSocket: WebSocket? = null
|
||||
private val _state = MutableStateFlow(VideoUiState())
|
||||
val state: StateFlow<VideoUiState> = _state
|
||||
|
||||
@@ -42,6 +50,37 @@ class VideoViewModel : ViewModel() {
|
||||
_state.value = _state.value.copy(selectedProfileId = savedId)
|
||||
}
|
||||
}
|
||||
if (webSocket == null) {
|
||||
connectWebSocket()
|
||||
}
|
||||
}
|
||||
|
||||
private fun connectWebSocket() {
|
||||
val wsUrl = ApiClient.BASE_URL.replace("http://", "ws://") + "ws"
|
||||
val request = Request.Builder().url(wsUrl).build()
|
||||
val client = OkHttpClient()
|
||||
webSocket = client.newWebSocket(request, object : WebSocketListener() {
|
||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||
val profileIds = text.split(",").mapNotNull { it.trim().toIntOrNull() }
|
||||
val selected = _state.value.selectedProfileId
|
||||
if (selected != null && selected in profileIds) {
|
||||
loadAllVideos()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||
Log.w("VideoViewModel", "WebSocket Fehler, reconnect in 5s", t)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
delay(5000)
|
||||
connectWebSocket()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
webSocket?.close(1000, null)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun loadProfiles() {
|
||||
@@ -79,9 +118,10 @@ class VideoViewModel : ViewModel() {
|
||||
fun loadDownloadedVideos() {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(isLoading = true, error = null)
|
||||
repository.getDownloadedVideos(profileId = _state.value.selectedProfileId)
|
||||
repository.getAllVideos(profileId = _state.value.selectedProfileId)
|
||||
.onSuccess { videos ->
|
||||
_state.value = _state.value.copy(downloadedVideos = videos, isLoading = false)
|
||||
val local = videos.filter { localStorage?.isLocallyAvailable(it.id) == true }
|
||||
_state.value = _state.value.copy(downloadedVideos = local, isLoading = false)
|
||||
}
|
||||
.onFailure { e ->
|
||||
_state.value = _state.value.copy(error = e.message, isLoading = false)
|
||||
@@ -113,6 +153,7 @@ class VideoViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
localStorage?.downloadAndSave(videoId)
|
||||
repository.deleteServerFile(videoId)
|
||||
_state.value = _state.value.copy(
|
||||
isDownloading = false,
|
||||
downloadStatus = "Lokal gespeichert"
|
||||
@@ -128,8 +169,9 @@ class VideoViewModel : ViewModel() {
|
||||
|
||||
fun deleteNotDownloaded() {
|
||||
val profileId = _state.value.selectedProfileId ?: return
|
||||
val localIds = localStorage?.getLocalVideoIds() ?: emptyList()
|
||||
viewModelScope.launch {
|
||||
repository.deleteNotDownloaded(profileId)
|
||||
repository.cleanupVideos(profileId, localIds)
|
||||
loadAllVideos()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user