diff --git a/app/frontend/src/main/AndroidManifest.xml b/app/frontend/src/main/AndroidManifest.xml index 5218c6a..fc6e766 100644 --- a/app/frontend/src/main/AndroidManifest.xml +++ b/app/frontend/src/main/AndroidManifest.xml @@ -3,7 +3,11 @@ + + + + diff --git a/app/frontend/src/main/java/com/youtubeapp/ui/components/VideoCard.kt b/app/frontend/src/main/java/com/youtubeapp/ui/components/VideoCard.kt index a54c84e..ab90037 100644 --- a/app/frontend/src/main/java/com/youtubeapp/ui/components/VideoCard.kt +++ b/app/frontend/src/main/java/com/youtubeapp/ui/components/VideoCard.kt @@ -1,5 +1,7 @@ package com.youtubeapp.ui.components +import androidx.compose.foundation.border +import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth @@ -8,7 +10,13 @@ import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +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.focus.onFocusChanged +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -17,9 +25,18 @@ import com.youtubeapp.data.Video @Composable fun VideoCard(video: Video, onClick: () -> Unit) { + var isFocused by remember { mutableStateOf(false) } + Card( onClick = onClick, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .onFocusChanged { isFocused = it.isFocused } + .then( + if (isFocused) Modifier.border(3.dp, Color.White, MaterialTheme.shapes.medium) + else Modifier + ) + .focusable() ) { Column { AsyncImage( diff --git a/app/frontend/src/main/java/com/youtubeapp/ui/screens/AllVideosScreen.kt b/app/frontend/src/main/java/com/youtubeapp/ui/screens/AllVideosScreen.kt index e2754f9..95befbb 100644 --- a/app/frontend/src/main/java/com/youtubeapp/ui/screens/AllVideosScreen.kt +++ b/app/frontend/src/main/java/com/youtubeapp/ui/screens/AllVideosScreen.kt @@ -47,7 +47,7 @@ fun AllVideosScreen(viewModel: VideoViewModel, onVideoClick: (Int) -> Unit) { } else -> { LazyVerticalGrid( - columns = GridCells.Fixed(2), + columns = GridCells.Adaptive(minSize = 250.dp), contentPadding = PaddingValues(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), diff --git a/app/frontend/src/main/java/com/youtubeapp/ui/screens/DownloadedScreen.kt b/app/frontend/src/main/java/com/youtubeapp/ui/screens/DownloadedScreen.kt index 70c5e3c..589c8a2 100644 --- a/app/frontend/src/main/java/com/youtubeapp/ui/screens/DownloadedScreen.kt +++ b/app/frontend/src/main/java/com/youtubeapp/ui/screens/DownloadedScreen.kt @@ -52,7 +52,7 @@ fun DownloadedScreen(viewModel: VideoViewModel, onVideoClick: (Int) -> Unit) { } else -> { LazyVerticalGrid( - columns = GridCells.Fixed(2), + columns = GridCells.Adaptive(minSize = 250.dp), contentPadding = PaddingValues(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), 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 256f535..05e73aa 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 @@ -1,6 +1,7 @@ package com.youtubeapp.ui.screens import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -73,82 +74,98 @@ fun VideoDetailScreen( if (video == null) { Text("Video nicht gefunden", modifier = Modifier.padding(innerPadding).padding(16.dp)) } else { - Column( + BoxWithConstraints( modifier = Modifier .fillMaxSize() .padding(innerPadding) ) { - AsyncImage( - model = video.thumbnail_url, - contentDescription = video.title, - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxWidth() - .aspectRatio(16f / 9f) - ) - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = video.title, - style = MaterialTheme.typography.headlineSmall + val isWide = maxWidth > 600.dp + + Column(modifier = Modifier.fillMaxSize()) { + AsyncImage( + model = video.thumbnail_url, + contentDescription = video.title, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth(if (isWide) 0.5f else 1f) + .aspectRatio(16f / 9f) ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = video.youtuber, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = when { - isLocal -> "Lokal gespeichert" - video.is_downloaded -> "Auf Server heruntergeladen" - else -> "Noch nicht heruntergeladen" - }, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.height(16.dp)) - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth() - ) { - Button( - onClick = onPlayClick, - modifier = Modifier.weight(1f) - ) { - Icon(Icons.Default.PlayArrow, contentDescription = null) - Text(" Abspielen") - } - 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") - } - } - } + VideoInfo(video, isLocal, state.isDownloading, viewModel, videoId, onPlayClick) + } + } + } + } +} + +@Composable +private fun VideoInfo( + video: com.youtubeapp.data.Video, + isLocal: Boolean, + isDownloading: Boolean, + viewModel: VideoViewModel, + videoId: Int, + onPlayClick: () -> Unit +) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = video.title, + style = MaterialTheme.typography.headlineSmall + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = video.youtuber, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = when { + isLocal -> "Lokal gespeichert" + video.is_downloaded -> "Auf Server heruntergeladen" + else -> "Noch nicht heruntergeladen" + }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(16.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + Button( + onClick = onPlayClick, + modifier = Modifier.weight(1f) + ) { + Icon(Icons.Default.PlayArrow, contentDescription = null) + Text(" Abspielen") + } + if (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 a3ad770..622e2f6 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 @@ -1,6 +1,7 @@ package com.youtubeapp.ui.screens import android.app.Activity +import android.view.LayoutInflater import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -14,6 +15,7 @@ import androidx.core.view.WindowInsetsControllerCompat import androidx.media3.common.MediaItem import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.PlayerView +import com.youtubeapp.R import com.youtubeapp.ui.viewmodel.VideoViewModel @Composable @@ -49,10 +51,12 @@ fun VideoPlayerScreen(videoId: Int, viewModel: VideoViewModel) { AndroidView( factory = { ctx -> - PlayerView(ctx).apply { - player = exoPlayer - useController = true - } + val view = LayoutInflater.from(ctx).inflate(R.layout.player_view, null) as PlayerView + view.player = exoPlayer + view + }, + update = { view -> + view.player = exoPlayer }, modifier = Modifier.fillMaxSize() ) diff --git a/app/frontend/src/main/res/drawable/tv_banner.png b/app/frontend/src/main/res/drawable/tv_banner.png new file mode 100644 index 0000000..453b2a9 Binary files /dev/null and b/app/frontend/src/main/res/drawable/tv_banner.png differ diff --git a/app/frontend/src/main/res/layout/player_view.xml b/app/frontend/src/main/res/layout/player_view.xml new file mode 100644 index 0000000..73d40e6 --- /dev/null +++ b/app/frontend/src/main/res/layout/player_view.xml @@ -0,0 +1,10 @@ + +