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.
|
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,
|
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
|
above the content, with no background gap. Card lift parameters are unchanged;
|
||||||
and art scale together at 1.08; buttons, avatars, chips and picker focus are
|
cast portraits and episode tiles now receive that lift too. The stroke and art
|
||||||
unchanged. Title shelves reserve vertical drawing room for the scaled ring.
|
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
|
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.
|
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`,
|
`LibraryCaptureTest.spacingShowsFirstFocused`,
|
||||||
`HomeCaptureTest.spacingNextUpFirstFocused` and
|
`HomeCaptureTest.spacingNextUpFirstFocused` and
|
||||||
`TitleCaptureTest.spacingCastFirstFocused` render the amended reference states
|
`TitleCaptureTest.spacingCastFirstFocused` render the amended reference states
|
||||||
at native resolution. Their files are `library/spacing-shows.png`,
|
at native resolution. Their files are `library/spacing-shows.png`,
|
||||||
`home/spacing-next-up.png` and `st-114.png` under `build/wolfskin-captures/`.
|
`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
|
## Title pages and people
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,8 @@ fun LibraryPoster(
|
||||||
if (title.ratings.isNotEmpty()) LibraryRatingPlate(title.ratings, visible = ratingsVisible)
|
if (title.ratings.isNotEmpty()) LibraryRatingPlate(title.ratings, visible = ratingsVisible)
|
||||||
val progress = title.progress
|
val progress = title.progress
|
||||||
if (progress > 0f && progress < 1f) {
|
if (progress > 0f && progress < 1f) {
|
||||||
Box(Modifier.align(Alignment.BottomCenter).padding(horizontal = WolfskinTokens.Layout.captionOffset, vertical = WolfskinTokens.Layout.navItemGap)
|
Box(Modifier.align(Alignment.BottomCenter).padding(horizontal = WolfskinTokens.Title.cardProgressInset, vertical = WolfskinTokens.Layout.navItemGap)
|
||||||
.fillMaxWidth().height(WolfskinTokens.Layout.progressBar).background(C.progressTrack, CircleShape)) {
|
.fillMaxWidth().height(WolfskinTokens.Layout.progressBar).testTag("poster-progress-${title.id}").background(C.progressTrack, CircleShape)) {
|
||||||
Box(Modifier.fillMaxHeight().fillMaxWidth(progress).background(C.accent, 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 headerCount = if (grid.hasToolbar && grid.status != LibraryStatus.Loading && grid.status != LibraryStatus.Unavailable) 2 else 1
|
||||||
val rowHeight = K.posterH + K.posterCaptionBand + L.gridRowGap
|
val rowHeight = K.posterH + K.posterCaptionBand + L.gridRowGap
|
||||||
LaunchedEffect(grid.index, grid.toolbar, grid.inRail, grid.railLetter, shell.navFocused, view) {
|
val focusedRow = (if (grid.inRail) items.indexOfFirst { it.letter == grid.railLetter } else grid.index).coerceAtLeast(0) / grid.columns
|
||||||
val index = if (grid.inRail) items.indexOfFirst { it.letter == grid.railLetter } else grid.index
|
LaunchedEffect(grid.index, grid.columns, focusedRow, headerCount, rowHeight, density,
|
||||||
val row = index.coerceAtLeast(0) / grid.columns
|
grid.toolbar, grid.inRail, grid.railLetter, shell.navFocused, view) {
|
||||||
if (grid.toolbar || row == 0 && active) {
|
if (grid.toolbar || focusedRow == 0 && active) {
|
||||||
animateLibraryScroll(view, headerCount, 0f, density)
|
animateLibraryScroll(view, headerCount, 0f, density)
|
||||||
} else if (row > 0) {
|
} else if (focusedRow > 0) {
|
||||||
val target = with(density) { (L.row_lhead + (if (headerCount == 2) L.row_chipsGrid else WolfskinTokens.Shell.zero) + rowHeight * row - L.pinnedLine).toPx() }
|
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)
|
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