Implement approved TV motion and shaped loading states #51

Merged
justinzeus merged 2 commits from fm/tv-motion-build into main 2026-11-07 15:51:39 +01:00
Owner

Outcome

Implement the approved direction A, “As recommended”: shaped breathing placeholders, network-only art reveal, reserved late-line geometry, bounded hero hand-offs and reduced motion. Existing navigation remains authoritative; pending Discover lanes are skipped rather than becoming empty focus targets.

AI-assisted (fork-internal): implementation, regression coverage and this review description were produced with GPT.

Reviewed motion follow-up — fdff85c8

Current pushed head: fdff85c8a11b3f5df1d4536d69afd3c4e351c683, one follow-up commit on the existing branch; based on the reviewed 16684454 integration head.

  • Late inline title/play-target ratings, genres and overview now use the existing 200 ms ease alpha-only lateLineFade. No slot or layout changes. Rating plates retain their existing fade, without nesting another one.
  • Caption translation advances only on entry. The 100 ms exit changes opacity only, holding even an interrupted entry at its current position; no extra layer or animation clock.
  • Loading shelves share one pulse clock under the screen theme, including shelves composed after loading began. Same 1400 ms / 0.55→1→0.55 curve, same one alpha layer per shelf; zero added pulse layers. The clock stops after the last loading shelf leaves; reduced motion does not start it.
  • 35/35 focused cases passed: design 18, app 17, zero failures/errors/skips. Includes three new native regressions for late-joining shelf phase, settled/interrupted caption exits, and visible late-metadata fades with stable geometry. Existing reduced-motion, main-thread I/O, state/focus/Back, hero and network-image cases passed. Token drift passed. The interrupted-exit probe samples after the queued hide commits, then asserts translation remains fixed across exit frames.
  • Per follow-up steering, no make test, no apk-debug, no new gallery capture round. Both production modules compiled through the focused gate. APK/checksum and gallery evidence below are explicitly earlier-head history, not rebuilt/captured evidence for this follow-up. No device access or performance measurement.
  • Read-only finding: rowReveal replays after lazy disposal/recomposition. Its ordinary remember(identity, enabled) loses the Animatable when disposed; changing a key cannot retain that state. Left unchanged because it requires saved/hoisted state rather than a one-line key fix. Per-card staggering and other review notes remain outside this follow-up.

Earlier rebased-head verification — 16684454 (historical)

At integration: mergeable. Rebased onto origin/main at fd48426fa7fa0982a67e7d788b394f871bd3f77f. That earlier head was 166844541821b155673b8d3a4e06af7b76f2c231. No merge was performed.

  • Preserve both main's sized Jellyfin/WebP requests and TMDB tiers, and this branch's network-only Coil transition/motion tokens. The only textual conflict was the generated tokens.sha256; Kotlin, CSS and drift hashes were regenerated from merged JSON, never hand-edited. Removed stale “crossfade off” token metadata.
  • 32/32 focused cases passed: MotionSurfaceTest, HeroMotionTest, NetworkArtTransitionTest, HomeRowsTest, WolfskinImagesTest, FocusRingTest, TitleAsyncFocusTest, DiscoverLoadingFocusTest, WolfskinArtworkTest, DiscoverSkeletonKeysTest, LibraryReadFacadeTest and ImageUrlServiceTest.
  • Token drift passed. One rebased-head make apk-debug passed, including branding checks for all four debug APKs (1016 resources each).
  • Armv7 debug APK: Wholphin-default-debug-1.1.0-151-g16684454-64-armeabi-v7a.apk; SHA-256 836ede23855d99b34a0bc3e7b69a3b4041f28ecf86a3d8a028495201b9f571c6.
  • Per integration steering, no full make test and no new native gallery capture round on the rebased head. Conflict resolution did not change motion/layout visuals; the gallery below remains explicitly pre-rebase capture evidence. Initial full-suite results and known picker timeouts are retained below as history, not a new rebased-head failure claim.

Change

  • Motion values and cubic Bézier curves live in tokens.json and the generator; generated Kotlin/CSS and drift hashes are committed. Remove the superseded 140 ms focus token.
  • One shared 1400 ms, 0.55→1→0.55 pulse clock across loading shelves/headers, with one alpha layer per shelf/header; surface2 fill, no loading sentences, no shimmer or per-card pulse clocks. Row entry is 180 ms / 24 px; focus is 180 ms ease-out; captions rise 10 px after 60 ms.
  • Both Coil loaders use NetworkArtTransition: network success fades for 160 ms; memory/disk/local results and errors do not fade. WolfskinImages.kt changes only the transition import/builder hook—no URL logic, DNS or read-facade edits.
  • Hero art waits for incoming art or its final fallback, then blends for 320 ms. Copy fades out over 120 ms and hands off at 140 ms into a 200 ms / 14 px rise. Fast selection changes retain the latest target.
  • Series readiness reserves 56 px from first content, even when empty or later removed. System MotionDurationScale == 0 or LocalReducedMotion disables pulse/translation/scroll animation; permitted fades cap at 80 ms.
  • Remove existing full-screen animated compositor layers: title scroll dim is a colour draw; Discover backdrop scrolling uses placement. Poster-only blur remains static.
  • Revision 6 notes and a replayable copy of the approved live prototype, including its media and 47-file SHA-256 manifest, are in data/tv-v2/motion-2026-11-06/.

Native proof

Native before/after gallery — 24 PNGs, all verified 1920×1080, plus layout JSON and checksums. Published files are mode 660, with the existing pages read ACL. Ten skeleton states are embedded below; the gallery also includes early/late series frames.

Skeleton state Before — native 1920×1080 After — native 1920×1080
Home hero Before Home hero After Home hero
Home tile shelves Before Home tile shelves After Home tile shelves
Home poster shelves Before Home poster shelves After Home poster shelves
Library grid Before Library grid After Library grid
Discover hero and lanes Before Discover hero and lanes After Discover hero and lanes
Discover pending lane Before Discover pending lane After Discover pending lane
Title header Before Title header After Title header
Title episodes Before Title episodes After Title episodes
Title cast Before Title cast After Title cast
Person header and filmography Before Person header and filmography After Person header and filmography

