After the app-wide `user-select: none` on body, content the user
legitimately needs to copy stopped being selectable: HiFi Wizard
commands and paths, snap/flatpak/Pulse/PipeWire setup blocks in
Settings, graphics recovery commands, environment-variable hints, log
viewers, keyboard shortcut chips.
The HTML5 elements that wrap literal text — `pre`, `code`, `kbd`, `samp`
— are already the right escape hatch and every wizard/settings/logs
block in the codebase uses them. Adding a single rule alongside the
existing form-input exception restores selection everywhere it matters
without scattering `.selectable` classes through the views.
cc
The compact 220x220 cards in Discovery V2 truncate long album titles in
the metadata row. With the marquee scroll removed there was no way to
read the full title without opening the album detail.
Pipe the title and artist through a native `title` attribute on the
text-stack wrapper; the global SystemTooltip already mounted in the
layout intercepts that and renders the custom tooltip with adaptive
positioning. Shows the full "Title — Artist" string regardless of
truncation, no extra component, no per-card hover state.
cc
`/favorite/getNewReleases` includes albums whose stream availability is
still in the future, so they appear in the Release Watch grid even
though `get_album` 404s as soon as the user clicks them. Same shape for
regional restrictions and label takedowns surfaced via `streamable=false`.
Add `release_date_stream` and `streamable` to the Album struct (both
optional with serde defaults so existing call sites stay happy) and
filter the response inside `v2_get_release_watch` after the artist
blacklist pass: skip any album marked `streamable=false` or whose
`release_date_stream` is strictly greater than today (UTC). The total
count gets adjusted alongside so pagination math stays consistent.
cc
The TrackRowLite click handler was constructing a DisplayTrack with only
the fields shown on the row (id, title, artist, album, albumArt), masked
by an `as DisplayTrack` cast. duration, durationSeconds, hires, bitDepth,
samplingRate and isrc never reached the player, so the NowPlayingBar
showed no time and the seekbar was unusable. The legacy HomeView path
(home:continue_listening) also wired setPlaybackContext +
replacePlaybackQueue before the play call; the discovery-v2 rewrite
dropped both, which left a stale context label and a stale queue after
clicking a track from the section.
Backend already returns the full TrackDisplayMeta (duration_seconds,
hires, bit_depth, sampling_rate, isrc); the wire and the display types
just weren't carrying it. Extend RecoTrackDisplayMeta and
DiscoveryTrackCard with those fields, propagate them in
fetchHomeResolved, and rebuild the click handler to set the
home:continue_listening context and replace the queue with the full
section before emitting onTrackPlay with a complete DisplayTrack.
cc
Bundle token extraction requires the network. When the host is fully
offline at startup, the failure used to propagate out of CoreBridge::new
and leave the bridge as None, which made every queue/playback V2 command
panic at `bridge.get().expect("CoreBridge not initialized")`.
QbzCore::init() now treats bundle extraction as best-effort: on failure
the core still finishes initialization, the queue manager and player stay
usable for local/offline playback, and API methods return
CoreError::NotInitialized cleanly instead of crashing the runtime. Adds
is_api_initialized and try_init_api so callers can rebuild the API
client lazily once the network returns.
cc
Two-pronged improvement to the graphics safety net for users hitting
the NVIDIA + Wayland + WebKit GBM issue:
1) New opt-in toggle 'NVIDIA Wayland compatibility (experimental)' in
Settings > Graphics. When ON:
- Sets __NV_DISABLE_EXPLICIT_SYNC=1 to bypass libnvidia-egl-gbm's
buggy implicit-sync paths that produce "Failed to create GBM
buffer of size NxN: Invalid argument" on driver 550+.
- Lifts the defensive compositing-mode shutdown that QBZ applies
to NVIDIA-only Wayland sessions — the user is signaling the real
fix is now in place, so the safety net is no longer needed.
- Persisted in graphics_settings.db column nvidia_compat_mode (new,
default off). Migrated via ALTER TABLE so existing installs don't
pick up a behaviour change.
2) Reset-graphics CLI affordance, surfaced only when the toggle is on:
inline copyable input + Copy button rendering `qbz --reset-graphics`
(or `flatpak run com.blitzfc.qbz --reset-graphics` based on detected
install method). Works around the global select-disable elsewhere
in SettingsView via user-select: all + onclick={e.select()} + the
explicit button. The boot watchdog still auto-reverts on crash, but
this is the manual escape hatch when the user wants to recover
without waiting for the watchdog to fire.
3) Watchdog recovery now lands in full CPU mode after ANY revert.
The single-knob revert (hw_accel / dmabuf / preferred_gpu) still
narrows the cause for the UI banner, but the safest post-crash
state is full CPU mode (HW accel off + DMA-BUF off + preferred_gpu
reset to auto). A partial revert can still hit the next crash if
the failure was stack-wide (driver bug, missing extension); going
all-the-way-CPU guarantees the user always reaches a paintable
window after a recovery — they can selectively re-enable from the
Settings banner.
4) preferred_gpu='software' now implies hardware_acceleration=off at
env-var application time. Forcing Mesa llvmpipe while keeping
WebKit GPU compositing on would just round-trip software pixels
through a "GPU" path that has no GPU — nonsensical configuration
that the watchdog had to clean up after. Now it's prevented at
source.
i18n keys (en/es/de/fr/pt): settings.graphics.nvidiaCompatMode,
nvidiaCompatModeDesc, resetCmdTitle, resetCmdDesc;
actions.copiedToClipboard.
cc
GitHub Security advisory for file-type < 21.3.1: malformed ASF input
with a zero-size sub-header triggers an infinite loop in the ASF parser
(medium severity). Our exposure is transitive — node-vibrant → jimp →
file-type — and only reached when extracting palette colors from album
artwork. Trusted Qobuz CDN + user-local library limit the practical
attack surface to near zero, but the alert is real and easy to close.
The upstream fix is a 6-line position-advance guard in core.js
(commit 319abf87). Cleanly backports to our 16.5.4 with identical
semantics. Applied via patch-package so the change survives every
`npm install` and is visible in the repo.
Direct upgrade to 21.3.1 wasn't viable: file-type went ESM-only at
17.0.0, and our jimp 0.22.x chain still uses CommonJS `require()` —
an override would have broken vibrant at runtime. Patch-package is
the right tool when upstream's only fix lives behind a breaking
module-system change.
Added `postinstall: patch-package` to package.json so the patch
applies automatically on fresh installs.
cc
DiscordIpcClient::new became infallible in 1.0 — it now returns Self
directly instead of Result<Self>. Failures surface at connect() time
where the catch already lives. Removed the now-spurious `if let Ok()`
wrap around the constructor.
Closes#428 (applied manually because the bump needed a code change
that Dependabot can't author).
cc
Three bugs were stacking up to make session resume drop the saved offset
for streamed Qobuz tracks:
1. Position save was debounced, not throttled. Each playback tick reset
the 5s timer, so the save never fired while a track played
continuously — the persisted position froze at whatever value got
written during the first quiet window after track start (typically
~2s). Switch to throttle: fire the first call immediately, then once
per 5s window, with a tail call to flush the latest value when the
window ends.
2. togglePlay tried to apply the restore offset via a separate cmdSeek
right after cmdPlayTrack. For Qobuz streaming the audio thread's
Seek handler bails when current_audio_data is unset — streaming
uses current_streaming_source instead — so the seek was silently
dropped and the track started at 0:00. Pass start_position_secs
directly to v2_play_track. The streaming backend (legacy + V2
player) waits for enough buffer to cover the offset plus 8s
headroom, then eagerly decode-and-discards decoder output up to
the offset before engaging the audio engine — no race, no
first-pull underrun. Plex and local tracks keep the old play+seek
path; they load full audio_data so Seek works as-is.
3. The seekbar showed 0:00 from app launch until the user hit play,
even when a saved position was queued. setPendingSessionRestore
now pre-positions currentTime when positionSecs > 0 so the UI
reflects "paused at X" the moment the app opens.
Other plumbing:
- AudioCommand::PlayStreaming carries start_position_secs and
content_length (used to estimate bytes-per-second for the resume
buffer target).
- Player::play_streaming{,_dynamic} grow a start_position_secs
parameter; QConnect and api_server callers pass 0 (remote-driven
playback always starts fresh).
- cmdPlayTrack forwards startPositionSecs as a 5th optional arg.
cc
Solves the long-running performance complaints (especially Optimus
laptops where the dGPU silently hijacks WebKit). The end-to-end shape:
- graphics_detection enumerates physical GPUs from /sys/class/drm and
cross-references EGL vendor JSONs under /usr/share/glvnd/egl_vendor.d
to expose only stacks the running process can actually use. Works
inside Flatpak/Snap because /sys is still mounted read-only and the
sandbox-visible EGL vendor JSONs are the right thing to read.
- preferred_gpu column in graphics_settings.db (string: auto, integrated,
discrete, software, or a specific PCI slot id). Migrated with
ALTER TABLE ... DEFAULT 'auto'.
- env_vars_for_preferred_gpu returns Set/Unset actions applied at startup
before WebKit init. Integrated/AMD path explicitly unsets
__NV_PRIME_RENDER_OFFLOAD so KDE/Optimus sessions that pre-set it to 1
no longer override the EGL pin and yank rendering back onto NVIDIA.
- Auto resolver: on hybrid systems (iGPU + dGPU both usable), Auto pins
to the iGPU. Single-GPU and no-GPU systems remain no-op. The dropdown
surfaces what Auto resolved to ("Auto (Intel Graphics)") so the user
sees the decision without picking explicitly.
- boot_watchdog writes a pending.json before WebKit init listing the
risky knobs in effect. Frontend calls v2_mark_boot_succeeded after
first paint to clear it and promote to last-good.json. If the next
launch sees pending without a success ack, it blames matching fields,
reverts them in-DB, and increments per-knob failure counters; two
consecutive failures lock the knob until the user clears it.
- Settings > Graphics gets a Rendering GPU dropdown (between HW accel
and DMA-BUF) and a crash-recovery banner that lets the user re-enable
any auto-disabled knob. Restart-required affordance shared with the
other risky toggles.
- i18n keys for the dropdown, banner copy, and autoWithTarget label
across all five locales.
cc
180x180 card with a 114x114 artwork inset, palette extraction for the
background gradient and mix-blend-mode multiply on the PNG bezel so the
station artwork sits in front of a cohesive colored frame. RADIO label
uses padding-left compensation to align with the visible bezel edge,
not the raw container. Hover overlay blurs the artwork and surfaces a
play button — same pattern as the iOS reference shipped in c8cee326.
cc
autoconfig_graphics gains a hybrid Intel/AMD + NVIDIA branch that keeps
GSK renderer on Auto and DMA-BUF off so the iGPU handles compositing
without the dGPU getting dragged in. detect_gpu_name is exposed and
labels hybrid setups explicitly (e.g. "NVIDIA (driver 580) + Intel
(i915)"). RuntimeDiagnostics gets runtime_gpu_name and
runtime_desktop_environment so the Diagnostics panel can show what the
app sees at runtime, which is the first thing useful to triage
performance reports from log uploads.
cc
The Library Albums tab carried three different quality-badge designs
across three rendering paths (grid card, list row, album detail
header), each rendered as raw text — visually disconnected from the
QualityBadgeStatic that Mixtape and Collection lists already use, and
the Plex source icon was inconsistent (present on grid cards, missing
on list rows and the album detail header).
Three coordinated swaps so all surfaces speak the same visual
language:
- AlbumCardLibraryLite (Library grid + Artist Detail tile). The
custom .quality-pill text badge is replaced by
<QualityBadgeStatic iconOnly>, matching the Discovery V2 layout —
icon-only square sitting to the right of a two-line title/artist
stack. Adds format / bitDepth / samplingRate props so the badge
can render its real tooltip ("FLAC: 16-bit / 44.1 kHz") instead
of falling back to the parsed quality string.
- VirtualizedAlbumList list mode. The plain-text .quality-badge
span is replaced by <QualityBadgeStatic> full mode (the
Mixtape/Collection badge), plus an inline <SourceBadge> when
showSourceBadge is true so Plex / Qobuz Purchase / Qobuz Download
surfaces are recognizable in the row. resolveSourceBadge maps
the LocalAlbum.source string onto the narrow
LibrarySourceBadge enum the card prop expects.
- LocalLibraryView album detail header. The audio-specs strip now
ends with a <SourceBadge> tied to selectedAlbum.source. Pairs
naturally with the FLAC / 16-bit / 44.1 kHz pills already there
and finally lets the user spot at a glance whether the album
they're looking at came from a network Plex server or local
disk.
Call sites in LocalLibraryView (the renderMetadataAlbumCell snippet
and the Artist Detail tile loop) pass album.bit_depth and
album.sample_rate / 1000 alongside the existing quality string so
the badge tooltips read accurate numbers. VirtualizedAlbumList
forwards the same trio to its inner AlbumCard.
cc
The Plex aggregation CTE used MAX(codec)/MAX(bit_depth) directly,
which produced two bugs that surfaced in the Albums grid for many
Plex-only libraries:
- format = "Unknown". Plex frequently omits the stream-level codec
when its analyzer hasn't seen the file yet. MAX(NULL) collapsed
the row's format to NULL, which the frontend parser mapped to
AudioFormat::Unknown — the Library grid then showed UNKNOWN even
though the file's container was clearly "flac" / "mp3".
- bit_depth = NULL. Plex commonly omits bitDepth on older releases
entirely; the album row inherited the gap and the UI rendered
"--/44.1" next to an otherwise valid quality string.
Two fallbacks applied to the plex_aggregated CTE only — the local
CTE is untouched because local indexing always writes a non-null
format string and a real bit depth via Symphonia:
- format: COALESCE(MAX(codec), MAX(container)). The `container`
column is populated for the same media and typically carries the
same value ("flac", "mp3"), so it's a safe fallback when codec
is fully NULL across the album.
- bit_depth: COALESCE(MAX(bit_depth), CASE WHEN <lossless> AND
sample_rate <= 48000 THEN 16 ELSE NULL END). Lossless formats
at CD-range rates are virtually always 16-bit redbook, so we
default to 16 in that narrow window. Higher rates stay NULL so
we don't lie about 24 vs 32. The per-track view shows the real
value when the user clicks in (lazy hydration via
plex_get_track_metadata).
cc
The bottom-right song card (artwork + title + album + artist + quality
badge) was duplicated inline in nine visualizer panels — LineBed,
CometFlow, NeonFlow, EnergyBands, Oscilloscope, Lissajous,
SpectralRibbon, TransientPulse, and TunnelFlow. Each carried its own
HTML structure and ~90 lines of identical CSS for the song-card
chrome. Any visual tweak (badge swap, layout shift, album/artist
inline join, artwork sizing) had to be replayed nine times and was
easy to miss one — the recent LineBed-only inline-album·artist change
exposed exactly this trap.
Extracts the song card into src/lib/components/immersive/Immersive
SongCard.svelte, which owns its own absolute positioning, the
two-line text stack with the new "album · artist" subline, the
QualityBadgeStatic mount, the 72px artwork thumb, the explicit-badge
overlay, and the responsive breakpoint. Each consumer panel now drops
in `<ImmersiveSongCard {artwork} {trackTitle} {artist} {album}
{explicit} {quality} {bitDepth} {samplingRate} {format} />` instead
of carrying its own copy.
Net effect: 268 insertions across all ten files vs 948 deletions of
duplicated markup and CSS — and the next visual change touches one
file, not ten. SuggestionsPanel uses a totally different layout
(queue list) and is intentionally NOT migrated.
cc
JavaScript bitwise shift operates on 32-bit signed integers, so
`1 << 48` is `1 << (48 % 32)` = `1 << 16` = 65536. Every Qobuz track
id sits above that, so the existing `id >= EPHEMERAL_ID_FLOOR` checks
were classifying every Qobuz track as ephemeral and disabling the
favorite / playlist / mixtape / collection actions for them — most
visibly in the immersive player favorite button, but also silently
stripping Qobuz tracks from the persisted session queue.
Five places shared the same broken constant, all now use `2 ** 48`
(281474976710656) — the actual ephemeral id floor defined in
src-tauri/src/ephemeral_library/mod.rs:
- src/routes/+page.svelte (two metadataActionsDisabled checks for
the immersive player and now-playing bar, plus the session-save
EPHEMERAL_ID_FLOOR filter).
- src/lib/components/QueuePanel.svelte (isEphemeralQueueTrack guard
that hides favorite / playlist / track-info actions for queue
rows).
- src/lib/stores/playerStore.ts (isEphemeralTransition flag in the
gapless carve-out and CUE virtual-track position translation).
- src/lib/components/views/LocalLibraryView.svelte (close-folder
cleanup check — only ephemeral folders should wipe the queue +
now-playing slot when closed).
Each call site keeps a short comment naming the trap so future edits
don't reach for the same bitwise expression.
cc
CSS-only flex stretch + aspect-ratio collapsed instead of sizing the
thumb against the track-meta column — the previous two attempts
landed with no visible change (the thumb stayed near its old fixed
72px or fell back to image-intrinsic). aspect-ratio interacts poorly
with align-self: stretch when the item has no natural width to drive
the stretched dimension off.
Drops the CSS-only attempt. Binds clientHeight on .track-meta with
Svelte's bind:clientHeight, derives 98% of that into artworkSize, and
applies the result as inline width/height on the thumb. Reactive —
when the badge or text lines re-flow, the column height changes and
the thumb resizes with it. No fallback fixed size needed because the
{#if artwork && artworkSize > 0} guard avoids the first-paint flash
before the bind resolves.
cc
The previous attempt wrapped the thumb in a flex container with the
inner thumb at height: 98% — percentage height didn't resolve
because the wrapper's own height was content-determined, leaving the
inner thumb at near-zero and falling back to natural width. Visual
result: artwork stayed near the old fixed size, much smaller than
the column.
Drops the wrapper, sets align-self: stretch directly on the thumb so
it inherits the column height through the parent's align-items:
stretch chain, and uses aspect-ratio: 1 with auto width so the thumb
stays square. The 2% breathing room comes from margin-block: 1px,
which subtracts 2px from the stretched height in a way percentage
sizing couldn't reach.
cc
The bottom-info song card on LineBed pairs a four-row column (title +
album + artist + quality badge) with a square artwork thumb. With the
quality badge added the column grew to ~100px while the thumb stayed
at its old 72px fixed size, breaking the visual weight balance — the
column dominated the card and the thumb read as an afterthought.
Wraps the thumb in a flex container that stretches to the column
height, then sizes the thumb to 98% of that height with aspect-ratio
1 so it stays square. The 2% breathing room reads as a deliberate
gutter rather than a flush match, and the thumb sits vertically
centered against the column instead of pinned to either edge.
The fixed 72/56 dimensions and the mobile media-query override drop
out as a side effect — the thumb now scales with the actual content
height, so the breakpoint clamp isn't needed.
cc
Visual check on real cards showed 8px was overcorrecting — the framed
icon ended further left than the visible Hi-Res mark instead of at
the same X. Halving the gutter lands closer to where the SVG's
transparent right space actually ends.
cc
The Hi-Res SVG canvas is 48.4x48.4 square but the visible "Hi-Res
AUDIO" mark occupies only ~65% of the width — there's permanent
transparent gutter to the right that we can't strip without editing
the SVG asset. That made the framed (CD/MP3) variant look like it
sat further right than the Hi-Res variant when both ended their
containers at the same X.
Two tweaks so both badges terminate at the same visual X:
- Hi-Res shrinks 1px in height (proportional): 46x30 -> 44x29.
Closer to the visual weight of the framed icon once both share
the same baseline.
- Framed gains margin-right: 8px, matching the SVG's transparent
right gutter so the visible icon ends at the same horizontal
position as the visible Hi-Res mark.
cc
The Discovery V2 cards rendered quality as a custom yellow gradient
pill underneath the title — visually disconnected from the badges used
in Mixtape and Collection lists, ugly at small sizes (the Artist
Spotlight section was the worst offender), and not honoring the same
hover tooltip pattern users are used to from the rest of the catalog
surfaces.
QualityBadgeStatic gains an `iconOnly` mode that mirrors Qobuz's web
client: for Hi-Res tier the badge renders the brand SVG bare (no
surrounding chrome — the logo carries its own); for CD / MP3 the
format icon sits centered in a small framed square the same height
as the Hi-Res mark. Hover surfaces the same tier + bitrate tooltip
as the full badge.
AlbumCardLite restructures the post-cover area into a single meta-row:
a two-line text stack (title + artist) on the left with truncation,
the icon-only badge on the right vertically centered across both
lines. The card's parent gap (4px) applies once between the cover and
this row, so the badge inherits the same separation from the album
art that the text already had.
The data adapters in discovery-v2/data.ts now propagate bitDepth and
samplingRate alongside the existing quality string / isHiRes flag so
the badge tooltip reads "Hi-Res: 24-bit / 96 kHz" with real numbers
instead of defaults parsed back out of the quality label.
Popular Albums uses the AlbumRowLite row layout and explicitly drops
the badge — the row already carries the play action and rank cell,
adding a quality mark crowded the line without earning its space.
AlbumRowLite loses its quality / isHiRes props as a cleanup since the
single consumer (DiscoveryView's mostStreamed snippet) no longer
passes them.
Sizing: Hi-Res 46x30, framed 30x30 — both square-ish, both 30px tall,
so they line up cleanly under each other when grids interleave both.
cc