From 6e96c5ee994ff270f4dc95f6d54b0d0b9d2c40ff Mon Sep 17 00:00:00 2001 From: Marek Date: Sun, 5 Apr 2026 15:38:01 +0200 Subject: [PATCH] update --- .../youtubeapp/data/LocalStorageService.kt | 37 ++++++++++ .../main/java/com/youtubeapp/data/Video.kt | 3 +- .../youtubeapp/ui/navigation/AppNavigation.kt | 3 + .../ui/screens/VideoDetailScreen.kt | 50 +++++++++---- .../ui/screens/VideoPlayerScreen.kt | 4 +- .../youtubeapp/ui/viewmodel/VideoViewModel.kt | 67 +++++++++++++++--- backend/models.py | 5 +- .../routes/__pycache__/videos.cpython-312.pyc | Bin 5552 -> 5590 bytes backend/routes/videos.py | 3 +- backend/schemas.py | 4 -- .../download_service.cpython-312.pyc | Bin 2454 -> 977 bytes backend/services/download_service.py | 31 -------- backend/services/stream_service.py | 32 +++++++++ 13 files changed, 174 insertions(+), 65 deletions(-) create mode 100644 app/frontend/src/main/java/com/youtubeapp/data/LocalStorageService.kt create mode 100644 backend/services/stream_service.py diff --git a/app/frontend/src/main/java/com/youtubeapp/data/LocalStorageService.kt b/app/frontend/src/main/java/com/youtubeapp/data/LocalStorageService.kt new file mode 100644 index 0000000..88c3d05 --- /dev/null +++ b/app/frontend/src/main/java/com/youtubeapp/data/LocalStorageService.kt @@ -0,0 +1,37 @@ +package com.youtubeapp.data + +import android.content.Context +import java.io.File +import java.io.FileOutputStream +import java.net.URL + +class LocalStorageService(private val context: Context) { + + private fun videosDir(): File { + val dir = File(context.filesDir, "videos") + if (!dir.exists()) dir.mkdirs() + return dir + } + + private fun videoFile(videoId: Int): File = File(videosDir(), "$videoId.mp4") + + fun isLocallyAvailable(videoId: Int): Boolean = videoFile(videoId).exists() + + fun getLocalFile(videoId: Int): File? { + val file = videoFile(videoId) + return if (file.exists()) file else null + } + + fun deleteLocalFile(videoId: Int): Boolean = videoFile(videoId).delete() + + fun downloadAndSave(videoId: Int): File { + val url = "${ApiClient.BASE_URL}videos/$videoId/file" + val file = videoFile(videoId) + URL(url).openStream().use { input -> + FileOutputStream(file).use { output -> + input.copyTo(output) + } + } + return file + } +} diff --git a/app/frontend/src/main/java/com/youtubeapp/data/Video.kt b/app/frontend/src/main/java/com/youtubeapp/data/Video.kt index d4c6d78..ed2f442 100644 --- a/app/frontend/src/main/java/com/youtubeapp/data/Video.kt +++ b/app/frontend/src/main/java/com/youtubeapp/data/Video.kt @@ -6,6 +6,5 @@ data class Video( val youtuber: String, val thumbnail_url: String, val youtube_url: String, - val is_downloaded: Boolean, - val created_at: String + val is_downloaded: Boolean ) diff --git a/app/frontend/src/main/java/com/youtubeapp/ui/navigation/AppNavigation.kt b/app/frontend/src/main/java/com/youtubeapp/ui/navigation/AppNavigation.kt index e9ab6e1..93dcf99 100644 --- a/app/frontend/src/main/java/com/youtubeapp/ui/navigation/AppNavigation.kt +++ b/app/frontend/src/main/java/com/youtubeapp/ui/navigation/AppNavigation.kt @@ -12,6 +12,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavType @@ -30,6 +31,8 @@ import com.youtubeapp.ui.viewmodel.VideoViewModel fun AppNavigation() { val navController = rememberNavController() val viewModel: VideoViewModel = viewModel() + val context = LocalContext.current + viewModel.init(context) val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route diff --git a/app/frontend/src/main/java/com/youtubeapp/ui/screens/VideoDetailScreen.kt b/app/frontend/src/main/java/com/youtubeapp/ui/screens/VideoDetailScreen.kt index 923c46f..256f535 100644 --- a/app/frontend/src/main/java/com/youtubeapp/ui/screens/VideoDetailScreen.kt +++ b/app/frontend/src/main/java/com/youtubeapp/ui/screens/VideoDetailScreen.kt @@ -11,9 +11,11 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -45,16 +47,12 @@ fun VideoDetailScreen( ) { val state by viewModel.state.collectAsState() val video = viewModel.getVideoById(videoId) + val isLocal = viewModel.isLocallyAvailable(videoId) val snackbarHostState = remember { SnackbarHostState() } LaunchedEffect(state.downloadStatus) { state.downloadStatus?.let { status -> - val message = when (status) { - "download_started" -> "Download gestartet" - "already_downloaded" -> "Bereits heruntergeladen" - else -> status - } - snackbarHostState.showSnackbar(message) + snackbarHostState.showSnackbar(status) viewModel.clearDownloadStatus() } } @@ -101,7 +99,11 @@ fun VideoDetailScreen( ) Spacer(modifier = Modifier.height(4.dp)) Text( - text = if (video.is_downloaded) "Auf Server heruntergeladen" else "Noch nicht heruntergeladen", + text = when { + isLocal -> "Lokal gespeichert" + video.is_downloaded -> "Auf Server heruntergeladen" + else -> "Noch nicht heruntergeladen" + }, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -117,12 +119,34 @@ fun VideoDetailScreen( Icon(Icons.Default.PlayArrow, contentDescription = null) Text(" Abspielen") } - OutlinedButton( - onClick = { viewModel.triggerDownload(videoId) }, - modifier = Modifier.weight(1f) - ) { - Icon(Icons.Default.Download, contentDescription = null) - Text(" Download") + if (state.isDownloading) { + OutlinedButton( + onClick = {}, + enabled = false, + modifier = Modifier.weight(1f) + ) { + CircularProgressIndicator( + modifier = Modifier.height(20.dp), + strokeWidth = 2.dp + ) + Text(" Download...") + } + } else if (isLocal) { + OutlinedButton( + onClick = { viewModel.deleteLocalVideo(videoId) }, + modifier = Modifier.weight(1f) + ) { + Icon(Icons.Default.Delete, contentDescription = null) + Text(" Loeschen") + } + } else { + OutlinedButton( + onClick = { viewModel.triggerDownload(videoId) }, + modifier = Modifier.weight(1f) + ) { + Icon(Icons.Default.Download, contentDescription = null) + Text(" Download") + } } } } diff --git a/app/frontend/src/main/java/com/youtubeapp/ui/screens/VideoPlayerScreen.kt b/app/frontend/src/main/java/com/youtubeapp/ui/screens/VideoPlayerScreen.kt index 4eeb09b..a3ad770 100644 --- a/app/frontend/src/main/java/com/youtubeapp/ui/screens/VideoPlayerScreen.kt +++ b/app/frontend/src/main/java/com/youtubeapp/ui/screens/VideoPlayerScreen.kt @@ -20,11 +20,11 @@ import com.youtubeapp.ui.viewmodel.VideoViewModel @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) fun VideoPlayerScreen(videoId: Int, viewModel: VideoViewModel) { val context = LocalContext.current - val streamUrl = viewModel.getStreamUrl(videoId) + val playbackUri = viewModel.getPlaybackUri(videoId) val exoPlayer = remember { ExoPlayer.Builder(context).build().apply { - setMediaItem(MediaItem.fromUri(streamUrl)) + setMediaItem(MediaItem.fromUri(playbackUri)) prepare() playWhenReady = true } diff --git a/app/frontend/src/main/java/com/youtubeapp/ui/viewmodel/VideoViewModel.kt b/app/frontend/src/main/java/com/youtubeapp/ui/viewmodel/VideoViewModel.kt index ab67104..9728449 100644 --- a/app/frontend/src/main/java/com/youtubeapp/ui/viewmodel/VideoViewModel.kt +++ b/app/frontend/src/main/java/com/youtubeapp/ui/viewmodel/VideoViewModel.kt @@ -1,9 +1,13 @@ package com.youtubeapp.ui.viewmodel +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.youtubeapp.data.LocalStorageService import com.youtubeapp.data.Video import com.youtubeapp.data.VideoRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -12,15 +16,23 @@ data class VideoUiState( val allVideos: List