Late-series geometry

The same native capture harness fails on the original surface and passes on the changed surface after a readiness response 3400 ms later:

Element Original vertical shift Motion-build vertical shift
Overview 55 px 0 px
Actions 56 px 0 px

Original coordinates · Changed coordinates

Animated-layer budget

Code-level active animation owners, not a measured allocation trace:

Loading screen/state Animated layers
Home 2 initially; one per visible shelf, at most 4 in deep loading
Library One per lazy skeleton row; at most 3
Discover 2 initially; one per pending lane
Title header 1
Title episodes / cast 1 each
Person 2: header and filmography row

One shared pulse clock drives these layers; it adds no compositing layer. Late title metadata adds at most 4 bounded alpha layers (title ratings, play-target ratings, genres, overview), never another nested rating-plate fade. A hero change adds at most 2 bounded owners (outgoing art and copy); focus changes can animate 2 card transforms, 1 caption and up to 2 rating plates. Image fades are painter draws without added compositing layers. No animated owner is full-screen. Hero blend bounds are 1920×820 on Home and 1920×740 on Discover. A native boundary regression checks both layer bounds and pixels: a 200 dp / 400 px test region ends at y=400, and the lower backdrop is already the incoming image. The earlier failed probe was a manually-clocked test holding the initial composition; explicit snapshot delivery fixed the harness, without weakening pixel assertions.

Initial verification — pre-rebase head d525f1ae

  • 29/29 targeted motion/native/focus cases passed before removing the throwaway capture harness. All eight permanent new regression cases also passed in the full gate.
  • One make test, on initial pre-rebase commit d525f1ae: design 198/198 passed; app 706 tests, 111 failures. Of those, 109 could not start because this isolated worktree lacked the declared offline Android 28/30 Robolectric jars (60 / 49 cases). The cache-population task resolved only the newer SDK; the two immutable jars were copied from the existing shared artifact cache into this worktree's ignored build directory. No source or shared-cache changes.
  • Targeted recheck: all 109/109 formerly SDK-blocked cases passed, across six upstream classes. Neither gate was repeated on that initial head; the separately authorized rebased-head APK build is recorded above.
  • The other failures in that initial full-suite run were the already-known picker timeouts:
    • PersonPickerSessionTest.fastLeftLeftOkKeepsPickerClosed
    • PersonPickerSessionTest.avatarRightThenOneOkKeepsPickerClosed
      Neither name is new relative to the launch brief's six-name prior-review baseline. Picker-specific code is untouched; these failures were not hidden or repinned.
  • Initial pre-rebase make apk-debug: passed, producing four debug APKs; branding inspection passed for 1016 resources per APK. The armv7 debug APK SHA-256 is 86d4cc5b6ab92f6f411a0e732a12a399cfcc4a356d509da95c73ff41be611f41. No optimized APK was built or installed.
  • Token drift check, 47 frozen-prototype checksums, native frame dimensions, publication permissions/ACL and published proof checksums passed.

Machine-readable verification · Exact original full-gate failure names

All 109 original missing-SDK setup failures — every case passed after cache repair

com.github.damontecres.wholphin.ui.playback.PlaybackViewModelTests

  • Direct play includes the opened live stream ID
  • Playback ends with no next up
  • Show next up never without automatic play
  • playItemInPlaylist
  • Show next up never with automatic play
  • Media segment not at current position
  • Media segment ask to skip
  • Play previous
  • Media segment ignore
  • Media segment ask to skip and clicked
  • Show next up at end of playback
  • Play two intros first
  • Media segment ask to skip and dismissed
  • Play next up
  • Media segments auto skip
  • Play intro first
  • Don't play intro
  • Intro playback ends always play next

com.github.damontecres.wholphin.test.TestVoiceInputManager

  • initial state is Idle
  • callbacks from previous recognizer are ignored after cleanup
  • onPartialResults updates partialResult
  • audio focus is abandoned when recognizer is destroyed
  • onResults resets soundLevel to zero
  • onReadyForSpeech transitions state to Listening
  • onRmsChanged clamps low values to 0
  • acknowledge works from Error state
  • onResults transitions to Result state with correct text
  • startListening fails when audio focus not granted
  • startListening is ignored when already listening
  • onError maps ERROR_SERVER to correct resource
  • initial partialResult is empty
  • onError maps ERROR_RECOGNIZER_BUSY to correct resource after retry exhausted
  • audio focus error is retryable
  • startListening calls recognizer startListening
  • startListening does not create recognizer when no network
  • acknowledge resets state to Idle
  • startListening creates SpeechRecognizer
  • onRmsChanged updates soundLevel with normalized value
  • cleanup resets state to Idle
  • startListening sets recognition listener
  • startListening fails immediately when no network capabilities
  • startListening requests audio focus
  • initial soundLevel is zero
  • onError maps ERROR_AUDIO to correct resource
  • cleanup calls destroy on recognizer
  • startListening resets partialResult
  • startListening transitions state to Starting
  • onRmsChanged clamps high values to 1
  • network error from fast-fail is retryable
  • onError resets soundLevel to zero
  • onError maps ERROR_SPEECH_TIMEOUT to correct resource
  • onPermissionDenied sets correct error resource
  • startListening fails immediately when no internet capability
  • onEndOfSpeech transitions to Processing state
  • onError maps ERROR_NETWORK_TIMEOUT to correct resource
  • onError transitions to Error state
  • onError maps ERROR_NETWORK to correct resource
  • startListening does not create recognizer when audio focus denied
  • cleanup resets soundLevel to zero
  • cleanup calls cancel on recognizer
  • isAvailable returns mocked value
  • onError maps ERROR_CLIENT to correct resource
  • onError maps ERROR_NO_MATCH to correct resource
  • startListening is ignored when in Starting state
  • onError maps ERROR_INSUFFICIENT_PERMISSIONS to correct resource
  • startListening fails immediately when no network
  • onPermissionGranted calls startListening
  • cleanup resets partialResult to empty
  • stopListening triggers cleanup
  • onError maps unknown error to unknown resource
  • onError ERROR_SPEECH_TIMEOUT is retryable
  • onPartialResults ignores blank text
  • onResults with null bundle transitions to Error state
  • onResults with blank text transitions to Error state
  • onPermissionDenied transitions to Error state
  • onResults with empty text transitions to Error state

