Files
memby/DESIGN_FIXES.md
T

217 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.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.