This commit is contained in:
Marek
2026-04-05 20:43:35 +02:00
parent f8de245e45
commit f269271c6e
16 changed files with 237 additions and 32 deletions

View File

@@ -6,5 +6,11 @@ data class Video(
val youtuber: String,
val thumbnail_url: String,
val youtube_url: String,
val is_downloaded: Boolean
val is_downloaded: Boolean,
val profile_ids: List<Int> = emptyList()
)
data class Profile(
val id: Int,
val name: String
)

View File

@@ -3,14 +3,18 @@ package com.youtubeapp.data
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
interface VideoApi {
@GET("videos")
suspend fun getAllVideos(): List<Video>
suspend fun getAllVideos(@Query("profile_id") profileId: Int? = null): List<Video>
@GET("videos/downloaded")
suspend fun getDownloadedVideos(): List<Video>
suspend fun getDownloadedVideos(@Query("profile_id") profileId: Int? = null): List<Video>
@POST("videos/{id}/download")
suspend fun triggerDownload(@Path("id") id: Int): Map<String, String>
@GET("profiles")
suspend fun getProfiles(): List<Profile>
}

View File

@@ -2,14 +2,18 @@ package com.youtubeapp.data
class VideoRepository(private val api: VideoApi = ApiClient.api) {
suspend fun getAllVideos(): Result<List<Video>> = runCatching { api.getAllVideos() }
suspend fun getAllVideos(profileId: Int? = null): Result<List<Video>> =
runCatching { api.getAllVideos(profileId) }
suspend fun getDownloadedVideos(): Result<List<Video>> = runCatching { api.getDownloadedVideos() }
suspend fun getDownloadedVideos(profileId: Int? = null): Result<List<Video>> =
runCatching { api.getDownloadedVideos(profileId) }
suspend fun triggerDownload(videoId: Int): Result<String> = runCatching {
val response = api.triggerDownload(videoId)
response["status"] ?: "unknown"
}
suspend fun getProfiles(): Result<List<Profile>> = runCatching { api.getProfiles() }
fun getStreamUrl(videoId: Int): String = "${ApiClient.BASE_URL}videos/$videoId/stream"
}

View File

@@ -3,14 +3,25 @@ package com.youtubeapp.ui.navigation
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.VideoLibrary
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -27,20 +38,60 @@ import com.youtubeapp.ui.screens.VideoDetailScreen
import com.youtubeapp.ui.screens.VideoPlayerScreen
import com.youtubeapp.ui.viewmodel.VideoViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppNavigation() {
val navController = rememberNavController()
val viewModel: VideoViewModel = viewModel()
val context = LocalContext.current
viewModel.init(context)
val state by viewModel.state.collectAsState()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
val showBottomBar = currentRoute in listOf(Route.AllVideos.route, Route.Downloaded.route)
val showBars = currentRoute in listOf(Route.AllVideos.route, Route.Downloaded.route)
var showProfileMenu by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
viewModel.loadProfiles()
}
val selectedProfileName = state.profiles.find { it.id == state.selectedProfileId }?.name ?: "Profil"
Scaffold(
topBar = {
if (showBars) {
TopAppBar(
title = { Text("YouTube App") },
actions = {
IconButton(onClick = { showProfileMenu = true }) {
Icon(Icons.Default.Person, contentDescription = "Profil")
}
DropdownMenu(
expanded = showProfileMenu,
onDismissRequest = { showProfileMenu = false }
) {
for (profile in state.profiles) {
DropdownMenuItem(
text = {
Text(
if (profile.id == state.selectedProfileId) "${profile.name}"
else profile.name
)
},
onClick = {
viewModel.selectProfile(profile.id)
showProfileMenu = false
}
)
}
}
}
)
}
},
bottomBar = {
if (showBottomBar) {
if (showBars) {
NavigationBar {
NavigationBarItem(
selected = currentRoute == Route.AllVideos.route,

View File

@@ -4,6 +4,7 @@ import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.youtubeapp.data.LocalStorageService
import com.youtubeapp.data.Profile
import com.youtubeapp.data.Video
import com.youtubeapp.data.VideoRepository
import kotlinx.coroutines.Dispatchers
@@ -15,6 +16,8 @@ import kotlinx.coroutines.launch
data class VideoUiState(
val allVideos: List<Video> = emptyList(),
val downloadedVideos: List<Video> = emptyList(),
val profiles: List<Profile> = emptyList(),
val selectedProfileId: Int? = null,
val isLoading: Boolean = false,
val isDownloading: Boolean = false,
val error: String? = null,
@@ -24,6 +27,7 @@ data class VideoUiState(
class VideoViewModel : ViewModel() {
private val repository = VideoRepository()
private var localStorage: LocalStorageService? = null
private var prefs: android.content.SharedPreferences? = null
private val _state = MutableStateFlow(VideoUiState())
val state: StateFlow<VideoUiState> = _state
@@ -31,12 +35,38 @@ class VideoViewModel : ViewModel() {
if (localStorage == null) {
localStorage = LocalStorageService(context.applicationContext)
}
if (prefs == null) {
prefs = context.applicationContext.getSharedPreferences("youtubeapp", Context.MODE_PRIVATE)
val savedId = prefs?.getInt("profile_id", -1) ?: -1
if (savedId > 0) {
_state.value = _state.value.copy(selectedProfileId = savedId)
}
}
}
fun loadProfiles() {
viewModelScope.launch {
repository.getProfiles()
.onSuccess { profiles ->
_state.value = _state.value.copy(profiles = profiles)
if (_state.value.selectedProfileId == null && profiles.isNotEmpty()) {
selectProfile(profiles.first().id)
}
}
}
}
fun selectProfile(profileId: Int) {
_state.value = _state.value.copy(selectedProfileId = profileId)
prefs?.edit()?.putInt("profile_id", profileId)?.apply()
loadAllVideos()
loadDownloadedVideos()
}
fun loadAllVideos() {
viewModelScope.launch {
_state.value = _state.value.copy(isLoading = true, error = null)
repository.getAllVideos()
repository.getAllVideos(profileId = _state.value.selectedProfileId)
.onSuccess { videos ->
_state.value = _state.value.copy(allVideos = videos, isLoading = false)
}
@@ -49,7 +79,7 @@ class VideoViewModel : ViewModel() {
fun loadDownloadedVideos() {
viewModelScope.launch {
_state.value = _state.value.copy(isLoading = true, error = null)
repository.getDownloadedVideos()
repository.getDownloadedVideos(profileId = _state.value.selectedProfileId)
.onSuccess { videos ->
_state.value = _state.value.copy(downloadedVideos = videos, isLoading = false)
}
@@ -63,7 +93,6 @@ class VideoViewModel : ViewModel() {
viewModelScope.launch(Dispatchers.IO) {
_state.value = _state.value.copy(isDownloading = true, downloadStatus = null)
try {
// 1. Server-Download triggern
val result = repository.triggerDownload(videoId)
if (result.isFailure) {
_state.value = _state.value.copy(
@@ -73,7 +102,6 @@ class VideoViewModel : ViewModel() {
return@launch
}
// 2. Warten bis Server-Download fertig
val status = result.getOrNull()
if (status == "download_started") {
while (true) {
@@ -84,7 +112,6 @@ class VideoViewModel : ViewModel() {
}
}
// 3. Lokal speichern
localStorage?.downloadAndSave(videoId)
_state.value = _state.value.copy(
isDownloading = false,