Files
qbz/docs/release-1.1.15/CHANGELOG_FULL.md
T

9.8 KiB

QBZ 1.1.15 Full Changelog

This document details all changes introduced in version 1.1.15 relative to the v1.1.14 tag.

Summary: ~200 commits across all branches merged into pre-release; 19 files changed in the multi-select/navigation work alone.


New Features

Purchases view

  • New PurchasesView component browses the authenticated user's Qobuz purchases via the V2 API
  • Local download registry stored in a dedicated SQLite table; tracks which purchases have been downloaded and to which local path
  • V2 backend commands: v2_get_purchases, v2_get_downloaded_purchases, v2_mark_purchase_downloaded
  • Downloads organized into quality subfolders to match the Qobuz file naming convention
  • Mock data removed; real API integration only

Your Mixes

  • New Your Mixes card section on the Home view
  • Four mix types surfaced: Daily Q (DynamicSuggestView), Weekly Q (WeeklySuggestView), Fav Q (FavQView), Top Q (TopQView)
  • Each view: full track list with artwork, quality badges, shuffle, search/filter, and now multi-select
  • Video/animated assets for mix cover placeholders

Multi-select bulk actions

All track list views support multi-select mode:

View Source
AlbumDetailView TrackRow checkbox via existing selectable props
ArtistDetailView Custom track rows with injected checkbox
PlaylistDetailView Virtualized list; VirtualizedTrackList extended with selectable/selected/onToggleSelect
FavoritesView Virtualized list via same props
DynamicSuggestView (Daily Q) TrackRow checkbox
WeeklySuggestView TrackRow checkbox
FavQView TrackRow checkbox
TopQView TrackRow checkbox
LabelView Custom track rows with injected checkbox

BulkActionBar component (src/lib/components/BulkActionBar.svelte):

  • Sticky-bottom bar with count label, queue split-button (Add to Queue / Play Next), Add to Playlist, Add/Remove Favorites, Remove from Playlist
  • Animated entry (slideUp 180 ms)
  • placement prop retained for future use; all current views use bottom placement
  • Positioned after all track rows; views already carry padding-bottom: 100px so no track is ever hidden

Backend V2 commands added (src-tauri/src/commands_v2.rs):

  • v2_add_tracks_to_queue_next — insert tracks at front of queue
  • v2_bulk_toggle_favorites — toggle favorite state for a list of track IDs in one call

i18n keys added (all four locales: en, es, de, fr):

  • actions.select
  • actions.cancelSelection
  • actions.selectedTracks (interpolated: {count} selected)

Custom artist images

  • customArtistImageStore — Svelte store keyed by artist ID; populated from a bulk backend fetch on login
  • Right-click context menu on any artist image opens a file picker; selected image is resized (max 800 px) and thumbnailed (200 px) in the Rust backend and stored in the app data directory
  • V2 backend commands: v2_set_custom_artist_image, v2_remove_custom_artist_image, v2_get_custom_artist_image, v2_bulk_get_custom_artist_images
  • resolveArtistImage(id, fallback) helper used across: HomeView, SearchView, FavoritesView, ArtistDetailView, LabelView
  • Images sync immediately on assignment without requiring navigation

Image lightbox

  • ImageLightbox component: full-screen overlay, click-outside or Escape to dismiss, natural image dimensions
  • Wired to artist photo (ArtistDetailView) and album artwork (AlbumDetailView)
  • Round mask removed by design — images display at their natural crop

Qobuz Radio

  • Album radio button in AlbumDetailView header
  • Artist radio and track radio in ArtistDetailView and track context menus
  • Submenu lists available Qobuz radio variants for the selected artist/track
  • V2 backend: v2_create_qobuz_artist_radio, v2_create_qobuz_track_radio

Session restore

  • App serializes active view + selected item IDs to a session file on navigation
  • On launch, restoreView in navigationStore rehydrates without triggering data re-fetches
  • Startup page setting in Settings → General: home, library, purchases, last
  • LinkResolverService — backend service that accepts any streaming platform URL
  • Phase 1: direct platform API calls for Spotify, Apple Music, Tidal, Deezer, YouTube Music, Amazon Music
  • Phase 2: falls back to Odesli universal API (song.link) when direct resolution fails
  • Falls back again to smart Qobuz search (title + artist) if Odesli has no Qobuz entry
  • Share outward: track context menu → "Share via Songlink" copies a song.link URL to clipboard
  • Paste inward: paste any recognized URL to open the Qobuz equivalent in-app

Font selector

  • fontFamily setting in Appearance; persisted in user config
  • Applied via CSS custom property on :root; immediate effect
  • Options include system-ui, Inter, Roboto, and a handful of monospace/serif choices