com.github.damontecres.wholphin.util.profile.TestDeviceProfileUtils

  • Test force enable overrides AV1 Profile 5 device support check
  • Test force disable overrides AV1 Profile 5 device support check
  • Test force enable overrides HEVC Profile 7 device support check
  • Test AV1 Profile 5 unsupported when device does not support it
  • Test force disable overrides HEVC Profile 7 device support check
  • Test HEVC Profile 7 unsupported when device does not support it

com.github.damontecres.wholphin.test.TestVoiceInputManagerAutoFocus

  • permanent audio focus loss stops listening
  • transient audio focus loss is ignored

com.github.damontecres.wholphin.ui.playback.PlayExternalViewModelTest

  • Explicit VLC starts from beginning at zero position
  • Implicit player preserves positive resume position
  • Playback list starts from beginning
  • Unavailable selected player falls back to implicit start from beginning
  • VLC starts from beginning without subtitles or duration
  • Explicit VLC preserves positive resume position
  • Implicit player starts VLC from beginning at zero position
  • Existing player extras and playback-start reporting remain unchanged

com.github.damontecres.wholphin.services.IntentServiceTest

  • Test parseIntent with view, itemId, and switch user
  • Test auto sign in, cold load, with protected profile
  • Test parseIntent with view and switch user
  • Test auto sign disabled in with unprotected profile
  • Test parseIntent with play itemId and params
  • Test auto sign in, hot load, with protected profile
  • Test parseIntent with search
  • Test parseIntent with play but no itemId
  • Test user specified with unprotected profile
  • Test parseIntent with view and no itemId
  • Test user specified does not exist
  • Test parseIntent with play itemId
  • Test auto sign in, no current user
  • Test parseIntent with search query
  • Test auto sign in with unprotected profile
  • Test user specified with protected profile
  • Test parseIntent with view item

On-device review checklist

  • Home → Movies/Shows → title → Back: shaped placeholders without loading copy; real controls retain focus and Back never opens the person picker unexpectedly. Repeat a visit: cache art appears immediately, without an additional image fade.
  • During loading, shelves appearing later must join the existing breathing phase. Move focus off a caption during its rise: it fades in place, never sinks on exit.
  • Shows → Adults: watch the first content for at least 4 seconds. Ratings/readiness may appear, but overview, actions, seasons and episodes must not move.
  • Discover: Left/Right through heroes, then Down into shelves. Art cross-fades without a screen-sized layer; pending lanes cannot capture Down/Up or OK; Back returns to nav.
  • Turn the system animator-duration scale off through Settings. Repeat Home/Discover/Library: no pulse or translation, nav/focus geometry snaps, fades ≤80 ms. Restore the scale afterward.
  • Repeat the approved Home scroll measurement: no worse than 39.6% janky / p90 65 ms. This remains an on-box acceptance check, not a claimed JVM result.

Scope and limits

No ADB, device access, installs, optimized APK, service restart or server configuration change. Native JVM rendering proves layout/focus/alpha behavior, not Xiaomi GPU frame timing. Published proof is behind the existing pages sign-in. No merge is performed by this lane.

