Skip to content

Group player song info and controls into a tinted card - #83

Merged
sebsto merged 2 commits into
mainfrom
ui/controls-card-panel
Aug 9, 2026
Merged

sebsto merged 2 commits into
mainfrom
ui/controls-card-panel

Conversation

@sebsto

@sebsto sebsto commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

What this changes

The phone/tablet player used to be one continuous surface: the Cover Flow carousel, song label, playback controls and volume row all sat directly on the artwork-derived background wash, with nothing dividing them.

This splits it into two zones. The carousel keeps the wash; everything below (portrait) or beside (landscape) it now sits on a rounded card.

The card is built from an opaque base — warm off-white in light mode, soft charcoal in dark — with a low-opacity tint of the current artwork's dominant colour layered over it. The base is what carries text contrast, so readability never depends on which album is playing; the tint only connects the panel to the artwork. When there's no artwork colour and the branded gradient is showing, the card is a single near-black panel that the gradient reads faintly through.

The card is anchored to the screen edges it touches and only rounds the corners facing the carousel:

orientation card position rounded corners
portrait full width, along the bottom top pair
landscape trailing column, flush right and bottom leading pair

The brand logo / version footer stays pinned to the bottom edge, drawn on top of the card.

Two layout invariants worth knowing about

These are the non-obvious parts, and both are load-bearing rather than cosmetic:

Landscape rows are pinned to the pane height. An HStack takes the height of its tallest child and centres every child against it. That made the carousel's vertical position a function of the info column's height — grow the column by one wrapped line of text and the carousel moved. Pinning the row to the pane makes the two columns independent: the card's own Spacers absorb its content instead.

The song label is capped at one line per field in landscape. SongLabelView gained an explicit maxLines, defaulted to 2 so portrait and the TV UI are unchanged. In landscape the label truncates at full type size rather than wrapping, which keeps its height constant. This is what stops the column from resizing the row it shares with the carousel. The trade-off is deliberate: long artist names truncate with an ellipsis in landscape instead of shrinking or taking a second line.

Relatedly, the root view's frame is now the GeometryReader's exact size rather than maxWidth/maxHeight: .infinity. A max frame adopts its child's size when the child asks for more height than the pane offers, and the bottom-aligned footer overlay anchors to that frame — so an over-tall layout could push the footer off screen. A fixed frame fills the pane identically while keeping the anchor on the pane's own bottom edge.

Other landscape adjustments

  • iPhone: artist/title step up from 26/19pt to 32/23pt, and a top inset holds the label clear of the card's top edge (the column's Spacers collapse to zero in a landscape pane, so they can't provide that clearance themselves).
  • iPad / macOS: the info column widens from 420pt to 520pt. Since the carousel takes whatever width the column leaves and the hero is a fraction of that, this trades hero size for text room — it also stops long artist names truncating at that size. The air time moves to its own "Played at …" line, as in portrait, since this column has the height for a third line.
  • Android: the volume slider is pinned to the speaker-glyph height. Compose draws the slider's track top-biased inside a taller touch target, so at its natural height the track floated above the centre-aligned glyphs and crowded the tray above.

Scope

Applies to iOS, iPadOS, macOS and Android, portrait and landscape.

The TV (TVRadioPlayerView) and CarPlay / Android Auto surfaces are untouched. They're separate entry points — Maxi80App branches to the TV view, and the car surfaces are CPTemplate/MediaSession rather than SwiftUI — so nothing in this diff is reachable from them. SongLabelView's new maxLines defaults to the previous behaviour, which is what the TV UI takes.

Files

file role
Theme/ControlsCardStyle.swift new — the card surface: fill, corner shape per attachment edge, safe-area bleed
RadioPlayer/RadioPlayerView+Card.swift new — controlsCard(attachedTo:) plus the card's contrast resolution
RadioPlayer/RadioPlayerView+Layout.swift all four layouts wrap their controls in the card; row pinning; landscape sizing
RadioPlayer/RadioPlayerView+Chrome.swift songLabel takes maxLines and a contrast override
RadioPlayer/RadioPlayerView.swift root frame pinned to the container size
RadioPlayer/SongLabelView.swift maxLines replaces a hard-coded lineLimit(2)
VolumeSliderView.swift Android slider height

Testing

  • swift build and skip android build clean, no new warnings.
  • swift test — 192 tests across 29 suites pass.
  • Rendered and reviewed by hand on iOS, iPadOS, macOS and Android, in both orientations.

