[PC TV] Derive playlist pill colour from front cover artwork#4561
[PC TV] Derive playlist pill colour from front cover artwork#4561david-gonzalez-a8c wants to merge 5 commits into
Conversation
The playlist pill background was hard-coded to one of four spec colours indexed by the playlist's sort position. Now it reflects the podcast whose artwork sits on the front of the cover stack: - Reads the podcast's `primaryColor` (server-derived brand colour for the artwork) and clamps saturation/brightness into a pill-friendly range so vivid tints still read as a calm background. - For greys / near-black tints (no usable hue, or metadata not yet downloaded), falls back to a 6-colour palette seeded by the playlist UUID's scalar sum — deterministic across launches and far more diverse than the previous 4-by-sort-position scheme. - Observes `Constants.Notifications.podcastColorsDownloaded` so pills refresh once `ColorManager` has fetched colour metadata for the cover podcast (newly-subscribed shows arrive with `colorVersion = 1`, which forces the default until the download lands). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Updates the Pocket Casts TV Playlists UI to derive each playlist pill’s background color from the front cover podcast’s artwork tint, with a deterministic fallback palette and a refresh path when color metadata downloads complete.
Changes:
- Derives
playlistColorfromColorManager.lightThemeTintForPodcastand clamps saturation/brightness for a consistent pill background. - Adds a deterministic fallback palette seeded by playlist UUID when tint data is unusable or unavailable.
- Observes
Constants.Notifications.podcastColorsDownloadedto refresh pill colors when downloaded metadata arrives.
- Drop the orphaned `cancellable` (singular) — only `cancellables` (the Set) is used now. - Replace `coverPodcastsUuids.first` / `.contains(uuid)` lookups with the O(1) `episodes.first?.podcastUuid`. The front-most cover is always the first episode's podcast, so we can avoid the de-duplicating scan over up to 1000 episodes inside the SwiftUI hot path and the notification handler. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
| /// Deterministic per-seed palette so playlists whose front cover has no | ||
| /// usable colour metadata still render distinct from each other. | ||
| private static func fallbackPillColor(for seed: String) -> Color { | ||
| let palette: [Color] = [ | ||
| Color(red: 0.15, green: 0.25, blue: 0.5), | ||
| Color(red: 0.5, green: 0.17, blue: 0.15), | ||
| Color(red: 0.21, green: 0.22, blue: 0.14), | ||
| Color(red: 0.5, green: 0.35, blue: 0.12), | ||
| Color(red: 0.15, green: 0.4, blue: 0.3), | ||
| Color(red: 0.3, green: 0.2, blue: 0.45) | ||
| ] | ||
| // `String.hashValue` is per-run randomised in Swift; sum the unicode | ||
| // scalars instead so a given playlist gets the same pill colour each | ||
| // launch. | ||
| let index = seed.unicodeScalars.reduce(0) { $0 + Int($1.value) } % palette.count | ||
| return palette[index] | ||
| } |
There was a problem hiding this comment.
Done in 7f2df70 — palette hoisted to a static let.
| /// Falls back to a deterministic palette while episodes haven't loaded, | ||
| /// for an empty playlist with no cover to sample, or when the server | ||
| /// hasn't provided usable colour metadata for the front cover podcast. | ||
| var playlistColor: Color { |
There was a problem hiding this comment.
It should be computer once and cached. It currently gets recomputed on every body invocation.
There was a problem hiding this comment.
Done in 0b6c840 — playlistColor is now a stored property, refreshed when episodes change or a colour download lands, so SwiftUI doesn't redo the lookup + reshaping on every body.
|
I'd suggest to reduce the number of comments. |
Co-Authored-By: Claude <noreply@anthropic.com>
Stored property updated when episodes change or a colour download lands, so SwiftUI no longer redoes the podcast lookup + tint reshaping on every `body` evaluation. Co-Authored-By: Claude <noreply@anthropic.com>
Drop the WHAT-describing doc comments and tighten the WHY-describing ones so the file leans on intent-revealing names. Co-Authored-By: Claude <noreply@anthropic.com>
|
@kean Re: reducing the number of comments — Done in 8c3ad3d. Dropped the doc comments that were just restating what the function name already says and tightened the remaining WHY ones. Re: the testing notes (darker palette, two playlists with the same tint) — happy to follow up on tuning the clamp ranges and extending the fallback palette in a separate PR if that suits you. The duplicate case in your screenshot is the fallback palette hashing two playlists into the same slot; a longer palette would reduce that. Wanted to keep this PR scoped to the structural review fixes. |
Review feedback addressed
|
| .sink { [weak self] notification in | ||
| guard | ||
| let self, | ||
| let uuid = notification.object as? String, | ||
| self.episodes.first?.podcastUuid == uuid | ||
| else { return } | ||
| self.refreshPlaylistColor() | ||
| } |
| private static func pillColor(from tint: UIColor) -> Color? { | ||
| var h: CGFloat = 0 | ||
| var s: CGFloat = 0 | ||
| var b: CGFloat = 0 | ||
| var a: CGFloat = 0 | ||
| tint.getHue(&h, saturation: &s, brightness: &b, alpha: &a) | ||
| guard s > 0.15, b > 0.1 else { return nil } |
| guard s > 0.15, b > 0.1 else { return nil } | ||
| return Color(UIColor( | ||
| hue: h, | ||
| saturation: min(max(s, 0.5), 0.85), | ||
| brightness: min(max(b, 0.3), 0.45), | ||
| alpha: a | ||
| )) |


Fixes PCIOS-758
Summary
Replaces the hard-coded 4-colour spec palette behind each playlist pill with a colour derived from the podcast whose artwork sits on the front of the cover stack.
To test
Checklist