feat(brand): draw the shard's logo and hero (M12 phase 4) #37
Reference in New Issue
Block a user
No description provided.
Delete Branch "feat/m12-phase-4-brand-assets"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 4 of M12 (
docs/android/THEMING_AND_NAV.md§5.6) — the brand assets, after the theme's three thirds (colors, structure, fonts). Targetsedge; the milestone reachesmainas oneedge→mainmerge.brand.logoandbrand.herohave ridden inBrandDtosince M1 and neither has ever been drawn — the app spells the instance out in text everywhere the website shows a mark. This renders them on the three surfaces §5.6 names: the logo above the name in the drawer header (32dp), the logo in place of the uppercased title in the top bar (24dp), and the hero as a band above Home's title block.Nothing new is fetched and nothing new is bundled.
LocalAssetResolver(M1) already turns a site-relative/uploads/…path into an absolute URL and Coil (M5) is already a dependency, so this phase adds no APK cost at all — one new file and three call sites.The empty slot is enforced by layout, not by a conditional at each site
§5.6's rule is that an empty slot renders nothing — not a placeholder, not a reserved gap. The way that holds without a
brand?.hero != nullcheck leaking intoHomeScreen: every size and spacing modifier hangs off the image itself, so when the image is not composed, neither is its padding. A caller that wants space below the hero passesModifier.padding(bottom = 16.dp)instead of a siblingSpacer, and gets both cases right without knowing which it got.A failed load is an empty slot too — no broken-image icon, no retry. The flag is
remember(url), so a resume refresh (§5.5) that swaps the logo gets a fresh attempt rather than inheriting the old one's failure for the life of the process.Three things §5.6 left open, all settled by the org lead before code
1. The top bar falls back to the text. It is the one place where "empty" is not "nothing": the logo is the title there, so a 404 — or a logo that can't be fetched because the shard is down — would strand the app in an unnamed shell until the next resume refresh. "The same as no asset" resolves to the text, because the text is what an instance with no logo shows. There is deliberately no fallback while the load is in flight: drawing text first would flash text → logo on every navigation for one frame, since Coil serves the second and later reads from memory.
2. The hero is a fixed 180dp band, cropped, not the intrinsic aspect the app's other images (
PostScreen,BlockRenderer) draw at. The website's hero is a CSS background driven byhero_layout, which the app does not port, so the app needs its own rule — and the website's default hero is a square emblem, so an uploaded square is a case to expect rather than an edge one. At the intrinsic aspect that square is a ~360dp block that pushes the status card off the first screenful. Cropped, a wide banner and a square give the same frame.3. It sits inside Home's existing 20dp padding, clipped to
shapes.medium, so the hero follows--radius-cardlike every other surface phase 2 gave the admin (§5.2) — rather than going full-bleed, which would have meant restructuringHomeScreen's single paddedColumn.Also worth noting the app takes no fallback hero image: where the website substitutes its own emblem for an unset hero, the app draws nothing, because §2 outranks the mirror.
Accessibility
The logo is decorative wherever the name is also on screen (the drawer, where the name is the very next line) and named only where it stands alone (the top bar, whose
contentDescriptionis the instance name). Describing the drawer's would have a screen reader say the name twice — the same call the website'salt=''makes. The hero is always decorative.Tests, and what they honestly cannot cover
BrandAssetsTest— 9 cases: every shape "not set" arrives in (null,"", whitespace), the defaultBrandDto,SiteAppearance.NONE, site-relative and absolute paths, and a resolver returning null or blank. That last pair is why the blank check runs on both sides of the resolver:BrandDtodefaults every asset field to""(the server publishes the empty string for "not set"), and a resolver with no base URL configured may hand a path straight back, so a blank slipping through either side would put a zero-size image request in the layout instead of no image.410 unit tests green (401 + 9),
lintDebugandassembleDebugclean.The drawing itself is untested and cannot be tested here. The app carries no Robolectric and has no
androidTestsource set, so a composable body cannot run in a JVM test — only the decision of whether to draw is pure, which is the whole reasonbrandAssetUrlis pulled out of the composables. Everything else is AC-5's to catch, and this is the phase with the most riding on that walk: a cropped hero, the fallback path, and a logo's contrast against the top bar are all things only a screen shows.Docs: RunicGateway/docs#116.