Commit Graph
147 Commits
Author SHA1 Message Date
vicrodh a5a6f0dcac feat(titlebar): system mode now mounts stripped strip when content is enabled
Plasma and system mode become structurally symmetric — both render OS
chrome on top (KWin SSD via Xwayland for plasma, GTK CSD / WM SSD for
system) plus an optional stripped QBZ strip below for search and nav.

Same content-driven rule applies to both: the strip mounts only if the
user has search-in-titlebar enabled OR any titlebar-nav item enabled.
With both off, the user sees just the bare OS chrome, matching the
previous system-mode behavior.

Effective rules table:
- showWindowControls / matchSystemChrome: user pref in qbz only
- searchBarLocation / titlebarNav: user pref in qbz/plasma/system,
  forced/n-a only in hidden

i18n disabledHint strings updated to reflect the new rules across all
five locales.

cc
2026-05-05 01:50:15 -06:00
vicrodh e4835b6c55 fix(i18n): move titlebar block from settings.appearance to settings root
The Task 9 SettingsView consumer reads settings.titlebar.* directly
(see lines 1030, 1037, 4666, 4678 of SettingsView.svelte). The original
Task 11 commit placed the block under settings.appearance.titlebar
which would not resolve. Moving the block one level up.

cc
2026-05-03 20:30:25 -06:00
vicrodh 4e854072fa i18n(titlebar): add mode labels, disabled hints, sandbox caveat
Five locale files updated in lockstep: en, es, de, fr, pt. Mexican
Spanish dialect for es.json per project convention.

cc
2026-05-03 20:26:02 -06:00
vicrodh 773fa390eb i18n(settings): make qconnect startup-mode title explicit
"Auto-connect on startup" → "Auto-connect Qobuz Connect on startup"
(and equivalents in es/de/fr/pt). The earlier wording was ambiguous —
it could read as auto-connecting QBZ itself, not the QConnect feature.

cc
2026-05-01 16:01:51 -06:00
vicrodh 74775aaebf Merge branch 'feature/internal/qconnect-startup-persistence' into pre-release 2026-05-01 14:25:59 -06:00
vicrodh aa0f3c97eb i18n(settings): add qconnect startup mode strings (en/es/de/fr/pt)
Six new keys under settings.integrations.qconnect.startupMode covering
the dropdown labels, description, and local-library compatibility note.

cc
2026-05-01 13:34:11 -06:00
vicrodh c6fb25275e poc(about): add Kawarp attribution
Adds @kawarp/core (MIT, by better-lyrics) to the About modal's
"Built with" section, in the same Lucide-anchored slot. New i18n
key about.attributions.kawarpDesc landed in all five locales.

cc
2026-04-29 09:03:36 -06:00
vicrodh 3ab83e6224 poc(immersive): retire standalone KawarpPanel — Static now uses kawarp via ImmersiveBackground
After the central refactor in c106928a, ImmersiveBackground in 'full'
mode renders kawarp directly. Static (and every other panel that
consumes ImmersiveBackground — Coverflow, Lyrics, Queue,
AlbumReactive, TrackInfo, Suggestions, the split modes) now inherits
the kawarp look automatically.

The standalone KawarpPanel was a temporary A/B vehicle for the POC
benchmarks; with the new architecture it's redundant and would only
confuse the panel selector.

Removed:
  - src/lib/components/immersive/panels/KawarpPanel.svelte
  - 'kawarp' from FocusTab and ImmersiveViewKey type unions
  - Menu entry in ImmersiveHeader
  - Routing branch in ImmersivePlayer
  - i18n key in en/es/de/fr/pt
  - 'kawarp' from the bg-skip exclusion list (no longer needed —
    Static no longer has its own canvas, it uses ImmersiveBackground)

cc
2026-04-29 02:58:47 -06:00
vicrodh 0373b9c1fc poc(immersive): add KawarpPanel as exact duplicate of StaticPanel
Baseline checkpoint for the kawarp benchmark. KawarpPanel is currently
identical to StaticPanel (same template, same CSS, same animation set
— just an equalizer on the title row when playing). Wired into the
immersive panel selector so it can be picked side-by-side with Static
for A/B measurement.

Subsequent commits will introduce the kawarp WebGL background only
inside this panel; if the GPU/CPU/RAM delta is unjustified the panel
gets removed without touching StaticPanel.

cc
2026-04-29 01:59:29 -06:00
vicrodh 17730e0233 fix(tray): icon variant picker with Color option and mono labels
Three issues with the 1.2.9-pre tray icon override:

  - The mono PNGs filled the entire canvas, so the tray icon read as
    larger than its panel neighbours (Plasma + GNOME).
  - The dropdown labels were inverted: picking "Light icon" produced
    the dark glyph and vice-versa, because the constants were named
    after the panel they suit ("light panel" -> dark glyph), not after
    the glyph colour the user expects to see.
  - There was no way to pick the colour vinyl logo even on desktops
    where a monochromatic glyph clashes with the rest of the tray.

Rebuild the mono pixmaps (22/32/44/64) from icon-symbolic.svg with a
2px content inset; render a new colour set from icon.svg at the same
sizes; rename the Rust constants to glyph-colour names and introduce
an IconVariant enum so the UI label, the persisted value, and the
file selection all line up.

Dropdown options now read Auto / Mono light / Mono dark / Color in
all five locales. normalize_tray_icon_theme on the Rust side and
normaliseTrayTheme in SettingsView migrate the inverted "light" /
"dark" values stored by 1.2.9-pre to the user-intent equivalents.

cc
2026-04-28 20:29:49 -06:00
vicrodh 388a150893 feat(settings): tray icon theme dropdown for Linux
Adds an Auto / Light icon / Dark icon dropdown next to the existing
tray toggles, hidden on macOS. Wires v2_set_tray_icon_theme so the
choice persists and the live SNI tray reflects it instantly.

i18n keys settings.appearance.tray.iconTheme.* added to en/es/de/
fr/pt.

cc
2026-04-28 19:02:50 -06:00
vicrodh 32c7cbd2ff fix(queue): use ICU plural for tracksRemoved toast
Per code review on commit cdf30a7b: the tracksRemoved key was
hardcoded plural ("1 tracks removed" reads wrong). Convert to ICU
plural form across all 5 locales using svelte-i18n's MessageFormat
support. Also align the new handlers with the existing closeMenu()
pattern for consistency.

Refs #356.

cc
2026-04-28 14:31:35 -06:00
vicrodh 813e4dd76e fix(i18n): restore player.queue string and put new keys in top-level queue namespace
Commit e40587e1 converted player.queue from a string to an object to
accommodate the new stop-after / remove-after keys. That broke five
call sites that read $t('player.queue') expecting the localized
"Queue" string (QueuePanel, ImmersiveHeader, ExpandedPlayer,
NowPlayingBar, immersive QueuePanel).

Restore player.queue to its original string form in all 5 locales,
and add a new top-level queue namespace for the four action keys
(stopAfterThis, cancelStopAfter, removeAllAfter, tracksRemoved).
This matches the plan's intended key paths: queue.stopAfterThis etc.

Refs #356.

cc
2026-04-28 14:23:01 -06:00
vicrodh e40587e1cb i18n(queue): add stop-after and remove-after keys to all 5 locales
en.json + es.json + de.json + fr.json + pt.json.

Refs #356.

cc
2026-04-28 14:16:18 -06:00
vicrodh 5bfb748876 feat(session): opt-in resume playback position on session restore (#317)
Adds a separate "Resume playback position" toggle in Settings >
Playback. Default is OFF — opening qbz the next morning lands on a
clean 0:00 of the saved track, which is the common preference. Users
who do want bookmark-style resume turn the toggle on.

Independent of "Restore session on startup" in the UI (top-level
toggle, not a sub-setting). The seek only applies when both are on:
without persistSession there's no saved track to resume to, so the
flag is inert. Keeping the toggles separate matches the Settings UX
elsewhere and lets users flip persistence on without inheriting the
old #317 behavior we never had.

Backend
-------
- New `PlaybackPreferences::resume_playback_position: bool` (default
  false). Migration adds the column to the existing
  `playback_preferences.db` and the default row gets 0; existing
  installs upgrade transparently.
- Setter `set_resume_playback_position(bool)` on both
  `PlaybackPreferencesStore` and the wrapping `State`.
- Tauri command `v2_set_resume_playback_position` registered in
  lib.rs invoke handler. Get-side already reads the new field via
  the existing `v2_get_playback_preferences`.

Frontend
--------
- `playbackPreferencesStore.ts` — new field on the type and cache,
  new `setResumePlaybackPosition()` exported.
- `SettingsView.svelte` — separate toggle row right below
  "Restore session on startup" (matches the existing Settings rhythm).
- `playerStore.ts` — `pendingSessionRestore` extended with optional
  `positionSecs`. When present, after the restored track loads, the
  position is queued through the existing `pendingSeekPosition` /
  `flushPendingSeek` path so the seek runs against an already-loaded
  stream and respects the in-flight guard. Skip the queue when the
  saved position is at or near 0 (the load already starts there).
- `routes/+page.svelte` — at session restore, reads
  `getCachedPreferences().resume_playback_position` and forwards
  `session.current_position_secs` only when the user opted in. The
  field has been persisted in the schema since v1.2.5 — what changes
  is whether we read it back.

i18n
----
- New keys in 5 locales: `settings.playback.resumePlaybackPosition` /
  `…Desc`.
- `persistSessionDesc` updated everywhere — it used to claim the
  feature restored playback position, which was always misleading
  (the code restored the track but paused at 0:00). The text now
  matches what the toggle does on its own: queue + view, paused at
  0:00. Resume-to-position becomes the explicit ask of the new toggle.

Tests
-----
- Backend: 196 tests pass. Frontend: 0 TS errors.

cc
2026-04-27 21:29:29 -06:00
vicrodh 603c78cadd fix(ui): rebuild TrackMixModal on shared Modal + Dropdown + .btn classes
Replaces the first-pass implementation that rolled its own backdrop and
pill-style size buttons. The pills had two problems: pills are forbidden
by the project design rules (qbz-nix-docs/design/FIGMA_PROMPT.md), and
the custom modal centered itself with `transform: translate(-50%, -50%)`
which created a containing block for `position: fixed` descendants —
that broke `Dropdown.svelte`'s portaled menu, which landed in the
bottom-right corner of the screen instead of below the trigger.

Now wraps `Modal.svelte` (portaled to body, flexbox-centered, ESC
built in), uses `Dropdown.svelte` for the size picker, and follows
the project convention of `<div class="modal-actions">` + the global
`.btn .btn-primary` / `.btn .btn-secondary` classes for the footer.

Copy is reworked to read like a sentence: title "Random queue", body
"Create a random queue with songs from this collection. How many songs
do you want?", confirm button "Add to queue". Synced across en/es/de/fr/pt.
Drops the now-unused `mixModal.subtitle` key.

Also removes the duplicated mixModalOpen entry from the view's
keydown ESC chain — `Modal.svelte` already handles ESC.

cc
2026-04-25 13:05:42 -06:00
vicrodh 48f4d130e4 feat(ui): add TrackMixModal with size picker + i18n keys
- TrackMixModal.svelte renders a centered radiogroup of queue sizes
  (50, 100, 150, …, "All (N)") backed by buildSizeOptions. Fetches the
  unique track count via v2_collection_unique_track_count when first
  opened for a collection and caches it for subsequent opens of the
  same collection.
- buildSizeOptions is a pure helper extracted to a separate file so it
  can be unit-tested cleanly. 8 vitest cases cover the empty/null,
  small, exact-50, multiple-of-50, and irregular paths.
- Adds 8 new i18n keys (common.shuffleTracksMix, mixModal.title /
  computing / subtitle / allOption / confirm / loadFailed,
  toast.mixTrimmed) across all 5 locale files.
- z-index 3000/3001 per ADR-003.

cc
2026-04-25 06:24:09 -06:00
vicrodh 4105d597a8 fix(i18n): add missing toast.playingAlbum key across all locales
Two $t('toast.playingAlbum', { values: { count } }) call sites in
+page.svelte (collection/mixtape detail play paths) were printing the
raw key because the translation was never added. The `|| fallback`
idiom after each call never fired either — $t() returns the key
itself on miss, which is truthy.

Add an ICU plural entry covering both callers: count=1 (detail-view
play button on an album) reads as "Playing album", count>1 (bulk
play-all) reads as "Playing {count} tracks". Mirrored across en, es,
de, fr, pt.

cc
2026-04-24 16:22:09 -06:00
vicrodh 0292c8650d feat(themes): add Stratego (imperial crimson + bone white, AAA contrast)
Dark theme inspired by a samurai mask + war mandala. Palette pulled
from the source image: near-black canvas, bone-white ivory for text
(matches the mask teeth and kanji), imperial crimson as accent, antique
gold for warnings (dragon crest horns), steel-grey for borders.

Contrast verified AAA on the critical pairs:
- accent-primary #a0161d on btn-primary-text #fcfaf2 -> 7.55:1
- text-primary #ece6d6 on bg-primary #0a0a0b -> 15.7:1
- text-secondary #b5afa0 on bg-primary -> 8.7:1

Registered in SettingsView picker next to Iscariot, translations added
to all five locales (proper name, not localized).

cc
2026-04-21 21:08:30 -06:00
vicrodh 24e224e802 i18n: align Customize Home labels with the new section titles + raise default
Two things:

1. home.recentAlbums and home.continueTracks (the labels shown in the
   "Customize Home" modal) used to read "Recently Played" / "Continue
   Listening". They now read "Recently Played Albums" / "Recently
   Played Tracks" so the modal labels match the section titles that
   were renamed earlier.

2. Default limit for continueTracks (now Recently Played Tracks)
   changes from 10 → 24. The new grid carousel renders 4×3 = 12
   visible cards and can scroll to one more page; 24 fills the two
   pages exactly.

i18n updated across all 5 locales.

cc
2026-04-21 10:59:53 -06:00
vicrodh 6d8f0f4011 i18n: clarify Home / For You section titles
Renames two home-tab / for-you-tab section titles so their contents
aren't ambiguous once they sit next to each other on the same screen:

  * home.continueListening:  "Continue Listening" → "Recently Played Tracks"
  * home.recentlyPlayed:     "Recently Played"    → "Recently Played Albums"

Both tabs (Home and For You) read the same keys, so a single change
covers both surfaces. Translated across all 5 locales (en/es/de/fr/pt).

cc
2026-04-21 10:40:59 -06:00
vicrodh e98bfd62d8 feat(settings): opt-out toggle for sidebar playlist cover collage
Adds Settings > Appearance > "Show playlist cover collages in sidebar"
(on by default, persisted to localStorage as
qbz-sidebar-playlist-collage).

When off, playlist rows in the sidebar fall back to the original
ListMusic icon at its natural 14×14 size — same row alignment as
every other nav item. When on, the 2×2 collage renders as before.
Toggle is reactive via a new subscribePlaylistCollage listener in
sidebarStore, so flipping it applies immediately without a refresh.

Intended for low-end machines where the four extra <img> tags per
visible playlist row (even at _50.jpg, even gated by virtual scroll
and loading="lazy") are still measurable overhead.

i18n: added keys to all 5 locales (en/es/de/fr/pt).

cc
2026-04-21 09:35:31 -06:00
vicrodh 739d5b0f7e feat(collections): index toolbar (search/sort/filter/view) + i18n
Root Collections and Mixtapes pages had only a title + New button + a
grid of cards. Added the same .list-controls vocabulary the detail view
uses so users see a consistent control surface across the feature.

- Search: matches name + description.
- Sort dropdown with asc/desc toggle on re-click: Position (manual),
  Name, Items, Recently updated.
- Kind filter on CollectionsView only (All / Collections / Artist
  Collections). MixtapesView omits it — one kind means the dropdown
  would be noise.
- View-mode segmented toggle (list / grid). Grid keeps the existing
  208px cards; list is a dense row with 48px mosaic cover + name +
  kind label + count.
- Empty-results state when search/filter returns zero.
- Preferences persisted globally (per user) under
  collections-index-prefs / mixtapes-index-prefs — viewMode, sortBy,
  sortDir, kindFilter round-trip across sessions. searchQuery stays
  transient per-session.

i18n (en / es-MX / de / fr / pt):
- collections.searchPlaceholder / sortByItems / sortByUpdated /
  kindFilterAll / kindFilterCollection / kindFilterArtistCollection
- mixtapes.searchPlaceholder
- nav.myQbz (used by the upcoming title bar entry)

cc
2026-04-20 17:48:49 -06:00
vicrodh 93eef97b4b feat(mixtape): Collection/Mixtape detail view rewrite
Full rework of MixtapeCollectionDetailView. Groups of changes:

Play + UX correctness
  - Play / Shuffle actually start audio: after enqueueCollection fires,
    fetch the first queue track via playQueueIndex(0) and hand off to
    playTrack() so the audio pipeline kicks in (bridge.play_index alone
    only updates queue state, never triggers playback).
  - Loading spinner swaps Play / Shuffle icons for LoaderCircle while
    the enqueue + track load is in flight — covers the noticeable
    delay on large collections.
  - Canonical scroll pattern (width/height/overflow-y auto).
  - Detail-view header Play / Shuffle / More buttons now use the
    global .action-btn-circle family instead of bespoke pill styles.

Item row chrome
  - Source indicators: canonical monochrome mask pattern
    (plex-mono.svg / hdd.svg / qobuz-logo.svg) tinted --accent-primary.
  - Quality column uses the full QualityBadge component.
  - Live-resolve per item: fetch v2_library_get_albums +
    v2_plex_cache_get_albums once on load so each row knows its true
    kind (plex vs qobuz_download vs qobuz_purchase vs user) and can
    show Plex album art via buildPlexArtworkUrl.
  - Qobuz items fetch v2_get_album in parallel to populate quality
    data (Qobuz-streaming items don't persist bit-depth / sample-rate
    at add-time).
  - Clickable title → onOpenItem; clickable artist subtitle (Qobuz
    only) → onOpenArtist runtime-resolves via search.
  - Row hover reveals a play overlay on the artwork.

Row context menu (⋯)
  - Play / Play Next / Add to queue entries.
  - Change release type → nested submenu with Album / EP / Single /
    Live / Compilation and "Use auto-detected" reset. Uses the
    shared releaseTypeOverridesStore so the override also shows in
    DiscographyBuilder and vice-versa.
  - Type column shows the effective release type for album items.

Controls row
  - Search (album title / subtitle + track titles when expanded).
  - Sort (Position / Name / Year / Tracks, asc/desc toggle).
  - Filter with Type + Source sections (multi-select Source).
  - Reset clears every filter + sort.
  - Select mode toggle (SquareCheckBig).
  - View-mode segmented group (List / Grid / Expanded).

View modes
  - List: canonical row listing (no chevron, no inline tracks).
  - Grid: AlbumCard-like tiles with corner-checkbox selection.
  - Expanded: auto-loads every album's tracks via v2_get_album
    (Qobuz), v2_library_get_album_tracks (local — with title+artist
    re-resolve fallback for re-indexed libraries) or
    v2_plex_cache_get_album_tracks (Plex album_key prefix). Tracks
    render as real TrackRow instances.

Selection + BulkActionBar
  - Selected positions tracked separately from view-mode.
  - BulkActionBar below the list with Play Next, Add to Queue, Add
    to Playlist, Add to Mixtape/Collection, Remove from Collection.

Custom cover + header actions
  - Hero mosaic + grid tiles honor the user-uploaded cover.
  - Header ⋯ menu: Upload cover / Clear custom cover.

i18n
  - collectionDetail.* keys across all 5 locales: itemActions,
    changeReleaseType, removeItem, cover{Uploaded,UploadFailed,
    Removed,RemoveFailed}, selectItem, sort, filter, sortBy*,
    filterAll, viewMode, view{List,Grid,Expanded}, searchPlaceholder.

cc
2026-04-19 22:47:05 -06:00
vicrodh 578a934c20 i18n: DiscographyBuilder column labels + release types + track count
Keys added to all 5 locales (en/es/de/fr/pt):
- discographyBuilder.col{Year,Type,Album,Tracks,Source,Quality}
- discographyBuilder.releaseType.{album,ep,single,live,compilation}
- discographyBuilder.{checkAll,uncheckAll,typeOverrideHint,typeOverrideReset}
- common.trackCount (reused across DiscographyBuilder + Collection
  detail tracks column tooltip)

Mexican Spanish convention for es.json.

cc
2026-04-19 22:31:24 -06:00
vicrodh 2460467e48 feat(mixtape): DiscographyBuilderView for Artist Collections
Cross-source artist discography builder. Fetches the artist's Qobuz
releases (via v2_get_artist_page) and LocalLibrary albums by artist
name (via v2_library_get_albums filtered client-side), normalizes
titles to group variants of the same album, ranks each group by
quality_score (bit_depth × 10M + sample_rate_hz + format_bonus), and
renders a primary row + indented alternate rows per group.

Compilations detected heuristically (best of / greatest hits /
anthology / ...) and unchecked by default. Default selection: primary
of each non-compilation group. User can check alternates to include
them too. Order by release date / title / manual (preserves input
order). Submit creates an artist_collection with
source_type='artist_discography' + source_ref=artistId and enqueues
items in display order.

ArtistDetailView gains a "Build Artist Collection" action button
(LibraryBig icon, secondary style, rendered only when callback prop is
present). Entry point from CollectionsView left as-is (artist picker
deferred). Added 'discography-builder' to ViewType union.

Toast keys toast.collectionCreated / toast.collectionCreateFailed
added to all five locale files (en, es, de, fr, pt).

cc
2026-04-18 20:57:02 -06:00
vicrodh 9d699ffe41 feat(mixtape): AddToMixtapeModal picker
Shared modal for "Add to Mixtape/Collection" entry points. Lists the
user's collections sorted by recency, with a kind icon + name + item
count + kind tag per row. Clicking a row calls addItem; dedup toast
when backend returns false. "+ Mixtape" / "+ Collection" footer opens
an inline create-panel that creates then adds the current item in one
shot. Search filters by name (case-insensitive substring). ESC closes
the create-panel first, then the modal.

Adds a single new i18n key (common.addedToMixtapeOrCollection) across
5 locales for the success toast.

cc
2026-04-18 20:25:19 -06:00
vicrodh 17e2f711e3 refactor(ui): rename user-facing "Favorites" to "Library"
Aligns with Qobuz's own Library rename in their mobile and desktop
beta apps. Command names, store names, i18n keys, file names, and
internal route identifiers are all unchanged — this is strictly a
user-visible text + icon rewrite.

- Sidebar / TitleBar nav Favorites entry uses /static/music-library.svg
  instead of the lucide Heart
- Artist and label favorite buttons use /static/user-add.svg (albums
  and tracks keep the Heart affordance)
- 5 locale files: all values containing "Favorites" / "Favoritos" /
  "Favoriten" / "Favoris" / "Favoritos" rewritten to "Library" and
  locale-appropriate equivalents (Biblioteca / Bibliothek /
  Bibliothèque / biblioteca)
- "Add to Favorites" / "Remove from Favorites" buttons and toasts
  renamed to "Add to Library" / "Remove from Library"
- Hardcoded title= tooltip strings on track Heart buttons replaced
  with $t() calls

cc
2026-04-18 19:50:28 -06:00
vicrodh bcd07cf084 feat(i18n): Mixtapes and Collections keys across 5 locales
Adds mixtapes / collections / itemType / discographyBuilder /
collectionDetail namespaces, plus 6 common.* entries. Mexican
Spanish per project convention for es.json.

cc
2026-04-18 19:00:26 -06:00
vicrodh c2da9512ff refactor(ui): extract SourceBadge component from AlbumCard
Zero visual change. The Mixtape/Collection detail view will import the
same component. Adds qobuz_streaming as a new value alongside the four
existing LocalLibrary values; AlbumCard's own prop type is unchanged.

cc
2026-04-18 17:13:29 -06:00
vicrodh b62ffc730a refactor(home): rename yourMixes to qobuzMixes
Disambiguates Qobuz's auto-generated "Your Mixes" section from the
upcoming user-owned Mixtapes feature (spec
qbz-nix-docs/specs/2026-04-18-mixtapes-collections-design.md).

- Renamed HomeSectionId type union entry from 'yourMixes' to 'qobuzMixes'
- Updated DEFAULT_SECTIONS in homeSettingsStore.ts
- Renamed translation keys in all 5 locale files (en, es, de, fr, pt)
- Updated section labels to "Qobuz Mixes", "Mixes de Qobuz", etc.
- Updated all component references in HomeView, DynamicSuggestView, FavQView,
  ForYouTab, TopQView, and WeeklySuggestView

cc
2026-04-18 16:53:06 -06:00
vicrodh 6b91bd3872 feat(lyrics): active-line color picker, uppercase toggle, bigger L/XL
Add two persisted prefs to the lyrics sidebar display controls:

- activeColor: hex color override for the karaoke/active-line accent.
  Empty string means follow the theme's --accent-primary. Rendered via
  a new --lyrics-active-color CSS custom property set inline on the
  LyricsLines container; all four references to --accent-primary in
  the active-line gradient and glow shadows now fall back through it.
  Popover row shows a native color input plus a 'Theme' clear button
  that appears only when a custom color is set.

- uppercase: render all lines as uppercase in the sidebar. Applied via
  text-transform on .lyrics-lines.compact.uppercase. Switch toggle in
  the popover.

Bump L and XL sidebar sizes by the requested amount:
- L: 17/20 -> 18/21
- XL: 20/24 -> 22/26
(small and medium are unchanged.)

Both new prefs persist per-user in the same localStorage payload via
the existing sanitize/reload flow, and the hex validator rejects
malformed values on read.

cc
2026-04-18 12:41:34 -06:00
vicrodh 539bc72d85 feat(lyrics): add copy-to-clipboard button in sidebar header
A Copy icon button to the left of the Display settings trigger writes
the current track's lyrics (joined with newlines) to the clipboard via
the Tauri clipboard-manager plugin. The icon briefly swaps to a Check
for 1.5s on success, and a success/error toast is shown. The button is
disabled while lyrics are loading, when an error is displayed, or when
no lyrics are available.

cc
2026-04-18 12:18:13 -06:00
vicrodh 6ee3b5c8cd feat(lyrics): use app Dropdown for font select and add XL size option
Swap the native <select> in LyricsControlsPopover for the shared Dropdown
component so the font picker matches the app's settings style. Widen the
popover to fit the Dropdown trigger and shrink its chrome to keep the
overall scale compact.

Add an XL font size (20/24 px) alongside S/M/L, extending the
LyricsFontSize type, the sanitize validator, the size class in
LyricsLines, and the i18n entry across all five locales.

cc
2026-04-18 08:54:22 -06:00
vicrodh d84d9a9836 feat(lyrics): add i18n keys for sidebar display controls
cc
2026-04-18 08:00:43 -06:00
vicrodh 260e5c499c feat(login): show MIT-license-aware trademark disclaimer on loading screen
Pin the API-usage + trademark disclaimer to the bottom of the initializing
state of LoginView so the legal notice is visible during startup, not only
on the login screen proper. Update legal.trademarkNotice in all five locales
to mention the MIT license explicitly.

cc
2026-04-18 00:15:17 -06:00
vicrodh b3ad548a9c feat(subscription-gate): extend offline grace window 3 → 30 days
Matches Qobuz's own mobile app posture. The previous 3-day window was
both user-hostile (punishes anyone on a flaky network) and a compliance
red flag (shorter windows read as 'we're hiding something', longer
windows read as 'we're lenient for a reason'). 30 days is the exact
parity answer.

The useful protection against bulk-download-and-ghost attacks comes
from the CMAF-at-rest offline cache (v2 format), not from aggressive
revalidation. This grace window is a belt over the suspenders.

Changes:
- GRACE_PERIOD_SECS: 3 → 30 days in config/subscription_state.rs, with
  a comment explaining the reasoning for future readers.
- Log message in session_lifecycle no longer says "3 days" (it quoted
  a constant, now says "grace window").
- Disclaimer string in all 5 locales (en/es/de/fr/pt) updated from
  "3 days" to "30 days" so the Offline Playback settings screen
  reflects the actual behavior.

No behavior changes for users with an active subscription. Users who
were within the old 3-day window don't lose cache — the window
extended, not contracted.

cc
2026-04-17 08:28:00 -06:00
vicrodh 16af395356 feat(report-issue): confirmation modal with logs redirect + opt-out
Settings menu > Report Issue now opens a modal instead of jumping
straight to the GitHub new-issue page. The modal explains that
sharing the (manually masked) logs speeds up triage and offers two
actions:

- Go to logs → navigates to Settings > Developer Mode and
  auto-opens the Logs modal so the user can review and upload.
- Create issue report → opens the GitHub issues page.

A persistent "Do not show this again" checkbox stores the preference
in localStorage (qbz-hide-report-issue-modal). When the flag is set,
the menu item skips the modal and goes straight to GitHub, matching
the previous behaviour.

Wiring: new settingsIntentStore lets UserCard cue SettingsView to
open at the `developer` section with showLogsModal=true, consumed and
cleared on mount so a plain Settings navigation afterwards uses the
normal default section.

i18n: new reportIssue block added to all 5 locales (en/es/de/fr/pt).

cc
2026-04-16 11:47:49 -06:00
vicrodh 321c87f353 feat(logs): opt-out toggle to bundle diagnostics into terminal paste
Settings > Developer Mode > View Logs now has a toggle (default ON,
persisted to localStorage as qbz-paste-bundle-diagnostics) that
prepends the full System Diagnostics snapshot plus a 10-second cast
scan to the terminal paste upload, so bug reporters share one link
instead of two. Console-tab uploads are unaffected.

- New service module diagnosticsSnapshot.ts centralises the snapshot
  payload (runtime + system + playback + qconnect + optional cast
  scan) so the panel and the paste flow don't drift apart.
- LogsModal shows "Collecting diagnostics..." while the scan is
  running, then "Uploading..." once it hands off to the paste server.

Also revert the auto-run cast scan on the diagnostics panel: bundling
into the paste covers the "one-shot bug report" use case, and the
panel opens instantly again.

cc
2026-04-16 11:20:50 -06:00
vicrodh d5e3fff45c feat(diagnostics): add OS / distro / install method / deps section
New v2_get_system_info command and matching UI section in the
System Diagnostics panel. What it reports:

- os (linux/macos/windows)
- arch (x86_64, aarch64)
- kernel_version from /proc/sys/kernel/osrelease
- Linux distro: id, version_id, pretty_name from /etc/os-release
  (falls back to /run/host/os-release so it works inside Flatpak)
- install_method: flatpak / snap / appimage / dev / native, detected
  via FLATPAK_ID + /.flatpak-info, SNAP, APPIMAGE env vars, and
  debug_assertions as the last fallback
- flatpak_runtime + version: parsed from /.flatpak-info
- loaded library versions (from /proc/self/maps, symlink-resolved):
  webkit2gtk-4.1, gtk-3 (or 4), glibc, alsa, pipewire, pulseaudio

All info is derived from local files/env; no sensitive data, no IDs,
no hashes. Included in the clipboard export.

cc
2026-04-16 11:14:26 -06:00
vicrodh d9b1efc091 feat(diagnostics): add Playback, Qobuz Connect, and Cast Discovery
Extend the System Diagnostics panel with three new sections aimed at
what debug tickets usually ask for, and include them in the clipboard
export:

- Playback: is-playing, volume, position/duration, current track
  (title, artist, album, source, local flag, quality/format/bit-depth/
  sample-rate). No track IDs or UUIDs — defensive regex redacts any
  hex/UUID-shaped strings before export so paste sites and CodeQL
  don't flag them as secrets.

- Qobuz Connect: running/transport_connected, role (controller /
  local-renderer / observer / none), active renderer friendly
  name/brand/model (no UUIDs or IPs), visible renderer count, last
  error (id-redacted).

- Cast Discovery: on-demand 10-second multicast scan for Chromecast
  and DLNA; reports per-protocol count and the friendly names that
  were discovered. No hosts, no ports, no UUIDs.

Playback rows stay live via a player-store subscription while the
panel is open.

cc
2026-04-16 11:05:07 -06:00
vicrodh ace9e93b84 feat(window): optional track metadata in window title (#324)
Add an opt-in setting under Appearance that lets the OS window title
reflect the currently playing track. Template supports {artist},
{track}, {album} placeholders; defaults to "{artist} — {track}".
Falls back to "QBZ" when disabled or with no track.

Persisted to localStorage (pure UX preference — no backend state).

cc
2026-04-16 08:58:03 -06:00
vicrodh 33d65681c4 fix(queue): restore infinite play (V2 reimplementation)
Issue #319: Infinite Play toggle had no backend wiring after the V2
migration — toggle only wrote to localStorage and the queue-end handler
ignored it, so playback stopped at the last track.

Reimplemented in V2 (no legacy wrappers):

- New `v2_create_infinite_radio` command that takes the last few played
  track IDs, uses the most recent track's artist as the radio seed via
  `RadioPoolBuilder::create_artist_radio` (Qobuz `/radio/artist`
  endpoint), and returns 50 V2QueueTrack ready to append. Does NOT
  mutate the queue or playback context — caller decides how to use them.

- `playbackPreferencesStore`: expose `'infinite'` in `AutoplayMode`
  type and add `isInfinitePlayEnabled()` helper.

- `+page.svelte`:
  - `handleToggleInfinitePlay` now syncs to backend via
    `setAutoplayMode('infinite' | 'continue')` so the choice persists
    in `playback_preferences.db` across restarts (was localStorage only).
  - On startup load infinite play from cached preferences (was
    localStorage).
  - When `nextTrack()` returns null AND infinite play is on: collect
    the current track + last 5 history tracks, call
    `v2_create_infinite_radio`, append the result via
    `v2_bulk_add_to_queue`, then advance — playback continues without
    interruption.

i18n: add `toast.failedToSavePreference` to all 5 locales.

cc
2026-04-15 21:20:04 -06:00
vicrodh 3d0df4afba feat(a11y): add WCAG Light, WCAG Dark, High Contrast, and Colorblind themes
Four accessibility-focused themes with WCAG AAA contrast ratios:
- WCAG Light/Dark: 7:1+ contrast for all text levels
- High Contrast: pure white on pure black, yellow accent (21:1)
- Colorblind: deuteranopia-safe palette (Wong colors), no red/green

Addresses discussion #313 (user with severe vision impairment).

cc
2026-04-15 20:30:26 -06:00
vicrodh b62a4b01f5 feat(audio): notify user when selected output device goes missing (issue #307)
Level A of issue #307. When the configured output device has
disappeared from the host device list at the time of play/resume
(KVM switched away, USB sink unplugged, PipeWire sink removed…)
the backend emits an 'audio:device-missing' event and the frontend
shows a warning toast:

  'Output device "XYZ" is no longer available. Using system default.'

The existing init_device fallback already routes audio to the system
default, so this is purely informational — the user now understands
why audio came out of a different sink instead of staring at a silent
progress bar.

Does NOT touch the sacred audio init path (pipewire_backend.rs,
init_device, audio_settings.rs). New module audio_device_watch.rs
adds a pure presence check, a new-state throttle so we don't emit on
every resume once the user's been notified, and a v2 command
v2_check_audio_device_presence so the frontend can poll on demand.

Ready for Level B (background watchdog) — the same helpers plug into
a timer without further refactor; Level B only needs to add a tokio
task that calls emit_missing_if_needed() every N seconds while
playback is active.

cc
2026-04-14 17:58:59 -06:00
vicrodh 7c2f2941fd feat(award): gold press ribbon + Other awards carousel on AwardView
1. Press ribbon restyled to a solid gold gradient (#f5c042 → #d49511)
   with dark (#1f1407) uppercase text and a subtle white text-shadow.
   Drops the red border-left. Qobuzissime / Album of the Week keep
   their dark-background treatment with coloured left accent.

2. AwardView now loads /award/explore on mount (first 30 entries,
   current award filtered out) and renders them as a horizontal
   'Other awards' carousel below the album grid — same layout as
   LabelView's 'More labels'. Each card shows the award laurel
   (or magazine cover when present) + name + magazine name; clicking
   routes into that award's AwardView.

i18n key award.otherAwards added for all five locales.

cc
2026-04-14 16:07:22 -06:00
vicrodh 9460838223 fix(album-sidebar): keep award card always clickable; toast if id missing
Previously disabled={!onAwardClick || !award.id} grayed out the award
card whenever /album/get returned an award entry without an id. User
reported the award was 'no longer clickable' on albums opened from the
artist page. Now the button always accepts clicks and the handler
shows a toast when no id is available instead of silently doing
nothing — keeps the affordance honest without sending 'null' to
/award/page.

cc
2026-04-14 14:51:54 -06:00
vicrodh a273174716 feat(award): use /award/getAlbums for the grid + follow award as entity
Two behavior fixes for AwardView based on user feedback.

1. Grid content. The previous implementation rendered the releases
   array from /award/page, which is user-scoped (only surfaces awards
   on albums the user already favorited), so the grid came up empty
   in almost every case. Switch to /award/getAlbums which is the
   public catalog listing with pagination. /award/page is still
   called, but only to hydrate the hero (name, image, magazine).

2. Follow-the-award. Awards themselves are followable on Qobuz mobile
   (it's what feeds the 'Awards' tab of Release Watch). Add:
   - favorite_awards table + store methods in favorites_cache.rs
   - 4 V2 commands (get/sync/cache/uncache) mirroring the label ones
   - awardFavoritesStore.ts — clone of labelFavoritesStore with string
     ids (Qobuz mobile API passes award_ids as strings)
   - Follow/Unfollow heart button in AwardView hero, matching the
     ArtistDetailView / LabelView visual
   - loadAwardFavorites() wired into the login bootstrap alongside
     loadArtistFavorites / loadLabelFavorites
   - i18n keys (award.follow / award.unfollow)

Also adds a Ver más button for paginated loading of the album grid.

cc
2026-04-14 13:02:37 -06:00
vicrodh 8730c891cc feat(award): dedicated AwardView with header matching Label/Album conventions
Adds the frontend for award detail pages. The hero header mirrors
LabelView's proportions 1:1 — 180px circular avatar left, inline
subtitle/name/magazine stack right, same 32px heading, same 12px
uppercase kicker, same 24px gap, same 40px bottom margin, same back
button treatment. Outer container uses the same paddings and scroll
behaviour as LabelView/AlbumDetailView.

Body pulls /award/page and builds one HorizontalScrollRow per
container (releases, new-releases, classics…). Container titles
localize via award.section.<id> with fallback to 'Acclaimed Releases'.

Wiring:
- 'award' added to ViewType and session restore map
- handleAwardClick in +page.svelte navigates to the view
- AlbumDetailView sidebar now passes onAwardClick so Phase 2 award
  cards become interactive
- Frontend AlbumAward.id is optional to match tolerant backend struct;
  sidebar disables the award button when id is absent
- i18n keys added in all five locales

cc
2026-04-14 12:37:41 -06:00
vicrodh 7bf9972846 feat(album-view): right sidebar with label and full awards stack
Ports the Qobuz web 'label + awards' sidebar pattern to AlbumView.
Track list and sidebar sit side-by-side above 1100px and collapse
vertically below it. Label card navigates to the existing LabelView.
Award cards call onAwardClick when provided (disabled until Phase 3
lands AwardView).

Wiring:
- Adds awards (Vec<PageArtistAward>) to both qbz-models Album and
  src-tauri api::models Album so /album/get payloads hydrate the
  field instead of silently dropping it.
- Adds awards to QobuzAlbum and AlbumDetail frontend types plus the
  convertQobuzAlbum adapter.
- New album.sidebar i18n keys in all five locales.

Card ribbon still shows only the LAST award (Phase 1 behavior);
sidebar renders every award as a tap target — mobile-style.

cc
2026-04-14 11:11:24 -06:00