Commit Graph

68 Commits

Author SHA1 Message Date
joren
9f178a1cc3 feat: expand toolbar now-playing context menu to full track actions
- Replace bare "Go to Album"/"Go to Artist" with the full unified menu:
  Play next, Add to queue, Add to favorites, Open album/artist with
  names, Add to playlist submenu — all with icons
- Wire up new favTrackRequested/addToPlaylistRequested signals through
  MainWindow
- Pass user playlists to toolbar so the submenu is populated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 00:17:41 +02:00
joren
d1a2bed593 refactor: unify context menus across all views
- Add icons to all context menus (search panel, genre browser, queue panel)
  matching the track list's established pattern
- Standardize navigation wording: "Open album: X" / "Open artist: X"
  consistently (was "Go to" in search panel, bare "Open Album" in genre browser)
- Genre browser album menu: add "Add to favorites" and "Open artist: X"
  (was missing both)
- Genre browser playlist menu: add icon, lowercase "playlist" for consistency
- Queue panel menu: add list-remove and go-up icons
- Track list: add icon to "Remove from this playlist" action
- Search panel album menu: add icons for all actions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 11:24:50 +02:00
joren
92d48e459e Merge branch 'refactor/color-constants'
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 11:19:50 +02:00
joren
2139bbb726 Merge branch 'refactor/mainwindow-setup'
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 11:19:50 +02:00
joren
c2e0ff41ac Merge branch 'refactor/playqueue-split'
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 11:19:50 +02:00
joren
28771e12d5 fix: lazy-load genre/playlist views and cap playlist search scrolling
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
- Defer eager-load scrollbar checks to next event loop iteration via
  QTimer::singleShot(0), fixing initial load not filling the viewport
- Playlist search: eagerly fill viewport, then show "Load more playlists…"
  button instead of infinite scroll to avoid loading thousands of results
- Increase search page size from 8 to 25
- Featured/discover playlists keep auto-scroll behavior unchanged

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 11:17:23 +02:00
joren
d1b9cb1210 refactor: centralize hardcoded color values into Colors namespace
Add src/util/colors.hpp with named constants for all QColor values
(brand accents, badge colors, text shades, surface backgrounds) and
replace scattered QColor constructor calls across 7 source files.
Stylesheet string colors are intentionally left inline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 11:01:45 +02:00
joren
5f79170f48 refactor: split MainWindow constructor into focused setup methods
Extract the ~300-line constructor body into setupDocks(), setupScrobbler(),
setupGapless(), setupMpris(), connectBackendSignals(), connectLibrarySignals(),
connectContentSignals(), and connectToolbarSignals(). No behavioral changes;
all signal/slot connections and widget creation remain identical.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 11:00:19 +02:00
joren
dea16676ce refactor: split PlayQueue from header-only into .hpp/.cpp pair
Move all method implementations from playqueue.hpp into a new
playqueue.cpp, keeping only declarations and trivial inline getters
in the header. No logic or behavior changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 10:58:06 +02:00
joren
86b5673e8a refactor: reduce duplication and replace magic stack indices
- Extract parseTrackItem() static helper in TrackListModel to eliminate
  ~45 lines of duplicated JSON parsing between setTracks() and appendTracks()
- Extract notifyFavChanged() helper to deduplicate addFavId/removeFavId loops
- Add StackPage enum to MainContent replacing magic integers 0-5 with
  named constants (PageWelcome, PageTracks, PageAlbumList, etc.)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 10:48:21 +02:00
joren
e453f8acf3 feat: add seamless lazy loading for genre and playlist views
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
Introduce paged loading with early prefetch for genre albums/playlists and playlist tracks, while preserving full-data behavior for deep shuffle and playlist play-all actions.
2026-03-31 10:43:36 +02:00
joren
4ebd5ed3f0 fix: detect followed playlists correctly in header
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
Load owner+subscriber playlists, track all playlist IDs for follow-state resolution, and keep audio ring buffer at 32k as requested.
2026-03-31 02:02:31 +02:00
joren
1ad3ba4e69 feat: add album favorites in header and unify browse controls
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
Improve album workflow with in-header favorite toggle, keep favorite state synced from backend, and normalize browse top-bar sizing while increasing audio output buffer headroom to reduce glitches.
2026-03-31 01:49:53 +02:00
joren
cdac82dbef fix: sync dock toggles and make autoplay toolbar-only
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
2026-03-31 01:14:43 +02:00
joren
2aff8fda47 fix: stabilize seek slider and clean backend lint issues
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
2026-03-31 00:57:09 +02:00
joren
5673d6cd30 feat: add deep shuffle in genre browse and tighten toolbar layout
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
2026-03-31 00:37:00 +02:00
joren
96bb21adff feat: add playlist browse/search discovery and follow controls
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
2026-03-31 00:23:56 +02:00
joren
07d6c8a88d perf: reduce queue and autoplay memory footprint
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
2026-03-30 23:24:30 +02:00
joren
cfd91f96b5 feat: add autoplay suggestions when queue runs out
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
2026-03-30 23:14:43 +02:00
joren
2da934f3f6 feat: restore most-popular search and top results badges
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
2026-03-30 22:53:41 +02:00
joren
3346b424b3 refactor: resync with qbqt baseline and restore genre browser
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
2026-03-30 22:36:39 +02:00
joren
200ef39d04 feat: most-popular search as default Top Results tab
Add /most-popular/get endpoint support (discovered via Burp):
- Returns mixed artists/albums/tracks ordered by popularity for a query
- New 'Top Results' tab shown first/default in the search panel
- Badge column: A (artist, blue), H (hi-res album, gold), A (album, gray), T (track, green)
- Full context menu support on all item types in the new tab
- Both most-popular and regular search fire on submit in parallel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 20:28:13 +02:00
joren
70810bd4b1 feat: qbz-1 streaming, gapless prefetch, accurate scrobbling, Range-seek
Port proven playback architecture from qbqt fork:
- Bounded VecDeque buffer with condvar backpressure (4MB cap)
- decrypt_and_extract_frames for clean FLAC frame extraction from ISOBMFF
- Cancel+restart seeking with sub-segment sample skipping
- start_prefetch / QueueNext for gapless transitions with pre-started downloads
- track_transitioned signaling for scrobbler during gapless playback
- Range-request HTTP seeking for non-segmented (MP3) tracks
- OnceLock HTTP client singleton with cancel-aware chunked downloads
- Accumulated listening time scrobbling (prevents false scrobbles from seeking)
- Array-format Last.fm scrobble params (artist[0], track[0], etc.)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 20:12:23 +02:00
joren
6c8d032ce9 feat: context menus on search panel, track info dialog, separate shuffle buttons
- Add right-click context menus to search panel tracks (play, queue,
  favorites, playlist, go to album/artist, track info) and albums
  (open, favorite, go to artist)
- Add "Track info..." dialog showing metadata (title, performer,
  composer, album, quality, hi-res status) — available everywhere:
  playlists, albums, favorites, search results
- Split artist page shuffle into "Shuffle" (popular tracks) and
  "Shuffle All" (deep shuffle across all releases)
- Remove magnifying glass emoji from welcome text

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 23:57:42 +01:00
joren
a21d0c8a33 refactor: UI polish — lock sidebar, remove nav buttons, uniform artist tables, deep shuffle
- Lock sidebar width (setFixedWidth) so it doesn't jump between views
- Remove back/forward navigation buttons and all NavPage history code
- Uniform column layout on artist page: hide Artist column from both
  Popular Tracks and release sections, set matching fixed column widths
  so columns align vertically across all sections
- Deep shuffle: new Rust FFI endpoint fetches tracks from all albums
  in parallel, combines them, and returns via EV_DEEP_SHUFFLE_OK
- Auto-paginate artist releases in Rust (loop until has_more=false)
  so all releases load at once sorted newest-first

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 23:42:00 +01:00
joren
333a620be2 fix: section alignment, pagination, fav state, playback error handling
**Section toggles left-aligned**
- Replace QToolButton with flat QPushButton for all section headers;
  QPushButton properly respects text-align: left in stylesheets

**Pagination via "Load More" button**
- QTreeWidget expands to fit all items so the scrollbar-based infinite
  scroll never triggered; replaced with an explicit "Load more…" button
  that appears when has_more is true and emits loadMoreRequested

**Favourite button reflects actual state**
- MainWindow preloads fav artist IDs on session restore (getFavArtists)
  and caches them in m_favArtistIds
