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
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:
parent
6d7513bd0e
commit
a159c835c5
14 changed files with 118 additions and 26 deletions
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class WolfskinImagesTest {
|
|||
Surface("grid/Home/Discover/person poster", 256, 384, "w342"),
|
||||
Surface("Home tile", 400, 225, "w780"),
|
||||
Surface("Home wide", 544, 306, "w780"),
|
||||
Surface("episode still", 400, 280, "w780"),
|
||||
Surface("episode still", 448, 280, "w780"),
|
||||
Surface("cast portrait", 200, 200, "w342"),
|
||||
Surface("person portrait", 240, 240, "w342"),
|
||||
Surface("hero poster", 420, 630, "w342", true),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
{
|
||||
"title": {
|
||||
"episodeCardWidth": {"dp": 224, "px": 448, "source": "tv-round2: episode width +12%; 16:9 art"},
|
||||
"episodeCardHeight": {"dp": 126, "px": 252, "source": "tv-round2: 16:9 episode art"},
|
||||
"episodeCardWidth": {"dp": 224, "px": 448, "source": "tv-round2: episode width +12%"},
|
||||
"episodeCardHeight": {"dp": 140, "px": 280, "source": "feedback-2026-11-06: preserve taller episode cards"},
|
||||
"actionMenuWidth": {"dp": 260, "px": 520, "source": "tv-round2: compact title action menu"},
|
||||
"seriesHeaderGap": {"dp": 20, "px": 40, "source": "feedback-2026-11-06: measured series header to seasons"},
|
||||
"posterCastGap": {"dp": 10, "px": 20, "source": "tv-round2: minimum poster-to-cast clearance"},
|
||||
"lineGap": {"dp": 11, "px": 22, "source": "index.html:214"},
|
||||
"lineLift": {"dp": -2, "px": -4, "source": "index.html:214"},
|
||||
"overviewGap": {"dp": 17, "px": 34, "source": "index.html:216"},
|
||||
|
|
|
|||
|
|
@ -291,9 +291,10 @@
|
|||
--lift-sheetBlur: 80.0px;
|
||||
--lift-sheetX: -30.0px;
|
||||
--title-episodeCardWidth: 448.0px;
|
||||
--title-episodeCardHeight: 252.0px;
|
||||
--title-episodeCardHeight: 280.0px;
|
||||
--title-actionMenuWidth: 520.0px;
|
||||
--title-seriesHeaderGap: 40.0px;
|
||||
--title-posterCastGap: 20.0px;
|
||||
--title-lineGap: 22.0px;
|
||||
--title-lineLift: -4.0px;
|
||||
--title-overviewGap: 34.0px;
|
||||
|
|
|
|||
|
|
@ -1543,9 +1543,9 @@
|
|||
"source": "feedback-2026-11-06: largest upright hero poster slot"
|
||||
},
|
||||
"episodeRequest": {
|
||||
"px": [400, 280],
|
||||
"px": [448, 280],
|
||||
"decode": "ARGB_8888",
|
||||
"source": "feedback-2026-11-06: taller episode cards"
|
||||
"source": "tv-round2: wider episode cards at their preserved height"
|
||||
},
|
||||
"castRequest": {
|
||||
"px": [
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ b3f096b692c395534177cc82ac68f5b801870bc7e28f0dd5cd7c17e2103920a3 discover-token
|
|||
c29dc64f3c3ca900c747b3db8b270e25908948bf5f76388d2c627a612fc7c99d generate_tokens.py
|
||||
eef4e9f8c91306362540eda617c9efad4e19ed688f205f6f486625a5918d8105 home-tokens.json
|
||||
192beb887fc34005db99c87ffbcbaf4a057ee2728475fc96224cf89445019b51 shell-tokens.json
|
||||
12420580fed6e6977c8762fb0649f0ed22ca6a8df32d65ea69b3965cd851bb65 src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt
|
||||
98054296d1de4df2f9f4808d505e512d5906f5f23f7a05e6d846391d93effa5e title-tokens.json
|
||||
40d763d9023abb56c197201a6a13d3fefacb681713010f849d450c96216e37cb tokens.css
|
||||
ba0da02b03047fbc2f29d538d980e87aa237f8ae36545aa4234600243fe14929 tokens.json
|
||||
0d69fccfbfada103268bfc38186205cf5f94daeb345a00a6aec585609e7ddb1e src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt
|
||||
98fa6414cc6663624d09329615cdf0e6599baec570a8d878f5214c95cb33cb48 title-tokens.json
|
||||
a68f336e8b4373e97a7acaeb02383b4a40dc9f7cc6f6e5413341f14fb2d308eb tokens.css
|
||||
b1cf8f8caea9bfacb7994e0ba39e71ffab33452f6f5227a524ba71874b045b4d tokens.json
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue