Preserve native title line boxes and async focus
This commit is contained in:
parent
a8951827ba
commit
43ae716157
9 changed files with 187 additions and 32 deletions
|
|
@ -104,7 +104,7 @@ fun NavBar(state: ShellState, scrolled: Boolean, modifier: Modifier = Modifier,
|
|||
Row(Modifier.align(Alignment.TopEnd)
|
||||
.offset(x = -(L.safeSide + L.navAvatar + L.navItemGap), y = S.navItemTop + (L.navAvatar - V.height) / 2)
|
||||
.height(V.height).border(V.border, C.warn.copy(alpha = V.borderOpacity), RoundedCornerShape(WolfskinTokens.Radius.button))
|
||||
.padding(horizontal = V.side), horizontalArrangement = Arrangement.spacedBy(V.gap), verticalAlignment = Alignment.CenterVertically) {
|
||||
.padding(horizontal = V.side + V.border), horizontalArrangement = Arrangement.spacedBy(V.gap), verticalAlignment = Alignment.CenterVertically) {
|
||||
Glyph("wifi", C.warn, Modifier.size(V.icon))
|
||||
Text("Saved $savedAt", color = C.warn, style = WolfskinTokens.Type.caption)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,15 +131,15 @@ private fun PersonPage(state: PersonState, shell: ShellState, modifier: Modifier
|
|||
Row(Modifier.offset(L.safeSide, D.personTop), horizontalArrangement = Arrangement.spacedBy(D.personGap), verticalAlignment = Alignment.CenterVertically) {
|
||||
PersonPortrait(state.person, Modifier.size(L.personPortrait).clip(CircleShape), true)
|
||||
Column(Modifier.width(L.contentWidth - L.personPortrait - D.personGap)) {
|
||||
Text(state.person.name, color = C.fg, style = WolfskinTokens.Type.screenTitle, modifier = Modifier.padding(bottom = D.personNameBottom))
|
||||
Text("Acting · ${state.credits.size} credit${if (state.credits.size == 1) "" else "s"} among this household’s titles", color = C.muted, style = WolfskinTokens.Type.body)
|
||||
if (state.person.biography.isNotBlank()) Text(state.person.biography, color = C.dim, style = WolfskinTokens.Type.caption, maxLines = 3)
|
||||
Text(state.person.name, color = C.fg, style = TitleTextStyles.screenTitle, modifier = Modifier.padding(bottom = D.personNameBottom))
|
||||
Text("Acting · ${state.credits.size} credit${if (state.credits.size == 1) "" else "s"} among this household’s titles", color = C.muted, style = TitleTextStyles.body)
|
||||
if (state.person.biography.isNotBlank()) Text(state.person.biography, color = C.dim, style = TitleTextStyles.caption, maxLines = 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item(key = "filmography", contentType = "header") {
|
||||
Text("Filmography", color = C.fg, style = WolfskinTokens.Type.rowHeading, modifier = Modifier.height(L.row_gh).padding(start = L.safeSide, top = D.gridHeadingTop))
|
||||
Text("Filmography", color = C.fg, style = TitleTextStyles.rowHeading, modifier = Modifier.height(L.row_gh).padding(start = L.safeSide, top = D.gridHeadingTop))
|
||||
}
|
||||
itemsIndexed(state.credits.chunked(columns), key = { _, row -> row.first().id }, contentType = { _, _ -> "grid" }) { rowIndex, row ->
|
||||
Row(Modifier.height(D.gridHeight).padding(horizontal = L.safeSide), horizontalArrangement = Arrangement.spacedBy(WolfskinTokens.Card.posterGap)) {
|
||||
|
|
|
|||
|
|
@ -70,24 +70,24 @@ fun TitleOptionsSheet(state: TitleState, shell: ShellState) {
|
|||
|
||||
@Composable
|
||||
private fun VersionChoices(state: TitleState) {
|
||||
Text("Version", color = C.fg, style = WolfskinTokens.Type.sheetTitle)
|
||||
Text("Version", color = C.fg, style = TitleTextStyles.sheetTitle)
|
||||
SheetSubtitle("${state.target?.name ?: state.data.item.name} · ${state.data.versions.size} files")
|
||||
state.data.versions.forEachIndexed { index, version ->
|
||||
PlaybackChoice(version.name, listOf(version.video, version.bitrate, version.size).filter { it.isNotBlank() }.joinToString(" · "), version == state.version, state.sheetIndex == index, { state.sheetIndex = index }) { state.clickSheet(index) }
|
||||
}
|
||||
Text("Remembered for ${state.person.name} on this title.", color = C.dim, style = WolfskinTokens.Type.caption)
|
||||
Text("Remembered for ${state.person.name} on this title.", color = C.dim, style = TitleTextStyles.caption)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrackChoices(state: TitleState) {
|
||||
val version = state.version ?: return
|
||||
Text("Audio and subtitles", color = C.fg, style = WolfskinTokens.Type.sheetTitle)
|
||||
Text("Audio and subtitles", color = C.fg, style = TitleTextStyles.sheetTitle)
|
||||
SheetSubtitle("${state.target?.let { if (it.kind == "episode") "${it.seriesName} · ${it.episodeLabel}" else it.name } ?: state.data.item.name} · ${version.name}")
|
||||
Text("Audio", color = C.dim, style = WolfskinTokens.Type.label, modifier = Modifier.padding(top = D.sheetLabelTop))
|
||||
Text("Audio", color = C.dim, style = TitleTextStyles.label, modifier = Modifier.padding(top = D.sheetLabelTop))
|
||||
version.audio.forEachIndexed { index, track ->
|
||||
PlaybackChoice(track.label, null, track.index == state.audio?.index, state.sheetIndex == index, { state.sheetIndex = index }) { state.clickSheet(index) }
|
||||
}
|
||||
Text("Subtitles", color = C.dim, style = WolfskinTokens.Type.label, modifier = Modifier.padding(top = D.sheetLabelTop))
|
||||
Text("Subtitles", color = C.dim, style = TitleTextStyles.label, modifier = Modifier.padding(top = D.sheetLabelTop))
|
||||
val start = version.audio.size
|
||||
PlaybackChoice("Off", null, state.subtitleIndex == -1, state.sheetIndex == start, { state.sheetIndex = start }) { state.clickSheet(start) }
|
||||
version.subtitles.forEachIndexed { index, track ->
|
||||
|
|
@ -107,8 +107,8 @@ private fun PlaybackChoice(label: String, why: String?, selected: Boolean, focus
|
|||
.clickable(onClick = onClick).then(sizing).padding(horizontal = D.sheetRowSide),
|
||||
horizontalArrangement = Arrangement.spacedBy(D.personGap), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(label, color = foreground, style = WolfskinTokens.Type.listTitle)
|
||||
if (why != null) Text(why, color = foreground.copy(alpha = .8f), style = WolfskinTokens.Type.caption)
|
||||
Text(label, color = foreground, style = TitleTextStyles.listTitle)
|
||||
if (why != null) Text(why, color = foreground.copy(alpha = .8f), style = TitleTextStyles.caption)
|
||||
}
|
||||
TitleRadio(selected, focused)
|
||||
}
|
||||
|
|
@ -117,7 +117,7 @@ private fun PlaybackChoice(label: String, why: String?, selected: Boolean, focus
|
|||
@Composable
|
||||
private fun MediaInformation(state: TitleState) {
|
||||
val version = state.version ?: return
|
||||
Text("Media info", color = C.fg, style = WolfskinTokens.Type.sheetTitle)
|
||||
Text("Media info", color = C.fg, style = TitleTextStyles.sheetTitle)
|
||||
SheetSubtitle(state.target?.let { if (it.kind == "episode") "${it.seriesName} · ${it.episodeLabel}" else it.name } ?: state.data.item.name)
|
||||
MediaInfoSection("General", listOf("Container" to version.container, "Size" to version.size.ifEmpty { "not known" }, "Bitrate" to version.bitrate, "Runtime" to (state.target?.runtime ?: "not known")))
|
||||
MediaInfoSection("Video", listOf("Stream" to version.video))
|
||||
|
|
@ -128,11 +128,11 @@ private fun MediaInformation(state: TitleState) {
|
|||
|
||||
@Composable
|
||||
private fun MediaInfoSection(label: String, values: List<Pair<String, String>>) {
|
||||
Text(label, color = C.dim, style = WolfskinTokens.Type.label, modifier = Modifier.padding(top = D.sheetLabelTop))
|
||||
Text(label, color = C.dim, style = TitleTextStyles.label, modifier = Modifier.padding(top = D.sheetLabelTop))
|
||||
Column(verticalArrangement = Arrangement.spacedBy(D.infoRowGap)) { values.forEach { (name, value) ->
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(D.infoColumnGap)) {
|
||||
Text(name, color = C.dim, style = D.infoKey, modifier = Modifier.width(D.infoLabelWidth))
|
||||
Text(value, color = C.fg, style = WolfskinTokens.Type.caption)
|
||||
Text(name, color = C.dim, style = TitleTextStyles.infoKey, modifier = Modifier.width(D.infoLabelWidth))
|
||||
Text(value, color = C.fg, style = TitleTextStyles.caption)
|
||||
}
|
||||
} }
|
||||
}
|
||||
|
|
@ -140,7 +140,7 @@ private fun MediaInfoSection(label: String, values: List<Pair<String, String>>)
|
|||
@Composable
|
||||
private fun WatchedConfirmation(state: TitleState) {
|
||||
val verb = if (state.watched) "unwatched" else "watched"
|
||||
Text("Mark the whole series as $verb?", color = C.fg, style = WolfskinTokens.Type.sheetTitle)
|
||||
Text("Mark the whole series as $verb?", color = C.fg, style = TitleTextStyles.sheetTitle)
|
||||
SheetSubtitle("Every episode of ${state.data.item.name} changes for ${state.person.name}.")
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(L.buttonGap)) {
|
||||
TitleControl(TitleOption("confirm", "Mark as $verb", "", primary = true, icon = false), state.sheetIndex == 0, onFocus = { state.sheetIndex = 0 }) { state.clickSheet(0) }
|
||||
|
|
@ -150,7 +150,7 @@ private fun WatchedConfirmation(state: TitleState) {
|
|||
|
||||
@Composable
|
||||
private fun SheetSubtitle(text: String) {
|
||||
Text(text, color = C.muted, style = WolfskinTokens.Type.body, modifier = Modifier.layout { measurable, constraints ->
|
||||
Text(text, color = C.muted, style = TitleTextStyles.body, modifier = Modifier.layout { measurable, constraints ->
|
||||
val child = measurable.measure(constraints)
|
||||
val lift = D.sheetSubLift.roundToPx()
|
||||
layout(child.width, (child.height + lift).coerceAtLeast(0)) { child.placeRelative(0, lift) }
|
||||
|
|
@ -163,9 +163,9 @@ private fun HiddenReceipt(state: TitleState) {
|
|||
verticalArrangement = Arrangement.spacedBy(D.receiptGap)) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(D.iconGap), verticalAlignment = Alignment.CenterVertically) {
|
||||
Glyph("check", C.fg, Modifier.size(D.receiptIcon))
|
||||
Text("Hidden from your picks", color = C.fg, style = D.receiptTitle)
|
||||
Text("Hidden from your picks", color = C.fg, style = TitleTextStyles.receiptTitle)
|
||||
}
|
||||
Text("${state.data.item.name} leaves every row of ${state.person.name}’s picks. It waits under Hidden picks at the end of Discover.", color = C.muted, style = WolfskinTokens.Type.body)
|
||||
Text("${state.data.item.name} leaves every row of ${state.person.name}’s picks. It waits under Hidden picks at the end of Discover.", color = C.muted, style = TitleTextStyles.body)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(L.buttonGap)) {
|
||||
TitleControl(TitleOption("undo", "Undo", "", primary = true, icon = false), state.sheetIndex == 0, onFocus = { state.sheetIndex = 0 }) { state.clickSheet(0) }
|
||||
|
|
|
|||
|
|
@ -201,12 +201,13 @@ private fun TitleReadError(state: TitleState) {
|
|||
Column(Modifier.offset(L.safeSide, D.errorTop).width(L.stateWidth)
|
||||
.background(C.surface, RoundedCornerShape(WolfskinTokens.Radius.panel))
|
||||
.border(WolfskinTokens.SavedIndicator.border, C.border, RoundedCornerShape(WolfskinTokens.Radius.panel))
|
||||
.padding(WolfskinTokens.SavedIndicator.border)
|
||||
.padding(horizontal = D.errorSide, vertical = D.errorVertical)) {
|
||||
Row(Modifier.padding(bottom = D.errorHeadingBottom), horizontalArrangement = Arrangement.spacedBy(D.iconGap), verticalAlignment = Alignment.CenterVertically) {
|
||||
Glyph("alert", C.warn, Modifier.size(D.receiptIcon))
|
||||
Text(if (state.data.item.kind == "person") "This person can’t be read" else "This title can’t be read", color = C.fg, style = D.errorTitle)
|
||||
Text(if (state.data.item.kind == "person") "This person can’t be read" else "This title can’t be read", color = C.fg, style = TitleTextStyles.errorTitle)
|
||||
}
|
||||
Text("${if (state.data.connection == TitleConnection.SignedOut) "Jellyfin" else "Conductarr"} didn’t answer, and this TV has no saved copy of the page yet.", color = C.muted, style = WolfskinTokens.Type.body, modifier = Modifier.padding(bottom = D.errorBodyBottom))
|
||||
Text("${if (state.data.connection == TitleConnection.SignedOut) "Jellyfin" else "Conductarr"} didn’t answer, and this TV has no saved copy of the page yet.", color = C.muted, style = TitleTextStyles.body, modifier = Modifier.padding(bottom = D.errorBodyBottom))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(L.buttonGap)) { state.options.forEachIndexed { index, option -> TitleControl(option, state.column == index, onFocus = { state.focus("header", index) }, overArtwork = false) { state.click("header", index) } } }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package org.justinvisser.wolfskin.design
|
||||
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.LineHeightStyle
|
||||
|
||||
/** Explicit styles retain CSS leading without changing shared nav or library typography. */
|
||||
internal object TitleTextStyles {
|
||||
private val lineBoxes = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
|
||||
private fun TextStyle.withLineBoxes() = copy(lineHeightStyle = lineBoxes)
|
||||
|
||||
val body = WolfskinTokens.Type.body.withLineBoxes()
|
||||
val caption = WolfskinTokens.Type.caption.withLineBoxes()
|
||||
val label = WolfskinTokens.Type.label.withLineBoxes()
|
||||
val sheetTitle = WolfskinTokens.Type.sheetTitle.withLineBoxes()
|
||||
val listTitle = WolfskinTokens.Type.listTitle.withLineBoxes()
|
||||
val screenTitle = WolfskinTokens.Type.screenTitle.withLineBoxes()
|
||||
val rowHeading = WolfskinTokens.Type.rowHeading.withLineBoxes()
|
||||
val errorTitle = WolfskinTokens.Title.errorTitle.withLineBoxes()
|
||||
val receiptTitle = WolfskinTokens.Title.receiptTitle.withLineBoxes()
|
||||
val infoKey = WolfskinTokens.Title.infoKey.withLineBoxes()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue