Preserve episode height and fix title overlay and poster collisions
All checks were successful
review/code-claude Blockers fixed on a159c835, delta reviewed
review/design-claude Blockers fixed on a159c835, delta reviewed

This commit is contained in:
justinzeus 2026-11-07 15:46:56 +01:00
parent 6d7513bd0e
commit a159c835c5
14 changed files with 118 additions and 26 deletions

View file

@ -32,6 +32,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.tv.material3.Text
@ -110,7 +111,7 @@ fun TitleBackdrop(item: TitleItem, modifier: Modifier = Modifier) {
val posterShadow = remember { Shadow(radius = D.posterShadowRadius, offset = DpOffset.Zero.copy(y = D.posterShadowY), color = Color.Black.copy(alpha = D.posterShadowOpacity)) }
TitleArtwork(item.poster, (L.heroSheetWidth.value * 2).toInt(), (L.heroSheetHeight.value * 2).toInt(),
Modifier.offset(WolfskinTokens.Shell.stageWidth - L.heroSheetWidth - D.heroSheetRight, D.heroSheetTop)
.size(L.heroSheetWidth, L.heroSheetHeight).dropShadow(posterShape, posterShadow).clip(posterShape), poster = true)
.size(L.heroSheetWidth, L.heroSheetHeight).testTag("title-poster-backdrop").dropShadow(posterShape, posterShadow).clip(posterShape), poster = true)
}
Box(Modifier.fillMaxSize().background(horizontal))
Box(Modifier.fillMaxSize().background(vertical))
@ -136,13 +137,13 @@ fun TitleTileArtwork(item: TitleItem) {
val filter = remember { ColorFilter.colorMatrix(ColorMatrix().apply {
for (index in 0 until 15) values[index] *= D.tileAmbientBrightness
}) }
TitleArtwork(item.poster, (D.episodeCardWidth.value * 2).toInt(), (D.episodeCardHeight.value * 2).toInt(), Modifier.fillMaxSize().artOverscan(D.tileAmbientInset).blur(D.tileAmbientBlur), colorFilter = filter)
TitleArtwork(item.poster, WolfskinTokens.Image.episodeRequestWidth, WolfskinTokens.Image.episodeRequestHeight, Modifier.fillMaxSize().artOverscan(D.tileAmbientInset).blur(D.tileAmbientBlur), colorFilter = filter)
val height = D.episodeCardHeight - D.tileSheetInset * 2
val shape = remember { RoundedCornerShape(D.tileSheetRadius) }
val shadow = remember { Shadow(radius = D.tileShadowRadius, offset = DpOffset.Zero.copy(y = D.tileShadowY), color = Color.Black.copy(alpha = D.tileShadowOpacity)) }
TitleArtwork(item.poster, (height.value * 2 * 2 / 3).toInt(), (height.value * 2).toInt(),
Modifier.offset(D.tileSheetLeft, D.tileSheetInset).size(height * (2f / 3), height).dropShadow(shape, shadow).clip(shape), poster = true)
} else TitleArtwork(item.backdrop, (D.episodeCardWidth.value * 2).toInt(), (D.episodeCardHeight.value * 2).toInt(), Modifier.fillMaxSize())
} else TitleArtwork(item.backdrop, WolfskinTokens.Image.episodeRequestWidth, WolfskinTokens.Image.episodeRequestHeight, Modifier.fillMaxSize())
}
}

View file

