Refactor SeriesScreen components and update UI text for clarity

This commit is contained in:
2026-03-30 08:32:46 +02:00
parent 1c5bb604f8
commit 94f1eb2883
4 changed files with 41 additions and 102 deletions

View File

@@ -42,10 +42,11 @@ class SeriesScreenContentTest {
composeRule.onNodeWithText("Severance").assertIsDisplayed() composeRule.onNodeWithText("Severance").assertIsDisplayed()
composeRule.onNodeWithText("Overview").assertIsDisplayed() composeRule.onNodeWithText("Overview").assertIsDisplayed()
composeRule.onNodeWithText("Up Next").assertIsDisplayed() composeRule.onNodeWithText("Continue Watching").assertIsDisplayed()
composeRule.onNodeWithTag(SeriesPlayButtonTag).assertIsDisplayed() composeRule.onNodeWithTag(SeriesPlayButtonTag).assertIsDisplayed()
composeRule.onNodeWithText("Season 1").assertIsDisplayed() composeRule.onNodeWithText("Season 1").assertIsDisplayed()
composeRule.onNodeWithText("Good News About Hell").assertIsDisplayed() composeRule.onNodeWithText("Good News About Hell").assertIsDisplayed()
composeRule.onNodeWithText("Episode 1 • 57m").assertIsDisplayed()
composeRule.onNodeWithContentDescription("Back") composeRule.onNodeWithContentDescription("Back")
.assertIsDisplayed() .assertIsDisplayed()
.assertIsFocused() .assertIsFocused()
@@ -70,7 +71,7 @@ class SeriesScreenContentTest {
composeRule.waitForIdle() composeRule.waitForIdle()
composeRule.onNodeWithText("Overview").assertIsDisplayed() composeRule.onNodeWithText("Overview").assertIsDisplayed()
composeRule.onNodeWithText("Library Status").assertIsDisplayed() composeRule.onNodeWithText("Choose a season below to start watching.").assertIsDisplayed()
composeRule.onNodeWithTag(SeriesFirstSeasonTabTag).assertIsDisplayed() composeRule.onNodeWithTag(SeriesFirstSeasonTabTag).assertIsDisplayed()
composeRule.onNodeWithContentDescription("Back") composeRule.onNodeWithContentDescription("Back")
.assertIsDisplayed() .assertIsDisplayed()

View File

@@ -56,7 +56,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.common.ui.MediaCastRow import hu.bbara.purefin.common.ui.MediaCastRow
import hu.bbara.purefin.common.ui.MediaMetaChip import hu.bbara.purefin.common.ui.MediaMetaChip
import hu.bbara.purefin.common.ui.components.MediaDetailsTopBar import hu.bbara.purefin.common.ui.components.MediaDetailsTopBar
import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle
import hu.bbara.purefin.common.ui.components.MediaProgressBar import hu.bbara.purefin.common.ui.components.MediaProgressBar
import hu.bbara.purefin.common.ui.components.MediaResumeButton import hu.bbara.purefin.common.ui.components.MediaResumeButton
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
@@ -228,6 +227,33 @@ internal fun SeriesHeroSection(
SeriesMetaChips(series = series) SeriesMetaChips(series = series)
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
if (nextUpEpisode != null) { if (nextUpEpisode != null) {
Text(
text = nextUpEpisode.heroStatusText(),
color = scheme.primary,
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(10.dp))
Text(
text = nextUpEpisode.title,
color = scheme.onBackground,
fontSize = 22.sp,
fontWeight = FontWeight.SemiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(6.dp))
Text(
text = "Episode ${nextUpEpisode.index}${nextUpEpisode.runtime}",
color = mutedStrong,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(24.dp))
MediaResumeButton( MediaResumeButton(
text = nextUpEpisode.playButtonText(), text = nextUpEpisode.playButtonText(),
progress = nextUpEpisode.progress?.div(100)?.toFloat() ?: 0f, progress = nextUpEpisode.progress?.div(100)?.toFloat() ?: 0f,
@@ -249,48 +275,6 @@ internal fun SeriesHeroSection(
} }
} }
@Composable
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) {
MediaDetailSectionTitle(
text = if (nextUpEpisode != null) "Up Next" else "Library Status",
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(14.dp))
Text(
text = if (nextUpEpisode != null) {
nextUpEpisode.title
} else {
"$seasonCount seasons ready to browse"
},
color = scheme.onSurface,
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(6.dp))
Text(
text = if (nextUpEpisode != null) {
"Episode ${nextUpEpisode.index}${nextUpEpisode.runtime}"
} else {
"$unwatchedEpisodeCount unwatched episodes"
},
color = mutedStrong,
fontSize = 14.sp,
fontWeight = FontWeight.Medium
)
}
}
@Composable @Composable
private fun EpisodeCard( private fun EpisodeCard(
viewModel: SeriesViewModel = hiltViewModel(), viewModel: SeriesViewModel = hiltViewModel(),
@@ -409,3 +393,7 @@ internal fun CastRow(cast: List<CastMember>, modifier: Modifier = Modifier) {
private fun Episode.playButtonText(): String { private fun Episode.playButtonText(): String {
return if ((progress ?: 0.0) > 0.0 && !watched) "Resume" else "Play" return if ((progress ?: 0.0) > 0.0 && !watched) "Resume" else "Play"
} }
private fun Episode.heroStatusText(): String {
return if ((progress ?: 0.0) > 0.0 && !watched) "Continue Watching" else "Up Next"
}

View File

@@ -15,12 +15,10 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.common.ui.PurefinWaitingScreen 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.MediaDetailOverviewSection
import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle
import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold
import hu.bbara.purefin.core.data.navigation.SeriesDto import hu.bbara.purefin.core.data.navigation.SeriesDto
import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.core.model.Season import hu.bbara.purefin.core.model.Season
import hu.bbara.purefin.core.model.Series import hu.bbara.purefin.core.model.Series
import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel
@@ -92,25 +90,13 @@ internal fun SeriesScreenContent(
) )
}, },
heroContent = { heroContent = {
MediaDetailHeaderRow(
leftContent = { headerModifier ->
SeriesHeroSection( SeriesHeroSection(
series = series, series = series,
nextUpEpisode = nextUpEpisode, nextUpEpisode = nextUpEpisode,
onPlayEpisode = { onPlayEpisode(it.id) }, onPlayEpisode = { onPlayEpisode(it.id) },
playFocusRequester = playFocusRequester, playFocusRequester = playFocusRequester,
firstContentFocusRequester = firstContentFocusRequester, firstContentFocusRequester = firstContentFocusRequester,
modifier = headerModifier modifier = Modifier.fillMaxWidth()
)
},
rightContent = { panelModifier ->
SeriesStatusPanel(
nextUpEpisode = nextUpEpisode,
seasonCount = series.seasonCount,
unwatchedEpisodeCount = series.unwatchedEpisodeCount,
modifier = panelModifier
)
}
) )
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
} }

View File

@@ -1,12 +1,10 @@
package hu.bbara.purefin.common.ui.components package hu.bbara.purefin.common.ui.components
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@@ -15,12 +13,10 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -29,7 +25,6 @@ import androidx.compose.ui.unit.sp
import hu.bbara.purefin.common.ui.MediaSynopsis import hu.bbara.purefin.common.ui.MediaSynopsis
internal val MediaDetailHorizontalPadding = 48.dp internal val MediaDetailHorizontalPadding = 48.dp
private val MediaDetailPanelShape = RoundedCornerShape(28.dp)
@Composable @Composable
internal fun TvMediaDetailScaffold( internal fun TvMediaDetailScaffold(
@@ -81,37 +76,6 @@ internal fun TvMediaDetailScaffold(
} }
} }
@Composable
internal fun MediaDetailHeaderRow(
modifier: Modifier = Modifier,
leftWeight: Float = 1.1f,
rightWeight: Float = 0.9f,
verticalAlignment: Alignment.Vertical = Alignment.Bottom,
leftContent: @Composable (Modifier) -> Unit,
rightContent: @Composable ColumnScope.(Modifier) -> Unit
) {
val scheme = MaterialTheme.colorScheme
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(40.dp),
verticalAlignment = verticalAlignment
) {
leftContent(Modifier.weight(leftWeight))
Column(
modifier = Modifier
.weight(rightWeight)
.background(
color = scheme.surface.copy(alpha = 0.9f),
shape = MediaDetailPanelShape
)
.padding(28.dp)
) {
rightContent(Modifier.fillMaxWidth())
}
}
}
@Composable @Composable
internal fun MediaDetailSectionTitle( internal fun MediaDetailSectionTitle(
text: String, text: String,