## Outcome Implement the approved direction A, “As recommended”: shaped breathing placeholders, network-only art reveal, reserved late-line geometry, bounded hero hand-offs and reduced motion. Existing navigation remains authoritative; pending Discover lanes are skipped rather than becoming empty focus targets. AI-assisted (fork-internal): implementation, regression coverage and this review description were produced with GPT. ## Reviewed motion follow-up — `fdff85c8` Current pushed head: **`fdff85c8a11b3f5df1d4536d69afd3c4e351c683`**, one follow-up commit on the existing branch; based on the reviewed `16684454` integration head. - Late inline title/play-target ratings, genres and overview now use the existing **200 ms ease** alpha-only `lateLineFade`. No slot or layout changes. Rating plates retain their existing fade, without nesting another one. - Caption translation advances only on entry. The **100 ms** exit changes opacity only, holding even an interrupted entry at its current position; no extra layer or animation clock. - Loading shelves share **one pulse clock under the screen theme**, including shelves composed after loading began. Same **1400 ms / 0.55→1→0.55** curve, same one alpha layer per shelf; **zero added pulse layers**. The clock stops after the last loading shelf leaves; reduced motion does not start it. - **35/35 focused cases passed**: design **18**, app **17**, zero failures/errors/skips. Includes three new native regressions for late-joining shelf phase, settled/interrupted caption exits, and visible late-metadata fades with stable geometry. Existing reduced-motion, main-thread I/O, state/focus/Back, hero and network-image cases passed. **Token drift passed.** The interrupted-exit probe samples after the queued hide commits, then asserts translation remains fixed across exit frames. - Per follow-up steering, **no `make test`, no `apk-debug`, no new gallery capture round**. Both production modules compiled through the focused gate. APK/checksum and gallery evidence below are explicitly earlier-head history, not rebuilt/captured evidence for this follow-up. No device access or performance measurement. - Read-only finding: **`rowReveal` replays after lazy disposal/recomposition**. Its ordinary `remember(identity, enabled)` loses the `Animatable` when disposed; changing a key cannot retain that state. Left unchanged because it requires saved/hoisted state rather than a one-line key fix. Per-card staggering and other review notes remain outside this follow-up. ## Earlier rebased-head verification — `16684454` (historical) **At integration: mergeable.** Rebased onto `origin/main` at `fd48426fa7fa0982a67e7d788b394f871bd3f77f`. That earlier head was **`166844541821b155673b8d3a4e06af7b76f2c231`**. No merge was performed. - Preserve both main's sized Jellyfin/WebP requests and TMDB tiers, and this branch's network-only Coil transition/motion tokens. The only textual conflict was the generated `tokens.sha256`; Kotlin, CSS and drift hashes were regenerated from merged JSON, never hand-edited. Removed stale “crossfade off” token metadata. - **32/32 focused cases passed**: `MotionSurfaceTest`, `HeroMotionTest`, `NetworkArtTransitionTest`, `HomeRowsTest`, `WolfskinImagesTest`, `FocusRingTest`, `TitleAsyncFocusTest`, `DiscoverLoadingFocusTest`, `WolfskinArtworkTest`, `DiscoverSkeletonKeysTest`, `LibraryReadFacadeTest` and `ImageUrlServiceTest`. - **Token drift passed. One rebased-head `make apk-debug` passed**, including branding checks for all four debug APKs (1016 resources each). - Armv7 debug APK: `Wholphin-default-debug-1.1.0-151-g16684454-64-armeabi-v7a.apk`; SHA-256 **`836ede23855d99b34a0bc3e7b69a3b4041f28ecf86a3d8a028495201b9f571c6`**. - Per integration steering, **no full `make test` and no new native gallery capture round** on the rebased head. Conflict resolution did not change motion/layout visuals; the gallery below remains explicitly pre-rebase capture evidence. Initial full-suite results and known picker timeouts are retained below as history, not a new rebased-head failure claim. ## Change - Motion values and cubic Bézier curves live in `tokens.json` and the generator; generated Kotlin/CSS and drift hashes are committed. Remove the superseded 140 ms focus token. - One shared 1400 ms, 0.55→1→0.55 pulse clock across loading shelves/headers, with one alpha layer per shelf/header; `surface2` fill, no loading sentences, no shimmer or per-card pulse clocks. Row entry is 180 ms / 24 px; focus is 180 ms ease-out; captions rise 10 px after 60 ms. - Both Coil loaders use `NetworkArtTransition`: network success fades for 160 ms; memory/disk/local results and errors do not fade. `WolfskinImages.kt` changes only the transition import/builder hook—no URL logic, DNS or read-facade edits. - Hero art waits for incoming art or its final fallback, then blends for 320 ms. Copy fades out over 120 ms and hands off at 140 ms into a 200 ms / 14 px rise. Fast selection changes retain the latest target. - Series readiness reserves 56 px from first content, even when empty or later removed. System `MotionDurationScale == 0` or `LocalReducedMotion` disables pulse/translation/scroll animation; permitted fades cap at 80 ms. - Remove existing full-screen animated compositor layers: title scroll dim is a colour draw; Discover backdrop scrolling uses placement. Poster-only blur remains static. - Revision 6 notes and a replayable copy of the approved live prototype, including its media and 47-file SHA-256 manifest, are in `data/tv-v2/motion-2026-11-06/`. ## Native proof [Native before/after gallery](https://pages.justinvisser.org/wolfskin/tv-motion-build/) — 24 PNGs, all verified **1920×1080**, plus layout JSON and checksums. Published files are mode **660**, with the existing pages read ACL. Ten skeleton states are embedded below; the gallery also includes early/late series frames. | Skeleton state | Before — native 1920×1080 | After — native 1920×1080 | |---|---|---| | Home hero | ![Before Home hero](https://pages.justinvisser.org/wolfskin/tv-motion-build/before/home-header.png) | ![After Home hero](https://pages.justinvisser.org/wolfskin/tv-motion-build/after/home-header.png) | | Home tile shelves | ![Before Home tile shelves](https://pages.justinvisser.org/wolfskin/tv-motion-build/before/home-shelf.png) | ![After Home tile shelves](https://pages.justinvisser.org/wolfskin/tv-motion-build/after/home-shelf.png) | | Home poster shelves | ![Before Home poster shelves](https://pages.justinvisser.org/wolfskin/tv-motion-build/before/home-posters.png) | ![After Home poster shelves](https://pages.justinvisser.org/wolfskin/tv-motion-build/after/home-posters.png) | | Library grid | ![Before Library grid](https://pages.justinvisser.org/wolfskin/tv-motion-build/before/library.png) | ![After Library grid](https://pages.justinvisser.org/wolfskin/tv-motion-build/after/library.png) | | Discover hero and lanes | ![Before Discover hero and lanes](https://pages.justinvisser.org/wolfskin/tv-motion-build/before/discover.png) | ![After Discover hero and lanes](https://pages.justinvisser.org/wolfskin/tv-motion-build/after/discover.png) | | Discover pending lane | ![Before Discover pending lane](https://pages.justinvisser.org/wolfskin/tv-motion-build/before/discover-lane.png) | ![After Discover pending lane](https://pages.justinvisser.org/wolfskin/tv-motion-build/after/discover-lane.png) | | Title header | ![Before Title header](https://pages.justinvisser.org/wolfskin/tv-motion-build/before/title-header.png) | ![After Title header](https://pages.justinvisser.org/wolfskin/tv-motion-build/after/title-header.png) | | Title episodes | ![Before Title episodes](https://pages.justinvisser.org/wolfskin/tv-motion-build/before/title-episodes.png) | ![After Title episodes](https://pages.justinvisser.org/wolfskin/tv-motion-build/after/title-episodes.png) | | Title cast | ![Before Title cast](https://pages.justinvisser.org/wolfskin/tv-motion-build/before/title-cast.png) | ![After Title cast](https://pages.justinvisser.org/wolfskin/tv-motion-build/after/title-cast.png) | | Person header and filmography | ![Before Person header and filmography](https://pages.justinvisser.org/wolfskin/tv-motion-build/before/person.png) | ![After Person header and filmography](https://pages.justinvisser.org/wolfskin/tv-motion-build/after/person.png) | ### Late-series geometry The same native capture harness fails on the original surface and passes on the changed surface after a readiness response 3400 ms later: | Element | Original vertical shift | Motion-build vertical shift | |---|---:|---:| | Overview | 55 px | 0 px | | Actions | 56 px | 0 px | [Original coordinates](https://pages.justinvisser.org/wolfskin/tv-motion-build/before/series-layout.json) · [Changed coordinates](https://pages.justinvisser.org/wolfskin/tv-motion-build/after/series-layout.json) ### Animated-layer budget Code-level active animation owners, not a measured allocation trace: | Loading screen/state | Animated layers | |---|---| | Home | 2 initially; one per visible shelf, at most 4 in deep loading | | Library | One per lazy skeleton row; at most 3 | | Discover | 2 initially; one per pending lane | | Title header | 1 | | Title episodes / cast | 1 each | | Person | 2: header and filmography row | One shared pulse clock drives these layers; it adds no compositing layer. Late title metadata adds at most 4 bounded alpha layers (title ratings, play-target ratings, genres, overview), never another nested rating-plate fade. A hero change adds at most 2 bounded owners (outgoing art and copy); focus changes can animate 2 card transforms, 1 caption and up to 2 rating plates. Image fades are painter draws without added compositing layers. **No animated owner is full-screen.** Hero blend bounds are **1920×820** on Home and **1920×740** on Discover. A native boundary regression checks both layer bounds and pixels: a 200 dp / 400 px test region ends at y=400, and the lower backdrop is already the incoming image. The earlier failed probe was a manually-clocked test holding the initial composition; explicit snapshot delivery fixed the harness, without weakening pixel assertions. ## Initial verification — pre-rebase head `d525f1ae` - **29/29 targeted motion/native/focus cases passed** before removing the throwaway capture harness. All eight permanent new regression cases also passed in the full gate. - **One `make test`**, on initial pre-rebase commit `d525f1ae`: design **198/198 passed**; app **706 tests, 111 failures**. Of those, **109 could not start** because this isolated worktree lacked the declared offline Android 28/30 Robolectric jars (**60 / 49 cases**). The cache-population task resolved only the newer SDK; the two immutable jars were copied from the existing shared artifact cache into this worktree's ignored build directory. No source or shared-cache changes. - **Targeted recheck: all 109/109 formerly SDK-blocked cases passed**, across six upstream classes. Neither gate was repeated on that initial head; the separately authorized rebased-head APK build is recorded above. - The other failures in that initial full-suite run were the already-known picker timeouts: - `PersonPickerSessionTest.fastLeftLeftOkKeepsPickerClosed` - `PersonPickerSessionTest.avatarRightThenOneOkKeepsPickerClosed` **Neither name is new** relative to the launch brief's six-name prior-review baseline. Picker-specific code is untouched; these failures were not hidden or repinned. - **Initial pre-rebase `make apk-debug`: passed**, producing four debug APKs; branding inspection passed for **1016 resources per APK**. The armv7 debug APK SHA-256 is `86d4cc5b6ab92f6f411a0e732a12a399cfcc4a356d509da95c73ff41be611f41`. No optimized APK was built or installed. - Token drift check, **47** frozen-prototype checksums, native frame dimensions, publication permissions/ACL and published proof checksums passed. [Machine-readable verification](https://pages.justinvisser.org/wolfskin/tv-motion-build/verification.json) · [Exact original full-gate failure names](https://pages.justinvisser.org/wolfskin/tv-motion-build/final-gate-failures.json) <details> <summary>All 109 original missing-SDK setup failures — every case passed after cache repair</summary> ### `com.github.damontecres.wholphin.ui.playback.PlaybackViewModelTests` - `Direct play includes the opened live stream ID` - `Playback ends with no next up` - `Show next up never without automatic play` - `playItemInPlaylist` - `Show next up never with automatic play` - `Media segment not at current position` - `Media segment ask to skip` - `Play previous` - `Media segment ignore` - `Media segment ask to skip and clicked` - `Show next up at end of playback` - `Play two intros first` - `Media segment ask to skip and dismissed` - `Play next up` - `Media segments auto skip` - `Play intro first` - `Don't play intro` - `Intro playback ends always play next` ### `com.github.damontecres.wholphin.test.TestVoiceInputManager` - `initial state is Idle` - `callbacks from previous recognizer are ignored after cleanup` - `onPartialResults updates partialResult` - `audio focus is abandoned when recognizer is destroyed` - `onResults resets soundLevel to zero` - `onReadyForSpeech transitions state to Listening` - `onRmsChanged clamps low values to 0` - `acknowledge works from Error state` - `onResults transitions to Result state with correct text` - `startListening fails when audio focus not granted` - `startListening is ignored when already listening` - `onError maps ERROR_SERVER to correct resource` - `initial partialResult is empty` - `onError maps ERROR_RECOGNIZER_BUSY to correct resource after retry exhausted` - `audio focus error is retryable` - `startListening calls recognizer startListening` - `startListening does not create recognizer when no network` - `acknowledge resets state to Idle` - `startListening creates SpeechRecognizer` - `onRmsChanged updates soundLevel with normalized value` - `cleanup resets state to Idle` - `startListening sets recognition listener` - `startListening fails immediately when no network capabilities` - `startListening requests audio focus` - `initial soundLevel is zero` - `onError maps ERROR_AUDIO to correct resource` - `cleanup calls destroy on recognizer` - `startListening resets partialResult` - `startListening transitions state to Starting` - `onRmsChanged clamps high values to 1` - `network error from fast-fail is retryable` - `onError resets soundLevel to zero` - `onError maps ERROR_SPEECH_TIMEOUT to correct resource` - `onPermissionDenied sets correct error resource` - `startListening fails immediately when no internet capability` - `onEndOfSpeech transitions to Processing state` - `onError maps ERROR_NETWORK_TIMEOUT to correct resource` - `onError transitions to Error state` - `onError maps ERROR_NETWORK to correct resource` - `startListening does not create recognizer when audio focus denied` - `cleanup resets soundLevel to zero` - `cleanup calls cancel on recognizer` - `isAvailable returns mocked value` - `onError maps ERROR_CLIENT to correct resource` - `onError maps ERROR_NO_MATCH to correct resource` - `startListening is ignored when in Starting state` - `onError maps ERROR_INSUFFICIENT_PERMISSIONS to correct resource` - `startListening fails immediately when no network` - `onPermissionGranted calls startListening` - `cleanup resets partialResult to empty` - `stopListening triggers cleanup` - `onError maps unknown error to unknown resource` - `onError ERROR_SPEECH_TIMEOUT is retryable` - `onPartialResults ignores blank text` - `onResults with null bundle transitions to Error state` - `onResults with blank text transitions to Error state` - `onPermissionDenied transitions to Error state` - `onResults with empty text transitions to Error state` ### `com.github.damontecres.wholphin.util.profile.TestDeviceProfileUtils` - `Test force enable overrides AV1 Profile 5 device support check` - `Test force disable overrides AV1 Profile 5 device support check` - `Test force enable overrides HEVC Profile 7 device support check` - `Test AV1 Profile 5 unsupported when device does not support it` - `Test force disable overrides HEVC Profile 7 device support check` - `Test HEVC Profile 7 unsupported when device does not support it` ### `com.github.damontecres.wholphin.test.TestVoiceInputManagerAutoFocus` - `permanent audio focus loss stops listening` - `transient audio focus loss is ignored` ### `com.github.damontecres.wholphin.ui.playback.PlayExternalViewModelTest` - `Explicit VLC starts from beginning at zero position` - `Implicit player preserves positive resume position` - `Playback list starts from beginning` - `Unavailable selected player falls back to implicit start from beginning` - `VLC starts from beginning without subtitles or duration` - `Explicit VLC preserves positive resume position` - `Implicit player starts VLC from beginning at zero position` - `Existing player extras and playback-start reporting remain unchanged` ### `com.github.damontecres.wholphin.services.IntentServiceTest` - `Test parseIntent with view, itemId, and switch user` - `Test auto sign in, cold load, with protected profile` - `Test parseIntent with view and switch user` - `Test auto sign disabled in with unprotected profile` - `Test parseIntent with play itemId and params` - `Test auto sign in, hot load, with protected profile` - `Test parseIntent with search` - `Test parseIntent with play but no itemId` - `Test user specified with unprotected profile` - `Test parseIntent with view and no itemId` - `Test user specified does not exist` - `Test parseIntent with play itemId` - `Test auto sign in, no current user` - `Test parseIntent with search query` - `Test auto sign in with unprotected profile` - `Test user specified with protected profile` - `Test parseIntent with view item` </details> ## On-device review checklist - [ ] Home → Movies/Shows → title → Back: shaped placeholders without loading copy; real controls retain focus and Back never opens the person picker unexpectedly. Repeat a visit: cache art appears immediately, without an additional image fade. - [ ] During loading, shelves appearing later must join the existing breathing phase. Move focus off a caption during its rise: it fades in place, never sinks on exit. - [ ] Shows → Adults: watch the first content for at least 4 seconds. Ratings/readiness may appear, but overview, actions, seasons and episodes must not move. - [ ] Discover: Left/Right through heroes, then Down into shelves. Art cross-fades without a screen-sized layer; pending lanes cannot capture Down/Up or OK; Back returns to nav. - [ ] Turn the system animator-duration scale off through Settings. Repeat Home/Discover/Library: no pulse or translation, nav/focus geometry snaps, fades ≤80 ms. Restore the scale afterward. - [ ] Repeat the approved Home scroll measurement: no worse than **39.6% janky / p90 65 ms**. This remains an on-box acceptance check, not a claimed JVM result. ## Scope and limits No ADB, device access, installs, optimized APK, service restart or server configuration change. Native JVM rendering proves layout/focus/alpha behavior, not Xiaomi GPU frame timing. Published proof is behind the existing pages sign-in. No merge is performed by this lane.
justinzeus added 1 commit 2026-11-07 01:29:00 +01:00
justinzeus force-pushed fm/tv-motion-build from d525f1aed3 to 1668445418 2026-11-07 01:48:21 +01:00 Compare
Author
Owner