- ArtistView receives the full set via setFavArtistIds() and checks
  it on every setArtist() call so the button starts gold if already faved
- Toggling updates the local cache immediately for back/forward nav

**Playback error → queue advances**
- player_loop now sets track_finished on Err (was only set on Ok(None)),
  so the toolbar's onTrackFinished handler advances to the next track
  instead of stalling on an unplayable track

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 23:22:30 +01:00
joren
b3cc2e3def chore: gitignore build-viz/, remove accidentally committed build artifacts 2026-03-25 18:25:39 +01:00
joren
3e96b6d7a8 feat: pagination, back/forward nav, context menu, artist fav, image fix
**Artist portrait**
- Fix CDN URL: images.portrait.{hash,format} →
  https://static.qobuz.com/images/artists/covers/large/{hash}.{format}

**Section alignment**
- Qt::ToolButtonTextOnly on all section toggles so text is truly left-aligned

**Auth 401 race condition**
- qobuz_backend_set_token now uses blocking_lock() instead of spawning an
  async task, guaranteeing the token is set before any subsequent API call

**Pagination (infinite scroll)**
- Release sections load 50 at a time (was 500)
- ArtistSection tracks has_more + loaded count; scrolling to the bottom
  emits loadMoreRequested → ArtistView calls getArtistReleases(offset=N)
- AlbumListView gains addAlbums() for append; setReleases routes to
  setAlbums (offset=0) or appendAlbums (offset>0)

**Back/Forward navigation**
- MainToolBar exposes Back/Forward QActions (go-previous/go-next icons)
- MainWindow keeps a NavPage vector + index; pushNav() on every album/artist
  navigation; goBack/goForward re-navigate without pushing history

**Context menu on now-playing label**
- Right-click on track label in toolbar → "Go to Album" / "Go to Artist"
- MainToolBar stores current track; emits albumRequested/artistRequested
  signals wired to MainWindow's existing handlers

