Move TV detail overview and playback into body

This commit is contained in:
2026-03-29 19:19:12 +02:00
parent c163a714d8
commit cc1e1d7df9
10 changed files with 134 additions and 139 deletions

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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"
}

View File

@@ -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)

View File

@@ -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"
}

View File

@@ -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)

View File

@@ -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,

View File

@@ -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,

View File

@@ -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