12 KiB
Design fixes — home screen and detail pages
Audit of 2026-08-01. Findings only; no product code was changed. The three items under
"Confirmed layout bugs" were reproduced by rendering the real composables at TV 1080p
(w960dp-h540dp-television-xhdpi) through the existing Roborazzi harness — everything
else is read from the source.
All of it is implemented. The findings below are left as written — they are the diagnosis, and each one says why the fix is shaped the way it is. What was done:
| # | Fixed in |
|---|---|
| 1 | HomeMovieHero.kt — column padding 28→22dp, and a wrapped title stands the synopsis down (onTextLayout line count) so Play is never what gets cut. Captured as df_home-movie-hero-long-title.png. |
| 2 | DetailPageComponents.detailPaneHeight() — the slot is derived from the viewport (250–420dp) instead of a hard 250dp. Every technical spec now renders; df_detail-pane-cast-details.png. Studio, which both columns claimed, is dropped from the technical column. |
| 3 | DetailFoldPeek (34dp) holds the tab strip off the bottom edge. |
| 4 | The hero's private heroFacts/runtime formatter is gone; it calls detail/DetailFacts.kt. HomeComponents.formatTvRuntime too — one formatter left in the app. |
| 5 | MembyScore token, and every rating goes through ratingLabel (Locale.US). The home metadata panel renders the score as its own run of text so it can carry the same gold. |
| 6 | "No favourite shows yet" / "Mark a series as a favourite…". |
| 7 | FactSeparator between facts, ValueSeparator inside a fact that holds a list. |
| 8 | ui/theme/DesignTokens.kt; HomeComponents and DetailPageComponents colours are aliases of it, and Theme.kt uses the same near-blacks. |
| 9 | Same — the detail page picked up the raised TV neutrals. |
| 10 | Three radii: MembyChipCorner 8, MembyCardCorner 10, MembyPanelCorner 14. |
| 11 | ui/MembyButtons.kt — MembyPlayButton, MembyPlayChip, MembyChoiceChip. The hero chip, the detail Play button, the metadata panel's "▶ Resume" and the For You time budget all use them. |
| 12 | DetailHero honours Settings.showTitleLogo (new EmbyRepository.showTitleLogo) and shares useTextTitleForLogo with the screensaver (ui/TitleLogo.kt). |
| 13 | One UHD_MIN_WIDTH (3800) for the badge and the (4K) suffix. Unit-tested. |
| 14 | mediaBadges reads dynamicRangeLabel, so HDR10+ stays HDR10+. Unit-tested. |
| 15 | One FocusRequester per pane in both overlays; none is attached to two live nodes. |
| 16 | A series passes mediaBadges(item), and DetailFactRow takes 4 badges so the airing badge is not squeezed out. |
| 17 | HomeRowHeaderIcon / HomeRowHeaderIconGap / HomeRowHeaderSpacing, used by MediaRow, MyShowsStrip and RecentSearchesRow — which also gained the vertical padding its focus-scaled chips needed. |
| 18 | HomeHeroPick carries the row a title was drawn from; the caption is no longer the card's slot. Unit-tested. |
| 19 | Deleted (≈260 lines: HomeHero, HomeRow, ContentCard, HomeRowData, HomeRowSkeleton and the two runtime formatters only they used). |
| 20 | The peek under the strip plus a chevron at its end. |
| Docs | CLAUDE.md's detail-page section rewritten to describe this code, with the token, button and header conventions above it. |
Screenshots of the result are app/build/screenshots/df_*.png
(.\gradlew.bat :app:testDebugUnitTest --tests "*ScreenshotTest").
Confirmed layout bugs
These clip real content on a real TV. Fix these first.
1. The featured home hero drops its Play button when the title wraps to two lines
ui/HomeMovieHero.kt:171-236, ui/MainActivity.kt:137 (homeHeaderHeight)
The card's content column measures ~229dp with a one-line title and ~261dp with two.
homeHeaderHeight(540dp, showHero = true) yields 248dp, minus the hero row's 16dp top and
10dp bottom padding, so the card gets 222dp — and FocusScaleContainer clips it to a
14dp rounded rect. Rendered with a two-line title, the kicker, title, fact line and
synopsis draw and the green Play chip is gone entirely. A one-line title is already
7dp over budget; it only survives because the part cut off is the chip's shadow.
Fix direction: the column is align(Alignment.CenterStart) inside a fixed-height box, so
overflow is split top and bottom and the button is always the first thing lost. Either
give the hero a height derived from its content, cap the title at one line, or drop the
synopsis when the title wraps.
2. The Cast & Details tab silently discards every technical spec
ui/DetailPageComponents.kt:258-278 (the 250dp pane), :563-580 (DetailFocusablePane),
:613-643 (DetailCastAndDetailsPane)
The tab content slot is a hard .height(250.dp) and DetailFocusablePane applies
.clip(RoundedCornerShape(10.dp)). releaseAndTechnical builds Released / Certificate /
Runtime and then addAll(specs) — Video, Codec, Audio, Subtitles, Studio. Rendered at the
real slot geometry, only the first three rows survive; the entire output of
technicalSpecs() is clipped below the fold of a pane that cannot scroll. That is the
whole reason the tab exists.
DetailOverviewPane shares the ceiling: a five-line synopsis plus the series "Up next"
supporting line pushes its credit rows past the same boundary.
Fix direction: the pane needs a height budget that accounts for its worst case, or the two-column meta block needs to page/scroll. Note the pane deliberately does not scroll (one screen per tab), so the honest fix is probably fewer rows per column, not a scroller.
3. The detail tab strip sits flush against the bottom screen edge
ui/DetailPageComponents.kt:209 (heroHeight = maxHeight - DetailTabHeight), :494-550
The selection underline is cut in half at y=1080 in both detail-movie-more-like-this.png
and detail-series-cast-details.png. On a TV with overscan the underline and part of the
labels are off-screen. This is the only element in the app with zero safe-area inset —
gutters are 36-58dp and the home clock keeps 18dp.
Copy and formatting
4. The home hero formats runtime differently from everywhere else
ui/HomeMovieHero.kt:351 is a private heroFacts shadowing ui/detail/DetailFacts.kt:62.
Different field order (year · certificate · runtime vs year · runtime · certificate) and
"${it}m" instead of formatRuntime. The checked-in home-movie-hero.png shows
"2026 • M • 124m" in the hero and "2026 • 2h 4m" on the card directly beneath it.
There are three runtime formatters in the app: DetailFacts.formatRuntime,
HomeComponents.formatTvRuntime (private, identical) and this one.
5. The community score changes colour and locale by screen
Gold 0xFFF5C518 on detail (DetailPageComponents.kt:484), grey MutedText on home
(HomeComponents.kt:1025). HomeMovieHero.kt:355 uses "★ %.1f".format(it) with the
default locale while every other rating goes through Locale.US — a comma decimal in a
non-US locale.
6. American spelling in two user-facing strings
ui/HomeComponents.kt:1549 "No favorite shows yet" and :1555 "Mark a series as a
favorite and it'll be waiting here." The rail says Favourites, the quick menu says
"Add to favourites", the detail hero action says "Add to Favourites".
7. Four separator styles for the same kind of fact list
" • " (home metadata), " • " (detail fact row, schedule metadata, home hero),
" · " (home genres), ", " (credit rows).
Design-token drift
8. Four near-blacks
Theme background 0xFF0B0E11 and surface 0xFF101418 (ui/theme/Theme.kt, effectively
unused), home 0xFF090B0D, detail 0xFF080A0C. The accent green is duplicated four ways:
EmbyGreen, DetailAccent, and the hero's 0xFF69C762 / 0xFF7BD574.
9. Secondary-text contrast diverged between the two screens
ui/HomeComponents.kt:129-132 carries a comment about raising the neutrals for TV distance
(MutedText 0xFFD0D6DB, QuietText 0xFFAEB7BF). The detail page still uses the pre-fix
values (DetailMutedText 0xFFB6BDC3, DetailQuietText 0xFF8C959D). The two sit side by
side the moment a detail page is opened from a row.
10. Corner radii are ad hoc
9dp home cards, 8dp related posters, 8/10dp cast cards, 14dp featured hero, 11dp mini hero, 12dp overlays, 7dp chips, 999dp search chips.
11. Three button languages
The hand-rolled DetailPlayButton (23/12dp padding, 16sp), the hero's hand-rolled play chip
(12/7dp, 13sp — same look, different metrics), and raw androidx.tv.material3.Button with
glyph text in MediaMetadataPanel ("▶ Resume") and ForYouTimeBudget ("✓ 30 min"), which
picks up theme colours nothing else in the app uses.
Logic and behaviour
12. showTitleLogo is ignored by the detail pages
The Settings copy promises "shows each title's logo artwork from Emby instead of plain
text", but only ui/screensaver/ScreensaverContent.kt:773 honours it;
ui/DetailPageComponents.kt:322 always fetches the logo. The screensaver also has
useTextTitleForLogo, a fallback for logos too dark to read — the detail hero has no
equivalent, so a dark logo is invisible on the near-black scrim.
13. Two different 4K thresholds
The badge fires at video width ≥ 3800 (HomeComponents.kt:1184); the (4K) suffix at
≥ 3400 (DetailFacts.kt:190). A 3600-wide file is 4K in the spec row and not on the badge.
14. HDR10+ is named in dynamicRangeLabel but collapses to plain "HDR" in mediaBadges
15. One FocusRequester attached to two live nodes
informationPane is attached by the Overview pane, the Cast & Details pane and the Episodes
empty states (ui/MediaDetailsOverlay.kt:202-216, ui/SeriesDetailsOverlay.kt:291-329).
AnimatedContent's 80ms fade-out keeps the outgoing pane composed, so a Down press landing
in that window can request focus on a pane that is disappearing.
16. A series can never show format badges
ui/SeriesDetailsOverlay.kt:248 passes badges = emptyList() where movies pass real ones.
Related: DetailFactRow does badges.take(3), so on a 4K/HDR/HEVC movie the airing badge
appended in ui/MediaDetailsOverlay.kt:147 is silently dropped.
17. Home row headers do not align
MediaRow and MyShowsStrip lead with a 28dp icon chip plus 10dp; RecentSearchesRow
(ui/MainActivity.kt:2084) has no chip, so its title starts 38dp further left. It also uses
9dp header spacing against everyone else's 6dp and gives its LazyRow no vertical padding,
so focus-scaled chips have no room to grow.
18. The mini hero labels are positional fiction
ui/HomeMovieHero.kt:124: listOf("POPULAR", "NEW RELEASE", "TRENDING")[index]. But
selectHomeHeroMovies interleaves new releases and popular picks and then falls back to
every movie in the response. In the current screenshot a 2025 title is labelled NEW RELEASE
and a 2026 one POPULAR.
19. A dead second home implementation
ui/MainActivity.kt:2752-2900: HomeHero, HomeRow, ContentCard, HomeRowData and
HomeRowSkeleton are unreferenced (only the gateway model named HomeRow is in use).
They carry a competing 48dp gutter and SemiBold header style — a live-looking template for
the wrong conventions.
20. Tab content sits entirely below the fold with no affordance
By design per the code comments, but nothing on screen tells the viewer that Down reveals anything.
Documentation
CLAUDE.md's detail-page section no longer describes this code. It documents a poster-left
layout with Play hanging off the poster's bottom-right corner, "nothing scrolls vertically",
a tab list of "Overview, Episodes, Cast, Details", and a DetailReasonStrip of several
short phrases. The code is a full-bleed scrolling hero with Overview / Episodes / More Like
This / Cast & Details and a single reason line (ui/DetailPageComponents.kt:375-385).
Worth correcting before it misleads the next change.