663 lines
41 KiB
Markdown
663 lines
41 KiB
Markdown
# Wolfskin TV container builds
|
||
|
||
Run from a full Git checkout with the upstream `v*` and `p*` tags available. The
|
||
host needs Docker, GNU Make, Bash, Git, and `flock` (util-linux), not Java or the
|
||
Android SDK.
|
||
|
||
```sh
|
||
git fetch origin --tags
|
||
make apk # clean, signed default release: three ABI splits + universal
|
||
make test # app default/debug suite plus wolfskin-design JVM/capture suite
|
||
make apk-debug # default debug, no secrets; MPV stub and no extra codec decoders
|
||
```
|
||
|
||
APKs remain under `app/build/outputs/apk/default/{release,debug}/`. Debug builds
|
||
are not playback-complete and must not be distributed as releases. Upstream's
|
||
`.github/workflows` and dependency version catalog are unchanged; release builds
|
||
use the catalog's exact Wholphin extension version (currently `0.2.4`). Do not
|
||
place local extension AARs in `app/libs`: upstream prefers them over the pinned
|
||
GitHub Packages dependencies.
|
||
|
||
Upload release assets renamed to `Wolfskin.apk` / `Wolfskin-<abi>.apk` for the
|
||
in-app updater. Gradle outputs
|
||
`Wholphin-default-release-<versionName>-<versionCode>[-<abi>].apk`; strip the
|
||
flavor, version, and code as well as replacing the prefix.
|
||
|
||
Upstream enables JVM unit tests only for debug variants; there is no
|
||
`testDefaultReleaseUnitTest` task. `make test` uses the closest default-flavor
|
||
task without changing upstream's test configuration.
|
||
|
||
## Upstream v1.1.0 integration
|
||
|
||
The release merge brings Kotlin `2.4.21`, Gradle `9.8.2`, AGP `9.5.0`, and
|
||
ExoplayerHdrUtils `0.4.1`. The pinned SDK/JDK image and serialized Gradle wrapper
|
||
remain compatible without changes; native Wholphin extensions remain `0.2.4`.
|
||
There are no committed dependency lockfiles or Gradle verification metadata.
|
||
Forgejo Actions stays disabled; upstream GitHub workflows remain dormant.
|
||
|
||
The app database upgrades from Room schema 35 through 40 to 41 on first launch
|
||
after installation. The upstream migrations add series track choices and a home
|
||
settings source preference. Downgrading with the upgraded database is not
|
||
supported: a TV installation is a one-way step and requires explicit approval.
|
||
The optional companion plugin can supply Discover credentials before locally
|
||
saved Seerr profiles; do not configure an administrator API key in place of the
|
||
household's per-profile Jellyfin login, which Conductarr uses for request routing.
|
||
|
||
Build only after committing a release merge: version names come from Git HEAD,
|
||
not the working tree. An uncommitted merge still builds with the old version name.
|
||
Neither an upstream merge nor its validation build authorizes a release or TV
|
||
installation.
|
||
|
||
## Wolfskin shell
|
||
|
||
Wolfskin UI lives in `app/src/wolfskin/java`, beside upstream source. The signed-in
|
||
entry in `MainContent.kt` calls `WolfskinApp`; Wolfskin-only routes use the single
|
||
`Destination.Wolfskin` subtype in the existing navigation stack. Other destinations
|
||
continue through upstream `DestinationContent`. Full-screen player/settings pages
|
||
cover the Wolfskin nav.
|
||
|
||
The independent `:wolfskin-design` module owns the shell components, fixed person
|
||
colours, focus policy, per-person root-view memory, palette and motion. Its
|
||
`tokens.json` is the copied approved TV-spec source, retaining the locked prototype
|
||
SHA and provenance. `shell-tokens.json` adds shared shell/grid/sheet measurements;
|
||
`title-tokens.json`, `home-tokens.json` and `discover-tokens.json` add their screen
|
||
measurements from the same locked prototype. Generate Kotlin, CSS and the checksum manifest together:
|
||
|
||
```sh
|
||
python3 wolfskin-design/generate_tokens.py
|
||
python3 wolfskin-design/generate_tokens.py --check
|
||
```
|
||
|
||
`preBuild` checks the manifest against the source JSON, generator and generated
|
||
outputs. Editing any of them without regenerating fails the build. The shell uses
|
||
the generated palette rather than the retired `WOLFSKIN` theme-enum value; the old
|
||
numeric value is reserved.
|
||
|
||
Compose output maps source weights 600/650 to the platform Bold (700) face, which
|
||
resolves consistently in native captures; the approved JSON weights stay unchanged.
|
||
Nav, picker names, labels and pill weights use that same mapping. Nav label colour
|
||
fades for 200 ms ease-out; the blob retains its critically damped spring.
|
||
|
||
Settings pills have a separate 1 dp stroke (2 px on the stage), not a focus ring.
|
||
The stroke is inset within the fixed 20 dp / 40 px height; horizontal border-box
|
||
padding includes it. The 6 px dash / 4 px gap follows the browser's 3:2 stroke-width
|
||
ratios. Resting borders are white at 22% opacity; focused borders use ink at 40%.
|
||
|
||
The small gear under the final person opens Settings with Requests focused.
|
||
Settings groups hand off to existing upstream pages; Requests opens the household
|
||
Coming-in sheet described below. **Advanced settings → Wolfskin screens** is the persistent
|
||
rollout switch: turning it off renders upstream Home and library destinations with
|
||
the upstream Backdrop and top scrim, while retaining Wolfskin's nav, picker and
|
||
Settings list. Native Up traversal stays in content until the first focusable row,
|
||
then enters the nav. Turning the switch on renders Wolfskin Home and Libraries.
|
||
Discover and Requests are integrated Wolfskin surfaces; title hand-offs remain labelled.
|
||
|
||
Saved Jellyfin identities use the existing upstream session. Conductarr supplies
|
||
household labels, requester capabilities and library audiences after sign-in.
|
||
The locked shell projection in `app/src/debug/assets/wolfskin-shell.json` is used
|
||
only when the Advanced debug fixture switch is enabled; release excludes it.
|
||
|
||
Discover adds the picks hero, type chips, catalogue shelves, service pages, continuous
|
||
See all grids, Hidden picks, request chooser and receipts. Its `discover-title/...`
|
||
destination is an explicit title-family integration stub exposing only Discover's
|
||
request and pick actions. Unheld titles have no trailer control until the pinned
|
||
Conductarr title contract supplies a trailer.
|
||
|
||
The debug-only fixture and local art live in
|
||
`app/src/debug/assets/wolfskin-discover/`. The Advanced preview switch selects a
|
||
network-free fixture host; real providers are the default in debug and release.
|
||
The mutation adapter accepts the selected person's `ConductarrClient` and uses
|
||
only typed routes. Request submission first reads title receipts. Unknown outcomes
|
||
are checked, never blindly re-sent, including after closing a sheet or choosing
|
||
the same uncertain recipient again. Whole-series requests require season metadata.
|
||
|
||
Discover hero and Restore text use explicit per-line CSS allocations so native
|
||
paragraph overflow cannot enlarge the bottom-anchored stack. Restore subtitles
|
||
retain the sheet's negative top margin and bottom gap. Recovery panels may
|
||
overflow their fixed-height scroll row so sign-in copy never shrinks the retry
|
||
button. Native regressions cover those three layout boundaries.
|
||
|
||
Discover's native capture suite covers its 28 named acceptance states and emits
|
||
text-layout metrics beside the PNGs. Data-driven Android key replays cover opening
|
||
Discover in 4 presses, requesting for yourself in 7, requesting for Mindel in 9 and
|
||
Not interested in 8. Additional regressions exercise held-OK safety, uncertain
|
||
receipts, per-recipient outcomes, collection recovery and Undo, unknown seasons,
|
||
Dutch decimal formatting and short-row/tail clamping. Discover reuses the Libraries
|
||
grid, poster, ratings plate, See all and recovery cards, bounded image loader, caption
|
||
separator token and the shell's single hold guard. Both frames share the 180 ms scroll tween.
|
||
|
||
The TV keeps platform Roboto with the approved type sizes and weights. Browser
|
||
system-font wrapping is not a per-title contract: no bundled font, forced title
|
||
breaks or width compensation is applied. Discover's own stacked text preserves
|
||
untrimmed CSS line boxes without changing merged shell/library typography. Hero,
|
||
sheet and shared focused-content shadows use tokenized drop shadows, not Material
|
||
elevation; their alpha animates in the draw phase. Native text-layout measurements
|
||
accompany each acceptance capture; remaining font-driven wrapping and text-block
|
||
positions must be listed explicitly in its comparison report.
|
||
|
||
The asynchronous debug fixture seam hands loading focus back to the first loaded
|
||
action when Discover is still the selected destination. A JVM regression releases
|
||
the actual host's suspended fixture load and presses Android OK: it must open the
|
||
hero rather than activate the nav. Fixture completion does not steal a different
|
||
destination or nav selection.
|
||
|
||
Native Roborazzi captures use Robolectric SDK 34 and
|
||
`w960dp-h540dp-xhdpi` (1920×1080 at density 2.0). `make test` writes capture PNGs and
|
||
the sampled Compose nav-spring curve to `build/wolfskin-captures/`.
|
||
Home, Library and Discover captures use `home/`, `library/` and `discover/`
|
||
subdirectories so shared spec state numbers cannot overwrite another renderer's image.
|
||
Key replays exercise the Android event adapter, including the 6/8/10/11-press shell paths.
|
||
The profile hand-off capture targets the real `HomeSettingsPage`; Playback is a
|
||
separate press-count path.
|
||
|
||
Robolectric SDK 28/30/34 runtimes resolve through Gradle's existing persistent
|
||
dependency cache, then copy to `wolfskin-design/build/robolectric-sdk/`. Test workers
|
||
use that local directory in offline mode instead of downloading Android JARs into
|
||
the container's ephemeral home. A cache miss still needs Maven Central; rate-limited
|
||
hosts can seed the existing cache from SHA512-verified local Maven artifacts without
|
||
changing the wrapper or adding volumes.
|
||
|
||
Default-flavour launcher artwork and English, Hungarian and Polish string overrides
|
||
live in `app/src/default/res/`. Legacy Wolfskin strings in 25 translated
|
||
`main` resource folders have not yet been moved; non-default `appstore`/`firetv`
|
||
flavours therefore combine upstream icons with some legacy Wolfskin text. Those
|
||
flavours and a full locale migration are not certified by this slice. Debug
|
||
assembly runs `checkWolfskinBranding` against every packaged APK through AGP's public
|
||
artifact provider and AAPT2. It checks all string, plural and array configurations,
|
||
rejecting user-visible upstream branding while preserving the upstream legal
|
||
attribution and project URL. The check does not depend on incremental merge files.
|
||
|
||
## Wolfskin Home
|
||
|
||
Home is one lazy frame: a non-rotating resume → Next up → newest → pick hero,
|
||
Jellyfin shelves, per-destination "Your …" shelves and the Discover continuation.
|
||
The final action opens the real merged Discover screen at its top through the
|
||
shell's root re-selection transition. Home's See all cards open `WolfskinSeeAllLists`, carrying
|
||
the complete underlying list rather than the visible shelf window. Title content
|
||
remains the later-slice target.
|
||
|
||
All Home components and immutable screen models live in `:wolfskin-design`.
|
||
The shell's remembered view owns vertical position, each row's selected card and
|
||
actual horizontal `LazyListState`, and the bounded six-hour ratings session cache.
|
||
Composing destinations bind `rememberedView(route)` before navigation effects run, so a
|
||
new See all grid cannot borrow or clamp Home's scroller. Re-entry restores the
|
||
remembered track before the page with `scrollToItem`, preserving exact indices and
|
||
pixel offsets rather than animating back. Android focus stays with the shell;
|
||
cards use its logical focus, without requests against uncomposed lazy items.
|
||
Back preserves the viewport; returning to Home does not re-run focus-follow over
|
||
a manually scrolled saved view. The next arrow re-enters visible content when
|
||
scrolling moved focus off-screen.
|
||
Pointing changes focus without activation, cards activate on key-up, and held
|
||
OK events cannot activate twice.
|
||
Between shelves, an unvisited row selects the nearest visible card center even
|
||
when card widths differ. A visited row restores its column. Horizontal movement
|
||
keeps the track still while focus remains in its visible middle.
|
||
The initial skeleton parks focus on the nav. When the first content arrives,
|
||
an untouched fresh entry focuses hero button zero; intervening nav input prevents
|
||
that automatic transfer. Home, Libraries and Discover use the same `ShellContentFocus`
|
||
release/repeat guard and Back ladder.
|
||
|
||
`HomeReadFacade` is the only Home code importing Jellyfin API clients. Release
|
||
variants read the selected user's Resume, Next up, newest titles and destination
|
||
shelves, with app-private last-known-good projections through the shared atomic
|
||
disk cache. A missing saved library becomes a recovery panel; a saved library
|
||
stays visible when Jellyfin is unreachable. Playback resolves an actual film or
|
||
episode and checks Jellyfin before opening upstream's player. No season,
|
||
recommendation stub or different person's session is promoted to playback.
|
||
An unresolved person switch guards Home and both owned/upstream library content
|
||
before mounting any source read with the prior person's Jellyfin session.
|
||
|
||
Debug variants additionally package the locked fixture and local artwork under
|
||
`wolfskin-design/src/debug/assets/wolfskin-home/`; release variants exclude them.
|
||
The explicit fixture provider supplies approved household data and Home states
|
||
without network reads in tests. Real signed-out Home hides every rating and shows
|
||
"Discover needs sign-in"; signed-in Home reads the Conductarr continuation and
|
||
ratings-only endpoint. No Seerr fallback is used.
|
||
|
||
Home, Libraries and Discover reuse `TitleCard`, `Caption` and `LibrarySeeAllCard`;
|
||
those screens and Requests share one `WolfskinImages` loader. Exact-slot requests use RGB565 for posters,
|
||
disable crossfades, share a 64 MiB memory/128 MiB disk cache, limit image work to
|
||
four reads and prefetch the next shelf. Rating plates start at 14 sp, shrink to 12 sp
|
||
and drop trailing entries rather than wrap. Their uncached read gate is 350 ms;
|
||
cached focus returns without that delay.
|
||
|
||
Focus rings and native lift draw outside fixed slots. The Saved nav chip uses its
|
||
own geometry; the Discover section uses a warning pill. Caption separators multiply
|
||
the existing muted colour alpha by the shared 40% token. Home-only untrimmed styles
|
||
and `exactLineBox` honor the declared CSS line boxes despite Android's intrinsic
|
||
paragraph minimums; shell and library typography retain their existing defaults.
|
||
The shared See all component uses its canonical locked 8px gap, not the generic
|
||
10px tail spacing. Focus lifts use the merged draw-only `FocusLift` implementation;
|
||
the obsolete Home shadow helper and duplicate gap/lift measurements are removed.
|
||
Vertical moves clamp to short target rows, including a final See all card.
|
||
|
||
`make test` includes `HomeCaptureTest` (23 native acceptance states) and the
|
||
data-driven paths in `wolfskin-home-presses.json`: play next 2, resume 3, Discover
|
||
4, deep-row Back/Down 9, Trending 13 and Trending's See all card 33 presses.
|
||
Home captures are written to `build/wolfskin-captures/home/`. Compare those images
|
||
side by side with the frozen spec captures; Compose Roboto/JPEG rasterisation,
|
||
the approved button-scale consolidation and removal of prototype sample pills
|
||
are recorded differences, not a claim of browser pixel equality.
|
||
|
||
## Requests under Settings
|
||
|
||
The Requests row and Coming-in sheet share one immutable household snapshot.
|
||
Ready requests do not count as open. Rows sort Failed, Downloading/Almost ready,
|
||
Checking/Searching, Waiting, Ready. The bounded window shows five rows, or four
|
||
with readiness expanded. Right expands Request/Download/Library/Playback; Left
|
||
collapses; Up/Down collapse while moving. Opening a title preserves the request
|
||
for Back, and closing the sheet restores its opener. Selecting a root or changing
|
||
person clears the title-return link. Shared's authenticated actor always reads
|
||
“by Shared”, never “by you”.
|
||
|
||
The debug fixture switch loads `app/src/debug/assets/wolfskin-requests.json` and
|
||
its eight local posters. Shared and focused-rating records also feed the native
|
||
acceptance states; the identical projection is in design-test resources.
|
||
No “sample” or “example stages” pill is drawn. The default provider instead maps
|
||
Conductarr household activity, authenticated actors and display artwork into
|
||
the same sheet. Release excludes all fixture assets. There is no Seerr fallback.
|
||
Foreground refreshes once; an open sheet or unsettled requests refresh every
|
||
30 seconds while started. The immutable debug projection is loaded once, not
|
||
decoded again on every poll.
|
||
Thumbnails reuse `WolfskinImages` with a 96×144-pixel request, RGB565, no crossfade,
|
||
the shared 64 MB memory/128 MB disk cache and four-read limit. Actor separator
|
||
dots use the surrounding text colour; the 40% rule applies only to poster captions.
|
||
|
||
Uncached focused ratings wait 350 ms; moving away cancels the read. Read ratings
|
||
show immediately on return; sign-out clears visibility. This follows the frozen
|
||
page and spec 6.12, resolving the contradictory no-dwell sentence in spec 7.8.
|
||
Deterministic coroutine-clock tests cover cancellation, reentry, cached return,
|
||
sign-out and refreshed locale scores; a thin Compose test verifies actual Dutch
|
||
rendering and sign-out suppression. The approved nine-key Requests path is JSON
|
||
data replayed through the real shell. The Requests-only harness starts at the
|
||
Home-to-nav boundary; Home's actual hero and asynchronous entry have separate key replays.
|
||
|
||
Sheet dimensions, padding, glyphs, scales, colours and durations are generated
|
||
tokens. Scoped sheet line boxes retain CSS heights and baselines independently
|
||
of native leading rounding; shared shell/library typography is unchanged. Platform
|
||
Roboto remains required: the host browser used DejaVu Sans, so its width,
|
||
wrapping, ellipsis and text-block positions are not a wrapping-parity target.
|
||
For example, the 44-pixel “Coming in” ink is 239 pixels wide in the reference
|
||
and 194 pixels in native captures. No per-title compensation or font bundle is
|
||
used. Native comparisons must report those structural differences explicitly.
|
||
Readiness glyphs sit inline on the text baseline with no flex gap, matching the
|
||
frozen block-span cascade. Items wrap intrinsically: Roboto fits the expanded
|
||
readiness into two lines where the reference uses three; no grouping is forced.
|
||
|
||
The six rating marks retain the exact locked SVG source in
|
||
`wolfskin-design/request-marks/`; native-size PNGs and their hashes are generated
|
||
from the JSON dimensions with the locked contain fit (`.rr .rb` outranks `.crow img`).
|
||
Inside Coming-in rows they retain the prototype's dark `surface3` chips with 12-pixel rounded corners.
|
||
Loaded poster thumbnails have no placeholder fill beneath their clipped edge.
|
||
|
||
```sh
|
||
uv run --with cairosvg==2.9.1 python wolfskin-design/generate_request_marks.py
|
||
```
|
||
|
||
`RequestsCaptureTest` writes `st-081`…`st-086`, `st-088` and `st-111` at 1920×1080,
|
||
density 2.0. Title selection opens the shared native title destination by media
|
||
type and TMDB identity. Back restores the exact Coming-in row and expansion;
|
||
the temporary `request-title/...` placeholder is removed. TV sign-in, release
|
||
and device deployment remain separate.
|
||
|
||
## Libraries and See all
|
||
|
||
Movies, Shows and Anime use the shell's retained view and a continuous six-column
|
||
lazy grid. Up from its first row reaches Sort/Filter, then the nav; Back preserves
|
||
the card and scroll. Sort cycles A–Z, Recently added and Rating. The letter rail
|
||
exists only at 200 titles with A–Z selected. Card OK activates on key-up through
|
||
the same shell hold guard, without a long-press menu.
|
||
Down clamps a short final title row before entering the separate Try again / Show
|
||
more row; an empty slot cannot activate recovery. An unavailable library hides
|
||
its unknown item count.
|
||
|
||
Caption separators use 40% of the existing muted colour's alpha. The rail follows
|
||
the locked prototype CSS: bold 24 px letters, with the focused letter bold 28 px,
|
||
overriding the spec table's generic caption style for this component.
|
||
Library and Settings header initials use the muted colour over the person's fill;
|
||
navigation and picker avatars retain their dark initials.
|
||
|
||
The debug-only `wolfskin-library.json` and `library/assets/` contain the locked
|
||
prototype's titles and local poster art. Release libraries read through
|
||
`LibraryReadFacade`, with 200-item Jellyfin chunks and ten-minute per-server/person
|
||
snapshots using the existing atomic disk cache. Saved data is painted before a
|
||
quiet refresh; a failed refresh keeps it. Production ratings remain hidden before
|
||
Conductarr sign-in and use the shared ratings-only provider afterwards.
|
||
|
||
Home/Discover shelves open a full list through
|
||
`WolfskinSeeAllLists.open(person, key, title, fullList, navigation, loadNextPage)`.
|
||
The optional suspend page provider returns `LibraryPage(items, hasMore)` using the
|
||
generated Conductarr client. The grid reads within ten cards of its end, deduplicates
|
||
title identities, and pauses on Try again or Show more rather than looping.
|
||
Title activation opens `title/{id}` through `Destination.Wolfskin` for prototype
|
||
identities, or the existing typed media destination for real Jellyfin identities.
|
||
Movies, series, episodes and people use the native title family; collections and
|
||
music keep their upstream destinations.
|
||
|
||
`LibraryCaptureTest` renders the sixteen approved library/See all states offline.
|
||
`LibraryKeysTest` reads the approved JSON path and reaches Spirited Away in exactly
|
||
20 Android key presses. Paging and snapshot tests cover duplicate-only reads,
|
||
failure/retry, stale copies, and person/server isolation. Library captures are written
|
||
to `build/wolfskin-captures/library/`; Discover's independent captures use `discover/`.
|
||
|
||
## Title pages and people
|
||
|
||
`TitleSurface` owns the film/series/episode header, season chips, episode tiles,
|
||
cast and related shelves. `PersonSurface` owns the real biography and continuous
|
||
six-column filmography. Both reuse the shell/nav/back ladder, `LibraryPoster`,
|
||
the adaptive `LibraryRatingPlate`, shared captions and the single bounded
|
||
`WolfskinImages` loader. Image requests use exact slot sizes, RGB565 for opaque
|
||
posters, no crossfade, four concurrent reads and next-row prefetch. The fixed
|
||
title backdrop remains visible behind deep rows; only off-screen row images
|
||
leave composition.
|
||
|
||
The options row preserves complete labels beyond the copy-column measure.
|
||
Version choices close after selection; Audio and subtitles stays open, including
|
||
an explicit Off choice. Real source IDs use the Jellyfin SDK UUID parser, and
|
||
stream indexes remain the server's indexes rather than positions in a list.
|
||
Selections persist through the same playback repository as the upstream player.
|
||
Marking a whole series asks first; an explicit episode remains the exact play and
|
||
watched target even while another season is selected. Nested person/title visits
|
||
retain their entry's focus and selection state.
|
||
Hardware and remote Back share the sheet/picker/request-overlay precedence, then
|
||
pop a pushed title/person from content **or** nav. Initial loading has no content
|
||
target: Down stays in nav, while Back can still leave the title. A missing rating
|
||
set emits no layout node, so it cannot reserve an invisible extra facts gap.
|
||
|
||
`TitleReadFacade` confines SDK reads, server/user-scoped last-good disk copies,
|
||
person credits and playback mutations to one boundary. `ConductarrTitlePages`
|
||
adds real detail, cast, related titles, performer/filmography, readiness/receipts
|
||
and source ratings. A catalogue title gains held/play controls only after an
|
||
exact TMDB lookup returns this Jellyfin user's actual item identity. Episode
|
||
votes enrich those real episode identities; no catalogue stub becomes playable.
|
||
Debug-only `wolfskin-titles.json` and local artwork remain behind the explicit
|
||
Advanced fixture switch; release never packages or selects them.
|
||
|
||
Held trailers use Jellyfin remote/local trailer data. YouTube URLs target an
|
||
installed YouTube handler such as SmartTube, never a browser. No handler leaves
|
||
the title visible with a calm message. Titles nobody holds have no Trailer
|
||
button until Conductarr's trailer field exists. Title requests use the same
|
||
actor-safe chooser and identity-lifetime uncertain-submission ledger as Discover.
|
||
Signed-out and offline pages cannot submit; no Seerr fallback is used.
|
||
|
||
`TitleCaptureTest` writes the 34 approved title/person/options frames plus native
|
||
text weights, colours, line boxes and bounds at 1920×1080, density 2.0. The approved
|
||
typeface ruling keeps platform Roboto, not the browser's DejaVu Sans: no per-title
|
||
compensation and no wrapping-parity claim. Explicit CSS line boxes constrain title
|
||
headers, sheets, cast and poster captions without changing shared type tokens.
|
||
Fractional stack placement rounds only the accumulated position, preventing a
|
||
one-pixel-per-row drift. Focused controls share the canonical lift shadow.
|
||
`TitleKeysTest` replays real Android keys, including modal Back/hold, nearest-row
|
||
entry, remembered columns, long-sheet visibility and full media-table geometry.
|
||
`TitleAsyncFocusTest` delays title and person replies through the real destination
|
||
and view-model, then exercises content focus and hardware Back. Subtitles Off
|
||
maps to the player's explicit disabled sentinel, not its unspecified default.
|
||
|
||
`TitleLaunchKeysTest` starts at Who's watching and replays the full 4/8/15/11
|
||
sequences through the real Home hero/shelves and title/person surfaces.
|
||
`title-press-paths.json` supplies every event; no prefix is skipped or padded.
|
||
The shorter 1/5/6/2 title-entry replays remain as focused regressions. SDK cache,
|
||
exact-episode, track-index, identity, locale-layout and trailer-handler tests
|
||
run offline with in-process service replies and local art.
|
||
|
||
## Offline states and recovery
|
||
|
||
Owned screens share `RecoveryToast`: the warning glyph, 760 px maximum width,
|
||
22/28 px padding, 14 px icon gap, 22 px corners and CSS-sized body line boxes.
|
||
Network failures never expose transport exception text. Playback failure reads
|
||
“Can’t reach Jellyfin to play. Check the network and try again.” and leaves the
|
||
originating card focused; no simulated player hand-off panel is shipped. Retry
|
||
failure reads “Still can’t reach it. The last saved view stays on screen.”
|
||
|
||
Library headers and the nav publish the same saved-copy timestamp. Failed library
|
||
refreshes retain cards and expose a reachable Try again tail. Discover retains its
|
||
current catalogue after a failed refresh, adds a recovery tail and disables request
|
||
and dismissal writes until recovery; a revoked sign-in discards that saved projection.
|
||
Previously observed plates can remain visible offline, but moving to an uncached
|
||
card cannot start a ratings read. `SavedPill` shares the compact cached-header
|
||
treatment across Home, Libraries and Requests; the nav retains its larger wifi chip.
|
||
|
||
Requests focus, observed ratings and retained rows are partitioned by selected person.
|
||
A failed authenticated read preserves the last rows and their timestamp; Down from
|
||
the final row reaches Try again and Up returns to the row. Signed-out data is not
|
||
retained as an authenticated copy. Returning to a foreground person triggers that
|
||
person's refresh rather than waiting for the prior person's polling timer.
|
||
|
||
Home disk snapshots remain server/user scoped. A new identity starts with its own
|
||
skeleton or saved copy, never the previous person's projection; late responses from
|
||
an older read cannot overwrite the current identity. A disk-write failure does not
|
||
turn a successful Jellyfin read into an outage. Skeleton-only Home, Library and
|
||
Discover screens keep focus in the nav even when Down is pressed repeatedly.
|
||
|
||
Library/Discover posters use the same title/glyph/160-degree gradient fallback as
|
||
Home, including an actual Coil read failure. Discover hero image failures use the
|
||
shared ambient background; missing artwork is not a successful data refresh.
|
||
|
||
`RecoveryStateTest`, `RecoveryCaptureTest`, `HomeRecoveryTest` and `PlayerHandoffTest`
|
||
exercise state transitions, person isolation, native recovery focus/geometry and the
|
||
real upstream playback destination with offline fixtures. New native captures and
|
||
rendered text/font metrics go to `build/wolfskin-captures/recovery/`; Dutch score
|
||
captures configure the Android resource locale as well as the JVM locale.
|
||
|
||
Conductarr transport and identity live outside the state components. A revoked
|
||
sign-in discards the current person's authenticated projections; an outage keeps
|
||
last-known-good (LKG) data and its Saved timestamp. No Seerr fallback or offline
|
||
request queue is used. Title and performer pages use the same authenticated cache.
|
||
|
||
## Conductarr production wiring
|
||
|
||
The default endpoint is `BuildConfig.CONDUCTARR_BASE_URL`:
|
||
`https://wolfskin-tv.justinvisser.org`. Override **Conductarr server URL** beside
|
||
**Wolfskin screens** in Advanced settings; blank restores the build default.
|
||
HTTPS is required except for loopback fake servers. **Wolfskin preview fixtures**
|
||
is a debug-only, off-by-default switch; it never sends HTTP mutations.
|
||
|
||
`ConductarrSession` borrows the currently selected user's existing Jellyfin access
|
||
token. It sends only `Authorization: MediaBrowser Token="<token>"`, with no cookie,
|
||
API key or second credential store. Person/server/token/endpoint changes discard
|
||
the old client and in-memory providers. LKG files are isolated by person and client
|
||
identity in app-private, backup-excluded storage. A 401 clears that identity's
|
||
cache and returns to sign-in states; Try again reconstructs and verifies the client.
|
||
|
||
Home publishes Jellyfin/LKG rows independently of Conductarr, including retry
|
||
authentication, and paints new Jellyfin data before writing its disk copy.
|
||
Production calls use 3-second connect, 5-second read and 8-second total limits.
|
||
Request recipients include authorized Conductarr profiles without saved Jellyfin
|
||
logins; that does not create selectable TV logins. Late name reads preserve the
|
||
open chooser's selected recipient and footer focus.
|
||
|
||
Artwork uses a fresh bounded Wolfskin loader, not the upstream loader's
|
||
authenticated fetcher. The app's network interceptor removes Authorization from
|
||
every non-Jellyfin artwork origin (scheme, host and port), including redirects;
|
||
current Jellyfin artwork still authenticates. A failed or empty library-audience response keeps
|
||
the existing mapping or Jellyfin name rule. Returning to the foreground retries
|
||
household metadata without rebuilding the selected identity.
|
||
|
||
| Screen seam | Production supplier |
|
||
| --- | --- |
|
||
| People and nav | Operator, profiles, library audiences; saved Jellyfin users remain the only selectable logins |
|
||
| Home continuation | Recommendation feed, catalogue options/discovery; Coming soon stays Discover-only |
|
||
| Discover | Feed/contributions, services, New on, Trending, genres, Coming soon, parameterized service shelves, dismissal reads/writes |
|
||
| Catalogue paging | Two-shelf read-ahead, distinct title identities, per-parameter disk pages, retry/duplicate-only tails; no page counters |
|
||
| Request sheet | Capability-limited requesters, season metadata, POST receipt, receipt lookup before any resubmit; actor comes only from the token |
|
||
| Requests | Household activity, stage precedence, actor/requester evidence and display posters; foreground/30-second refresh |
|
||
| Ratings | Ratings-only read after 350 ms card dwell; immediate hero/title/Coming-in reads; per-type six-hour/300-entry cache; episode season votes |
|
||
| Titles and people | Detail/enrichment/display, cast, related, real Jellyfin season episodes plus votes, performer/filmography, title activity and uncached receipts; shared request/hide workflows |
|
||
|
||
The pinned client exposes exactly the 22 TV operations, including ratings and
|
||
display. Conditional reads send ETags and reuse the saved body on 304. The fake
|
||
server tests use representative wire JSON, never a live Conductarr or Jellyfin.
|
||
`ConductarrSessionTest`, `ConductarrProjectionTest`, `ConductarrTitlePagesTest`,
|
||
`ConductarrSurfaceTest`, `ConductarrHomeSurfaceTest` and `ConductarrTitleSurfaceTest` cover identity/cache
|
||
isolation, 401/retry, saved offline data, actor-safe requests, ratings timing,
|
||
title playback permissions/episode votes and native real-provider surfaces.
|
||
The existing fixture screen tests remain in the normal `make test` gate.
|
||
`WolfskinArtworkTest` exercises the actual Coil loader against two HTTP hosts
|
||
and a same-host/different-port origin with an authenticated upstream parent,
|
||
and verifies successful decoding with credentials only on Jellyfin. `ConductarrAudienceRecoveryTest` covers
|
||
operator success plus failed/empty audiences, name-rule retention and foreground healing.
|
||
|
||
Not done here: deploying the dedicated hostname or widening server authority;
|
||
hardware installation/playback verification; releases, signing or version changes.
|
||
Unheld trailers remain absent because the server contract has no trailer field.
|
||
Saved Jellyfin logins are not fabricated from Conductarr profiles: a person
|
||
without an upstream token remains signed out. Fixtures require explicit debug opt-in.
|
||
|
||
### Manual smoke after endpoint deployment (no build command)
|
||
|
||
Use an already authorized test build and a sandbox that accepts its selected
|
||
Jellyfin login. No token needs to be copied, displayed or entered into Wolfskin.
|
||
|
||
1. In Advanced settings, set the sandbox URL with fixtures **off**. Select Shared,
|
||
then another saved person; verify their operator/requester and independent views.
|
||
Its hostname must be in Conductarr's TV allowed-host list; arbitrary hosts or
|
||
IP addresses are refused even with the correct token.
|
||
2. Open Home, Discover, a service, Hidden picks and Requests. Open a title, cast
|
||
member and filmography credit; check real facts, related titles, episode votes,
|
||
stage/actor labels, card dwell ratings and continuous See all loading.
|
||
3. Make the sandbox return 503 after a successful read. Reopen: saved content and
|
||
Saved time remain, requests cannot submit, and Try again performs another read.
|
||
4. Return 401, then restore authentication. Verify ratings/request actions disappear
|
||
and this person's saved Conductarr data is cleared. Try again restores data
|
||
without changing Jellyfin login or copying a token.
|
||
5. Have the sandbox accept a Shared-on-behalf request but lose its POST response.
|
||
Use Check again: the receipt says Shared, and the sandbox records one POST.
|
||
6. Restore the blank/default URL when the dedicated TV hostname and server's
|
||
22-operation authority are deployed. This checklist is manual, not a live gate.
|
||
|
||
Title and person surfaces publish saved timestamps through the same per-person
|
||
`RememberedView` partition and nav Saved indicator. Their timed notices render
|
||
the shared `RecoveryToast`; failed Try again and playback use `RecoveryMessage`
|
||
without transport details. A selected-person mismatch never renders the former
|
||
person's title/person projection. Title and performer disk copies remain isolated
|
||
by both server and authenticated user; native retry and cache-isolation regressions
|
||
exercise these boundaries without a live service.
|
||
|
||
## TV fidelity and source gates
|
||
|
||
Library/Discover page headings use the existing exact CSS line-box layout.
|
||
Request sheets reserve the inherited inline text descent around their ratings;
|
||
person rows and receipt panels therefore share the locked frame's vertical
|
||
coordinates. Settings row title/facts also retain their CSS line boxes. These
|
||
are local component fixes: shared typography tokens, Home and caption styling
|
||
do not shift.
|
||
|
||
Home ratings observations use independent six-hour, 300-entry title and
|
||
series/season caches. A season retains each observed episode's own score and
|
||
timestamp; changing a Jellyfin episode UUID does not lose the observation, and
|
||
another episode or season never borrows that score. Episodes without a complete
|
||
catalogue identity remain isolated by their Jellyfin identity.
|
||
|
||
Cold library grids publish each cumulative 200-item SDK chunk before the next
|
||
read completes. An existing complete saved copy stays visible throughout a
|
||
refresh, and only a completed read replaces it on disk. A cold later-chunk
|
||
failure keeps the already-readable titles with a retry tail, without saving an
|
||
incomplete copy. Appended titles preserve the focused title even when the
|
||
destination's sort inserts them before it. Catalogue row keys use the same
|
||
media-type/TMDB identity as their cards.
|
||
|
||
Undo restores a dismissed pick silently; Show again announces “Back in your
|
||
picks.” Title pages own their own toast so the request adapter never duplicates
|
||
it. The saved Discover retry tail is labelled More picks, not Try again twice.
|
||
|
||
People retry retains an already signed-in Conductarr client and its uncertain
|
||
submission ledger; only a signed-out identity is rebuilt. Home authorizes its
|
||
Jellyfin read by the selected person's saved Jellyfin user ID, so a lost
|
||
Conductarr operator or a renamed household profile does not hide that user's
|
||
own library. A different saved user ID still fails closed.
|
||
|
||
`make test` runs `OwnedSourceAuditTest` through the design module's JVM suite.
|
||
It scans the owned Kotlin sources in `wolfskin-design/src/main/java` and
|
||
`app/src/wolfskin`, rejecting untokened `.dp`/`.sp` conversions and numeric Color
|
||
constructors outside generated tokens, token font sizes below 12 sp, raw
|
||
household library labels, and page-counter/navigation wording. Its negative
|
||
cases plant each violation independently. Existing native capture checks also
|
||
verify rendered font sizes and wording. `LibraryProgressiveSurfaceTest` captures
|
||
the usable first chunk while the final read is still suspended.
|
||
|
||
## Toolchain and disk use
|
||
|
||
`Dockerfile` pins Temurin JDK `21.0.11+10` by image digest, command-line tools
|
||
`23.0` and platform-tools `37.0.1` by archive and checksum, and build-tools
|
||
`36.0.0` by SDK package version. The SDK manager registers platform `37.0` and
|
||
its licenses; the build rejects any platform revision other than 2. An SDK
|
||
revision upgrade therefore requires a deliberate Dockerfile update.
|
||
Gradle comes from the committed wrapper. The image is built for `linux/amd64`;
|
||
the default local image tag is `wolfskin-build:sdk37-jdk21`.
|
||
Upstream's build timestamp means APKs are not byte-for-byte reproducible.
|
||
|
||
Gradle downloads and caches live in the named volume `wolfskin-gradle`, not
|
||
`/tmp`. Module outputs stay in the checkout. Builds run with the host UID/GID,
|
||
`--no-daemon`, and at most four workers. The repository's Git metadata is mounted
|
||
read-only, including when the checkout is a linked worktree.
|
||
|
||
Override `WOLFSKIN_BUILD_IMAGE` to change the locally built image tag and
|
||
`WOLFSKIN_GRADLE_VOLUME` to isolate a cache. `make build-image` builds only the
|
||
image. Do not delete shared caches during another build.
|
||
|
||
Every Gradle container holds a host-side exclusive lock for its entire run.
|
||
Concurrent targets sharing a cache queue rather than timing out on Gradle's
|
||
cross-container cache journal lock; a waiting invocation prints one line.
|
||
The default lock is `/var/tmp/${WOLFSKIN_GRADLE_VOLUME}.gradle.lock` (using
|
||
`wolfskin-gradle` when unset). Override `WOLFSKIN_GRADLE_LOCK_FILE` with a path
|
||
whose parent directory already exists. All callers sharing a volume must use
|
||
the same lock path on the Docker host. Do not delete the file while builds are
|
||
running or waiting. The lock releases automatically when the invocation exits.
|
||
|
||
## Release credentials
|
||
|
||
The default escrow is
|
||
`/opt/stacks/ventoy/machine-parity/manifest-tank/secrets/wolfskin/`. Check
|
||
`mountpoint -q /mnt/ventoy` before using that path. Set `WOLFSKIN_SECRETS_DIR`
|
||
to use another existing directory. Its required files are:
|
||
|
||
| File | Contents |
|
||
| --- | --- |
|
||
| `release.jks` | Separate Wolfskin PKCS12 keystore, RSA 4096, alias `wolfskin` |
|
||
| `release.properties` | Java properties `storePassword` and `keyPassword` |
|
||
| `github-packages-token` | GitHub token with `read:packages`, username `JustinZeus` |
|
||
|
||
The directory must be mode `0700`, each file mode `0600`, owned by the building
|
||
user. Never commit them or put their contents in logs. Back up the keystore and
|
||
passwords together: losing the release key forces a reinstall. Do not reuse
|
||
wolfskin-android's key or the gh CLI login token.
|
||
|
||
Release builds mount escrow read-only. The token is loaded inside the container
|
||
into upstream's `ORG_GRADLE_PROJECT_WholphinExtensionsUsername` and
|
||
`ORG_GRADLE_PROJECT_WholphinExtensionsPassword` environment hooks, never a Docker
|
||
command argument. The signing init script registers `wolfskin`; the nonsecret
|
||
`release-signing.properties` is mounted as `local.properties` so upstream's
|
||
`release.signing.config` hook selects it. Any host `local.properties` is hidden
|
||
only for the duration of this container. Keystore and passwords stay outside the
|
||
checkout and image. Missing credentials fail the release rather than silently
|
||
shipping the MPV stub.
|
||
|
||
## Versioning and local release markers
|
||
|
||
Upstream computes `versionName` from
|
||
`git describe --tags --long --match='v*'`, dropping the leading `v`, and computes
|
||
`versionCode` from the count of all local `v*` and `p*` tags. Preserve and fetch
|
||
those tags; a shallow or incomplete checkout can give an incorrect version.
|
||
Use the full `git describe` output as the human release name.
|
||
|
||
After committing the intended release, with a clean checkout and fetched tags:
|
||
|
||
```sh
|
||
make release-tag
|
||
make apk
|
||
```
|
||
|
||
The helper creates one lightweight `pwolfskin-YYYY_MM_DD` tag in UTC, appending
|
||
`_1`, `_2`, etc. on collision. Each marker adds one to `versionCode` without
|
||
changing upstream's `v*`-based version name. It never pushes tags, publishes a
|
||
release, or installs an APK. Do not delete published release markers. Tag
|
||
creation and publication must be serialized between release operators.
|
||
|
||
## Inspecting release artifacts
|
||
|
||
Use `apksigner verify --verbose --print-certs APK` and
|
||
`aapt2 dump badging APK` inside the build image. Compare the signer SHA-256 with
|
||
`keytool -list -v -alias wolfskin -keystore /path/to/release.jks` (enter its
|
||
password privately). Inspect APK ZIP entries for real native MPV and ffmpeg
|
||
libraries for each shipped ABI; a successful Java compilation alone does not
|
||
prove that codec extensions are included.
|