**Artist favourites button**
- ♡ Favourite / ♥ Favourited toggle in artist header
- Calls new addFavArtist / removeFavArtist (Rust + Qt backend wiring)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 18:25:12 +01:00
joren
963c9ad232 refactor: uniform artist page layout — header + collapsible sections
- Artist header now mirrors TrackContextHeader: 120×120 portrait with
  rounded rect background, large bold name, compact scrollable bio,
  Play/Shuffle buttons in the same row (gold #FFB232 style)
- Play/Shuffle moved out of Popular Tracks into the header area
- Popular Tracks becomes a collapsible toggle section identical in
  appearance to Albums/EPs/Live/Compilations/Other
- All section headers share kToggleStyle: left-aligned bold text,
  ▼/▶ arrow, subtle bottom border, hover highlight
- Portrait loading tries hash-based CDN URL first, falls back to
  image.large / image.small direct URL fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 18:03:47 +01:00
joren
8310eceeb2 feat: artist portrait, race condition fix, and uniform button styling
- Load artist portrait from images.portrait.hash via QNetworkAccessManager
- Fix race condition: fire getArtistReleases after setArtist() clears sections,
  not before (from onArtistLoaded instead of onSearchArtistSelected)
- Apply uniform gold (#FFB232) play/shuffle button style matching album view
- Make biography scrollable (QTextEdit with max height + scroll on overflow)
- Extend track artist name parsing to handle top_tracks {display:...} format

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 14:17:36 +01:00
joren
fb58c0ac8c fix: artist page top tracks and scrollable biography
- top_tracks is a flat array in the API response, not {items:[...]}
- Replace bio QLabel with scrollable QTextEdit (max 110px, scrolls if longer)
- Track model: handle artist.name as {display:...} object for top_tracks format

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 14:02:26 +01:00
joren
e37de6d897 fix: correct top_tracks field name and remove unused extra param
Qobuz API uses snake_case so the field is top_tracks, not topTracks.
Also remove the extra=topTracks query param — top tracks are included
in the default artist/page response without it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 13:58:51 +01:00
joren
5ae18afa08 feat: full artist release list via artist/getReleasesList
Instead of relying on the limited preview in artist/page, fire a
separate artist/getReleasesList request per release type (album,
epSingle, live, compilation) in parallel when loading an artist.
Each result updates its section independently as it arrives, so the
page populates progressively without a single large request.

Also fixes the artist name in the status bar (was reading wrong field).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 13:53:57 +01:00
joren
6f11b364aa feat: show top tracks on artist profile with play/shuffle
- Adds extra=topTracks to artist/page API request
- Embeds a List::Tracks widget at the top of ArtistView showing
  the artist's most popular tracks, with Play and Shuffle buttons
- Bubbles playTrackRequested through MainContent up to MainWindow
- Also adds the viz PCM ring buffer FFI infrastructure (for future
  spectrum widget) to the Rust backend

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 13:45:19 +01:00
joren
4ba6d00748 fix: artistview nullptr connect warning + album version in header
- Move updateToggleText() after m_list creation to avoid null connects
- Show version (e.g. "Deluxe") in album header title

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 23:55:12 +01:00
joren
5bda2396d1 fix: security hardening and code quality improvements
Build hardening:
- Add -fstack-protector-strong, -D_FORTIFY_SOURCE=2, PIE, full RELRO
- Enable overflow-checks in Rust release profile

Rust backend:
- Return null (not panic) if Tokio runtime or QobuzClient init fails
- Strip null bytes in FFI JSON callback to prevent CString panics
- Document MD5 and password-in-query as Qobuz API constraints

C++ frontend:
- Validate JSON document before accessing fields in onEvent()
- Handle null backend pointer from failed init
- Set biography label to PlainText and decode HTML entities to prevent
  rendering injected content from API responses
- Clamp slider position and guard negative durations
- Use qint64 for duration formatting to avoid int truncation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 23:19:04 +01:00
joren
872fdecdce feat: artist/page endpoint, multi-disc fix, playlist ownership, UX improvements
- Switch artist view to artist/page API (proper sections: Albums, Singles & EPs,
  Live, Compilations; version in titles like "Deluxe")
- Fix artist sections categorization using releases[].type from artist/page
- Add getUser() backend call; fetch on session restore when userId=0 to fix
  playlist ownership (Remove from playlist / Delete playlist were missing)
- Fix multi-disc double-click / Play Now queue start index (disc headers were
  counted in row index but excluded from currentTracksJson)
- Hide redundant Album column when viewing an album
- Make artist name in context header clickable (navigates to artist page)
- Fix gap between title and artist name in context header (addStretch)
- Fix queue panel track titles to include version field
- Fix album list to show version in title column

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 23:09:04 +01:00
joren
56473cae6f feat: artist sections, fav indicator, art scaling fix, volume popup fix
- Artist profile: collapsible Albums / EPs & Singles / Other sections
  keyed on release_type; fetches up to 200 albums per artist
- Favorites: starred icon on favorited tracks, context menu shows
  Add or Remove (not both); IDs cached when fav tracks are loaded
- Shuffle button: one-time shuffle via shuffleNow() without touching
  global shuffle flag, so double-click still plays in order
- Now-playing art: replaced setFixedHeight hack with ArtWidget that
  overrides hasHeightForWidth() — scales smoothly up and down, no min-size
- Volume popup: replaced QMenu (laggy, broken drag) with Qt::Popup QFrame;
  appears below button; fixed size locked at 100% label width

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 17:56:47 +01:00
joren
75429faffe feat: playlist art fix + Play/Shuffle buttons in context header
- Fix playlist art: API returns images/images150/images300 arrays, not
  image_rectangle; update Rust model and Qt header to use images300 →
  images150 → images with fallback
- Add Play (▶) and Shuffle (⇄) buttons to the album/playlist header;
  Play starts from track 1, Shuffle enables shuffle mode and plays from
  a randomised position

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 17:20:00 +01:00
joren
9327147021 fix: art scaling on shrink + skip unavailable tracks in queue
- Now-playing art: use setFixedHeight (not setFixedSize) so the dock
  width constraint is removed and can shrink freely; use event->size()
  in resizeEvent to get the correct new width without layout lag
- PlayQueue: filter non-streamable tracks out of setContext and
  reorderContext so unavailable tracks are never in the queue and
  pressing Next always lands on a playable track

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 17:15:49 +01:00
joren
9ca17b4406 feat: now-playing album art scales to full dock width
Stores the raw downloaded pixmap and rescales it (smooth, aspect-ratio
preserved) whenever the dock is resized, so the image always fills
the available width.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 11:46:43 +01:00
joren
7b0e5dcfa8 feat: show resolution in album header (e.g. 24-bit / 96 kHz)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 11:44:35 +01:00
joren
1e4c234b5c fix: gapless toggle now actually controls audio output lifecycle
When gapless is off, the AudioOutput is dropped after each track ends
naturally, producing a real gap on the next play. When on, the output
stays alive so tracks transition seamlessly. Also re-adds URL prefetch
gating behind the same toggle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 11:39:24 +01:00
joren
c035ce2dee fix: remove gapless toggle — architecture is always gapless
The player keeps the audio output alive between tracks unconditionally.
The toggle only controlled URL prefetching, not actual audio gaplessness.
Remove the setting and always prefetch the next track URL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 11:36:54 +01:00
joren
8088412d4b feat: album/playlist header and playlist ownership filtering
- Add TrackContextHeader widget: shows album art (fetched via NAM),
  title, subtitle (artist/description), and metadata (year · tracks · duration)
  above the track list when an album or playlist is opened
- Hide header for favorite tracks and search results
- Store user ID in AppSettings on login
- Only show "Delete playlist" for playlists the user owns
- "Add to playlist" submenu only lists owned playlists
- "Remove from this playlist" only appears when viewing an owned playlist

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 11:34:04 +01:00
joren
75e2b623b8 feat: refresh views on all playlist mutations
- Add EV_PLAYLIST_TRACK_ADDED (22) — emitted when a track is
  successfully added to a playlist
- If the currently-open playlist is the one modified, re-fetch it
  so the track appears in the list immediately
- After creating a playlist, open it automatically so the user
  lands in the new (empty) playlist view right away
- Sidebar already refreshes on create/delete; this ensures the
  track list view also stays in sync

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 11:23:45 +01:00
joren
f30e2058c1 feat: playlist management, gapless playback, ReplayGain, Qobuz theme
Playlist management:
- Add/remove tracks from playlists via right-click context menu
- Create new playlists (right-click Playlists sidebar header)
- Delete playlists with confirmation dialog (right-click playlist item)
- Playlist view removes track immediately on delete (optimistic)
- Deleting currently-open playlist clears the track view

Gapless playback:
- Single long-running audio thread owns AudioOutput; CPAL stream stays
  open between tracks eliminating device teardown/startup gap
- Decode runs inline on the audio thread; command channel polled via
  try_recv() so Pause/Resume/Seek/Stop/Play all work without spawning
- New Play command arriving mid-decode is handled immediately,
  reusing the same audio output for zero-gap transition
- Position timer reduced from 500 ms to 50 ms for faster track-end detection
- URL/metadata prefetch: when gapless is enabled Qt pre-fetches the next
  track while the current one is still playing

ReplayGain:
- Toggled in Settings → Playback
- replaygain_track_gain (dB) from track audio_info converted to linear
  gain factor and applied per-sample alongside volume

Qobuz dark theme:
- Background #191919, base #141414, accent #FFB232 (yellow-orange)
- Selection highlight, slider fill, scrollbar hover all use #FFB232
- Links use Qobuz blue #46B3EE
- Hi-res H badges updated to #FFB232 (from #FFD700)
- Now-playing row uses #FFB232 (was Spotify green)
- QSS stylesheet for scrollbars, menus, inputs, buttons, groups

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 11:19:32 +01:00
joren
c8086e1f2f fix: single-line queue items — Title · Artist on left, duration right
Compact single row: title in medium weight, separator dot and artist
name dimmed, duration right-aligned. Height scales with font.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 10:37:17 +01:00
joren
6f4029c4bb fix: compact queue item height based on font metrics
Height is now 2 line-heights + 10px padding instead of a fixed 52px,
keeping the two-line layout without wasting vertical space.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 10:34:34 +01:00
joren
d8619d9efe feat: custom queue item delegate with two-line layout
Each queue item now shows:
- Track title (medium weight) top-left
- Duration (small, dimmed) top-right
- Artist name (small, dimmed) bottom-left

Replaces the flat "Artist — Track" single-line text.
Play-next items get a slightly brighter title tint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 10:32:17 +01:00