update
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user