@ -43,7 +43,7 @@ internal fun TitleMenu(state: TitleState, shell: ShellState) {
opacity.animateTo(if (visible) 1f else 0f,
tween(motionDuration(if (visible) WolfskinTokens.Motion.base else WolfskinTokens.Motion.captionOut, reduced), easing = WolfskinTokens.Motion.easeOut))
}
if (visible || opacity.value > 0f) {
if ((visible || opacity.value > 0f) && (state.sheet == null || state.sheet == TitleSheet.Menu)) {
Box(Modifier.fillMaxSize().background(C.sheetScrim)
.clickable { if (visible) state.back(shell) }
.focusRequester(focus).onPreviewKeyEvent { event ->

View file

@ -93,7 +93,10 @@ fun TitlePage(state: TitleState, shell: ShellState, modifier: Modifier = Modifie
val focus = remember { FocusRequester() }
var measuredHeaderHeight by remember(state.data.item.id) { mutableStateOf(L.row_dhead) }
val compactHeader = state.data.status == TitleReadStatus.Ready
val headerHeight = if (compactHeader) measuredHeaderHeight else L.row_dhead
val item = state.data.item
val posterFloor = if (item.kind == "movie" && item.poster != null && (item.backdrop == null || item.isPosterBackdrop))
D.heroSheetTop + L.heroSheetHeight + D.posterCastGap - L.rowHeadingBand else WolfskinTokens.Shell.zero
val headerHeight = if (compactHeader) measuredHeaderHeight.coerceAtLeast(posterFloor) else L.row_dhead
LaunchedEffect(state.sheet, shell.navFocused, shell.pickerVisible) {
if (state.sheet == null && !shell.navFocused && !shell.pickerVisible) focus.requestFocus()
}

View file

@ -22,7 +22,7 @@ internal fun TitleRowPrefetch(data: TitlePageData, row: String?) {
queue(data.cast[index].portrait, WolfskinTokens.Image.castRequestWidth, WolfskinTokens.Image.castRequestHeight)
"episodes" -> for (index in 0 until minOf(data.episodes.size, WolfskinTokens.Card.tileSlots)) {
val item = data.episodes[index]
queue(if (item.isPosterBackdrop) item.poster else item.backdrop, (WolfskinTokens.Title.episodeCardWidth.value * 2).toInt(), (WolfskinTokens.Title.episodeCardHeight.value * 2).toInt())
queue(if (item.isPosterBackdrop) item.poster else item.backdrop, WolfskinTokens.Image.episodeRequestWidth, WolfskinTokens.Image.episodeRequestHeight)
}
"related" -> for (index in 0 until minOf(data.related.size, WolfskinTokens.Card.posterSlots))
queue(data.related[index].poster, WolfskinTokens.Image.posterRequestWidth, WolfskinTokens.Image.posterRequestHeight, true)

View file

@ -26,7 +26,7 @@ class TitleState(
var sheetIndex by mutableStateOf(0)
private var mediaInfoFromMenu = false
val menuOptions: List<TitleOption> by derivedStateOf { buildList {
if (target != null) add(TitleOption("info", "Media info", "more", icon = false))
if (target != null) add(TitleOption("info", "Media info", "", icon = false))
if (data.connection == TitleConnection.SignedIn && data.requestsAllowed) {
add(TitleOption("add", "Add for someone", "users", icon = false))
}
@ -72,6 +72,7 @@ class TitleState(
if (!pendingSubtitle) subtitleIndex = value.selectedSubtitle
if (row !in rows) { row = "header"; column = 0 }
column = column.coerceAtMost(rowSize(row).minus(1).coerceAtLeast(0))
if (sheet == TitleSheet.Menu && menuOptions.isEmpty()) sheet = null
}
/** Pointer focus and D-pad use the same row memory. */

View file

@ -262,7 +262,7 @@ object WolfskinTokens {
const val heroRequestHeight = 1080
const val heroPosterRequestWidth = 420
const val heroPosterRequestHeight = 630
const val episodeRequestWidth = 400
const val episodeRequestWidth = 448
const val episodeRequestHeight = 280
const val castRequestWidth = 200
const val castRequestHeight = 200
@ -452,9 +452,10 @@ object WolfskinTokens {
}
object Title {
val episodeCardWidth = 224.0.dp
val episodeCardHeight = 126.0.dp
val episodeCardHeight = 140.0.dp
val actionMenuWidth = 260.0.dp
val seriesHeaderGap = 20.0.dp
val posterCastGap = 10.0.dp
val lineGap = 11.0.dp
val lineLift = -2.0.dp
val overviewGap = 17.0.dp