Files
memby/DESIGN_FIXES.md
T

12 KiB
Raw Blame History

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 (250420dp) 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.ktMembyPlayButton, 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.

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.