System / Auto-theme

  • autoThemeStore and V2 commands v2_generate_auto_theme, v2_detect_system_theme
  • Desktop environment detection: KDE (reads ~/.config/kdeglobals), GNOME (GSettings schema), others
  • On DEs without an accent API: extracts a dominant palette from the current album artwork using a k-means color quantizer in the Rust backend
  • Generated colors injected as CSS custom properties; can be individually overridden with the inline color picker in Appearance settings
  • Bootstrap restore: theme is re-applied from stored config on startup before the first paint

Redesigns

Label view

Previous design was a flat list of popular tracks. New design is a full-page layout with:

  1. Hero header — label logo/image, name, album count, carousel jump-links
  2. Popular Tracks section — play-all controls, multi-select, show-more/show-less (5 → 20 → 50)
  3. Releases — horizontal scroll carousel with "See All" pagination
  4. Playlists — horizontal scroll carousel
  5. Artists — horizontal scroll carousel with custom artist image support

Improvements

Audio

  • PipeWire force bit-perfect — new option in Audio settings; sets node.rate to track's native sample rate and disables PipeWire's built-in resampler, delivering raw PCM to the hardware node without any mixer involvement
  • Bit-perfect remains non-negotiable: try_from_device_config() path is preserved; this option is additive

Performance

  • L1 cache: 400 MB, L2 cache: 800 MB — raised from previous defaults; reduces backend re-fetches for users with large streaming histories
  • Label view artist image carousel deferred to idle frames via batched Promise.allSettled; prevents paint jank on first render

UI/UX

  • UI scale options — 85% and 95% added between the existing 80% and 100% steps
  • Track multi-select persists during scroll — selection state held in a Set<number> (not DOM state); survives virtual scroll culling in Favorites and Playlist views

Bug Fixes

Multi-select action bar positioning (ADR-001 + layout)

Problem: BulkActionBar placed before the column header (position: sticky; bottom: 0) was rendered at the top of the track list, not the bottom. On scroll, it floated over tracks.

Root cause (ADR-001): AlbumDetailView and WeeklySuggestView used filter(t => ...) inside async functions — t shadows the svelte-i18n $t store, breaking vite-plugin-svelte's CSS extraction pipeline. This caused the entire component's CSS to be dropped at runtime.

Fix:

  • filter(t => ...) → filter(track => ...) / filter(trk => ...) in all affected functions
  • BulkActionBar moved to the end of the track row container, after {/each}, still inside the scrollable section
  • All affected views already had padding-bottom: 100px; no track is ever hidden under the sticky bar

Scroll position bleeding between items

Problem: scrollPositions: Map<ViewType, number> used a single key per view type. Opening Album B after Album A would restore Album A's saved scroll offset in Album B.

Fix: Key changed to "viewType:itemId" — e.g. "album:MAQJXZA123456", "artist:300213", "playlist:42". Views without a specific item (search, favorites tabs) retain the view-type-only key unchanged. The scrollKey() helper builds the composite string; saveScrollPosition and getSavedScrollPosition accept an optional itemId parameter.

Scroll position TTL

Problem: Saved positions were permanent; an album visited weeks ago would restore a stale offset.

Fix: ScrollEntry now stores { scrollTop, savedAt: Date.now() }. getSavedScrollPosition returns 0 if Date.now() - savedAt > 3_600_000 (1 hour). Constant SCROLL_TTL_MS at the top of navigationStore.ts for easy adjustment.


Architecture (Internal)

This section is for contributors. Users can skip it.

V2 multi-crate migration (Codex)

The entire Tauri command layer was migrated from a monolithic src-tauri/src/ module to a workspace of independent Rust crates with no Tauri dependency:

Crate Responsibility
qbz-core Shared types, error types, DB utilities
qbz-models Domain model structs (Track, Album, Artist, …)
qbz-player Audio engine, queue, playback state
qbz-audio PipeWire/ALSA backend wrappers
qbz-qobuz Qobuz API client
qbz-cache Two-level cache (L1 in-memory, L2 SQLite)
qbz-library Local library scan and indexing
qbz-integrations Last.fm, MPRIS, Odesli, Plex

All frontend invoke() calls use v2_* or runtime_* prefixes. Legacy command modules (the old src-tauri/src/library/commands.rs et al.) have been deleted or reduced to stubs; no V2 command delegates to a legacy function. This separation means the player, library, and integrations layers can run headless (TUI, CLI) without Tauri being present.

The commands_v2.rs file added in this release adds the multi-select bulk commands and the purchase registry commands on top of the already-migrated stack.