Known follow-ups

  • The landscape info column is roughly 35–40pt over its height budget on iPhone even at one line per field. The top inset covers the symptom; the visible consequence is that the card's top edge meets the pane top rather than sitting below it. Reclaiming space would mean trimming the 140pt control tier, the footer clearance, or the inter-element gaps.
  • The hero sits a few points right of its column's centre in landscape. This predates these changes and is still undiagnosed.

Split the phone/tablet player into two visual zones: the Cover Flow carousel
sits on the artwork wash, and a rounded card carries the song label, status
slot, playback controls and volume row. The card is tinted from the current
artwork's dominant colour over an opaque base, so it reads as a distinct
surface without depending on the artwork for text contrast.

The card is attached to the screen edges it touches and only rounds the
corners facing the carousel: the top pair in portrait, the leading pair in
landscape, where it also bleeds to the trailing edge.

Two layout invariants make the result stable:

- Both landscape rows are pinned to the pane height. An unpinned HStack takes
  its tallest child's height and centres every child against it, which made
  the carousel's vertical position a function of the card column's height.
- The song label is capped at one line per field in landscape, holding its
  height constant at full type size rather than wrapping.

Also in landscape: the iPhone label steps up to 32/23pt, the expanded
(iPad/macOS) info column widens to 520pt with the air time on its own line,
and the Android volume slider is pinned to the speaker-glyph height so its
track shares their centre line.

Applies to iOS, iPadOS, macOS and Android in both orientations. The TV and
CarPlay/Android Auto surfaces are separate entry points and are untouched.
Copilot AI lite review requested due to automatic review settings August 9, 2026 15:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the phone/tablet Radio Player UI to visually separate the cover-flow “wash” area from the song info + controls by introducing a rounded, artwork-tinted controls card that adapts its corner rounding and attachment based on orientation.

Changes:

  • Introduces a ControlsCardStyle surface (base + artwork tint) and a controlsCard(attachedTo:) wrapper that also resolves card-specific contrast.
  • Updates portrait and landscape layouts to place song label, status, playback controls, and volume inside the new card; pins landscape rows to the pane height to avoid carousel re-centering when the info column height changes.
  • Adds maxLines support to SongLabelView (used to cap landscape label height) and tweaks Android volume slider layout.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
Sources/Maxi80/Views/VolumeSliderView.swift Pins Android slider height to speaker glyph size; refactors glyph sizing.
Sources/Maxi80/Views/RadioPlayer/SongLabelView.swift Adds maxLines parameter to control truncation/wrapping behavior.
Sources/Maxi80/Views/RadioPlayer/RadioPlayerView+Layout.swift Wraps controls content in the new card and adjusts portrait/landscape sizing + spacing logic.
Sources/Maxi80/Views/RadioPlayer/RadioPlayerView+Chrome.swift Extends songLabel(...) wrapper to pass maxLines and a contrast override.
Sources/Maxi80/Views/RadioPlayer/RadioPlayerView+Card.swift Adds the controlsCard(attachedTo:) wrapper and card-specific contrast resolution.
Sources/Maxi80/Views/RadioPlayer/RadioPlayerView.swift Pins the root layout frame to the GeometryReader size to keep the footer anchored.
Sources/Maxi80/Theme/ControlsCardStyle.swift New card fill + shape logic for bottom/trailing attachment styles.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Sources/Maxi80/Views/VolumeSliderView.swift Outdated
Comment thread Sources/Maxi80/Views/RadioPlayer/RadioPlayerView+Layout.swift Outdated
…earance

Both from PR review:

- SkipUI forwards a Slider's tint to Compose as the thumb and active-track
  colors, and `.secondary` does not follow the forced color scheme on Android
  (the same reason the speaker glyphs use an explicit adaptive gray). Left as
  `.secondary` the thumb and filled track render too dark to read on the card's
  charcoal surface in dark mode, so they now take the same gray as the glyphs
  flanking them. Apple keeps `.secondary`.

- The portrait hero's chrome budget hard-coded 36pt of footer clearance while
  the card reserves 34pt through `footerClearance`. That is one gap described by
  two literals, so the hero's estimate could drift from the space actually
  reserved. `footerClearance` is now a static shared by both.
@sebsto
sebsto merged commit 2387ac8 into main Aug 9, 2026
3 checks passed
@sebsto
sebsto deleted the ui/controls-card-panel branch August 9, 2026 18:12
@sebsto sebsto added this to the 5.2 milestone Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants