Match Requests sheets to locked design frames
|
|
@ -0,0 +1,16 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
package org.justinvisser.wolfskin.design
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.FirstBaseline
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
||||
/** A CSS line box has its own height and baseline, independent of native leading rounding. */
|
||||
internal fun Modifier.comingInLineBox(height: Dp, baseline: Dp): Modifier = layout { measurable, constraints ->
|
||||
val text = measurable.measure(constraints.copy(minHeight = 0))
|
||||
val baselineOffset = baseline.roundToPx() - text[FirstBaseline]
|
||||
layout(text.width, height.roundToPx()) {
|
||||
text.placeRelative(0, baselineOffset)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,10 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.tv.material3.Text
|
||||
import java.util.Locale
|
||||
import org.justinvisser.wolfskin.design.WolfskinTokens.Requests as T
|
||||
|
|
@ -35,9 +39,13 @@ fun ComingInRatings(ratings: List<ComingInRating>, color: Color) {
|
|||
}
|
||||
val wide = rating.mark == "imdb" || rating.mark == "tmdb"
|
||||
Image(painterResource(mark), contentDescription = rating.mark,
|
||||
modifier = Modifier.height(T.readinessGlyph).width(T.readinessGlyph * if (wide) T.ratingsWordmarkRatio else 1f))
|
||||
modifier = Modifier.height(T.readinessGlyph).width(T.readinessGlyph * if (wide) T.ratingsWordmarkRatio else 1f)
|
||||
.clip(ratingMarkShape).background(WolfskinTokens.Color.surface3),
|
||||
contentScale = ContentScale.Crop)
|
||||
Text(formatComingInRating(rating), style = ComingInTypography.rating, color = color)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val ratingMarkShape = RoundedCornerShape(WolfskinTokens.Radius.thumb)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import androidx.compose.ui.platform.testTag
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.tv.material3.Text
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.compose.AsyncImagePainter
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import org.justinvisser.wolfskin.design.WolfskinTokens.Color as C
|
||||
import org.justinvisser.wolfskin.design.WolfskinTokens.Layout as L
|
||||
import org.justinvisser.wolfskin.design.WolfskinTokens.Requests as T
|
||||
|
|
@ -57,7 +59,7 @@ fun ComingInSheet(shell: ShellState, modifier: Modifier = Modifier) {
|
|||
}
|
||||
.padding(horizontal = T.sheetPaddingX, vertical = T.sheetPaddingY)
|
||||
.testTag("coming-in"), verticalArrangement = Arrangement.spacedBy(T.sheetGap)) {
|
||||
Text("Coming in", style = ComingInTypography.title, color = C.fg)
|
||||
Text("Coming in", Modifier.comingInLineBox(T.sheetTitleLineHeight, T.sheetTitleBaseline), style = ComingInTypography.title, color = C.fg)
|
||||
when {
|
||||
data.status == ComingInStatus.Loading && data.items.isEmpty() -> {
|
||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(T.listGap)) {
|
||||
|
|
@ -123,6 +125,7 @@ fun ComingInRow(item: ComingInRequest, viewer: ShellPerson, focused: Boolean, ex
|
|||
val secondary = if (focused) C.onFocus else C.muted
|
||||
val context = LocalContext.current
|
||||
val request = remember(item.poster, context) { LibraryImages.request(context, item.poster, L.thumbWidth, L.thumbHeight) }
|
||||
val posterReady = remember(request) { mutableStateOf(false) }
|
||||
Row(Modifier.fillMaxWidth().focusScale(focused, WolfskinTokens.Focus.scaleRow)
|
||||
.background(if (focused) C.fg else Color.Transparent, RoundedCornerShape(WolfskinTokens.Radius.panel))
|
||||
.pointerInput(item.id) {
|
||||
|
|
@ -133,26 +136,26 @@ fun ComingInRow(item: ComingInRequest, viewer: ShellPerson, focused: Boolean, ex
|
|||
.padding(horizontal = T.rowPaddingX, vertical = T.rowPaddingY).testTag("coming-row-${item.id}"),
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(T.rowGap)) {
|
||||
AsyncImage(model = request, imageLoader = LibraryImages.loader(context), contentDescription = null,
|
||||
modifier = Modifier.size(L.thumbWidth, L.thumbHeight).clip(RoundedCornerShape(WolfskinTokens.Radius.thumb)).background(C.surface3),
|
||||
modifier = Modifier.size(L.thumbWidth, L.thumbHeight).clip(RoundedCornerShape(WolfskinTokens.Radius.thumb))
|
||||
.background(if (posterReady.value) Color.Transparent else C.surface3),
|
||||
onState = { posterReady.value = it is AsyncImagePainter.State.Success },
|
||||
contentScale = androidx.compose.ui.layout.ContentScale.Crop)
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(item.title, style = ComingInTypography.listTitle, color = foreground, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Row {
|
||||
Text("for ${item.recipient(viewer)}", style = ComingInTypography.caption, color = secondary)
|
||||
Text(" · ", style = ComingInTypography.caption, color = secondary.copy(alpha = secondary.alpha * WolfskinTokens.Library.captionSeparatorOpacity))
|
||||
Text("by ${item.actor(viewer)}", style = ComingInTypography.caption, color = secondary)
|
||||
}
|
||||
FlowRow(Modifier.padding(top = T.stageMargin), horizontalArrangement = Arrangement.spacedBy(T.readinessGlyph)) {
|
||||
Text(item.title, Modifier.comingInLineBox(T.titleLineHeight, T.titleBaseline),
|
||||
style = ComingInTypography.listTitle, color = foreground, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text("for ${item.recipient(viewer)} · by ${item.actor(viewer)}", Modifier.comingInLineBox(T.factsLineHeight, T.factsBaseline),
|
||||
style = ComingInTypography.caption, color = secondary)
|
||||
FlowRow(Modifier.padding(top = T.stageMargin).heightIn(min = T.stageLineHeight), horizontalArrangement = Arrangement.spacedBy(T.readinessGlyph)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(T.stageGap)) {
|
||||
Glyph(item.stage.glyph, secondary, Modifier.size(T.stageGlyph))
|
||||
Text(item.stage.label, style = ComingInTypography.label, color = secondary)
|
||||
Text(item.stage.label, Modifier.comingInLineBox(T.stageLabelLineHeight, T.stageBaseline), style = ComingInTypography.label, color = secondary)
|
||||
}
|
||||
if (showRatings) ComingInRatings(item.ratings, foreground)
|
||||
}
|
||||
if (expanded) FlowRow(Modifier.padding(top = T.readinessMargin),
|
||||
horizontalArrangement = Arrangement.spacedBy(T.readinessGapX), verticalArrangement = Arrangement.spacedBy(T.readinessGapY)) {
|
||||
item.stage.readiness.forEachIndexed { index, word ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(T.stageGap)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(T.readinessItemGap)) {
|
||||
val glyph = when (word) { "Done" -> "check"; "Failed" -> "alert"; "Not yet", "Unknown" -> null; else -> "clock" }
|
||||
if (glyph != null) Glyph(glyph, secondary, Modifier.size(T.readinessGlyph))
|
||||
Text("${READINESS_NAMES[index]} · $word", style = ComingInTypography.readiness, color = secondary)
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ fun SettingsList(state: ShellState, modifier: Modifier = Modifier, openRequests:
|
|||
item(key = "settings-header") {
|
||||
Row(Modifier.height(L.row_lhead).padding(start = L.safeSide, top = S.settingsHeaderTop),
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(S.settingsHeaderGap)) {
|
||||
Avatar(state.person, S.settingsHeaderAvatar, WolfskinTokens.Type.avatarLetterLibHeader)
|
||||
Avatar(state.person, S.settingsHeaderAvatar, WolfskinTokens.Type.avatarLetterLibHeader, letterColor = C.muted)
|
||||
Text("Settings", Modifier.alignByBaseline(), style = WolfskinTokens.Type.screenTitle, color = C.fg)
|
||||
Text("Requests and Wholphin settings on this TV · ${state.person.name}", Modifier.alignByBaseline(), style = WolfskinTokens.Type.nav.copy(fontWeight = FontWeight.Normal), color = C.muted)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,6 +233,15 @@ object WolfskinTokens {
|
|||
val sheetPaddingY = 40.0.dp
|
||||
val sheetPaddingX = 32.0.dp
|
||||
val sheetGap = 9.0.dp
|
||||
val sheetTitleLineHeight = 28.0.dp
|
||||
val sheetTitleBaseline = 22.0.dp
|
||||
val titleLineHeight = 19.5.dp
|
||||
val titleBaseline = 15.5.dp
|
||||
val factsLineHeight = 15.6.dp
|
||||
val factsBaseline = 12.0.dp
|
||||
val stageLineHeight = 19.8.dp
|
||||
val stageLabelLineHeight = 16.9.dp
|
||||
val stageBaseline = 12.0.dp
|
||||
val emptyMargin = 10.0.dp
|
||||
val rowPaddingY = 7.0.dp
|
||||
val rowPaddingX = 9.0.dp
|
||||
|
|
@ -240,7 +249,8 @@ object WolfskinTokens {
|
|||
val listGap = 3.0.dp
|
||||
val stageMargin = 2.0.dp
|
||||
val stageGlyph = 14.0.dp
|
||||
val stageGap = 4.0.dp
|
||||
val stageGap = 5.0.dp
|
||||
val readinessItemGap = 4.0.dp
|
||||
val readinessGapX = 11.0.dp
|
||||
val readinessGapY = 5.0.dp
|
||||
val readinessMargin = 4.0.dp
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 804 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 813 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 964 B After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 1,023 B After Width: | Height: | Size: 1.4 KiB |