From cc1e1d7df93810a1566ff2443717c61bf49175a0 Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Sun, 29 Mar 2026 19:19:12 +0200 Subject: [PATCH] Move TV detail overview and playback into body --- .../episode/EpisodeScreenContentTest.kt | 4 ++ .../content/movie/MovieScreenContentTest.kt | 5 +- .../content/series/SeriesScreenContentTest.kt | 2 + .../app/content/episode/EpisodeComponents.kt | 40 --------------- .../app/content/episode/EpisodeScreen.kt | 49 ++++++++++++------- .../app/content/movie/MovieComponents.kt | 40 --------------- .../purefin/app/content/movie/MovieScreen.kt | 47 +++++++++++------- .../app/content/series/SeriesComponents.kt | 23 ++------- .../app/content/series/SeriesScreen.kt | 18 +++++-- .../common/ui/components/MediaDetailShell.kt | 45 +++++++++++++++++ 10 files changed, 134 insertions(+), 139 deletions(-) diff --git a/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/episode/EpisodeScreenContentTest.kt b/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/episode/EpisodeScreenContentTest.kt index 42274f2..aa1ad6a 100644 --- a/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/episode/EpisodeScreenContentTest.kt +++ b/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/episode/EpisodeScreenContentTest.kt @@ -40,6 +40,8 @@ class EpisodeScreenContentTest { composeRule.onNodeWithText("Severance").assertIsDisplayed() composeRule.onNodeWithText("The You You Are").assertIsDisplayed() composeRule.onNodeWithText("Episode 4").assertIsDisplayed() + composeRule.onNodeWithText("Overview").assertIsDisplayed() + composeRule.onAllNodesWithText("Playback").assertCountEquals(1) composeRule.onNodeWithTag(EpisodePlayButtonTag).assertIsDisplayed().assertIsFocused() composeRule.onNodeWithText("Series").assertIsDisplayed() } @@ -63,6 +65,8 @@ class EpisodeScreenContentTest { composeRule.waitForIdle() + composeRule.onNodeWithText("Overview").assertIsDisplayed() + composeRule.onAllNodesWithText("Playback").assertCountEquals(1) composeRule.onNodeWithTag(EpisodePlayButtonTag).assertIsDisplayed() composeRule.onAllNodesWithText("Series").assertCountEquals(0) } diff --git a/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/movie/MovieScreenContentTest.kt b/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/movie/MovieScreenContentTest.kt index 6ac6856..e7078b9 100644 --- a/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/movie/MovieScreenContentTest.kt +++ b/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/movie/MovieScreenContentTest.kt @@ -1,9 +1,11 @@ package hu.bbara.purefin.app.content.movie import androidx.activity.ComponentActivity +import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsFocused import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText @@ -34,7 +36,8 @@ class MovieScreenContentTest { composeRule.waitForIdle() composeRule.onNodeWithText("Blade Runner 2049").assertIsDisplayed() - composeRule.onNodeWithText("Playback").assertIsDisplayed() + composeRule.onNodeWithText("Overview").assertIsDisplayed() + composeRule.onAllNodesWithText("Playback").assertCountEquals(1) composeRule.onNodeWithTag(MoviePlayButtonTag).assertIsDisplayed().assertIsFocused() composeRule.onNodeWithContentDescription("Back").assertIsDisplayed() } diff --git a/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/series/SeriesScreenContentTest.kt b/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/series/SeriesScreenContentTest.kt index 2bf3390..36934a3 100644 --- a/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/series/SeriesScreenContentTest.kt +++ b/app-tv/src/androidTest/java/hu/bbara/purefin/app/content/series/SeriesScreenContentTest.kt @@ -35,6 +35,7 @@ class SeriesScreenContentTest { composeRule.waitForIdle() composeRule.onNodeWithText("Severance").assertIsDisplayed() + composeRule.onNodeWithText("Overview").assertIsDisplayed() composeRule.onNodeWithText("Up Next").assertIsDisplayed() composeRule.onNodeWithTag(SeriesPlayButtonTag).assertIsDisplayed().assertIsFocused() composeRule.onNodeWithText("Season 1").assertIsDisplayed() @@ -55,6 +56,7 @@ class SeriesScreenContentTest { composeRule.waitForIdle() + composeRule.onNodeWithText("Overview").assertIsDisplayed() composeRule.onNodeWithText("Library Status").assertIsDisplayed() composeRule.onNodeWithTag(SeriesFirstSeasonTabTag).assertIsDisplayed().assertIsFocused() } diff --git a/app-tv/src/main/java/hu/bbara/purefin/app/content/episode/EpisodeComponents.kt b/app-tv/src/main/java/hu/bbara/purefin/app/content/episode/EpisodeComponents.kt index 2fbf9b8..bb7bd1a 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/app/content/episode/EpisodeComponents.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/app/content/episode/EpisodeComponents.kt @@ -21,10 +21,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import hu.bbara.purefin.common.ui.MediaMetaChip -import hu.bbara.purefin.common.ui.MediaSynopsis import hu.bbara.purefin.common.ui.components.MediaDetailsTopBar import hu.bbara.purefin.common.ui.components.MediaDetailsTopBarShortcut -import hu.bbara.purefin.common.ui.components.MediaPlaybackSettings import hu.bbara.purefin.common.ui.components.MediaResumeButton import hu.bbara.purefin.core.data.navigation.Route import hu.bbara.purefin.core.model.Episode @@ -137,44 +135,6 @@ internal fun EpisodeHeroSection( } } -@Composable -internal fun EpisodeOverviewPanel( - episode: Episode, - modifier: Modifier = Modifier -) { - val scheme = MaterialTheme.colorScheme - val mutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.85f) - - Column(modifier = modifier) { - MediaSynopsis( - synopsis = episode.synopsis, - title = "Overview", - titleColor = scheme.onSurface, - bodyColor = mutedStrong, - titleFontSize = 20.sp, - bodyFontSize = 16.sp, - bodyLineHeight = 24.sp, - titleSpacing = 10.dp, - collapsedLines = 5, - collapseInitially = false - ) - Spacer(modifier = Modifier.height(24.dp)) - Text( - text = "Playback", - color = scheme.onSurface, - fontSize = 20.sp, - fontWeight = FontWeight.Bold - ) - Spacer(modifier = Modifier.height(14.dp)) - MediaPlaybackSettings( - backgroundColor = scheme.surfaceContainerHigh, - foregroundColor = scheme.onSurface, - audioTrack = "ENG", - subtitles = "ENG" - ) - } -} - private fun Episode.playButtonText(): String { return if ((progress ?: 0.0) > 0.0 && !watched) "Resume" else "Play" } diff --git a/app-tv/src/main/java/hu/bbara/purefin/app/content/episode/EpisodeScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/app/content/episode/EpisodeScreen.kt index 2303fb8..65ae788 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/app/content/episode/EpisodeScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/app/content/episode/EpisodeScreen.kt @@ -2,6 +2,7 @@ package hu.bbara.purefin.app.content.episode import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -14,7 +15,8 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import hu.bbara.purefin.common.ui.MediaCastRow import hu.bbara.purefin.common.ui.PurefinWaitingScreen -import hu.bbara.purefin.common.ui.components.MediaDetailHeaderRow +import hu.bbara.purefin.common.ui.components.MediaDetailOverviewSection +import hu.bbara.purefin.common.ui.components.MediaDetailPlaybackSection import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold import hu.bbara.purefin.core.data.navigation.EpisodeDto @@ -99,30 +101,39 @@ internal fun EpisodeScreenContent( ) }, heroContent = { - MediaDetailHeaderRow( - leftContent = { headerModifier -> - EpisodeHeroSection( - episode = episode, - seriesTitle = seriesTitle, - onPlay = onPlay, - playFocusRequester = playFocusRequester, - modifier = headerModifier - ) - }, - rightContent = { panelModifier -> - EpisodeOverviewPanel( - episode = episode, - modifier = panelModifier - ) - } + EpisodeHeroSection( + episode = episode, + seriesTitle = seriesTitle, + onPlay = onPlay, + playFocusRequester = playFocusRequester, + modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(12.dp)) } ) { + item { + Column(modifier = it.fillMaxWidth()) { + Spacer(modifier = Modifier.height(16.dp)) + MediaDetailOverviewSection( + synopsis = episode.synopsis, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(20.dp)) + } + } + item { + Column(modifier = it.fillMaxWidth()) { + MediaDetailPlaybackSection( + audioTrack = "ENG", + subtitles = "ENG", + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(20.dp)) + } + } if (episode.cast.isNotEmpty()) { item { - Column(modifier = it) { - Spacer(modifier = Modifier.height(8.dp)) + Column(modifier = it.fillMaxWidth()) { MediaDetailSectionTitle(text = "Cast") Spacer(modifier = Modifier.height(14.dp)) MediaCastRow(cast = episode.cast) diff --git a/app-tv/src/main/java/hu/bbara/purefin/app/content/movie/MovieComponents.kt b/app-tv/src/main/java/hu/bbara/purefin/app/content/movie/MovieComponents.kt index f6dfc9a..d9edae5 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/app/content/movie/MovieComponents.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/app/content/movie/MovieComponents.kt @@ -21,9 +21,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import hu.bbara.purefin.common.ui.MediaMetaChip -import hu.bbara.purefin.common.ui.MediaSynopsis import hu.bbara.purefin.common.ui.components.MediaDetailsTopBar -import hu.bbara.purefin.common.ui.components.MediaPlaybackSettings import hu.bbara.purefin.common.ui.components.MediaResumeButton import hu.bbara.purefin.core.model.Movie @@ -94,44 +92,6 @@ internal fun MovieHeroSection( } } -@Composable -internal fun MovieOverviewPanel( - movie: Movie, - modifier: Modifier = Modifier -) { - val scheme = MaterialTheme.colorScheme - val mutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.85f) - - Column(modifier = modifier) { - MediaSynopsis( - synopsis = movie.synopsis, - title = "Overview", - titleColor = scheme.onSurface, - bodyColor = mutedStrong, - titleFontSize = 20.sp, - bodyFontSize = 16.sp, - bodyLineHeight = 24.sp, - titleSpacing = 10.dp, - collapsedLines = 5, - collapseInitially = false - ) - Spacer(modifier = Modifier.height(24.dp)) - Text( - text = "Playback", - color = scheme.onSurface, - fontSize = 20.sp, - fontWeight = FontWeight.Bold - ) - Spacer(modifier = Modifier.height(14.dp)) - MediaPlaybackSettings( - backgroundColor = scheme.surfaceContainerHigh, - foregroundColor = scheme.onSurface, - audioTrack = movie.audioTrack, - subtitles = movie.subtitles - ) - } -} - private fun Movie.playButtonText(): String { return if ((progress ?: 0.0) > 0.0 && !watched) "Resume" else "Play" } diff --git a/app-tv/src/main/java/hu/bbara/purefin/app/content/movie/MovieScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/app/content/movie/MovieScreen.kt index 24d7b1e..6c11656 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/app/content/movie/MovieScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/app/content/movie/MovieScreen.kt @@ -2,6 +2,7 @@ package hu.bbara.purefin.app.content.movie import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -14,7 +15,8 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import hu.bbara.purefin.common.ui.MediaCastRow import hu.bbara.purefin.common.ui.PurefinWaitingScreen -import hu.bbara.purefin.common.ui.components.MediaDetailHeaderRow +import hu.bbara.purefin.common.ui.components.MediaDetailOverviewSection +import hu.bbara.purefin.common.ui.components.MediaDetailPlaybackSection import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold import hu.bbara.purefin.core.data.navigation.MovieDto @@ -67,29 +69,38 @@ internal fun MovieScreenContent( ) }, heroContent = { - MediaDetailHeaderRow( - leftContent = { headerModifier -> - MovieHeroSection( - movie = movie, - onPlay = onPlay, - playFocusRequester = playFocusRequester, - modifier = headerModifier - ) - }, - rightContent = { panelModifier -> - MovieOverviewPanel( - movie = movie, - modifier = panelModifier - ) - } + MovieHeroSection( + movie = movie, + onPlay = onPlay, + playFocusRequester = playFocusRequester, + modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(12.dp)) } ) { + item { + Column(modifier = it.fillMaxWidth()) { + Spacer(modifier = Modifier.height(16.dp)) + MediaDetailOverviewSection( + synopsis = movie.synopsis, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(20.dp)) + } + } + item { + Column(modifier = it.fillMaxWidth()) { + MediaDetailPlaybackSection( + audioTrack = movie.audioTrack, + subtitles = movie.subtitles, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(20.dp)) + } + } if (movie.cast.isNotEmpty()) { item { - Column(modifier = it) { - Spacer(modifier = Modifier.height(8.dp)) + Column(modifier = it.fillMaxWidth()) { MediaDetailSectionTitle(text = "Cast") Spacer(modifier = Modifier.height(14.dp)) MediaCastRow(cast = movie.cast) diff --git a/app-tv/src/main/java/hu/bbara/purefin/app/content/series/SeriesComponents.kt b/app-tv/src/main/java/hu/bbara/purefin/app/content/series/SeriesComponents.kt index e54654b..5dbb030 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/app/content/series/SeriesComponents.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/app/content/series/SeriesComponents.kt @@ -55,7 +55,6 @@ import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import hu.bbara.purefin.common.ui.MediaCastRow import hu.bbara.purefin.common.ui.MediaMetaChip -import hu.bbara.purefin.common.ui.MediaSynopsis import hu.bbara.purefin.common.ui.components.MediaDetailsTopBar import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle import hu.bbara.purefin.common.ui.components.MediaProgressBar @@ -249,28 +248,16 @@ internal fun SeriesHeroSection( } @Composable -internal fun SeriesOverviewPanel( - series: Series, +internal fun SeriesStatusPanel( nextUpEpisode: Episode?, + seasonCount: Int, + unwatchedEpisodeCount: Int, modifier: Modifier = Modifier ) { val scheme = MaterialTheme.colorScheme val mutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.85f) Column(modifier = modifier) { - MediaSynopsis( - synopsis = series.synopsis, - title = "Overview", - titleColor = scheme.onSurface, - bodyColor = mutedStrong, - titleFontSize = 20.sp, - bodyFontSize = 16.sp, - bodyLineHeight = 24.sp, - titleSpacing = 10.dp, - collapsedLines = 5, - collapseInitially = false - ) - Spacer(modifier = Modifier.height(24.dp)) MediaDetailSectionTitle( text = if (nextUpEpisode != null) "Up Next" else "Library Status", modifier = Modifier.fillMaxWidth() @@ -280,7 +267,7 @@ internal fun SeriesOverviewPanel( text = if (nextUpEpisode != null) { nextUpEpisode.title } else { - "${series.seasonCount} seasons ready to browse" + "$seasonCount seasons ready to browse" }, color = scheme.onSurface, fontSize = 18.sp, @@ -293,7 +280,7 @@ internal fun SeriesOverviewPanel( text = if (nextUpEpisode != null) { "Episode ${nextUpEpisode.index} • ${nextUpEpisode.runtime}" } else { - "${series.unwatchedEpisodeCount} unwatched episodes" + "$unwatchedEpisodeCount unwatched episodes" }, color = mutedStrong, fontSize = 14.sp, diff --git a/app-tv/src/main/java/hu/bbara/purefin/app/content/series/SeriesScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/app/content/series/SeriesScreen.kt index c4f409f..6551036 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/app/content/series/SeriesScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/app/content/series/SeriesScreen.kt @@ -2,6 +2,7 @@ package hu.bbara.purefin.app.content.series import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -15,6 +16,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import hu.bbara.purefin.common.ui.PurefinWaitingScreen import hu.bbara.purefin.common.ui.components.MediaDetailHeaderRow +import hu.bbara.purefin.common.ui.components.MediaDetailOverviewSection import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold import hu.bbara.purefin.core.data.navigation.SeriesDto @@ -103,9 +105,10 @@ internal fun SeriesScreenContent( ) }, rightContent = { panelModifier -> - SeriesOverviewPanel( - series = series, + SeriesStatusPanel( nextUpEpisode = nextUpEpisode, + seasonCount = series.seasonCount, + unwatchedEpisodeCount = series.unwatchedEpisodeCount, modifier = panelModifier ) } @@ -114,7 +117,16 @@ internal fun SeriesScreenContent( } ) { item { - Spacer(modifier = Modifier.height(16.dp)) + Column(modifier = it.fillMaxWidth()) { + Spacer(modifier = Modifier.height(16.dp)) + MediaDetailOverviewSection( + synopsis = series.synopsis, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(24.dp)) + } + } + item { SeasonTabs( seasons = series.seasons, selectedSeason = selectedSeason.value, diff --git a/app-tv/src/main/java/hu/bbara/purefin/common/ui/components/MediaDetailShell.kt b/app-tv/src/main/java/hu/bbara/purefin/common/ui/components/MediaDetailShell.kt index 6e91e83..48962a7 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/common/ui/components/MediaDetailShell.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/common/ui/components/MediaDetailShell.kt @@ -7,8 +7,10 @@ import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope @@ -22,6 +24,7 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import hu.bbara.purefin.common.ui.MediaSynopsis internal val MediaDetailHorizontalPadding = 48.dp private val MediaDetailPanelShape = RoundedCornerShape(28.dp) @@ -114,6 +117,48 @@ internal fun MediaDetailSectionTitle( ) } +@Composable +internal fun MediaDetailOverviewSection( + synopsis: String, + modifier: Modifier = Modifier +) { + val scheme = MaterialTheme.colorScheme + + MediaSynopsis( + synopsis = synopsis, + modifier = modifier, + title = "Overview", + titleColor = scheme.onBackground, + bodyColor = scheme.onSurfaceVariant.copy(alpha = 0.85f), + titleFontSize = 22.sp, + bodyFontSize = 16.sp, + bodyLineHeight = 24.sp, + titleSpacing = 14.dp, + collapsedLines = 5, + collapseInitially = false + ) +} + +@Composable +internal fun MediaDetailPlaybackSection( + audioTrack: String, + subtitles: String, + modifier: Modifier = Modifier +) { + val scheme = MaterialTheme.colorScheme + + Column(modifier = modifier) { + MediaDetailSectionTitle(text = "Playback") + Spacer(modifier = Modifier.height(14.dp)) + MediaPlaybackSettings( + backgroundColor = scheme.surfaceContainerHigh, + foregroundColor = scheme.onBackground, + audioTrack = audioTrack, + subtitles = subtitles + ) + } +} + @Composable private fun MediaDetailHeroGradientOverlay() { val background = MaterialTheme.colorScheme.background