Keep library focus on visible rows and match caption facts
This commit is contained in:
parent
3da635b4cb
commit
a6dd04f390
8 changed files with 68 additions and 12 deletions
|
|
@ -298,6 +298,7 @@
|
|||
"chipTop": {"$type":"dimension","px":16,"dp":8,"source":"index.html:184"},
|
||||
"chipIcon": {"$type":"dimension","px":26,"dp":13,"source":"index.html:185"},
|
||||
"captionWidth": {"$type":"dimension","px":640,"dp":320,"source":"index.html:151"},
|
||||
"captionSeparatorOpacity": {"$type":"number","$value":0.4,"source":"index.html:1207; spec 4.4"},
|
||||
"plateTop": {"$type":"dimension","px":14,"dp":7,"source":"index.html:162"},
|
||||
"plateSide": {"$type":"dimension","px":16,"dp":8,"source":"index.html:162"},
|
||||
"plateBottom": {"$type":"dimension","px":46,"dp":23,"source":"index.html:162"},
|
||||
|
|
@ -312,6 +313,8 @@
|
|||
"railWidth": {"$type":"dimension","px":52,"dp":26,"source":"index.html:204"},
|
||||
"railLetterHeight": {"$type":"dimension","px":30,"dp":15,"source":"index.html:205"},
|
||||
"railFocusHeight": {"$type":"dimension","px":44,"dp":22,"source":"index.html:208"},
|
||||
"railLetter": {"$type":"typography","px":24,"sp":12,"weight":700,"$value":{"fontWeight":700},"source":"index.html:205; prototype wins over spec 4.3 caption weight"},
|
||||
"railFocusedLetter": {"$type":"typography","px":28,"sp":14,"weight":700,"$value":{"fontWeight":700},"source":"index.html:208; prototype wins over spec 4.3 caption size"},
|
||||
"panelPaddingX": {"$type":"dimension","px":40,"dp":20,"source":"index.html:194"},
|
||||
"panelPaddingY": {"$type":"dimension","px":34,"dp":17,"source":"index.html:194"},
|
||||
"panelTextGap": {"$type":"dimension","px":24,"dp":12,"source":"index.html:198"},
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import androidx.compose.ui.graphics.graphicsLayer
|
|||
import androidx.tv.material3.Text
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
|
||||
/** Captions fade without moving the reserved band under the focused poster. */
|
||||
@Composable
|
||||
|
|
@ -23,10 +22,25 @@ fun Caption(title: String, facts: String, visible: Boolean, modifier: Modifier =
|
|||
}
|
||||
Column(modifier.graphicsLayer { this.alpha = alpha.value }) {
|
||||
Text(title, color = WolfskinTokens.Color.fg, style = WolfskinTokens.Type.button, maxLines = 1)
|
||||
val styledFacts = buildAnnotatedString {
|
||||
val suffix = if (accentLastFact) facts.lastIndexOf(" · ").takeIf { it >= 0 }?.plus(" · ".length) ?: facts.length else facts.length
|
||||
append(facts.substring(0, suffix))
|
||||
withStyle(SpanStyle(color = WolfskinTokens.Color.accentText)) { append(facts.substring(suffix)) }
|
||||
val styledFacts = remember(facts, accentLastFact) {
|
||||
buildAnnotatedString {
|
||||
val separator = " · "
|
||||
append(facts)
|
||||
if (accentLastFact) {
|
||||
val suffix = facts.lastIndexOf(separator).takeIf { it >= 0 }?.plus(separator.length) ?: facts.length
|
||||
if (suffix < facts.length) addStyle(SpanStyle(color = WolfskinTokens.Color.accentText), suffix, facts.length)
|
||||
}
|
||||
var start = facts.indexOf(separator)
|
||||
if (start >= 0) {
|
||||
val muted = WolfskinTokens.Color.muted
|
||||
val separatorStyle = SpanStyle(color = muted.copy(alpha = muted.alpha * WolfskinTokens.Library.captionSeparatorOpacity))
|
||||
val dotOffset = separator.indexOf('·')
|
||||
while (start >= 0) {
|
||||
addStyle(separatorStyle, start + dotOffset, start + dotOffset + 1)
|
||||
start = facts.indexOf(separator, start + separator.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(styledFacts, color = WolfskinTokens.Color.muted, style = WolfskinTokens.Type.caption, maxLines = 1)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,10 +109,12 @@ class LibraryGridState(
|
|||
}
|
||||
if (toolbar) return toolbarKey(key)
|
||||
val currentItems = items
|
||||
val last = if (tail != GridTail.None) tailIndex else currentItems.lastIndex
|
||||
when (key) {
|
||||
ShellKey.Up -> if (index >= columns) index -= columns else if (hasToolbar) toolbar = true else return false
|
||||
ShellKey.Down -> if (index + columns <= last) index = (index + columns).coerceAtMost(last) else if (index / columns < last / columns) index = last
|
||||
ShellKey.Down -> when {
|
||||
index / columns < currentItems.lastIndex / columns -> index = (index + columns).coerceAtMost(currentItems.lastIndex)
|
||||
tail != GridTail.None && index < tailIndex -> index = tailIndex
|
||||
}
|
||||
ShellKey.Left -> if (index % columns > 0) index--
|
||||
ShellKey.Right -> if (index % columns < columns - 1 && index < currentItems.lastIndex) index++ else if (railVisible) { inRail = true; railReturn = index; railLetter = currentItems[index].letter }
|
||||
ShellKey.Ok -> currentItems.getOrNull(index)?.let(onTitle) ?: if (pagination != null) pageRevision++ else onRecover()
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ private fun LibraryHeader(grid: LibraryGridState, person: ShellPerson) {
|
|||
Row(Modifier.offset(L.safeSide, L.navBand), horizontalArrangement = Arrangement.spacedBy(WolfskinTokens.Shell.settingsHeaderGap), verticalAlignment = Alignment.CenterVertically) {
|
||||
Avatar(person, WolfskinTokens.Shell.settingsHeaderAvatar, WolfskinTokens.Type.avatarLetterLibHeader)
|
||||
Text(grid.title, modifier = Modifier.alignByBaseline(), color = C.fg, style = WolfskinTokens.Type.screenTitle)
|
||||
Text(if (grid.status == LibraryStatus.Loading) "" else "$count ${if (grid.catalogue) "titles" else if (grid.title == "Movies") if (count == 1) "film" else "films" else "series"}", modifier = Modifier.alignByBaseline(), color = C.muted, style = WolfskinTokens.Type.nav.copy(fontWeight = FontWeight.Normal))
|
||||
Text(if (grid.status != LibraryStatus.Ready) "" else "$count ${if (grid.catalogue) "titles" else if (grid.title == "Movies") if (count == 1) "film" else "films" else "series"}", modifier = Modifier.alignByBaseline(), color = C.muted, style = WolfskinTokens.Type.nav.copy(fontWeight = FontWeight.Normal))
|
||||
grid.savedAt?.let { Text("Saved $it", color = C.warn, style = WolfskinTokens.Type.caption) }
|
||||
}
|
||||
}
|
||||
|
|
@ -245,7 +245,8 @@ private fun BoxScope.LibraryRail(grid: LibraryGridState) {
|
|||
Box(Modifier.width(G.railWidth).height(if (current && grid.inRail) G.railFocusHeight else G.railLetterHeight)
|
||||
.background(if (current && grid.inRail) C.fg else androidx.compose.ui.graphics.Color.Transparent, CircleShape)
|
||||
.clickable(enabled = letter in letters) { grid.railLetter = letter; grid.inRail = true; grid.key(ShellKey.Ok) }, contentAlignment = Alignment.Center) {
|
||||
Text(letter.toString(), color = if (current) if (grid.inRail) C.onFocus else C.accent else if (letter in letters) C.muted else C.railEmpty, style = WolfskinTokens.Type.caption)
|
||||
Text(letter.toString(), color = if (current) if (grid.inRail) C.onFocus else C.accent else if (letter in letters) C.muted else C.railEmpty,
|
||||
style = if (current && grid.inRail) G.railFocusedLetter else G.railLetter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -287,6 +287,7 @@ object WolfskinTokens {
|
|||
val chipTop = 8.0.dp
|
||||
val chipIcon = 13.0.dp
|
||||
val captionWidth = 320.0.dp
|
||||
const val captionSeparatorOpacity = 0.4f
|
||||
val plateTop = 7.0.dp
|
||||
val plateSide = 8.0.dp
|
||||
val plateBottom = 23.0.dp
|
||||
|
|
@ -301,6 +302,8 @@ object WolfskinTokens {
|
|||
val railWidth = 26.0.dp
|
||||
val railLetterHeight = 15.0.dp
|
||||
val railFocusHeight = 22.0.dp
|
||||
val railLetter = TextStyle(fontSize = 12.0.sp, fontWeight = FontWeight.Bold, lineHeight = 15.600000000000001.sp, letterSpacing = 0.0.em)
|
||||
val railFocusedLetter = TextStyle(fontSize = 14.0.sp, fontWeight = FontWeight.Bold, lineHeight = 18.2.sp, letterSpacing = 0.0.em)
|
||||
val panelPaddingX = 20.0.dp
|
||||
val panelPaddingY = 17.0.dp
|
||||
val panelTextGap = 12.0.dp
|
||||
|
|
|
|||
|
|
@ -141,6 +141,37 @@ class LibraryKeysTest {
|
|||
assertEquals(8, grid.items.size)
|
||||
}
|
||||
|
||||
@Test fun downClampsShortLastRowBeforeEnteringRecoveryTail() {
|
||||
var recoveries = 0
|
||||
val grid = LibraryGridState("Trending",
|
||||
(0 until 20).map { LibraryTitle("$it", "Title $it", "movie") },
|
||||
catalogue = true, onRecover = { recoveries++ },
|
||||
).apply { tail = GridTail.Retry; index = 14 }
|
||||
shell.pickerVisible = false
|
||||
shell.enterDestination("discover")
|
||||
rule.setContent {
|
||||
WolfskinTheme {
|
||||
ShellSurface(shell) { modifier ->
|
||||
LibraryScreen(shell, grid, modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
press(Key.DirectionDown)
|
||||
assertEquals(19, grid.index)
|
||||
assertEquals("Title 19", grid.focusedTitle!!.title)
|
||||
assertEquals(0, recoveries)
|
||||
press(Key.DirectionDown)
|
||||
assertEquals(24, grid.index)
|
||||
assertEquals("grid-tail", grid.cardKey)
|
||||
press(Key.DirectionDown)
|
||||
assertEquals(24, grid.index)
|
||||
press(Key.DirectionCenter)
|
||||
assertEquals(1, recoveries)
|
||||
press(Key.DirectionUp)
|
||||
assertEquals(18, grid.index)
|
||||
assertEquals("Title 18", grid.focusedTitle!!.title)
|
||||
}
|
||||
|
||||
@Test fun scoresFollowLocaleAndUnratedSortsLast() {
|
||||
assertEquals("7,8", LibraryRating("tmdb", 7.8).formatted(Locale.forLanguageTag("nl-NL")))
|
||||
assertEquals("7.8", LibraryRating("tmdb", 7.8).formatted(Locale.US))
|
||||
|
|
|
|||
|
|
@ -186,6 +186,8 @@
|
|||
--library-railWidth: 52.0px;
|
||||
--library-railLetterHeight: 30.0px;
|
||||
--library-railFocusHeight: 44.0px;
|
||||
--library-railLetter: 24.0px;
|
||||
--library-railFocusedLetter: 28.0px;
|
||||
--library-panelPaddingX: 40.0px;
|
||||
--library-panelPaddingY: 34.0px;
|
||||
--library-panelTextGap: 24.0px;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
1438f7ca4a2b6f951c9e643ab0afacbf2f7fc98e57905295d518c6225e054de4 generate_tokens.py
|
||||
9d37e43db493a52cc5be6c9418d0c3c132dae875f70cfd0cd1bf97608e5a1425 shell-tokens.json
|
||||
3f8a3a996f1dd1743dac970e9815431df5c72336f338598e8f6608aae6e16377 src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt
|
||||
25c87d3e0cf602764b20df29248b091764709c3b9b8bf6e7f966ed30ac8f7435 tokens.css
|
||||
2d98b185e68c2f964cb744bb640dea94d3c5eaad183816d422646806b41327a3 shell-tokens.json
|
||||
965e802974d8fd440c65a1178f49e7824e986648c49f4068efcc587f9b5b384f src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt
|
||||
3db2663abf8fc15dc8ddee8adb18ad411065aff5ca8486b533c344596514e1d9 tokens.css
|
||||
5b939f1f3eb90b428008fda75720f6e3a4e844c486668931db3d52bffb987b53 tokens.json
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue