Group player song info and controls into a tinted card - #83
Merged
Merged
Conversation
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.
There was a problem hiding this comment.
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
ControlsCardStylesurface (base + artwork tint) and acontrolsCard(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
maxLinessupport toSongLabelView(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.
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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
HStacktakes 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 ownSpacers absorb its content instead.The song label is capped at one line per field in landscape.
SongLabelViewgained an explicitmaxLines, defaulted to2so 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 thanmaxWidth/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
Spacers collapse to zero in a landscape pane, so they can't provide that clearance themselves).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 —Maxi80Appbranches 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 newmaxLinesdefaults to the previous behaviour, which is what the TV UI takes.Files
Theme/ControlsCardStyle.swiftRadioPlayer/RadioPlayerView+Card.swiftcontrolsCard(attachedTo:)plus the card's contrast resolutionRadioPlayer/RadioPlayerView+Layout.swiftRadioPlayer/RadioPlayerView+Chrome.swiftsongLabeltakesmaxLinesand a contrast overrideRadioPlayer/RadioPlayerView.swiftRadioPlayer/SongLabelView.swiftmaxLinesreplaces a hard-codedlineLimit(2)VolumeSliderView.swiftTesting
swift buildandskip android buildclean, no new warnings.swift test— 192 tests across 29 suites pass.Known follow-ups