Clicking a track inside an expanded Mixtape/Collection item now plays
it. Qobuz albums and playlists build the queue directly; Plex and
local-library albums delegate to the backend collection-item resolver,
then jump the queue cursor to the clicked track.
cc
Expanded playlist items showed "No results found"; fetch their tracks
via v2_get_playlist. Also carry the remix/edition version field so
titles render consistently.
cc
Add playback/audio/graphics settings to the diagnostics snapshot via
v2_get_playback_preferences. No sensitive data (no username or paths);
gated by the existing diagnostics opt-in toggle.
cc
Qobuz Connect renderer mode and casting are mutually exclusive output modes. While qbz is registered as a QConnect renderer, the Qobuz cloud drives playback through the renderer-command path, which bypasses the cast path entirely — the cast device receives no LOAD and stays silent.
connectToDevice() now checks QConnect status and, if it is on, suspends it (v2_qconnect_disconnect) before connecting the cast device. When the cast ends, disconnect() restores QConnect only if it was on beforehand; a failed cast connection restores it too. Applies to Chromecast and DLNA alike.
Fixes#439.
cc
Version bumped across Cargo.toml, tauri.conf.json, package.json, snapcraft.yaml (version + source-tag), flake.nix, and the lockfiles. metainfo.xml gains a 1.2.13 release entry, the About modal codename becomes "400+ stars", and the README contributors list adds @Vudgekek and @DoubleGate.
cc
Session resume of a Qobuz track no longer forces the streaming start-position path, which buffered the CMAF stream sequentially from byte 0 up to the saved offset (up to ~30s wait, with no buffering indication).
v2_play_track is now always called without a start position. The track resumes at the saved offset only on a cache hit (format_id === null), where the audio is a full in-memory buffer and Seek lands like a local file. On a cache miss it starts from 0:00 instead of blocking on a sequential buffer fill.
No backend or audio-engine change; the start_position_secs streaming path is now unused.
cc
The graphics recommendation banner no longer writes settings: the Apply button and the v2_apply_graphics_recommendation command are removed, so the banner only detects and explains. The user applies the change with the real toggles, matching the HiFi Wizard's advisory model. write_recommendation stays for the qbz --autoconfig-graphics CLI.
The Force DMA-BUF help text is now GPU-aware: the instability warning only shows on NVIDIA-only systems; non-NVIDIA and hybrid hosts are told it is safe to enable. On hybrid hosts the rendering GPU dropdown marks the integrated GPU as recommended.
cc
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
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
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
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 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
Two interactions missing from the original banner landing:
- Dismiss. The banner was always-on while the recommendation differed
from persisted state — fine the first time the user sees it, but
annoying if they read it, decided to keep their tuning, and didn't
want the banner returning every time they opened the Graphics tab.
Adds a Dismiss button that writes the recommendation's rationale
string (joined) to a localStorage key as the dismissed signature.
The banner stays hidden as long as the currently computed
recommendation produces the same signature; if the environment
changes enough to switch matrix branches (different GPU vendor,
X11<->Wayland flip, VM detected), the signature mismatches and
the banner re-surfaces so the user can re-evaluate.
- Manual re-detect. Always-visible "Detect recommended settings"
link above the banner area runs detection on demand and clears
the dismiss flag. If the recommendation already matches the
persisted state (no diff) the user gets a success toast confirming
so — otherwise it would look like the button did nothing.
Apply now also clears the dismiss flag as a hygiene step — after Apply
the diff goes to false and the banner naturally hides, but the stale
signature would otherwise sit in localStorage waiting to suppress a
future legitimately-different recommendation.
i18n: 3 new keys per locale (dismiss / redetect / noChanges) across
en/es/de/fr/pt.
cc
The detection + decision matrix in autoconfig_graphics.rs already knew
the right combination for every (GPU vendor x display server x desktop)
case, but the only way to consume it was the qbz --autoconfig-graphics
CLI tool which most users never run. The recommendation sat invisible
while users hunted toggles by hand.
Lifts the same logic into the Settings UI as a persistent banner at
the top of the new Graphics tab:
- autoconfig_graphics: Environment and Recommendation structs become
public with Serialize, the three pieces (detect_environment,
compute_recommendation, write_recommendation) become pub so they
can be re-used. The existing apply_recommendation CLI helper now
delegates writes to the shared write_recommendation, keeping the
behaviour identical (and inverting the DMA-BUF mapping so that
rec.disable_dmabuf=false now writes force_dmabuf=true, matching
the 1.2.13 opt-in semantics).
- commands_v2/graphics_recommendation.rs (new): two thin Tauri
commands — v2_get_graphics_recommendation returns the environment
plus the computed recommendation so the frontend can compare it
to persisted settings; v2_apply_graphics_recommendation writes
the whole bundle in one call.
- Settings UI: the Graphics tab loads the recommendation on mount.
A $derived computes whether at least one recommended value
diverges from the four persisted settings (HW accel / force_x11 /
gsk_renderer / force_dmabuf with the inverted DMA-BUF axis).
When it does, a violet-accent banner appears above the profile
grid showing the detected environment, the rationale lines from
the matrix, and an Apply button. Persistent (no dismiss) — vanishes
the moment the persisted state matches the recommendation, no
second copy of state to keep in sync.
- i18n: 4 new keys per locale (title / detected / apply / applied)
across en/es/de/fr/pt. The rationale strings stay in English
because they're hardcoded in autoconfig_graphics.rs and reword
them per scenario would mean a structured i18n key per case —
not worth it for diagnostic copy that's read once and dismissed.
cc
Appearance was getting kilometric — themes, layout, immersive panels,
immersive FPS, and an embedded collapsible "Composition" subsection
(GDK / GSK / X11 / Wayland / DMA-BUF / env-var reference) all under one
tab. The Composition piece is its own business unit (GPU infrastructure
that affects whether the app runs, not how it looks), and burying it
inside a collapsible inside an unrelated tab made the recommendation
work in the next commit awkward to surface.
Moves the entire Linux-only Composition block to a new top-level
Graphics section. Flattens the collapsible wrapper since the whole page
is the section now — the collapse-to-summary affordance only made
sense when it was a sub-block. The Graphics nav entry is injected
dynamically after Appearance and only on Linux (matches the existing
gating; macOS/Windows have no GDK/GSK/DMA-BUF surface to expose).
What stays in Appearance: themes, fonts, layout density, immersive
background mode, immersive panels per-panel toggles, immersive FPS
preset. Those control what the user sees, not the GPU infrastructure.
Locale subtrees move with the section — settings.appearance.composition
becomes settings.graphics across en/es/de/fr/pt. Title and summary
strings rewritten for the new "Graphics" framing in each locale; the
~30 leaf keys (hardwareAcceleration, forceDmabuf, profiles, env-vars
reference, etc.) keep their existing translations and just change path.
The unused compositionCollapsed state var drops out as a side effect.
cc
Follow-up to #423. The new app-wide user-select: none rule covers
toasts too, which is correct for ephemeral info toasts but loses
copy-paste from error toasts — those carry track ids, file paths,
and backend error strings that we routinely copy into issues and
diagnostics.
Adds .selectable to the toast container so all toast types stay
copyable. Trade-off accepted: a stray drag-to-select gesture on a
success toast won't initiate a window drag, but error visibility
matters more than that edge case.
cc
Today the startup orchestration skips the local session restore whenever
qconnectRuntimeActive — defensive against the visual flicker of seeing
the last local queue and then watching QConnect overwrite it. That left
QConnect users staring at "No track playing" while the WebSocket
bootstrapped, and the queue was effectively unrecoverable if QConnect
failed to push state.
Inverts the policy per #315: always restore the local snapshot. When
QConnect is bootstrapping at launch, the restore is held back behind a
1s flicker guard timer. The QueueUpdated event listener cancels the
timer if the remote queue snapshot arrives first — the user sees only
QConnect state, no flicker. If 1s elapses without a remote snapshot
(slow network, server hiccup, controller offline), the timer fires and
local restore proceeds so the user isn't stuck.
Two related wires:
- restoreLastView runs unconditionally now (view restore is unrelated
to playback source, shouldn't be gated on QConnect).
- The QConnect listener also calls debouncedFullSessionSave after a
successful QueueUpdated sync, covering the case where QConnect
reorders/replaces upcoming tracks while the current_track stays the
same — the track-change effect wouldn't fire, and without this hook
the local snapshot would drift behind remote.
Acceptance from issue #315: queue persistence on every mutation
(prior commit), QConnect state read at startup (already wired via
bootstrap_remote_presence → AskForQueueState → QueueStateChanged),
QConnect wins on divergence (cancel-guard makes this the typical
path), local-only path unchanged when QConnect is off (else branch).
The auto-advance / delete-timing guard logic in corebridge.rs and
commands_v2/playback.rs is untouched — the change lives in the route
component's restore orchestration, not in the QConnect runtime.
cc
The PersistedQueueTrack struct already had streamable and parental_warning
fields but the load path hardcoded streamable=true and parental_warning
=false because the columns didn't exist in queue_tracks. source_item_id
_hint was missing entirely.
Adds the three columns via the same idempotent ALTER-TABLE pattern the
existing migrations use, persists them in save_session, and reads them in
load_session. PersistedQueueTrack gains source_item_id_hint as an
Option<String> with serde default so saves from older builds deserialize
cleanly.
User-visible effect: a restored queue now keeps explicit-content badges,
unstreamable visual state, and Mixtape/Collection source-item ids
matching the live queue instead of resetting to defaults at every
launch. Backwards-compatible: columns get safe defaults for older rows
(streamable=1, parental_warning=0, hint=NULL) so users upgrading from
1.2.12 don't see a regression.
cc
The local session restore today saves the full queue only on track-change
(debounced 2s), pause, clear, the 15s playback tick, and app-close. Pure
queue mutations — add/addNext/setQueue/clearQueue/moveQueueTrack/
removeAfter/stop-after-consume — left the persisted queue stale until the
next track boundary, which meant a crash mid-album lost any tracks
queued after the playing one.
queueStore exposes registerQueueMutationObserver so the route component
can plug debouncedFullSessionSave into the mutation path without the
store knowing about session-save internals. Each mutator that funnels
through syncQueueState now also calls notifyMutation, the observer
coalesces via the existing 2s debounce, and the SQLite write happens on
the same path the track-change save already uses (single source of
truth, idempotent on rapid mutations).
The cursor-only mutators (next/previous/playQueueIndex) intentionally
do NOT notify — they are already covered by the track-change effect at
+page.svelte:5055 which saves on current_index change.
cc