Queue/History:
- Removed History as separate tab from header
- Added History as sub-tab within QueuePanel
- Sub-tabs: Queue | History with counts
- Removed history-focus from focus mode tabs
- Removed keyboard shortcut 'H' and '5' for history
Background transition:
- Added 400ms crossfade when artwork changes
- Works for both WebGL and CSS fallback renderers
- Fades out, loads new texture, fades back in
- Removed book collage (playlist.images are same image at different sizes)
- Using medium quality image (images[1]) for playlist cards
- Fixed z-index on Qobuz/QBZ badges (z-index: 10, above overlay)
- Increased badge icons by 2pt (Qobuz: 22px, QBZ: 19px)
- Replaced native title tooltip with Tooltip component for radio info
- Shows 3 covers like album spines when available
- Center cover fully visible, left/right at -15% offset
- Falls back to medium-quality single image if < 3 images
- Uses images[1] (medium) instead of images[0] (largest)
Working state:
- Atmospheric background (no recognizable artwork)
- Multi-sample blur in shader
- Gradient tint (cool top, warm bottom)
- Oswald font for lyrics (28px/30px)
- Lyrics panel 700px wide
- Text shadow for contrast
- Karaoke effect disabled in immersive
Ready for Heavy Immersion shader upgrade.
- Font size: 28px inactive, 30px active
- Disabled background-clip karaoke effect in immersive mode
- Active line now pure white (#ffffff)
- Past lines dimmed to 35% opacity
Shader:
- Fixed luminance formula that was killing blue (30% blend instead of 100%)
Lyrics:
- Added Oswald font for immersive lyrics
- Increased font size by 2pt (26px inactive, 28px active)
- Active lines now pure white with higher weight for clarity
Implement proper atmospheric background pipeline:
- Random NON-CENTERED crop (150-300% zoom) to break composition
- Extreme blur at small size (destroys all semantic detail)
- Scale up with smoothing for additional blur
- Color adjustments (saturation 1.4x, brightness 0.5x)
- Vignette overlay
The background is now an unrecognizable color field derived from
artwork colors. Acceptance test: "Can I identify the album from
the background alone?" must be NO.
Applies to both WebGL and CSS fallback renderers.
- texture-loader.ts: Multi-pass blur (4×15px) at 256x256 for CSS-equivalent quality
- ambient.frag: Simplified to UV drift, zoom breathing, and color breathing only
- Blur computed ONCE at load time, shader only does motion effects
- No per-frame blur, image processing, or texture uploads
- Source canvas now 256x256 (was 64x64)
- Light pre-blur (8px), GPU shader does main blur
- Increased shader blur radius and samples (16 samples)
- Should produce much smoother results
- Multi-sample blur: 12 samples in circular pattern
- Noise-based UV distortion to break up pixelation
- Random sample offsets for organic blur effect
- All blur computation is GPU-side (no CPU cost)
- Canvas size back to 64x64 (like original)
- Blur radius 20px applied 3 times (20 + 20 + 10 = ~50px total)
- Expanded canvas for blur overflow, then crop to final
- Brightness 0.55, saturation 1.3 (matching original)
Blur fix:
- Use three separate canvases (source, blur, final)
- Apply blur filter when drawing BETWEEN canvases (not to self)
- Canvas size reduced to 16x16 for extreme blur when scaled
- Double blur pass for extra smoothness
CPU fix:
- Reduce animation from 30fps to 15fps
- Slow ambient motion doesn't need high frame rate
- Reduce canvas to 32x32 (was 128) for more natural scaling blur
- Apply triple-pass blur technique:
1. Scale down (interpolation blur)
2. Apply blur filter and redraw
3. Third pass for extra smoothness
- Adjust brightness to 0.45 (darker) and saturation to 1.3
- Lower JPEG quality to 0.7 (blur hides artifacts anyway)
- ImmersiveBackground now checks WebGL2 availability on mount
- Dynamically imports ImmersiveAmbientCanvas when WebGL2 available
- Falls back to CSS blur when WebGL2 unavailable
- New props: enableAmbient (default true), ambientIntensity
Automatic renderer selection:
1. Check BUILD_IMMERSIVE_ENABLED (compile-time)
2. Check isRuntimeEnabled() (user preference)
3. Check isWebGL2Available() (browser capability)
4. If all pass: use WebGL2 with ambient motion
5. Otherwise: use CSS fallback (original behavior)
- Add utils/texture-loader.ts with:
- Async pre-blur generation using canvas 2D
- Blur cache (max 20 entries)
- Request cancellation for rapid track changes
- Abort signal support throughout
- Update ImmersiveAmbientCanvas.svelte:
- Use texture-loader for blurred artwork
- Handle cancellation gracefully
- Report texture metrics
- Clean up pending loads on destroy
Pre-blur is computed once per track change (CPU), then uploaded
to GPU texture. No per-frame blur computation.
- Add utils/webgl-utils.ts with context creation, shader compilation,
texture loading, and cleanup utilities
- Add shaders/fullscreen.vert for fullscreen quad rendering
- Add shaders/static.frag for basic texture display
- Add shaders/index.ts for Vite ?raw shader imports
- Add ImmersiveAmbientCanvas.svelte component with:
- WebGL2 context management
- Context loss/restore handling
- Automatic resize handling
- Performance metrics reporting
- Update app.d.ts with shader file type declarations
This is a static renderer only - no animation yet.
Phase 2 will connect to real artwork with pre-blur.
- Create src/lib/immersive/ module structure
- Add types.ts with ImmersiveState, ImmersiveConfig interfaces
- Add config.ts with build-time and runtime feature flags
- Add ImmersiveRenderer.ts with lifecycle management and stores
- Add index.ts as public API boundary
- Add VITE_IMMERSIVE_ENABLED to vite.config.js
- Add app.d.ts for TypeScript env declarations
Build without immersive: QBZ_IMMERSIVE=false pnpm build
No visual changes yet - this is capability detection and guards only.
- Create HistoryPanel component for viewing playback history
- Add 'history' tab to split mode tabs
- Add 'history-focus' tab to focus mode tabs
- Sync history when immersive mode opens (fix coverflow prev tracks)
- Keyboard shortcuts: H for history, 5 for history-focus
- Rename old CoverflowPanel to StaticPanel (single artwork view)
- Create new CoverflowPanel with actual coverflow effect:
- Shows 5 album covers (2 prev, current, 2 next)
- 3D perspective rotation on side covers
- Click on side covers to navigate queue
- Smooth animation on track change
- Add 'static' tab to focus mode (4 tabs total)
- Update keyboard shortcuts: 1-4 for focus tabs
- Add expandable window controls in header (hover to expand)
- Fullscreen toggle (F11)
- Exit immersive (Esc)
- Close app
- Move Repeat button to left group: Fav | Shuffle | Repeat
- Remove QualityBadge from player controls
- Add F11 keyboard shortcut for fullscreen toggle
- Use Tauri window API for fullscreen functionality
- Remove PlayerControlsMedium variant (unused)
- Keep only PlayerControlsCompact as the single player style
- Add QualityBadge above player controls
- Center play/pause button with balanced left/right groups
- Remove expand/compact toggle (no longer needed)
- Pass quality, bitDepth, samplingRate props from parent
- Add front:CARD=X,DEV=Y device enumeration from aplay -L output
- Recognize front: devices as bit-perfect in AlsaDirectStream
- Group front:CARD and "bit-perfect" named devices correctly in UI
- Add tooltip to device dropdown showing full device name
- Add current selection indicator in device dropdown
- Extend dropdown idle timeout to 10 seconds for long device names
Addresses issue #30 for USB DACs that expose front: but not hw: devices
- Initialize volume from localStorage instead of hardcoded 75
- Pass volume and onVolumeChange to NowPlayingBar even when no track is playing
- Users can now adjust volume before playing anything (important to prevent
loud surprises when volume was left at max)
Systems without a notification daemon (e.g., minimal Linux setups) would
crash when the app tried to show notifications. Now we log a warning and
continue instead of propagating the error.
Fixes#32
1. play_track now returns PlayTrackResult with format_id from Qobuz API
- Returns actual format (5=MP3, 6=FLAC16, 7=FLAC24, 27=Hi-Res)
- Returns None for cache hits (format unknown)
2. Frontend updates track.format based on format_id
- QualityBadge can now correctly show "MP3" when format_id=5
- Previously badge showed "CD" even for MP3 because format was empty
3. Device name now set after backend system init
- PipeWire and ALSA backends now set current_device from settings
- Fixes tooltip showing no device when using backend system
Added onTrackClick handler to NowPlayingBar that opens TrackInfoModal when
the track title is clicked. Uses cursor: help for informative visual cue.
Only works for non-local tracks.
When streaming_only is enabled, prefetch is now skipped entirely.
This ensures tracks are always streamed fresh from the network
without being pre-cached in the background.
When streaming_only is disabled, prefetch works normally.
Logs now clearly show:
- [CACHE HIT] when playing from memory/disk/offline cache
- [STREAMING] when streaming from network with cache_after flag
- [DOWNLOAD] when using full download mode
- [CACHED] / [NOT CACHED] after download completes
- [Playback Settings] shows stream_first, streaming_only, buffer values
- Quality badge now has border (alpha-15) and background (alpha-10)
- Added 5px top margin for separation
- Title and subtitle now use ticker animation on hover for long text
- Animation matches AlbumCard behavior (scroll left then back)
- Card now has background like artist-card (bg-secondary, rounded)
- Artwork reduced to 148x148 with 6px padding around
- Quality badge moved to bottom of card (outside artwork)
- Genre only visible on hover overlay with slide-in animation
- Text centered and sized down for consistency
- Quality badge now at bottom-right with lighter background (0.55 opacity)
- Genre badge at top-left for albums (tracks don't have genre data)
- Both badges use consistent styling with blur backdrop
- New popular-card with square image, title, and subtitle
- Hover overlay with heart, play, and menu buttons
- Dropdown menu with play controls and navigation options
- Track cards show "Go to album", album cards show "Go to artist"
- Artists still use the existing circular card design
- Make auth_token() public for catalog/search endpoint authentication
- Fix setSearchQuery to skip notification when value unchanged
- Fix setSearchState to only notify listeners when query actually changed
- Add title case transformation for better most_popular API results
- Add stale result detection to ignore outdated search responses
- Preserve most_popular when API returns inconsistent results
- Clean up debug logging
Replace heuristic-based most popular detection with Qobuz's actual
most_popular field from the catalog/search endpoint.
- Add CATALOG_SEARCH endpoint path
- Make app_id() and get_http() public on QobuzClient
- Add MostPopularItem enum with tagged serialization (artists/albums/tracks)
- Update search_all to use catalog/search and parse most_popular field
- Update SearchView template to use allResults.most_popular with new types
- Add MostPopularItem type to searchState store
The Most Popular result in search now intelligently determines
the best match based on query:
- Exact name match: artist > album > track
- Starts with match: artist > album > track
- Fallback: first artist, then album, then track
Added track card display for when a track is the most popular result.
- Update "Streaming Only" description to be less technical:
"Disables temporary cache. Not recommended without a fast connection.
Offline library tracks always play first."
- Fix account card vertical centering with explicit align-self on
avatar, info container, and logout button
Adds UI toggle for streaming_only mode that skips L1/L2 cache writes.
When enabled, tracks are streamed fresh each time but offline cache
still works for downloaded tracks (local-first principle).
Add streaming_only setting that skips L1+L2 cache writes while still
respecting offline cache (local-first). When enabled:
- download_and_stream() skips final cache.insert()
- Standard download path skips cache.insert()
- Offline cache continues to work normally
This reduces storage usage for users who prefer pure streaming.
- Clear both L1 memory and L2 disk caches when quality setting changes
This ensures cached tracks at wrong quality are removed
- Fix device name not showing in popup on first hover
Now refreshes status on each hover instead of only when null
- Add quality downgrade indicator when stream quality is lower than
track's original quality (shows yellow arrow with tooltip)
- Store original track quality values for comparison with actual stream
Issue #34 - Display actual stream quality, not track metadata
Backend changes:
- Add sample_rate and bit_depth to PlaybackEvent
- Add stream quality tracking to SharedState
- Extract bit_depth from FLAC/M4A headers in streaming path
- Player sets stream quality when decoding audio
Frontend changes:
- Update PlaybackEvent interface with new fields
- Update currentTrack with actual stream quality from backend
This allows the UI to show the actual quality being played (e.g., 44.1kHz
when streaming a 192kHz track at CD Quality) instead of the track's
maximum advertised quality.
Issue #34 - Max quality setting not respected
Changes:
- Remove 'Prefer Highest Available' toggle (was dead code, conflicted with quality dropdown)
- Clear playback cache (L1+L2) when streaming quality changes
- Show toast notification when cache is cleared
- Add description to streaming quality setting
- Update i18n for new messages (en/es)
This ensures the quality setting takes effect immediately, important for:
- Users with metered internet connections
- Users with hardware sample rate limitations (e.g., DACs limited to 96kHz)
The Qobuz API respects format_id and downsamples server-side, so this
change guarantees the correct quality is fetched for new streams.
- Log requested vs returned format_id and sample_rate in get_stream_url_with_fallback
- Log quality_str, parsed quality, and format_id in play_track and prefetch_track commands
- Add console.log in getStreamingQuality for frontend debugging
This will help diagnose why 192kHz is played despite lower quality settings.
- Add branch workflow section explaining pre-release integration
- Clarify that all PRs must target pre-release, not main
- Update maintainer procedure for new workflow
- Add release procedure section
- Add TrackReplacementModal to search for replacement tracks
- Add onFindReplacement action to TrackMenu context menu
- Show replacement option only for owned playlists with unavailable tracks
- Replacement searches by track title + artist
- Shows quality info (bit depth, sample rate) to help user choose
- Supports preview playback before confirming
- Replace old track with new one in playlist
Also fix tooltip for unavailable tracks to show a complete message
instead of "is no longer available" (now shows "Track no longer
available in Qobuz library")
- Add isUnavailable prop to TrackRow to show AlertCircle instead of track number
- Show unavailable icon only for user's own playlists (not Qobuz official playlists)
- Gray out unavailable tracks in both owned and non-owned playlists
- Remove all interactions for unavailable tracks in non-owned playlists
- Keep only "remove from playlist" option for unavailable tracks in owned playlists
- Create unavailableTracksStore for persisting unavailable track IDs to localStorage
- Load unavailable tracks on app startup in bootstrap.ts
- Auto-mark tracks as unavailable when playback fails with TrackUnavailable error
- Add streamable field to PlaylistTrack and DisplayTrack interfaces
- Add isTrackRemovedFromQobuz() check for tracks with streamable: false
- Show red AlertCircle icon for unavailable tracks
- Gray out and apply grayscale filter to unavailable track rows
- Limit menu actions to only 'Remove from Playlist' for removed tracks
- Hide favorite and download buttons for removed tracks
- Keep wrapper interactive for context menu access
Backend:
- Add TrackUnavailable error type for tracks removed from Qobuz
- Validate stream URL response - detect missing URL with restrictions
- Add SampleRestrictedByRightHolders to restriction check
- Return specific error when track is unavailable vs quality unavailable
Frontend:
- Detect track unavailable errors in playTrack()
- Show friendly toast: Track title is no longer available
- Auto-skip to next track in queue after 500ms delay
- Add i18n translations (en/es) for trackUnavailable message
- Add validation to check if URL is empty before returning StreamUrl
- Add debug logging for JSON response keys
- Return proper error instead of silently using empty URL
- This prevents 'builder error' from HEAD request to empty URL
- Use lazy_static to create a single reusable HTTP client instead of
creating a new client for each stream info request
- Add retry logic (3 attempts with 100ms delay) to handle transient
network/TLS initialization failures
- This fixes 'Failed HEAD request: builder error' toasts that required
pressing play button twice
- Adjust badge icons: QBZ 17px (was 22px), Qobuz 20px (was 18px)
QBZ fills its viewBox completely while Qobuz has whitespace,
so QBZ needs to be smaller to appear visually equal
- Adjust glyph icons: Playlist 32px (was 28px), Radio 26px (was 24px)
Playlist SVG only fills ~77% of its 20x20 viewBox, needs larger
pixel size to match Lucide Radio which fills its box completely
Backend:
- Add get_artist_tracks command for fetching artist's popular tracks
Frontend:
- Fallback to artist's popular tracks when tracks_appears_on is sparse
- Remove card hover movement, only image moves up on hover
- Add hover overlay with (add to queue) (play) (play next) buttons
- Use playlist.svg icon for playlist cards and glyph
- Radio glyph shown by default, replaced by action buttons on hover
- Fix icon sizes: QBZ (22px) vs Qobuz (18px) for visual balance
- Track playlist.svg asset
- Change from aggressive title normalization to exact title match only
- Keeps different versions (Remaster, Live, etc.) as valid options
- Add debug logging for sparse recommendation data
- This should fix the "only one recommendation" issue for some artists
- Fix $derived to $derived.by for complex logic (was causing CPU spike)
- Remove badge backgrounds from QBZ and Qobuz icons
- Move info icon (i) to top-left corner of radio card
- Add play button to radio card
- Increase badge icon sizes for visibility