- Add time-based greeting translations (goodMorning, goodAfternoon,
goodEvening, goodNight) to homeSettings section
- Update homeSettingsStore to export getGreetingInfo for i18n support
- Update HomeView.svelte to use translated greetings
- Add comprehensive toast translations (~60 keys) for all user-facing
notifications including queue, playback, offline, favorites, etc.
- Update +page.svelte to use $t() for all showToast calls
- Complete Spanish translations for all new keys
Add semaphore to limit concurrent prefetch operations to 1 at a time.
This helps prevent potential race conditions in native audio libraries
(CPAL/PipeWire/ALSA) that can cause malloc corruption.
Also:
- Reduce QOBUZ_PREFETCH_COUNT from 3 to 2
- Reduce PREFETCH_LOOKAHEAD from 15 to 10
- Add 50ms delay before cache insertion
Addresses #38 - random crashes during prefetch
Implement 9/10 check strategy:
- 9 out of 10 connectivity checks go to 1.1.1.1 (Cloudflare DNS)
- 1 out of 10 checks go to Qobuz to verify service availability
This prevents false offline detections when the app is making many
concurrent API calls (e.g., loading genre hierarchies), which could
trigger Qobuz rate limiting and cause timeout-based false positives.
Also reduced timeout from 15s to 10s and retry wait from 2s to 1s
since 1.1.1.1 responds much faster than Qobuz.
Only show expand carets in the advanced view for genres that actually
have children loaded, or where children haven't been loaded yet.
Applies to all levels (parent, child, grandchild).
- Add silent parameter to loadChildren to skip notification
- loadAllDescendants now loads children and grandchildren in parallel
- Only one notification after all loads complete (no triple reload)
- Lazy load children only when tree node is expanded (faster startup)
- Fix popup positioning to grow left when toggling advanced view
- Eagerly load all descendants when selecting a parent for filtering
- This ensures basic mode also filters by Genre > Children > Grandchildren
- Show loading spinner while fetching children
- Parent genres shown as headers with checkbox
- Children displayed in grid below each parent
- Clicking parent toggles all its children
- Partial selection shows indeterminate state on parent
- Popup expands 150px taller when in advanced view
- Default view shows only parent genres (less cluttered like Qobuz)
- When selecting a parent, filtering includes all its sub-genres
- Added "Show all sub-genres" toggle for granular control
- Store now tracks parent/child relationships for proper filtering
- getFilterGenreNames() returns selected + children for filtering
Fetch parent genres first, then fetch sub-genres for each parent in
parallel. Combine into a flat list sorted alphabetically. This fixes
the issue where albums with sub-genres (like Alternative & Indie,
Opera, Concertos) were filtered out even when all genres were selected.
Add FavoritePlaylistCard component with hover overlay containing:
- Heart button (remove from favorites)
- Play button (start playlist playback)
- More menu (play next, add to queue, share, remove)
Card click navigates to playlist page. Updates FavoritesView to use
the new component with all action handlers wired through +page.svelte.
- Add 'control' variant for Favorites style matching other dropdowns
- Add 'align' prop: 'left' (Home) or 'right' (Favorites)
- FavoritesView uses control variant with right alignment
- Control variant shows "Genre: X" with chevron icon
- Add GenreFilterButton to Favorites > Albums toolbar
- Filter albums by genre name (multi-select supported)
- Use genreFilterVersion for reactivity tracking
Note: Artists tab doesn't have genre filter since artist
objects don't contain genre info directly.
- Allow multiple genre selection (checkbox behavior)
- Add getSelectedGenreNames() for multiple name retrieval
- Merge results from multiple API calls for featured albums
- Clear button now closes the popup
- Button shows "N genres" when multiple selected
- Add getSelectedGenreName() to genre filter store
- Add filterAlbumsByGenre() helper in HomeView
- Apply genre filter to Recently Played and Favorite Albums
- Fetch 3x more items when filtering to ensure enough results
- Reduce card height from aspect-ratio to fixed 36px
- Use neutral bg-tertiary background instead of colored gradients
- Horizontal layout with name and check circle
- Selected state uses accent-primary
- Better spacing to prevent overlap
Use onMount instead of $effect to load genres once.
Add initialized flag to prevent onFilterChange from firing
during initial subscription setup, which was causing
loadHome -> re-render -> loadGenres -> notify -> loadHome loop.
Remove genre_id from base schema and let migration add it.
This ensures existing databases don't fail when trying to create
index on non-existent column during initial schema execution.
- Add GenreFilterButton to home header
- Pass genre_id to fetchFeaturedAlbums API calls
- Reload home content when genre filter changes
- Featured sections now filter by selected genre
- Add reco_backfill_genres command to populate genre_id for existing events
- Add reco_needs_genre_backfill command to check if backfill is needed
- Fetches album details from Qobuz API and updates events in batches
- Includes rate limiting to avoid API throttling
- Add genre_id column to reco_events table
- Add migration to add column to existing databases
- Update RecoEventInput and RecoEventRecord structs
- Update insert_event and get_events_since to handle genre_id
- Add get_album_ids_without_genre() for backfill
- Add update_genre_for_album() for backfill
- Add optional genre_id parameter to client method
- Add optional genre_id parameter to Tauri command
- Qobuz API filters featured albums by genre when provided
- Add playPlaylistById to set queue and play first track
- Add queuePlaylistNextById to add playlist tracks next
- Add queuePlaylistLaterById to add playlist tracks later
- Add copyPlaylistToLibraryById using subscribe_playlist
- Add sharePlaylistQobuzLinkById to copy link to clipboard
- Connect all handlers to SearchView component
- Repositioned toggle to appear right after Streaming Quality dropdown
- Updated description to clarify it overrides quality setting when device
doesn't support the selected sample rate
Frontend changes for quality limiting feature:
- Add limitQualityToDevice state and handler
- Add AudioSettings interface fields for limit_quality_to_device and device_max_sample_rate
- Update handleBackendDeviceChange to send max_sample_rate when device changes
- Update handleOutputDeviceChange to find max_sample_rate from backend devices
- Add Settings UI toggle for "Limit Quality to Device"
This completes the quality limiting feature that prevents requesting tracks
that exceed the device's sample rate capabilities, ensuring bit-perfect playback.
- Add limit_quality_to_device setting (enabled by default)
- Add device_max_sample_rate field to store selected device's max rate
- Implement limit_quality_for_device() to restrict quality based on device
- Apply quality limiting in play_track and prefetch_track commands
- Add set_audio_device_max_sample_rate Tauri command
This ensures bit-perfect playback by not requesting tracks that
exceed the device's sample rate capabilities. When enabled:
- Devices <= 48kHz: limited to Lossless (44.1kHz)
- Devices <= 96kHz: limited to HiRes (up to 96kHz)
- Devices > 96kHz: allow UltraHiRes (up to 192kHz)
Add new audio setting to limit streaming quality based on device
capabilities. When enabled (default: true), QBZ will avoid requesting
tracks that exceed the device's max supported sample rate.
This ensures bit-perfect playback by preventing the need to resample
high-res tracks on devices that don't support them.
- Add limit_quality_to_device field to AudioSettings
- Add database migration for new column
- Add Tauri command for frontend integration
cc
- Add AlsaDirectError enum for structured error classification
- Add BitPerfectMode enum to track runtime bit-perfect status
- Implement automatic fallback: hw fails with format error → try plughw
- Abort on non-recoverable errors (busy, permissions, invalid params)
- Respect ALSA plugin selection from UI settings
- Log runtime mode for debugging
This allows SMSL-class DACs that fail on hw: to work via plughw:
while maintaining bit-perfect guarantees (format conversion only,
no resampling).
cc
- Add S24_3LE (S243LE) to format priority list as first choice
- Implement S24_3LE write handler with 3-byte packed conversion
- Add S24LE write handler (was missing explicit implementation)
- Reorder format priority: S24_3LE > S32LE > S24LE > S16LE > FloatLE
This fixes bit-perfect playback on USB DACs that only support
the S24_3LE format (SMSL, Topping, Fosi with TAS1020B chip).
Closes#30
cc
- Reduce FFT to 30fps with 16 bins (mirrored to 32 visual bars)
- Use 1024 FFT size for faster processing
- Extract dominant colors from album artwork
- Center artwork with track info and quality badge
- Bars render from edges inward with color gradient
- Increase artwork to 108px (20% larger)
- Reorder info: Artist (bold) > Album (italic) > Track
- Add album prop to VisualizerPanel
- Adjust text styling hierarchy
- Position track info at bottom-right with margin
- Reduce artwork to 90x90px
- Text aligned right, to the left of artwork
- Compact layout that doesn't obstruct visualization
- Add 'visualizer' to FocusTab type
- Add Visualizer tab with Activity icon in ImmersiveHeader
- Update VisualizerPanel with mirror mode (bars up and down from center)
- Integrate VisualizerPanel in ImmersivePlayer focus view
- Three-color gradient: teal -> purple -> pink
- Track info overlay with artwork, title, artist
- Instanced bar rendering with vertex/fragment shaders
- Receives binary frequency data via Tauri events
- R32F texture for efficient frequency data upload
- Cyan-to-magenta gradient coloring
- Auto-enables/disables backend when mounted/unmounted
- 64 frequency bars with logarithmic mapping from backend
- Add VisualizerTap to Player for sample capture
- Create wrap_source helper in audio thread
- Wrap all audio sources with TappedSource at 4 append points
- Player now captures samples for visualization when enabled
- Zero overhead when disabled (fast atomic check)
Audio samples flow: Source -> TappedSource -> RingBuffer -> FFT Thread -> Frontend
- Doubled horizontal range to ±120px
- Doubled vertical range to ±35px
- Added power curve (0.7) for hypersensitivity near center
- Small mouse movements now produce dramatic response
- Vinyl stays perfectly centered, only rotates
- Removed X and Y parallax from vinyl position
- Removed parallax uniform from shader
- Only the cover art (sleeve) responds to mouse movement
- Set rotation speed to 33.333 RPM (0.0582 rad/frame at 60fps)
- Remove Y-axis parallax from vinyl position (stays centered)
- Remove Y parallax from shader for perfectly concentric rotation
- Add mipmaps and trilinear filtering for anti-aliasing
- Enable anisotropic filtering when available
- Use PNG instead of SVG for better WebGL compatibility
- Extreme horizontal parallax: cover 200px, vinyl 120px
- Reduced vertical parallax for subtle depth
- Shader parallax: 0.15 horizontal, 0.05 vertical
- Disc appears to slide out of sleeve with minimal mouse movement
- Add padding around vinyl (scale UV by 1.15x)
- Discard pixels outside bounds to avoid edge artifacts
- Much more aggressive parallax: cover 80/50px, vinyl 50/35px
- Shader parallax offset increased to 0.08
- Vinyl size reduced to 85% for more breathing room
- Updated qbz-logo.svg and qbz-logo.png
- Added qbz-vinyl-v3.svg for VinylPanel
- Regenerated all Tauri icons from new logo
- Updated Android icons and favicon
- Use WebGL2 canvas for vinyl rendering instead of CSS
- Load qbz-vinyl-v3.svg as texture
- Shader-based rotation when playing
- Parallax effect via uniforms (no DOM updates)
- Album cover slides left to reveal vinyl
- Much lower CPU usage than CSS animation
- Double parallax sensitivity (30/20px cover, 18/12px disc)
- Cover and disc now vertically centered together
- Cover slides 15% left when playing to reveal disc
- Simplified transform calculations
- Correct z-ordering: cover in front (z-10), disc behind
- Cover slides left when playing to reveal disc
- Add mouse parallax effect for depth
- More visible grooves with alternating brightness
- Neutral gray colors instead of amber vintage
- Use system typography instead of Oswald
- Radial gradient on disc for realistic shading
- Add VinylPanel.svelte with spinning vinyl disc animation
- Album cover slides left when playing to reveal disc
- CSS-based grooves (40 rings) and amber center label
- Vintage typography with "33 1/3 RPM" badge
- Add 'vinyl' as new Focus tab option in ImmersiveHeader
- Fix MPRIS/PipeWire volume feedback loop by checking if volume
actually changed before calling setVolume
- Increase animation speed to 1.8x for more dynamic movement
- Add per-channel histogram normalization to expand color range
- Boost saturation to 2.5x and brightness to 0.85
- Add 12% red channel boost for warmer tones
- Target color range 30-240 to lift shadows and preserve highlights
Problem: brightness(0.5) made whites gray while blacks stayed black,
causing dark areas to dominate even in mostly-white artwork.
Solution:
- Increased brightness from 0.5 to 0.7
- Added shadow lift: maps 0-255 to 25-255
- Black now becomes dark gray (~10% brightness floor)
- Whites remain visible and balanced
Result: 'Panda/cow' effect - both light and dark areas visible.
New effects added:
- Layer rotation: each layer rotates slowly (1-2° range)
- Scale breathing: zoom levels pulse in/out per layer
- Pulsing vignette: edge darkening varies over time
- Increased drift amplitudes: far ±0.20, mid ±0.14, near ±0.08
- Stronger brightness breathing: ±12% (was ±8%)
All effects are decorrelated (different speeds/phases) for organic feel.
Default intensity now 1.0 for maximum visibility.
Replace expensive properties with GPU-accelerated ones:
- transform: scaleX() instead of width (no layout recalc)
- Solid color instead of gradient
- Removed box-shadow blur (very expensive)
- Added will-change: transform hint
This runs entirely on GPU compositor thread.
Revert to glowing underline approach with QBZ brand blue (#5281ff):
- Text stays bright white (no background-clip: text)
- Underline sweeps from left to right during line duration
- Blue glow effect with box-shadow
- Much better readability than gradient text
Replace underline with metallic shimmer that sweeps across text:
- Mostly white (40% each side) with purple highlight band in center
- Gradient moves from right to left during line duration
- Uses ease-in-out for natural feel
- GPU-accelerated, single element, no JS polling
The background-clip: text technique made white appear gray.
New approach uses a pseudo-element underline that:
- Sweeps from left to right with the line duration
- Has purple glow effect (#a78bfa)
- Keeps text bright white and readable
- No impact on text color/brightness
Replace JS-polled karaoke with pure CSS animation:
- Calculate line duration from timing data (timeMs)
- Use CSS animation with background-position sweep
- Animation runs once per line change, no polling needed
- Uses purple (#c4b5fd) to white gradient for visibility
Data flow: Pass full LyricsLine objects (with timeMs) through
component chain instead of stripping timing data.
Problem: Lyrics split view consumed more CPU than coverflow due to:
- Polling every 80-200ms for karaoke progress updates
- Inline styles on ALL ~50+ lines recalculated on every render
- Transitions tracked on all lines (4 properties each)
Solution:
1. lyricsStore: Add setProgressTrackingEnabled() to skip progress
updates in immersive mode (only track line index changes)
2. LyricsLines: Use CSS classes for opacity instead of inline styles
in immersive mode (distance-1, distance-2, distance-3, distance-far)
3. LyricsLines: Add CSS containment (contain: layout style) to isolate
paint/layout recalculations
4. LyricsLines: Reduce transitions on non-active lines (2 vs 4 props)
5. ImmersivePlayer: Disable progress tracking on mount, re-enable on exit
Result: Immersive lyrics now updates only on line changes (~10-20/min)
instead of continuous progress updates (~300-750/min)
- Book collage: 3 album covers with left/center/right positioning
- Rotated side covers (-12deg/+12deg) with hover animation
- Custom tooltip: opens bottom-right with high z-index (1000)
- Info trigger: 24px with dark background for visibility
- Fade-in animation for tooltip appearance
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.