Code review (Claude): PASS, no blocking finding.

Reviewed head: 166844541821b155673b8d3a4e06af7b76f2c231 (rebased onto main fd48426f, mergeable). Read-only review: no Gradle/make run.

What I read: the full non-asset diff (40 source/token files), DiscoverState key path, hero art/copy hand-off, both Coil loaders, post-rebase WolfskinImages.kt/WolfskinTokens.kt (PR 49/50 content intact; only the transition hook and the Motion token group changed), and all 4 new test files (no existing test touched or weakened). generate_tokens.py --check passes; 47/47 prototype checksums OK.

Checked and fine: pending Discover lanes are skipped in both directions and Down/Up/OK/Back can't land on or be swallowed by a pending lane; Discover loading restores nav focus; hero buttons use the latest selection, fast changes retain the latest target; hero art releases on success, error, no-art fallback and local bitmap; only network successes fade (cache/disk/local/errors do not); pulse/translation off and fades capped at 80 ms under both reduced-motion controls.

Non-blocking notes (full text in the review report):

  1. Hero art wait has no cap, and two hero identities sharing the same art URL never get an onSuccess (stale outgoing layer kept; visually harmless). Suggest a ~1.5 s release cap.
  2. Likely one-frame gap on each hero change: the outgoing layer is set in a LaunchedEffect, so the new loading art can flash before it. Check on the box.
  3. Discover hero copy layer is full-width x hero-height, not tight to the text.
  4. rowReveal replays whenever a lazy row is composed anew; watch the on-box Home scroll measurement.
  5. NetworkArtTransition.reduced is a global mirrored from the theme (stock flavour never sets it).
  6. Readiness line blinks on stage change; incoming art fades over the surface plate.
  7. PR body still names head d525f1ae and "non-mergeable"; gate results are from the pre-rebase commit.
