Skip to content

[PC TV] Derive playlist pill colour from front cover artwork#4561

Open
david-gonzalez-a8c wants to merge 5 commits into
trunkfrom
playlist-colors
Open

[PC TV] Derive playlist pill colour from front cover artwork#4561
david-gonzalez-a8c wants to merge 5 commits into
trunkfrom
playlist-colors

Conversation

@david-gonzalez-a8c

@david-gonzalez-a8c david-gonzalez-a8c commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

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.

  • Reads the cover podcast's `primaryColor` via `ColorManager.lightThemeTintForPodcast` and clamps saturation/brightness so vivid tints still read as a calm card background.
  • For greys / near-black tints (covers `getHue` reporting `H = 0` on a true grey — what made every metadata-less pill read as brown before), falls back to a 6-colour palette seeded by the playlist UUID's scalar sum. Stable across launches and more diverse than the previous 4-by-sort-position scheme.
  • Observes `Constants.Notifications.podcastColorsDownloaded` so a pill refreshes once `ColorManager` has finished fetching colour metadata in the background (freshly-subscribed shows arrive with `colorVersion = 1`, forcing the default until the download lands).

To test

  1. Returning user, podcasts with cached colour metadata. Open the Playlists tab. Each pill should render immediately with a colour that visibly echoes the front-most cover artwork (e.g. blue cover → blue-ish pill).
  2. Fresh install / freshly-subscribed podcasts. Log out and back in. Open the Playlists tab — pills first render in the deterministic fallback palette, then within a moment (after `ColorManager` finishes the colour download) flip to artwork-derived colours.
  3. Playlists with no episodes / no cover. Should render in the fallback palette and stay stable across launches.
  4. Diversity. Multiple playlists with metadata-less cover podcasts should land on visibly different colours (not the same brown).
  5. No regressions on focus. Focusing a pill still applies the `pcBackgroundActive` highlight (unchanged); leaving focus returns to the derived/fallback colour.

Checklist

  • I have considered if this change warrants user-facing release notes and have added them to `CHANGELOG.md` if necessary.
  • I have considered adding unit tests for my changes.
  • I have updated (or requested that someone edit) the spreadsheet to reflect any new or changed analytics.
Screenshot 2026-06-18 at 8 14 06 PM

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>
@david-gonzalez-a8c david-gonzalez-a8c added this to the 8.15 milestone Jun 18, 2026
@david-gonzalez-a8c david-gonzalez-a8c marked this pull request as ready for review June 18, 2026 18:17
@david-gonzalez-a8c david-gonzalez-a8c requested a review from a team as a code owner June 18, 2026 18:17
@david-gonzalez-a8c david-gonzalez-a8c requested review from Copilot and kean and removed request for a team June 18, 2026 18:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 playlistColor from ColorManager.lightThemeTintForPodcast and 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.podcastColorsDownloaded to refresh pill colors when downloaded metadata arrives.

Comment thread Pocket Casts TV App/UI/Playlists/PlaylistDetailsViewModel.swift Outdated
Comment thread Pocket Casts TV App/UI/Playlists/PlaylistDetailsViewModel.swift
Comment thread Pocket Casts TV App/UI/Playlists/PlaylistDetailsViewModel.swift Outdated
- 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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment on lines 172 to 188
/// 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]
}

@david-gonzalez-a8c david-gonzalez-a8c Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 7f2df70 — palette hoisted to a static let.

@kean kean left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It worked well on my test playlists.

Notes from testing it:

The colors are a bit on the darker side, and the two of my playlists ended up with the same color.

Image

I also tested the fallback colors:

Image

In light mode + selection (black tint):

Screen.Recording.2026-06-18.at.2.29.10.PM.mov

/// 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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It should be computer once and cached. It currently gets recomputed on every body invocation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 0b6c840playlistColor 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.

@kean

kean commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

I'd suggest to reduce the number of comments.

david-gonzalez-a8c and others added 3 commits June 19, 2026 15:36
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>
@david-gonzalez-a8c

Copy link
Copy Markdown
Contributor Author

@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.

@david-gonzalez-a8c

Copy link
Copy Markdown
Contributor Author

Review feedback addressed

  • PlaylistDetailsViewModel.swift:188 (@copilot): hoist fallback palette to a static let — Done in 7f2df70
  • PlaylistDetailsViewModel.swift:140 (@kean): cache playlistColor so it isn't recomputed on every body — Done in 0b6c840
  • General (@kean): reduce the number of code comments — Done in 8c3ad3d
  • Testing notes (@kean): darker tints / two playlists landing on the same fallback colour — acknowledged; deferred to a follow-up PR so this one stays scoped to the review fixes

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comment on lines +51 to +58
.sink { [weak self] notification in
guard
let self,
let uuid = notification.object as? String,
self.episodes.first?.podcastUuid == uuid
else { return }
self.refreshPlaylistColor()
}
Comment on lines +145 to +151
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 }
Comment on lines +151 to +157
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
))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants