Preserve poster progress and focus across library rail changes
All checks were successful
review/code-gpt GPT review blockers fixed and read by firstmate: poster progress inset back to Title.cardProgressInset (220 px track) with test tag + regression; library focus-follow effect keyed on columns/focusedRow/header/rowHeight with a 199<->200 regression; token values/ring bounds/toolbar gap passed review on c17447c5; walked on the real box
All checks were successful
review/code-gpt GPT review blockers fixed and read by firstmate: poster progress inset back to Title.cardProgressInset (220 px track) with test tag + regression; library focus-follow effect keyed on columns/focusedRow/header/rowHeight with a 199<->200 regression; token values/ring bounds/toolbar gap passed review on c17447c5; walked on the real box
This commit is contained in:
parent
b9ea593735
commit
92c5661be0
4 changed files with 75 additions and 12 deletions
|
|
@ -358,18 +358,28 @@ chips stay at y 252–320 and the first poster row starts at y 372 (52 px clear)
|
|||
Discover and season-chip rows retain their 104 px height.
|
||||
|
||||
Cards and cast circles draw a single 4 px accent stroke centred on the art edge,
|
||||
above the content, with no background gap and the existing card lift. The stroke
|
||||
and art scale together at 1.08; buttons, avatars, chips and picker focus are
|
||||
unchanged. Title shelves reserve vertical drawing room for the scaled ring.
|
||||
above the content, with no background gap. Card lift parameters are unchanged;
|
||||
cast portraits and episode tiles now receive that lift too. The stroke and art
|
||||
scale together at 1.08; buttons, avatars, chips and picker focus are unchanged.
|
||||
Title shelves reserve vertical drawing room for the scaled ring.
|
||||
Before scaling the stroke, ring-to-neighbour clearances round to 28/22/16 px
|
||||
for poster/tile/wide cards; including stroke scale they are 27.6/21.8/16.1 px.
|
||||
|
||||
Poster progress uses the independent 18 px title-card inset, not caption spacing:
|
||||
the track remains 220 px wide on a 256 px poster. Library focus-follow restarts
|
||||
when the effective column count, focused row or header geometry changes, retaining
|
||||
deep focus across live snapshots that add or remove the letter rail.
|
||||
`LibrarySpacingRegressionTest` renders both progress geometry and the unchanged
|
||||
focused index across the 199↔200-title boundary.
|
||||
|
||||
`LibraryCaptureTest.spacingShowsFirstFocused`,
|
||||
`HomeCaptureTest.spacingNextUpFirstFocused` and
|
||||
`TitleCaptureTest.spacingCastFirstFocused` render the amended reference states
|
||||
at native resolution. Their files are `library/spacing-shows.png`,
|
||||
`home/spacing-next-up.png` and `st-114.png` under `build/wolfskin-captures/`.
|
||||
Platform Roboto metrics and fixture ordering are not browser pixel parity.
|
||||
These captures are recordings, not automated reference-pixel assertions; pair
|
||||
measurements are a separate check. Platform Roboto metrics and fixture ordering
|
||||
are not browser pixel parity.
|
||||
|
||||
## Title pages and people
|
||||
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ fun LibraryPoster(
|
|||
if (title.ratings.isNotEmpty()) LibraryRatingPlate(title.ratings, visible = ratingsVisible)
|
||||
val progress = title.progress
|
||||
if (progress > 0f && progress < 1f) {
|
||||
Box(Modifier.align(Alignment.BottomCenter).padding(horizontal = WolfskinTokens.Layout.captionOffset, vertical = WolfskinTokens.Layout.navItemGap)
|
||||
.fillMaxWidth().height(WolfskinTokens.Layout.progressBar).background(C.progressTrack, CircleShape)) {
|
||||
Box(Modifier.align(Alignment.BottomCenter).padding(horizontal = WolfskinTokens.Title.cardProgressInset, vertical = WolfskinTokens.Layout.navItemGap)
|
||||
.fillMaxWidth().height(WolfskinTokens.Layout.progressBar).testTag("poster-progress-${title.id}").background(C.progressTrack, CircleShape)) {
|
||||
Box(Modifier.fillMaxHeight().fillMaxWidth(progress).background(C.accent, CircleShape))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,13 +88,13 @@ fun LibraryScreen(
|
|||
}
|
||||
val headerCount = if (grid.hasToolbar && grid.status != LibraryStatus.Loading && grid.status != LibraryStatus.Unavailable) 2 else 1
|
||||
val rowHeight = K.posterH + K.posterCaptionBand + L.gridRowGap
|
||||
LaunchedEffect(grid.index, grid.toolbar, grid.inRail, grid.railLetter, shell.navFocused, view) {
|
||||
val index = if (grid.inRail) items.indexOfFirst { it.letter == grid.railLetter } else grid.index
|
||||
val row = index.coerceAtLeast(0) / grid.columns
|
||||
if (grid.toolbar || row == 0 && active) {
|
||||
val focusedRow = (if (grid.inRail) items.indexOfFirst { it.letter == grid.railLetter } else grid.index).coerceAtLeast(0) / grid.columns
|
||||
LaunchedEffect(grid.index, grid.columns, focusedRow, headerCount, rowHeight, density,
|
||||
grid.toolbar, grid.inRail, grid.railLetter, shell.navFocused, view) {
|
||||
if (grid.toolbar || focusedRow == 0 && active) {
|
||||
animateLibraryScroll(view, headerCount, 0f, density)
|
||||
} else if (row > 0) {
|
||||
val target = with(density) { (L.row_lhead + (if (headerCount == 2) L.row_chipsGrid else WolfskinTokens.Shell.zero) + rowHeight * row - L.pinnedLine).toPx() }
|
||||
} else if (focusedRow > 0) {
|
||||
val target = with(density) { (L.row_lhead + (if (headerCount == 2) L.row_chipsGrid else WolfskinTokens.Shell.zero) + rowHeight * focusedRow - L.pinnedLine).toPx() }
|
||||
animateLibraryScroll(view, headerCount, target, density)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
package org.justinvisser.wolfskin.design
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
/** Caption spacing and live rail changes must preserve poster geometry and retained focus. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
|
||||
class LibrarySpacingRegressionTest {
|
||||
@get:Rule val rule = createComposeRule()
|
||||
|
||||
@Test fun partiallyWatchedPosterKeepsItsOriginalProgressInsets() {
|
||||
val title = LibraryTitle("partial", "Partially watched", "movie", progress = .5f)
|
||||
rule.setContent { WolfskinTheme { LibraryPoster(title, false, false, false) } }
|
||||
val poster = rule.onNodeWithTag("poster-partial").fetchSemanticsNode().boundsInRoot
|
||||
val track = rule.onNodeWithTag("poster-progress-partial").fetchSemanticsNode().boundsInRoot
|
||||
assertEquals(256f, poster.width, .1f)
|
||||
assertEquals(220f, track.width, .1f)
|
||||
assertEquals(18f, track.left - poster.left, .1f)
|
||||
assertEquals(18f, poster.right - track.right, .1f)
|
||||
}
|
||||
|
||||
@Test fun crossingRailThresholdKeepsTheSameDeepFocusedPosterVisible() {
|
||||
val titles = List(200) { LibraryTitle("$it", "Title ${it.toString().padStart(3, '0')}", "tv") }
|
||||
val shell = ShellState(fixturePeople(), "justin").apply { pickerVisible = false; enterDestination("shows") }
|
||||
val grid = LibraryGridState("Shows", titles.take(199)).apply { index = 198 }
|
||||
rule.setContent { WolfskinTheme { ShellSurface(shell) { LibraryScreen(shell, grid, it) } } }
|
||||
rule.mainClock.advanceTimeBy(1000)
|
||||
rule.waitForIdle()
|
||||
val poster = rule.onNodeWithTag("poster-198")
|
||||
poster.assertIsDisplayed()
|
||||
val original = poster.fetchSemanticsNode().boundsInRoot
|
||||
for (count in listOf(200, 199)) {
|
||||
rule.runOnIdle { grid.source = titles.take(count) }
|
||||
rule.mainClock.advanceTimeBy(1000)
|
||||
rule.waitForIdle()
|
||||
poster.assertIsDisplayed()
|
||||
val current = poster.fetchSemanticsNode().boundsInRoot
|
||||
assertEquals(198, grid.index)
|
||||
assertEquals("198", grid.focusedTitle!!.id)
|
||||
assertEquals(original.top, current.top, 1f)
|
||||
assertEquals(original.height, current.height, 1f)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue