- Hide button only shows when content filtering is enabled
- Use custom blind-eye.svg icon
- Remove border for cleaner look
- Banner also conditional on filtering being enabled
cc
- Replaced prominent blacklist button with smaller tertiary button
- Button now opens dropdown with descriptive help text:
"Hide this artist - Don't show in searches, playlists, discover, etc."
- Updated terminology from "blacklist" to "hide/show"
- Updated banner and toast messages for consistency
- Uses EyeOff icon instead of Ban for softer visual
cc
- PlaylistModal: convert $derived with get(t) to regular function
- LocalLibraryTagEditorModal: rename 't' to 'track' in map to avoid
shadowing the t store
cc
vite-plugin-svelte CSS extraction fails when $t() is used inside $derived().
This causes "[postcss] Unknown word <script" errors that appear to be cache
issues but are actually a known Svelte 5 / vite-plugin-svelte incompatibility.
Files fixed:
- DownloadButton.svelte: use titleKey pattern with function resolver
- DACSetupWizard.svelte: use labelKey pattern resolved in WizardStepper
- RemoteControlSetupGuide.svelte: use labelKey pattern
- WizardStepper.svelte: support optional labelKey with $t() in template
Also added download.* translation keys to en.json and es.json.
cc
- Add backend endpoint for /discover/playlists API
- Add PlaylistTagFilter dropdown component for filtering by genre
- Add QobuzPlaylistCard with CSS blur background effect (color-thief style)
- Add HorizontalScrollRow header snippet for custom headers with scroll controls
- Add session cache store for caching API responses
- Add i18n keys for playlist filter and discover section
cc
Old database schemas created before locale support was added
caused "no such column: locale" errors when fetching artists.
The migration detects this and drops the old table so it gets
recreated with the correct schema.
Fixes Top Artists section not appearing in Home view.
Security improvements based on code review:
1. Credential Encryption (Critical)
- Replace XOR obfuscation with AES-256-GCM encryption
- Derive key from machine ID (tied to specific machine)
- Add migration support for legacy XOR format
- Automatic re-encryption on first load of legacy credentials
2. HTTPS Default (Medium)
- Change default `secure` setting to `true` for new installs
- Existing users keep their current setting
3. Configurable CORS Origins (High)
- Replace hardcoded domain whitelist with SQLite-backed storage
- Default origins: vicrodh.github.io, control.qbz.lol
- Add commands: get/add/remove origins, restore defaults
- Origins can be customized for third-party clients
- Server restarts automatically when origins change
New dependencies:
- aes-gcm: AES-256-GCM authenticated encryption
- sha2: SHA-256 for key derivation
- Move Remote Control section to end of Settings (after Updates)
- Make section collapsible, closed by default
- Keep Experimental badge visible when collapsed
- Update nav sidebar order to reflect new position
- Remove unused .section-title-row CSS selectors
Add /api/events endpoint using Server-Sent Events (SSE) as a more
efficient alternative to WebSocket for pushing playback state updates
to PWA clients. SSE provides automatic reconnection and is simpler
to implement on the client side.
- Add tokio-stream dependency with sync feature for BroadcastStream
- Implement sse_handler with 15s keep-alive ping
- Keep /api/ws for backwards compatibility
- Add shuffle/repeat fields to PlaybackEvent struct
- Emit queue:state event when shuffle/repeat changes via API
- Add queue event listener in desktop frontend
- Include shuffle/repeat in WebSocket broadcasts
- PWA updates shuffle/repeat from WebSocket messages
This enables bidirectional sync: changes from PWA update desktop UI,
and changes from desktop update PWA via WebSocket.
Update the local volume state when receiving playback events from the
backend. This ensures the volume slider reflects changes made via
remote control or other external sources.
The playback event listener was only started when currentTrack existed,
creating a chicken-and-egg problem: external playback (from PWA) couldn't
set currentTrack because the listener wasn't active to receive the event.
Now the listener is always active, allowing remote control to update the
player state even when nothing is playing locally.
Call notifyListeners() right after setting currentTrack when track
changes from remote control. Also update isPlaying and currentTime
from the event to ensure player UI reflects correct state.
Added debug logging for queue track mismatches.
When a track change is detected from an external source (e.g., PWA
remote control), also sync the full queue state. This fixes the issue
where desktop UI showed empty queue after PWA started playback.
Add emit_playback_update() to notify both desktop UI (Tauri event) and
PWA (WebSocket) when playback state changes from remote commands.
Previously, desktop UI would only update on the next polling cycle (up
to 5 seconds), making the remote control feel unresponsive.
- GET /api/favorites - Get user favorites (albums, tracks, artists)
- POST /api/favorites/add - Add item to favorites
- POST /api/favorites/remove - Remove item from favorites
- POST /api/album/play - Play album (clears queue and adds all tracks)
- Add RemoteControlSetupGuide component with step-by-step instructions
- Include certificate installation guides for iOS, macOS, Windows, Android
- Mark remote control section as "Experimental" with red badge
- Add Setup Guide button to open the wizard
- Add translations for wizard (en/es)
- POST /api/queue/shuffle - set shuffle mode
- POST /api/queue/repeat - set repeat mode (off/all/one)
- Return current shuffle/repeat state in response
When tracks are changed externally (via PWA remote control), the desktop
player now detects the track_id mismatch and fetches the new track info
from the backend queue. This fixes bidirectional sync where skipping
tracks on the PWA wasn't reflected in the QBZ desktop UI.
Features:
- Axum REST API server for remote control
- WebSocket real-time playback updates
- Token-based authentication with QR pairing
- LAN-only security middleware
- Settings UI for enabling/configuring remote control
Features:
- Redesigned welcome step with comprehensive disclaimer
- Added DAC info extension to all wizard steps
- Fixed stepper navigation when going backwards
- Added custom gandalf tooltip with nerdy text
- Applied ChatGPT micro-adjustments (warnings, hints)
- Full i18n support (EN/ES/FR/DE)
Step 1 (Precheck):
- Add inactive warning: "If any service is inactive, stop here..."
Step 2 (Other distro):
- Change to "Ensure these packages (or equivalents) are installed"
Step 3 (Detect DAC):
- Add input warning: "This must match exactly..."
Step 4 (Backup):
- Update hint about user responsibility for keeping backup
Step 6 (Pulse):
- Change "browsers" to "browsers and apps"
Step 7 (WirePlumber):
- Add rules note: "These rules apply only to the detected DAC"
All translations updated for EN/ES/FR/DE
- Reference "The Hi-Fi Wizard/Mage" for the gandalf icon
- Add "if you haven't already" to acknowledge power users
- Keep it playful without being patronizing
- Match button height to dropdown (40px)
- Increase gandalf icon size (24px) for better visibility
- Add custom hover tooltip with larger gandalf icon
- Tooltip shows title and description explaining the wizard
- Add tooltip translations for EN/ES/FR/DE
- Add DAC short name to pulse config filename for multi-DAC support
- Add getCreatedConfigPaths() for dynamic file path generation
- Show configured DAC name/description in done step
- Add configuredFor translation to all locales (EN/ES/FR/DE)
Step 5 (PipeWire config):
- Add sample rate checkboxes (44.1, 48, 88.2, 96, 176.4, 192 kHz)
- Pre-fill from DAC query if available, otherwise empty for manual selection
- Show "detected" badge for rates found by query
- Dynamic command updates based on selection
Step 7 (WirePlumber config):
- Use DAC short name in filename: 99-qbz-dac-{name}.conf
- Extract name from node.name or description
- Include DAC description in config file comment
- Use selected sample rates instead of hardcoded values
This enables proper multi-DAC separation - each DAC gets its own config file.
- Welcome step: checkbox to confirm understanding of system changes
- Backup step: checkbox to confirm backup was created
- Start/Next buttons disabled until checkboxes are checked
- Add translations for checkboxes (EN/ES/FR/DE)
- Clear default input value (use empty string)
- Add ALSA hardware capabilities query via:
- /proc/asound/cardX/stream0 for USB devices
- aplay --dump-hw-params for detailed hw params
- Parse rate ranges [min max] and expand to common rates
- Now shows actual DAC capabilities, not just current config
- Remove redundant subtitle text from DAC wizard welcome (all languages)
- Fix collapsed sidebar settings menu to expand upward instead of down
- Menu now opens to the right and grows upward, avoiding player overlap
- Add complete French (FR) translations for dacWizard section
- Add complete German (DE) translations for dacWizard section
- Add qobuz-player (TUI) to bit-perfect app selector in all 4 languages
- Replace single settings button with dropdown menu in UserCard
- Menu opens upward in expanded mode, to the right in collapsed mode
- Use openUrl from Tauri plugin for external links (bug report)
- Make dropdown compact (similar to context menu) and wider
- Pass onKeybindingsClick prop through Sidebar to +page.svelte
- Support collapsed prop in UserCard for sidebar collapsed mode
- Create KeyboardShortcutsModal.svelte with categorized shortcuts display
- Add ? shortcut to open modal (ui.showShortcuts action)
- Integrate modal in +page.svelte with Settings navigation
- Use i18n for all UI text
- Add animations and accessibility attributes
$state and $derived at module level cause "Cannot access uninitialized
variable" error during SSR. Changed to lazy initialization with getter
functions.
Keep only start/result summary at INFO for user issue reports.
Move all intermediate steps (MBID resolution, config, engine steps) to DEBUG.
Also moved to debug:
- playlist_get_settings, playlist_get_stats
- playlist_get_local_tracks_with_position
- All [SuggestionsEngine] step logs
Convert hw:X,0 to front:CARD=name,DEV=0 format when saving device selection.
This makes device IDs stable across reboots and USB reconnections since they
use the card name (e.g., C20) instead of the card number which can change.
- Add normalize_device_id_to_stable() to convert hw:X,0 to front:CARD=name,DEV=0
- Add resolve_stable_to_current_hw() for reverse lookup when needed
- Apply normalization automatically in set_audio_output_device command
- Export functions from audio module for use across codebase
- Clear stale device IDs from DB when they don't exist in enumeration
- Add warning when device lookup fails in handleBackendDeviceChange
- Add logging to set_audio_output_device command for debugging
ALSA card numbers (hw:X,0) can change between boots when USB devices
are connected/disconnected. This fix prevents using invalid device IDs
that cause playback failures.
cc
- Add fr.json with complete French translations
- Add de.json with complete German translations
- Register new locales in i18n/index.ts
- Add missing keys (playlist, metadata, genreFilter sections)
- All 869 translation keys present in both files
- Translated modal title and form labels
- Translated search buttons and results text
- Translated table headers (track, title, disc)
- Translated persistence options (sidecar vs direct)
- Translated progress states (writing tags, saving)
- Added placeholder translations for year and genre examples
Using $t() store subscriptions inside $derived() was causing
vite-plugin-svelte CSS extraction to fail, resulting in PostCSS
receiving the entire file content instead of just the style block.
Changed networkFsOptions to use labelKey pattern with $t() called
directly in the template instead.
- Update play menu items (Play now, Play next, Add to queue)
- Update navigation items (Go to artist, Go to album)
- Translate View more buttons
- Add noPlaylistsFor translation key
cc
- Update Last.fm API key and shared secret labels
- Update Last.fm authorize button text
- Update ListenBrainz labels and token hint
- Add translations for connecting/opening states
- Add apiKey, sharedSecret, userToken translation keys
cc
- Update Dropdown.svelte with search placeholder translation
- Update DeviceDropdown.svelte with searchDevices placeholder
- Update GenreFilterPopup.svelte with searchGenres placeholder
- Update PlaylistModal.svelte with name and description labels
- Add playlist.name and playlist.descriptionOptional keys
cc
- Add translations for loading/error states
- Translate empty states for albums, artists, tracks views
- Translate header titles and subtitle
- Update button titles (Scan, Settings, Play All, Shuffle)
- Translate offline mode notice
- Translate filter panel labels
- Add editAlbumMetadataTitle key for tooltips
cc
- Add t import and use translations for all empty state messages
- Translate loading states (favorites, playlists, albums)
- Translate section headers (Discography, EPs & Singles, Live Albums)
- Translate button titles (Play All, Shuffle, More, Search, Clear)
- Add hint translations for liking items to favorites
- Update getSortLabel to use sort.* translation keys
- Update getTabLabel to use favorites.tabLabels.* keys
cc
Document i18n requirements for contributors:
- No hardcoded strings policy
- svelte-i18n interpolation format ({ values: {} })
- Translation key naming conventions
- Checklist for PRs with UI text
svelte-i18n requires { values: { key: value } } format for variable
interpolation, not { key: value } directly. Fixed across all toast
and greeting calls.
- Update LocalLibraryTagEditorModal to use toast translations
- Update PlaylistImportModal to use toast translations
- Update PlaylistModal to use toast translations
- Update LocalLibraryView to use toast translations
- Update SettingsView to use toast translations for error messages
- 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 second level of protection by filtering blacklisted artists
in Rust queue commands: set_queue, add_to_queue, add_to_queue_next,
and add_tracks_to_queue. This ensures blacklisted tracks are never
added to the queue regardless of frontend implementation.
- TrackRow: Add isBlacklisted prop with grayout styling and ban icon
- VirtualizedTrackList: Check blacklist status for each track
- PlaylistDetailView: Gray out and restrict actions for blacklisted tracks
- AlbumDetailView: Apply album-level blacklist check to all tracks
- HomeView: Gray out blacklisted tracks in Continue Listening section
- ArtistDetailView: Banner shown when viewing blacklisted artist page
Blacklisted tracks show reduced opacity, ban icon instead of track
number, and restricted menu actions (navigation only, no playback).
- Show success/error toasts when adding/removing artists
- Show feedback when enabling/disabling blacklist
- Show confirmation toast when clearing all artists
- Replace direct navigateTo import with onBlacklistManagerClick callback
- Pass navigation callback from +page.svelte to SettingsView
- Follows same pattern as other view navigation (playlist-manager, etc)
- Create BlacklistManagerView component with list of blocked artists
- Add enable/disable toggle, search filter, and clear all function
- Integrate navigation to blacklist manager from Settings
- Add Content Filtering section in Settings with artist count
- Add i18n translations for English and Spanish
- Wire up view routing in +page.svelte
- most_popular now iterates through all items to find first non-blacklisted
- Added album filtering to search_all by album.artist.id
- Added BlacklistState to search_albums with filtering
- Fixes edge case where most_popular was empty when first item was blacklisted
Phase 3 (minimal) - UI for testing:
- Add artistBlacklistStore.ts with O(1) local checks
- Initialize store on app startup
- Add blacklist toggle button to ArtistDetailView
- Button shows red when artist is blacklisted
cc