**Code review (Claude): PASS, no blocking finding.** Reviewed head: `166844541821b155673b8d3a4e06af7b76f2c231` (rebased onto main `fd48426f`, mergeable). Read-only review: no Gradle/make run. What I read: the full non-asset diff (40 source/token files), DiscoverState key path, hero art/copy hand-off, both Coil loaders, post-rebase `WolfskinImages.kt`/`WolfskinTokens.kt` (PR 49/50 content intact; only the transition hook and the Motion token group changed), and all 4 new test files (no existing test touched or weakened). `generate_tokens.py --check` passes; 47/47 prototype checksums OK. Checked and fine: pending Discover lanes are skipped in both directions and Down/Up/OK/Back can't land on or be swallowed by a pending lane; Discover loading restores nav focus; hero buttons use the latest selection, fast changes retain the latest target; hero art releases on success, error, no-art fallback and local bitmap; only network successes fade (cache/disk/local/errors do not); pulse/translation off and fades capped at 80 ms under both reduced-motion controls. Non-blocking notes (full text in the review report): 1. Hero art wait has no cap, and two hero identities sharing the same art URL never get an `onSuccess` (stale outgoing layer kept; visually harmless). Suggest a ~1.5 s release cap. 2. Likely one-frame gap on each hero change: the outgoing layer is set in a LaunchedEffect, so the new loading art can flash before it. Check on the box. 3. Discover hero copy layer is full-width x hero-height, not tight to the text. 4. `rowReveal` replays whenever a lazy row is composed anew; watch the on-box Home scroll measurement. 5. `NetworkArtTransition.reduced` is a global mirrored from the theme (stock flavour never sets it). 6. Readiness line blinks on stage change; incoming art fades over the surface plate. 7. PR body still names head `d525f1ae` and "non-mergeable"; gate results are from the pre-rebase commit.
Author
Owner

