The AppImage EGL_BAD_PARAMETER crash is caused by how Tauri's
linuxdeploy bundles newer WebKitGTK versions (tauri-apps/tauri#11994).
Fix: pin WebKitGTK to 2.44.0-2 on ubuntu-24.04, matching the proven
fix from other Tauri apps (audioling#40, OmniKee).
Also revert runtime env var workarounds (LIBGL_ALWAYS_SOFTWARE,
default-off hardware accel) since the fix is at the build level.
Keep only the NVIDIA DMA-BUF workaround which is still needed.
WEBKIT_DISABLE_DMABUF_RENDERER alone is not enough — WebKit still calls
eglGetDisplay() for compositing and crashes with EGL_BAD_PARAMETER on
systems without working EGL (VMs, some driver combinations).
Setting LIBGL_ALWAYS_SOFTWARE=1 forces Mesa to use llvmpipe so the EGL
initialization succeeds through software rasterization.
EGL/DMA-BUF renderer crashes on many GPU/driver/compositor combinations,
not just NVIDIA. Default to software rendering for all GPUs and allow
opt-in via QBZ_HARDWARE_ACCEL=1.
Change loadAllAlbumDownloadStatuses from Promise.all(all) to
chunked batches of 10 with setTimeout(0) between batches to
yield to the UI thread and prevent blocking during large
album collections.
Virtualize albums, tracks, and artists tabs with binary search
and absolute positioning. Each tab uses its own scroll container
with ResizeObserver for responsive grid columns. Load More buttons
render as virtual items at the bottom of each list.
Replace 4 album render paths (grouped grid, grouped list,
ungrouped grid, ungrouped list) with single virtualized component.
Remove ~110 lines of duplicated album CSS now in component.
Enable no-outer-scroll for albums tab.
Binary search virtualization for favorite albums in both grid
and list modes. Grid uses AlbumCard with responsive columns,
list uses inline rows with QualityBadge. Supports grouping,
scroll-to-group, and all album action callbacks.
Replace raw {#each} loops in artist sidepanel left column with
virtualized list component. Remove ticker animation state from
parent (now encapsulated in VirtualizedFavoritesArtistList).
Clean up unused CSS for artist-list-* classes.
Binary search virtualization for artist sidepanel list.
Includes ticker animation for long names, group headers,
and scroll-to-group support. 64px row height, 10-item buffer.
Replace raw {#each} loops for artist grid (grouped + ungrouped)
with virtualized component. Artists now render only visible cards
in DOM, fixing 20-second UI freezes with large artist collections.
Binary search virtualization for favorite artists grid view.
Supports grouped/ungrouped modes, responsive columns via
ResizeObserver, scroll-to-group, and lazy image loading.
Part of issue #56 fix for 20-second UI freezes.
GDK_BACKEND=x11 alone is not enough: WebKitGTK still initializes
EGL for GPU compositing under X11, and the bundled libEGL sees
WAYLAND_DISPLAY, tries the Wayland EGL platform, and aborts with
EGL_BAD_PARAMETER.
Add WEBKIT_DISABLE_COMPOSITING_MODE=1 which prevents WebKitGTK
from initializing EGL entirely. This flag caused ghost app on
native Wayland but is safe on X11 (which we force for AppImage).
The previous workaround guarded the GDK_BACKEND override with
is_none(), so if the AppImage runtime or desktop session had already
set GDK_BACKEND=wayland, the fix was silently skipped.
AppImage builds have a fundamentally broken EGL stack on Wayland:
libEGL is present but incomplete, Mesa EGL loaders are missing,
and eglGetDisplay() returns EGL_BAD_PARAMETER with no possible
runtime fallback.
Changes:
- AppImage + Wayland now unconditionally sets GDK_BACKEND=x11,
overriding any existing value
- Also sets QT_QPA_PLATFORM=xcb and WEBKIT_DISABLE_DMABUF_RENDERER=1
in the same block for belt-and-suspenders safety
- Adds QBZ_FORCE_WAYLAND=1 escape hatch for power users
- Skips all other GDK/DMABUF logic when AppImage forced X11
- Simplifies NVIDIA DMA-BUF logic (no longer Wayland-gated)
- Does NOT affect RPM, DEB, Flatpak, or native builds
Replace the non-virtualized playlist rendering in the Sidebar with
an inline virtual scroll implementation. Users with 500+ playlists
experienced 20-second UI freezes because all playlists were mounted
as DOM nodes simultaneously.
Now only ~20-30 visible items (plus buffer) are rendered at any time,
using absolute positioning with translateY and binary search for
efficient viewport calculation. Follows the existing
VirtualizedArtistList pattern.
Fixes#56
WEBKIT_DISABLE_DMABUF_RENDERER alone is not enough: WebKitGTK's
Wayland platform init calls eglGetPlatformDisplay() in
PlatformDisplayWayland before any renderer decision, and the
AppImage's bundled EGL loader is incompatible with the host
Mesa/driver, causing EGL_BAD_PARAMETER abort.
Force GDK_BACKEND=x11 for AppImage + Wayland so WebKitGTK uses
GLX via XWayland instead of EGL, bypassing the crash entirely.
Users who want native Wayland can override with GDK_BACKEND=wayland.
Non-AppImage builds (Flatpak, RPM, DEB) are unaffected and continue
using native Wayland.
cc
WEBKIT_DISABLE_COMPOSITING_MODE=1 was set automatically on Wayland
(both in the VM path and the general Wayland path), which caused a
"ghost app": process alive, tray/MPRIS initialized, but UI never
renders on Fedora + GNOME + Wayland.
Rework the defensive strategy:
- Remove WEBKIT_DISABLE_COMPOSITING_MODE from all automatic paths;
gate it behind opt-in QBZ_WEBKIT_DISABLE_COMPOSITING=1
- Treat AppImage + Wayland as the primary condition (not VM-only):
disable DMA-BUF renderer for AppImage on Wayland to prevent
eglGetPlatformDisplay() EGL_BAD_PARAMETER abort
- Keep LIBGL_ALWAYS_SOFTWARE=1 scoped to VMs and QBZ_SOFTWARE_RENDER
- Keep NVIDIA + Wayland DMA-BUF mitigation unchanged
- Flatpak / RPM / DEB builds unaffected (no APPIMAGE env var)
Fixes: #59
cc
Accept originalBitDepth, originalSamplingRate, and format in
ImmersivePlayer Props and pass them through to all 4 focus panels
(Coverflow, Static, Vinyl, Visualizer) and the inline QualityBadge
in split mode. This enables hardware-based downgrade detection in
all immersive views.
Poll get_hardware_audio_status every 2s to get the actual hardware
sample rate from /proc/asound. Compare decoded rate (kHz) vs
hardware rate (Hz) for real downgrade detection instead of relying
on originalSamplingRate which comes from the same decoder.
Replace native title attribute with custom tooltip (matching
AudioOutputBadges style) for the degraded state, showing source
rate, output rate, resampling warning, and settings fix hint.
Replace single degradedTooltip string with quality.tooltip.*
keys (source, output, resampled, fix) for the new custom tooltip
in QualityBadge. Updated all 4 locales (en, es, de, fr).
Renamed t -> trk (or tab as contextually appropriate) in all
lambda/callback parameters across 6 files that import t from i18n.
This prevents vite-plugin-svelte CSS extraction failures (PostCSS
"Unknown word" errors) caused by t shadowing the i18n store.
Files: +page.svelte, PlaylistDetailView, FavoritesView,
TrackReplacementModal, Sidebar, SuggestionsPanel
Upload is now per-tab (Terminal/Console separately). Each tab shows a
checkmark after upload. URL displayed inline with copy button. Help text
appears after first upload encouraging users to share both URLs.
Developer Mode: collapsible section (hidden from jump-nav) with Force DMA-BUF
toggle and View Logs button. LogsModal shows Terminal/Console tabs with
0x0.st upload. Console capture wraps console.log/warn/error/info.
Bugfix: Factory Reset row was outside {#if !storageCollapsed}, making it
always visible even when Storage section was collapsed.
Developer settings: SQLite-backed store for force_dmabuf toggle.
Logging: TeeWriter captures env_logger output to ring buffer;
log_startup() captures pre-logger startup messages.
Upload: multipart POST to 0x0.st for log sharing.
CLI: --reset-dmabuf flag resets force_dmabuf and exits for recovery.
Adds is_virtual_machine() detection (DMI product name, sys vendor,
hypervisor type) that triggers LIBGL_ALWAYS_SOFTWARE=1 to bypass
EGL hardware initialization entirely on VirtualBox, VMware, QEMU,
Xen, Hyper-V, and Parallels.
Also adds QBZ_SOFTWARE_RENDER=1 env var for users on real hardware
who experience transparent outlines or EGL failures (issue #59).
- Moves Stream Uncached Tracks, Initial Buffer Size, and Streaming Only
from Audio section to Playback section
- Adds Reset Audio & Playback Settings button at bottom of Audio section
- Adds Delete All Application Data danger zone at bottom of Storage section
with confirmation checkbox and secondary dialog
- Adds CSS for reset-btn, danger-zone, checkbox-label, factory-reset-btn
Moves streamUncached, initialBuffer, streamingOnly and related keys
from settings.audio to settings.playback in all 4 locale files.
Adds reset audio/playback keys to settings.audio section.
Adds factory reset keys to settings.storage section.
Tears down all per-user stores, clears credentials from keyring,
then removes ~/.local/share/qbz/, ~/.cache/qbz/, and ~/.config/qbz/.
Makes teardown_type_alias_state public for reuse.
Adds reset_all() to both AudioSettingsStore and PlaybackPreferencesStore.
The reset_audio_settings tauri command resets both audio and playback
preferences to their default values in a single call.
Applies the same fix pattern from issue #56 (search) to Home view commands.
Backend: scope client.lock().await to just the HTTP call in get_featured_albums,
get_album, get_track, get_artist_basic, and get_discover_index so concurrent
Tauri commands no longer serialize through a single lock.
Frontend: await tick() after setting section data before firing download status
checks, and add .catch() to prevent unhandled rejections from blocking the UI.
Add display:block, font:inherit, appearance:none to ensure the button
renders identically to the div variant. Without font:inherit, buttons
use the UA stylesheet font-family which causes text width measurement
mismatches and breaks the overflow detection for the ticker.
Restore quality badge to original values (padding 4px 6px, no
line-height override). Fix artist-link button: replace all:unset with
targeted button resets and width:100% so the button is constrained by
its parent and overflow/text-overflow/ticker actually work.
Set badge padding to 2px 6px with line-height 1.2 for proper text
breathing room. Add ticker scroll animation to the clickable artist
name on hover so long artist names scroll into view.
Pass artistId and onArtistClick to AlbumCard in HomeView, SearchView,
FavoritesView, LabelView, and AlbumDetailView. Add artistId to
AlbumCardData interface. Add onArtistClick prop to LabelView and wire
it from +page.svelte.
Add artistId and onArtistClick props. When both are provided, the
artist name renders as a button with hover highlight that navigates
to the artist detail page independently of the card click.
After first hover, title is stripped and stored as data-sys-tooltip.
Subsequent hovers need to match [data-sys-tooltip] since [title] no
longer exists on the element.
Global event delegation intercepts pointerenter on [title] elements,
strips the native title attribute, and shows a styled tooltip matching
QBZ's visual language. Handles viewport edge clamping with
top/bottom/right/left fallback positioning.
Back button now sits above the header icon/title as its own row,
pushing the header down — matching the pattern used in AlbumDetailView
and other detail views.
Save and restore scroll position for SearchView, AlbumDetailView,
ArtistDetailView, and PlaylistDetailView. Each view saves its
scroll position on scroll events and restores it via
requestAnimationFrame on mount. Most useful for the Search -> Album
and Search -> Artist back-navigation flow.
FavoritesView now has a conditional back button (shown when onBack
prop is provided). Added scroll position save/restore infrastructure
to navigationStore (saveScrollPosition/getSavedScrollPosition keyed
by ViewType). FavoritesView saves scroll position on scroll and
restores it when returning to the view.
Hide the square button background on hover when downloading (the
ring needs no bg). Also hide the download indicator completely when
idle (opacity 0 + pointer-events none) instead of showing a faint
ghost that triggers hover artifacts across all track row buttons.
The Lucide Loader icon has multiple line segments that create a
stroboscopic flicker when rotated. Replace with a clean SVG circle
progress ring: shows actual download progress via stroke-dashoffset
with smooth 300ms transitions, and a simple indeterminate rotation
when queued. No more flickering.
The DownloadButton in TrackRow receives onDownload from direct props,
not from menuActions. Continue Listening was only passing onDownload
inside menuActions (for context menu) but not as a direct prop, so
clicking the download icon did nothing. Pass onDownload and
onRemoveDownload as direct TrackRow props.
Frontend: yield to UI loop with tick() after setting search results
before running background download status checks (fire-and-forget
instead of blocking await). Backend: drop client mutex lock
immediately after HTTP request completes so parsing and blacklist
filtering don't hold the lock and block other commands.
Remove gapless button from NowPlayingBar and place it in
SettingsView under Playback section. The toggle is now sensitive to
blocking settings: auto-disables when ALSA Direct backend or
streaming-only mode is activated. Gapless itself never blocks other
settings. Added inline experimental label and descriptive tooltips
with proper i18n for all 4 locales.
Add a toggleable gapless button next to the repeat button in the
NowPlayingBar. The setting persists in audio_settings.db and controls
whether the audio thread pre-queues the next track for gapless
transitions. Includes i18n keys for all 4 locales.
Backend: Add PlayNext command to audio thread that appends decoded
sources to the existing Rodio Sink for seamless transitions. The audio
thread detects when ~5s remain and signals gapless_ready via SharedState.
Position-based transition detection updates track metadata when the
queued source begins playing.
Frontend: playerStore detects gapless_ready flag, invokes
play_next_gapless (cache-only lookup), and handles transitions by
advancing the queue without stop/play cycle. playbackService gains
gaplessTransition option to skip stop_playback when backend already
transitioned.
Gapless is only eligible when: same sample rate/channels, cached data,
Rodio backend (not ALSA Direct), not streaming, not a user skip.
Falls back to normal stop-play flow when ineligible.
Replace static ReplayGain-only normalization with real-time loudness
analysis that works with any audio source including Qobuz streams.
New components:
- DynamicAmplify: Source wrapper that reads gain from shared atomic
with 50ms linear ramp to prevent clicks on gain changes
- AnalyzerTap: Source wrapper that batches samples and sends them
to the analyzer thread via bounded channel (never blocks audio)
- LoudnessAnalyzer: Background thread computing EBU R128 integrated
LUFS, first measurement at ~10s, refinement every ~5s
- LoudnessCache: SQLite persistence for computed LUFS values so
replayed tracks get instant gain application
Pipeline when normalization ON:
Decode -> Diagnostic -> AnalyzerTap -> DynamicAmplify -> Visualizer
Pipeline when normalization OFF (bit-perfect, unchanged):
Decode -> Diagnostic -> Visualizer
Re-enable normalization toggle in NowPlayingBar UI.
ReplayGain-only normalization is not useful for Qobuz streaming
(streams lack RG tags). Icon will be re-enabled once EBU R128
real-time analysis is implemented.
Adds 3-state normalization icon between fullscreen and volume controls.
Uses mask-image SVGs for theme-compatible coloring. Reduces song card
min-width by 72px to accommodate the new control.
Add normalization_gain field to SharedState and PlaybackEvent so the
frontend can display whether normalization is actively applied to the
current track. The gain factor is stored as AtomicU32 (f32 bits) and
emitted with each playback:state event.
cc
Add ReplayGain-based volume normalization to the audio pipeline.
When enabled, extracts ReplayGain track gain/peak tags from audio
file metadata (Vorbis comments, ID3v2 TXXX) and applies a gain
factor via rodio's Amplify wrapper.
Pipeline order: Diagnostic (raw) -> Amplify (normalization) -> Visualizer
When normalization is OFF (default), no Amplify wrapper is inserted
and the pipeline remains 100% bit-perfect.
- New module: audio/loudness.rs (extraction + gain calculation)
- New settings: normalization_enabled, normalization_target_lufs
- New Tauri commands: set_audio_normalization_enabled/target
- Supports -14 LUFS (default), -18 LUFS, -23 LUFS targets
- Clipping prevention using peak metadata
- Gain preserved across resume/seek operations
cc
DAC badge tooltip now says "Active" instead of "Bit-perfect" since
PipeWire passthrough is best-effort, not guaranteed bit-perfect like
ALSA Direct hw:X,Y. Added a brief note in Settings when DAC Passthrough
is enabled clarifying the distinction. Also i18n'd the hardcoded
dacPassthroughTooltipOverride string in SettingsView.
Moved all hardcoded English/Spanish tooltip text to i18n keys
under audioBadges section in all 4 locale files.
Removed dead dacTooltip derived property (unused in template).
Lock-free OR-mask analysis of f32 samples converted to i32,
bypassing PipeWire processing entirely. DiagnosticSource wraps
the audio source at decode level for both rodio and ALSA Direct
paths. Tauri commands: start_bitdepth_capture / stop_bitdepth_capture.
cc
decideLaunchModals() was racing activate_user_session() because
setLoggedIn() triggers the $effect before the backend store opens.
Preferences defaulted to showWhatsNewOnLaunch=true, ignoring the
user's saved setting.
Add sessionReady flag set after activate_user_session completes.
The $effect now waits for session + home view before running.
Also reset all update state on logout for clean re-login cycle.
cc
+layout.svelte: $isLoading inside $derived() -> isReady() function
HomeView.svelte: $t() inside $derived.by() -> getGreetingText() function
These caused vite-plugin-svelte CSS extraction to fail, cascading
PostCSS errors to unrelated components during HMR.
+layout.svelte: $isLoading inside $derived() -> isReady() function
HomeView.svelte: $t() inside $derived.by() -> getGreetingText() function
These caused vite-plugin-svelte CSS extraction to fail, cascading
PostCSS errors to unrelated components during HMR.
ImmersiveHeader splitTabs used $t() inside $derived(), causing
vite-plugin-svelte CSS extraction to receive raw script content
instead of CSS — cascading PostCSS errors to unrelated components.
ShortcutInput had the same pattern for displayShortcut.
Both now use the labelKey/function pattern instead.
initUpdatesStore() set its initialized flag before the async preference
fetch completed. If get_update_preferences failed (backend store not
yet ready), preferences stayed at defaults (both true) and were never
re-fetched. This caused:
- "Show What's New" toggle appearing enabled despite being disabled
- "Check for updates on launch" toggle not persisting
- Changelog modal reappearing on every restart
Changes:
- Split initialized flag into versionLoaded/prefsLoaded so preference
loading retries until the backend store is ready
- Call refreshUpdatePreferences() in decideLaunchModals() to always
get fresh data before making modal decisions
- Call refreshUpdatePreferences() in handleLoginSuccess() after
session activation so SettingsView reflects saved values
cc
init_at() loaded credentials from the user-scoped SQLite cache but
never called restore_token() on the in-memory client. The client
remained empty, so get_status() always returned disconnected after
app restart. Also restore the enabled flag from persisted settings.
Fixes#54
cc
On release events pkgrel defaults to 1. On manual dispatch it can be
set to any value (default 1), allowing same-version rebuilds like
1.1.9-2 without editing the workflow.
initUpdatesStore used Promise.all for get_current_version and
get_update_preferences. Since preferences need the SQLite store
(only available after login), the failure killed both promises,
leaving currentVersion as empty string.
Split into independent calls so the version (a compile-time constant
that never needs a session) is always available regardless of
preference store state.
Remove LIBSQLITE3_SYS_USE_PKG_CONFIG and related env vars that forced
linking against system SQLite 3.37.2 on core22. The rusqlite bundled
feature compiles SQLite 3.38+ from source, which provides the
sqlite3_error_offset symbol needed by rusqlite 0.31.
The button previously failed silently when version was unavailable or
the GitHub API request failed. Now shows loading state while fetching
and a toast notification on failure.
The Svelte 5 CSS extraction breaks when t is used as a callback
parameter inside $derived() or $derived.by() blocks. Renamed to
track, item, or lt in PlaylistDetailView, FavoritesView,
PlaylistSuggestions, and PlaylistTagFilter.
Split import into two-step flow: Fetch (preview) then Import.
After fetching, user can edit the playlist name and optionally
assign it to a folder before importing. Backend already supported
name_override — this just exposes it in the UI. Also i18n all
new strings and existing hardcoded modal text.
When the user changes any audio setting (output device, exclusive mode,
DAC passthrough, backend type, ALSA plugin), the stream is reinitialized.
Previously playback would die and the user had to manually skip
forward/backward to recover. Now playback automatically resumes at the
same position after reinit, since the backend already preserves position
and audio data across ReinitDevice.