This commit is contained in:
Marek
2026-04-05 23:26:55 +02:00
parent b3b4410ee0
commit b6635a107d
21 changed files with 229 additions and 75 deletions

View File

@@ -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)

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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),

View File

@@ -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),

View File

@@ -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) {

View File

@@ -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()
}
}