Skip to content

feat(ui): full-screen artwork viewer for album cover art - #440

Open
tsibog wants to merge 6 commits into
sozercan:mainfrom
tsibog:feat/artwork-viewer
Open

feat(ui): full-screen artwork viewer for album cover art#440
tsibog wants to merge 6 commits into
sozercan:mainfrom
tsibog:feat/artwork-viewer

Conversation

@tsibog

@tsibog tsibog commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Description

Adds an artwork viewer accessible via the player bar thumbnail context menu (right-click > "View Artwork"). Shows the high-resolution album cover centered on a dark background. Dismiss with click, Escape, or the close button.

This keeps the existing click-to-navigate-to-album behavior on the thumbnail intact. The artwork viewer is a separate gesture (context menu) so both behaviors coexist.

AI Prompt (Optional)

🤖 AI Prompt Used
N/A - Manual implementation

AI Tool: Claude

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to change)
  • 📚 Documentation update
  • 🎨 UI/UX improvement
  • ♻️ Refactoring (no functional changes)
  • 🧪 Test update
  • 🔧 Build/CI configuration

Related Issues

Closes #432
Depends on #447 for artwork that survives track changes

Changes Made

  • New file: Sources/Kaset/Views/ArtworkViewer.swift — square artwork sheet with CachedAsyncImage, dark background, tap-to-dismiss, Escape support, and close button
  • ArtworkViewerSource: Resolves primary vs. fallback artwork, extracted so the selection logic is unit-testable (mirrors the existing SongThumbnailSource pattern)
  • PlayerBar.swift: Added @State showsArtworkViewer, .sheet presentation, and the "View Artwork" context menu item on the thumbnail view
  • Extensions.swift: Added URL.artworkURL(side:) to request full-size artwork
  • Localization: Added "View Artwork", "Album Artwork", "Artwork Unavailable", and "This track has no artwork available." to Localizable.xcstrings and all 17 .lproj mirrors
  • Tests: ArtworkViewerSourceTests for the fallback chain, plus ExtensionsTests coverage for the URL size rewriting

Fixes from manual testing

Three issues turned up while testing this on a packaged build:

1. Permanent spinner on some tracks. The viewer passed a bare URL to CachedAsyncImage with no fallback. CachedAsyncImage has no error branch — on failure it keeps showing placeholder(), so a failed load renders as a spinner that never resolves. Every other thumbnail surface avoids this by falling back to i.ytimg.com/vi/<id>/hqdefault.jpg, which is also why the small player-bar thumbnail still looked fine while the viewer hung. The viewer now uses the same fallback chain and shows an "Artwork Unavailable" state when both sources fail.

The underlying cause of those failures — currentTrack.thumbnailURL being set to the YouTube Music page URL — is a pre-existing bug on main and is fixed separately in #447.

2. Sheet was portrait, not square. maxWidth/maxHeight: .infinity gave AppKit no ideal size, so the sheet collapsed to a small box and resolved width and height independently. Album art is square, so the viewer is now explicitly square and scaled against the active screen (62% of the shorter dimension, clamped 380–700).

3. Artwork was upscaled. Art was fetched through highQualityThumbnailURL, which only lifts list thumbnails to 226px, so the viewer displayed a soft, upscaled image. URL.artworkURL(side:) rewrites the dimension tokens in YouTube's option suffix (…=w544-h544-l90-rj…=w1200-h1200-l90-rj) and the viewer decodes at its display size for a Retina-correct result. Hosts cap at the source resolution, so an oversized request degrades to the original artwork. highQualityThumbnailURL is untouched, so list thumbnails keep their cheap 226px fetches.

Failure handling keys on the pre-upgrade URL, so the size rewriting does not break the fallback chain.

Screencast

kaset-440-artwork-viewer.mp4

Testing

  • Unit tests pass (swift test --skip KasetUITests) — 2963 tests in 233 suites
  • Manual testing performed
  • UI tested on macOS 26+

Built and tested locally on macOS 26 (Apple Silicon) in a packaged build: opened the viewer across multiple tracks and after track changes, confirmed the artwork is sharp and the sheet is square, and exercised all three dismissal paths.

Checklist

  • My code follows the project's style guidelines
  • I have run swiftlint --strict && swiftformat . — both clean
  • I have added tests that prove my fix/feature works
  • New and existing unit tests pass locally
  • I have updated documentation if needed (localization catalog + mirrors updated)
  • I have checked for any performance implications
  • My changes generate no new warnings

Additional Notes

Uses Color.black.opacity(0.85) for the background instead of .ultraThinMaterial per AGENTS.md coding rules. The context menu approach avoids conflicting with the existing click-to-navigate-to-album behavior on the thumbnail.

This PR is functional on its own, but without #447 the artwork viewer will still show "Artwork Unavailable" on tracks whose thumbnail URL was corrupted by the observer script, since the fallback chain is the only thing rescuing them.

@tsibog

tsibog commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

tsibog added a commit to tsibog/kaset that referenced this pull request Aug 15, 2026
SwiftFormat lint flagged redundantViewBuilder on PlayerBar.swift:165.
The Group already provides a @ViewBuilder context, so the attribute
on the property is unnecessary.

Fixes SwiftFormat CI failure on PR sozercan#440.
tsibog added 6 commits August 17, 2026 14:10
Add ArtworkViewer, a full-screen overlay accessible via the player
bar thumbnail context menu (right-click > View Artwork). Shows the
high-resolution thumbnail centered on a dark background with tap-
to-dismiss and Escape support.

Closes sozercan#432
fullScreenCover is unavailable on macOS. Use .overlay with a
conditional ZStack and opacity transition instead.

Also remove .ultraThinMaterial from background (AGENTS.md
requires .glassEffect() for macOS 26+).
Claude Code review found .overlay constrained the viewer to PlayerBar's
60pt strip instead of full screen. .sheet provides proper full-screen
layout, Escape dismissal, and key focus. Also guard nil artwork URL.
SwiftFormat lint flagged redundantViewBuilder on PlayerBar.swift:165.
The Group already provides a @ViewBuilder context, so the attribute
on the property is unnecessary.

Fixes SwiftFormat CI failure on PR sozercan#440.
…s art

The viewer passed a bare artwork URL to CachedAsyncImage with no fallback and
no error branch, so any failed load left a spinner up forever. Every other
thumbnail surface falls back to the public video thumbnail via
SongThumbnailView; the viewer now does the same through ArtworkViewerSource,
and shows an unavailable state when both sources fail.

Sizing used maxWidth/maxHeight .infinity, which gave AppKit no ideal size and
produced a small portrait sheet. Album art is square, so the viewer is now
square and scaled against the active screen.

Artwork was fetched via highQualityThumbnailURL, which caps at 226px, so the
image was upscaled. Add URL.artworkURL(side:) to rewrite YouTube's dimension
options and request 1200px, decoding at the display size. Failure handling
keys on the pre-upgrade URL so the fallback chain still works.
@tsibog
tsibog force-pushed the feat/artwork-viewer branch from 645f29c to d796661 Compare August 17, 2026 14:11
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.

[Feature]: Click album artwork to view full-resolution cover art

1 participant