Design review (Claude, design lane): PASS, no blocking finding

Reviewed head: 166844541821b155673b8d3a4e06af7b76f2c231 (base fd48426fa7fa0982a67e7d788b394f871bd3f77f). Read-only review; no build run.

Verdict. No visible unapproved deviation that blocks, no regression of a locked screen. Every motion number in the PR equals direction A on the motion page (pulse 1400 ms 0.55 to 1 to 0.55 with ease-in-out, 160 ms network-only art reveal, 180 ms ease-out focus, caption 60 ms delay + 180 ms in / 100 ms out with 10 px rise, row reveal 180 ms with 24 px, copy entry 16 px, hero backdrop 320 ms ease-in-out, copy out 120 ms / in at 140 ms for 200 ms with 14 px, late line 200 ms alpha only, 56 px reserved slot, reduced motion fades capped at 80 ms). Focus ring code is unchanged apart from its timing. Token drift check (generate_tokens.py --check) passes at this SHA.

Skeleton shapes. Poster 256x384 / pitch 296, tile 400x225 / pitch 440, cast circle 200 / pitch 244, hero blocks 640x84, 460x34, 260x76, title header: all match the locked captures and the prototype geometry; Library row positions are identical before and after. Fill is surface2; sampled native pixels are (26,26,36) at peak and (18,18,24) at trough, which is surface2 at 1.0 and 0.55 over the near-black background. No loading sentence, spinner or shimmer anywhere. Discover (hero + lane) and Person have new silhouettes; the prototype has no frames for them.

Late lines. Published layout JSON: overview/actions move 55/56 px before and 0 px after. The prototype's early and late series frames show the same reserved slot.

Evidence (on the firstmate host): /root/.treehouse/firstmate-7bab20/2/firstmate/data/review-51-design/evidence/ holds ten side-by-side images (prototype or locked capture on the left, this PR's native after on the right, 01 to 11), the before/after pixel diffs, and the harness used to freeze the prototype's own animation clock. Full report: /root/.treehouse/firstmate-7bab20/2/firstmate/data/review-51-design/report.md. The prototype was rendered at 1920x1080 in headless Chromium (chrome-headless-shell, fresh profile, --no-sandbox; full --headless=new hung on this box). The prototype uses its own fixtures, so I compared geometry, fill, phase and layout rather than content.

Non-blocking notes (details and fixes in the report):

  1. Lost stagger and shelf phase: the prototype staggers cards 90 ms in the pulse and 30-40 ms in the reveal; the build has one layer per shelf and none of that (stated in the PR's spec revision). Each shelf also runs its own clock, so two shelves can be at opposite phases in one frame (native home-shelf: 0.97 vs 0.55). Suggest one shared clock per screen.
  2. Late ratings, genres and overview pop in; the prototype fades ratings with the readiness line (200 ms). Slot is reserved, only the fade is missing.
  3. Caption exit also sinks 10 px while fading; the page's exit is opacity only.
  4. Discover hero skeleton reuses the title header shape: three button blobs where the real hero has one Details pill, anchored about 27 px lower than the real hero copy.
  5. Gallery captured at d525f1ae (before the rebase); the rebase only brought main's image-size changes into the design module. Nav band differs (gradient before, solid after) on scrolled captures, which looks like a harness scroll-state difference since ShellSurface.kt:95 is unchanged.
  6. [INFERENCE] rowReveal is keyed on composition, so lazy Library/Home rows that are recomposed may replay the 180 ms rise on scroll; not observable in stills. Worth checking in the on-device recording.

Commit status review/design-claude set to success on this SHA.

## Design review (Claude, design lane): PASS, no blocking finding Reviewed head: `166844541821b155673b8d3a4e06af7b76f2c231` (base `fd48426fa7fa0982a67e7d788b394f871bd3f77f`). Read-only review; no build run. **Verdict.** No visible unapproved deviation that blocks, no regression of a locked screen. Every motion number in the PR equals direction A on the motion page (pulse 1400 ms 0.55 to 1 to 0.55 with ease-in-out, 160 ms network-only art reveal, 180 ms ease-out focus, caption 60 ms delay + 180 ms in / 100 ms out with 10 px rise, row reveal 180 ms with 24 px, copy entry 16 px, hero backdrop 320 ms ease-in-out, copy out 120 ms / in at 140 ms for 200 ms with 14 px, late line 200 ms alpha only, 56 px reserved slot, reduced motion fades capped at 80 ms). Focus ring code is unchanged apart from its timing. Token drift check (`generate_tokens.py --check`) passes at this SHA. **Skeleton shapes.** Poster 256x384 / pitch 296, tile 400x225 / pitch 440, cast circle 200 / pitch 244, hero blocks 640x84, 460x34, 260x76, title header: all match the locked captures and the prototype geometry; Library row positions are identical before and after. Fill is `surface2`; sampled native pixels are (26,26,36) at peak and (18,18,24) at trough, which is surface2 at 1.0 and 0.55 over the near-black background. No loading sentence, spinner or shimmer anywhere. Discover (hero + lane) and Person have new silhouettes; the prototype has no frames for them. **Late lines.** Published layout JSON: overview/actions move 55/56 px before and 0 px after. The prototype's early and late series frames show the same reserved slot. **Evidence** (on the firstmate host): `/root/.treehouse/firstmate-7bab20/2/firstmate/data/review-51-design/evidence/` holds ten side-by-side images (prototype or locked capture on the left, this PR's native after on the right, 01 to 11), the before/after pixel diffs, and the harness used to freeze the prototype's own animation clock. Full report: `/root/.treehouse/firstmate-7bab20/2/firstmate/data/review-51-design/report.md`. The prototype was rendered at 1920x1080 in headless Chromium (chrome-headless-shell, fresh profile, `--no-sandbox`; full `--headless=new` hung on this box). The prototype uses its own fixtures, so I compared geometry, fill, phase and layout rather than content. **Non-blocking notes** (details and fixes in the report): 1. Lost stagger and shelf phase: the prototype staggers cards 90 ms in the pulse and 30-40 ms in the reveal; the build has one layer per shelf and none of that (stated in the PR's spec revision). Each shelf also runs its own clock, so two shelves can be at opposite phases in one frame (native home-shelf: 0.97 vs 0.55). Suggest one shared clock per screen. 2. Late ratings, genres and overview pop in; the prototype fades ratings with the readiness line (200 ms). Slot is reserved, only the fade is missing. 3. Caption exit also sinks 10 px while fading; the page's exit is opacity only. 4. Discover hero skeleton reuses the title header shape: three button blobs where the real hero has one Details pill, anchored about 27 px lower than the real hero copy. 5. Gallery captured at `d525f1ae` (before the rebase); the rebase only brought main's image-size changes into the design module. Nav band differs (gradient before, solid after) on scrolled captures, which looks like a harness scroll-state difference since `ShellSurface.kt:95` is unchanged. 6. [INFERENCE] `rowReveal` is keyed on composition, so lazy Library/Home rows that are recomposed may replay the 180 ms rise on scroll; not observable in stills. Worth checking in the on-device recording. Commit status `review/design-claude` set to success on this SHA.
justinzeus added 1 commit 2026-11-07 02:16:59 +01:00
Refine late metadata fades and synchronize loading motion
All checks were successful
review/code-claude PASS on 16684454, delta fdff85c8 reviewed
review/design-claude PASS on 16684454, delta fdff85c8 reviewed
fdff85c8a1
Author
Owner

Delta review of the follow-up commit fdff85c8a1 on top of 16684454 (code PASS and design PASS were posted on 16684454; both statuses are carried to this head). Read the full delta (7 files, +149/-15): late ratings, genres and overview now use the same lateLineFade as the readiness row (slot already reserved, no geometry change); the caption rise is driven by an entry-progress state so the 100 ms exit is opacity only; one shared skeleton pulse clock per screen (SkeletonPulseScope inside WolfskinTheme, subscribers counted, the infinite transition runs only while a shelf is loading and reduced motion keeps it idle) so all loading shelves breathe in phase, same 1400 ms 0.55-1-0.55. All skeletonPulse call sites sit in design composables rendered under WolfskinTheme. 35 focused cases and token drift pass on the lane. No new blocking finding.

NOT merged: the prototype card-by-card stagger difference is a held captain call (Justin decides on the TV during the morning walk); the PR merges after his word. Known: rowReveal replays when a lazy row is disposed and recomposed (not a one-line fix; check on the box).

Delta review of the follow-up commit fdff85c8a11b3f5df1d4536d69afd3c4e351c683 on top of 16684454 (code PASS and design PASS were posted on 16684454; both statuses are carried to this head). Read the full delta (7 files, +149/-15): late ratings, genres and overview now use the same lateLineFade as the readiness row (slot already reserved, no geometry change); the caption rise is driven by an entry-progress state so the 100 ms exit is opacity only; one shared skeleton pulse clock per screen (SkeletonPulseScope inside WolfskinTheme, subscribers counted, the infinite transition runs only while a shelf is loading and reduced motion keeps it idle) so all loading shelves breathe in phase, same 1400 ms 0.55-1-0.55. All skeletonPulse call sites sit in design composables rendered under WolfskinTheme. 35 focused cases and token drift pass on the lane. No new blocking finding. NOT merged: the prototype card-by-card stagger difference is a held captain call (Justin decides on the TV during the morning walk); the PR merges after his word. Known: rowReveal replays when a lazy row is disposed and recomposed (not a one-line fix; check on the box).
justinzeus merged commit c3e9b3cd3b into main 2026-11-07 15:51:39 +01:00
Sign in to join this conversation.
No reviewers
No labels
exploring
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: justinzeus/wolfskin#51
No description provided.