Commit Graph
5444 Commits
Author SHA1 Message Date
vicrodh e71f0ded46 perf: stop immersive atmosphere animation while paused
The atmosphere mount in ImmersiveView kept the default animated:true, so
the 4-layer warped background forced full-window redraws at display rate
even with playback paused. Gate it on NowPlayingState.playing (the other
three mount sites already pass animated:false).

cc
2026-07-01 18:05:14 -06:00
vicrodh f37b6b6ab5 perf: bound decoded-artwork memory, decode covers at display size
- DECODED_PIXEL_CACHE is now byte-budgeted (48 MB, 24 MB on <8 GB RAM
  hosts) instead of a 256-entry cap that could silently hold ~370 MB of
  1000px now-playing decodes.
- Route the full-resolution Image::load_from_path callsites (offline
  manager/favorites, local library ephemeral albums, MyQBZ, playlist
  covers) through the existing decode-to-display-size pipeline.
- Cap Home playlist cards at 40 (was 100).

cc
2026-07-01 18:05:14 -06:00
vicrodh d7716d7c67 perf: skip idle now-playing pushes, move palette work off the UI thread
- The 450ms poll pushed ~10 NowPlayingState properties every tick even
  with nothing playing; Slint properties have no equality guard, so this
  forced a full-window repaint ~2.2x/s forever. Snapshot-compare the
  pushed values and skip the event-loop hop when nothing changed (poll
  cadence untouched - QConnect reporting and gapless detection depend
  on it).
- Palette/atmosphere extraction ran on the UI thread with four full
  pixel-buffer copies per track change; do one shared downscale off the
  UI thread and only set the results in the event loop.

cc
2026-07-01 18:05:04 -06:00
vicrodh dbe818c7d3 perf: fix shader-underlay leak, idle 30Hz wakeups, per-frame GPU churn
- Gate the shader drain on the immersive view being OPEN, not just
  shader-mode > 0: the three close handlers never reset shader-mode, so
  a picked shader scene kept rendering a full-res pass at 30fps forever
  after closing the immersive view.
- Start/stop the drain timer from set-enabled instead of running it 30x/s
  for the app lifetime; park the FFT producer thread on a condvar while
  the tap is disabled.
- Compute the 5 energy bands in one spectrum pass (was two identical
  scans) and reuse the waveform buffer.
- Shader underlay: rotate a small pool of render targets instead of
  creating a fresh 2560x1440 texture every frame (~440 MB/s alloc churn),
  clamp the target to the window physical size, and build pipelines
  lazily on first use instead of compiling all six WGSL shaders at
  startup.

cc
2026-07-01 18:05:04 -06:00
vicrodh cafd521b0f fix: keep Linux binary compatible with glibc older than 2.43
glibc 2.43 re-versioned six libm float functions (sinhf, coshf, atan2f,
acosf, asinf, acoshf); a binary linked on such a host requires
GLIBC_2.43 and refuses to start on older distros. Wrap each symbol via
-Wl,--wrap to an f64-based shim in glibc_compat.rs so the binary only
needs the ancient symbol versions (runs on glibc >= 2.39).

cc
2026-07-01 18:04:52 -06:00
vicrodh e3a7531ccb build: raise cross container memory ceiling above qbz_ui demand
--memory-swap=26g OOM-killed the ~30-33 GB qbz_ui rustc. Bump to 24g RAM /
48g total — above the demand, still well under the host's 30G+46G so a
runaway is caught without freezing the box.

cc
2026-06-30 15:55:22 -06:00
vicrodh 04cbd0b627 build: mount static/ into the cross container for qbz-ui fonts
qbz-ui's Slint build imports the lyrics fonts from the repo root
(../../../../static/fonts/*.ttf), which is outside the crates/ workspace
cross mounts — so the cross build couldn't find them. Mount static/ at its
host path via CROSS_CONTAINER_OPTS.

cc
2026-06-30 15:53:30 -06:00
vicrodh 5fdbdb3ab3 build: fix aarch64 cross — newer image + xenial pkg name
cross 0.2.5's default aarch64 image is Ubuntu 16.04 (xenial): libfreetype-dev
doesn't exist there (it's libfreetype6-dev) and its OpenSSL 1.0.2 / archived
repos break modern deps. Pin the :main image (Ubuntu 20.04+) and use the
libfreetype6-dev name (valid on both).

cc
2026-06-30 15:45:41 -06:00
vicrodh b89ee385b3 build: aarch64 Linux build script (foundation for the Slint ARM workflow)
Native (on an ARM runner: apt deps + cargo build -p qbz) and cross (from
x86-64 via cross + Cross.toml arm64 sysroot) paths, auto-selected by host
arch. Documents the ~30 GB qbz_ui RAM wall (8 GB Macs / 4 GB Pis cannot build
natively). Cross path is best-effort/untested pending a first run.

cc
2026-06-30 15:38:11 -06:00
vicrodh bf20144622 fix(hotkeys): solid arrow glyphs + centered reset-all icon
The thin Unicode arrows (left/right/up/down) were nearly headless at keycap
size even after the font bump, so the arrow-key shortcuts use solid triangle
glyphs now. The footer Reset All to Defaults icon rode high next to its label
(a bare fixed-size icon in the row) — wrapped in a centered column.

cc
2026-06-30 15:26:37 -06:00
vicrodh fe7abd6e32 fix(header): wire the Documentation menu item to the project Wiki
The hamburger "Documentation" entry had no clicked handler, so it did
nothing. Point it at github.com/vicrodh/qbz/wiki via the generic
external-link handler.

cc
2026-06-30 14:51:00 -06:00
vicrodh 6e1ea24d63 feat(hotkeys): three-column customize editor + larger keycap glyphs
The editable Customize Shortcuts modal now lays its categories across three
columns (extracted KbEditGroup, reusing the cheatsheet's column models) so it
matches the cheatsheet and avoids a tall scroll; panel widened to fit. Keycap
text bumped to 15px in both modals so the arrow-key glyphs read clearly.

cc
2026-06-30 14:51:00 -06:00
vicrodh 7002e7773e fix(about): credit Rust, drop Kawarp, crisp logo, tighter disclaimer
- Acknowledgments now lead with Rust (the app's core, previously unmentioned)
  and drop Kawarp, which is no longer used (es/de/fr/pt updated).
- Logo renders from a 128px source instead of the 512px original scaled to
  56px, which aliased badly (femtovg has no mipmaps).
- Qobuz trademark notice dropped to 11px so it wraps to two lines, not three.

cc
2026-06-30 14:51:00 -06:00
vicrodh a7681945e9 fix(hotkeys): suppress single-key hotkeys while typing in LineEdit fields
The window-level hotkey handler only suppressed single-key shortcuts
(Space/q/?/1-5) when UiFocusState.text-input-focused was set. The ~24
std-widget LineEdit fields across 14 modals/settings never set it, so those
keys fired (and were swallowed) mid-typing. Each now mirrors its focus via
`changed has-focus`; import added where missing.

cc
2026-06-30 14:08:01 -06:00
vicrodh 1a9c132760 feat(hotkeys): lay the shortcuts cheatsheet out in three columns
The read-only shortcuts modal listed every category in one tall column that
scrolled. Split the groups round-robin into three column models (Rust) and
render them side by side; the panel widened to fit. The editable customize
editor still uses the full single-column list.

cc
2026-06-30 14:07:50 -06:00
vicrodh e44aa07acf feat(startup): restore the exact last view, not just top-level
"Where you left off" only restored five id-free top-level surfaces, so
browsing Discover or any album/artist always reopened on Home. Persist the
full nav destination (serde-encoded NavEntry in ui_prefs.last_nav) and
restore it via apply_entry — album/artist/playlist/mix/label/etc. re-fetched
by id. Online restores the exact view; offline keeps the local top-level
fallback; Search/Settings are not persisted; Home stays the safe root.

cc
2026-06-30 14:07:44 -06:00
vicrodh 7a31988767 feat(player): show album release version in now-playing + MPRIS
The now-playing bar, MPRIS metadata, and the desktop notification showed
the clean album name; for a re-edition (e.g. Octavarium (2009 Remaster))
the variant was lost. Carry the album-level version on QueueTrack
(populated on the album-play path) and append it to the displayed album.
Last.fm scrobbling keeps the CLEAN album name so editions don't fragment
stats. New album_version field defaults to None on every other queue source.

cc
2026-06-30 14:07:33 -06:00
vicrodh 8e02fecd27 feat(album/search/artist): show release version in album titles
Albums with a distinct version (remaster, Hi-Res re-edition, deluxe, etc.)
rendered as identical duplicates because the version was dropped. The web
player appends it in parens; mirror that via format_album_title across the
discography (map_release), search (map_album), the album header (map_album),
and the shared card mapper (suggestions/label/favorites). Verified against
live API: some variants carry the suffix in title (version null), others in
the version field — appending only when present matches the web player with
no double suffix. Add the version field to the Album model.

cc
2026-06-30 13:14:07 -06:00
vicrodh 9b9bffbf64 feat(album): Last.fm similar-albums carousel under suggestions
Second album-page row sourced from Last.fm: the album's primary artist ->
artist.getSimilar -> one top album per similar artist, resolved to Qobuz and
deduped against the Qobuz /album/suggest row (by artist|title and by id).
Gated on Last.fm being connected; resolved row cached per album for 30 days
(re-opens make zero API calls; only non-empty results cached so a transient
failure re-fetches). New build_similar_albums_seeded in qbz-external-reco.
reset_album now also clears this row so the previous album's recommendations
don't linger while the next loads.

cc
2026-06-30 13:13:50 -06:00
vicrodh 8c18287577 fix(album): single-click album context menus
The album header (⋯) and cover-art PopupWindows used the default
close-on-click policy, which races the row's TouchArea — the popup can
dismiss on the press before the item's clicked fires, so some rows (e.g.
Share Qobuz link) needed a second open+click. Switch both to
close-on-click-outside and close explicitly per row, matching the existing
Sidebar/QueueSidebar fix.

cc
2026-06-30 13:13:39 -06:00
vicrodh 9e2a1d1266 fix(models): tolerate missing pagination fields in album page
/album/suggest returns a page with only {limit, items} (no total or
offset). SearchResultsPage required them, so the whole AlbumSuggestResponse
failed to deserialize and the album Suggestions carousel silently never
showed. Default the pagination scalars so only items is required.

cc
2026-06-30 13:13:31 -06:00
vicrodh 0ddcad40a6 fix(album): credit composer from album-level field, not per-track
The header credit line derived composers by scanning every track's
composer, which over-credited songwriters on non-classical albums
(e.g. Anthrax showed Spitz/Bello/Benante). The official web player
instead uses the single album-level composer field and suppresses the
Various Composers placeholder.

Mirror releaseArtistsMapper: append one album.composer, skipping it
when the name contains VARIOUS (case-insensitive) or the id is already
credited. Add the composer field to the Album model.

cc
2026-06-30 11:03:35 -06:00
vicrodh a67dd2c2df i18n: translate Recommendations/external-reco strings + JACK note (es/de/fr/pt)
The 11 Recommendations msgids (IntegrationsSettings RECOMMENDATIONS/toggle/
description, HomeView tab, ExternalRecoView row titles + empty state) had
zero catalog entries and fell back to English everywhere. Translate them in
es/de/fr/pt, plus the 2 new JACK-note strings. es is Mexican (register-
checked: no voseo/Castilian). ru/ja stay English-fallback pending a reviewed
pass.

cc
2026-06-30 09:27:07 -06:00
vicrodh 6f7ff9956c feat(audio): surface "JACK is not bit-perfect" in Audio settings
JACK routes through the JACK graph (resamples to the graph rate), so it
can't be bit-perfect — but nothing told the user. Add a `backend-is-jack`
flag (settings.rs computes it beside alsa/pipewire at both set-sites;
state.slint property) and a WarningBanner in AudioSettings, gated on it,
pointing at ALSA-direct / PipeWire-passthrough for bit-perfect. UI-only;
backend behavior unchanged.

Blind change (no build this session) — validates on the next qbz_ui build.

cc
2026-06-30 09:27:07 -06:00
vicrodh 67f5569af4 fix(ui): About modal density/height + splash disclaimer placement
- About: contributors 4->5 per row so the 10 handles fill the widened
  840px modal in 2 even rows instead of 3 sparse ones; modal height
  720->770px.
- Splash: move the legal disclaimer out of the card to the bottom of the
  screen (the main container, matching Tauri) and shrink it to 10px.

cc
2026-06-30 08:08:41 -06:00
vicrodh fb172335be fix(blacklist): align album-cover artwork jobs with the filtered model
Blocked albums looked like they "stayed" and clicking a card opened the
adjacent album: the model dropped the blocked album but the artwork jobs
were built from the UNFILTERED data with pre-filter indices, so every
cover landed on the shifted card (and the card's data was the next album).

- artist: artwork_jobs now skips blocked releases and uses a post-filter
  album_idx that matches apply_artist's filtered+sorted model; the
  "Latest release" is dropped at the source so the section hides and queues
  no stale cover.
- favorites albums + home Recently Played: filter at the data source so the
  model AND the artwork jobs derive from the same filtered list.
- albumview E1: append composers from the tracklist to the credit line —
  album.artists carries only performers, so the composer (e.g. Mozart) was
  missing; now derived distinct from track.composer with a Composer role.

cc
2026-06-30 08:08:41 -06:00
vicrodh 66889af421 feat(blacklist): album-block context menu + manager Albums tab; albumview polish
Phase 2 (Slint UI + wiring) for album-level blocking, plus the album-view
enhancements that share these files.

Album blacklist UI:
- "Block this album" in all three album menus: the page-header menu as a
  Block/Unblock toggle (gated to Qobuz via AlbumState), and the AlbumCard +
  AlbumListRow grid menus (one-way, drop-on-reload; local/Plex excluded).
  Menu heights bumped for the extra item.
- Blacklist Manager gains an Artists | Albums tab bar (segmented, ADR-008);
  an AlbumRow with a cover thumbnail (blind-eye fallback) + title + artist +
  "Added {date}"; tab-aware controls (shared enable toggle, per-tab search /
  Clear-All / count) and confirm modal. Settings row retitled "Blacklist",
  status shows both counts.
- state.slint: BlacklistedAlbumItem + album-items/album-count/active-tab on
  BlacklistState; set-tab/album-select/remove-album/clear-all-albums/
  block-album on BlacklistActions; AlbumState.is-album-blocked.
- main.rs: ("album","block"/"unblock") arm (header, resolves display fields
  from AlbumState); 5 album BlacklistActions callbacks; seed_blacklist_status
  pushes album_count.
- blacklist_manager.rs: album build/push + set_tab/block_album/remove_album/
  clear_all_albums; covers resolve async via a new ArtworkTarget::BlacklistAlbum
  through spawn_loads.

Album-view enhancements (same files):
- E1: full credited-artist header line with localized roles, each name a
  clickable artist link (Album.artists drove this; was interpreter-only).
- E3: classical work-header composer name is now a clickable artist link
  (work title + composer id/name split through TrackData/TrackItem).

i18n: es (Mexican) for all new strings; de/fr/pt/ru/ja fall back to English
pending a translator pass.

cargo check -p qbz green (Slint codegen + bin).

cc
2026-06-30 00:06:05 -06:00
vicrodh c85ad5f661 feat(blacklist): enforce album-level blocking across core + all consumers
Phase 1 (headless). Adds the album-id filter axis on top of the artist
blacklist, orthogonal to it (an album is hidden by its OWN id regardless
of artist — the fix for Qobuz same-name artist merges).

- qbz-core: AlbumBlacklistFilter (HashSet<String>); album_blacklisted /
  track_blacklisted / discover_album_blacklisted / pick_most_popular /
  parse_search_all / search_all gain an album-id arm; fail-open guards
  widened so an album-only block is not skipped. Artist arms (artist
  search retain, pick_most_popular artists) stay artist-only. +6 tests.
- bridge: is_album_blacklisted / album_ids_snapshot / card_blacklisted;
  stamp_row + is_track_blacklisted take the row's album id (source!="qobuz"
  guard preserved; render/queue stay in lockstep).
- enforce at every album-bearing consumer, threading the album snapshot:
  search (page-1 + load-more), home, discover, for-you (+spotlight),
  album rows + queue + carousels, playlist/mix/favorites rows.
- close pre-existing leaks (both axes at once, owner decision E): label
  pages (were entirely unfiltered — the prime merged-artist surface),
  search load-more/replace-category, favorites albums grid, artist
  discography (own-id), myqbz collection albums.
- albumview: full readable release date in the header ("Feb 19, 2026")
  via the localized %b formatter, replacing the year-only slice.

cargo test -p qbz-app (12) + qbz-core (39) green; cargo check -p qbz green.

cc
2026-06-29 23:30:23 -06:00
vicrodh 3fbb166b3e feat(blacklist): add String-keyed album blacklist store in qbz-app
Parallel album-id axis alongside the u64 artist blacklist. New
album_blacklist TEXT-PK table in the same artist_blacklist.db (additive,
idempotent, no migration), an in-memory HashSet<String> mirror, and
is_album_blacklisted/add_album/remove_album/get_all_albums/album_count/
clear_all_albums. Shares the single enabled flag with the artist axis.
12 unit tests green (incl. shared-flag + axis-independence).

cc
2026-06-29 23:03:47 -06:00
vicrodh be487dca4c feat(slint): work grouping, ru/ja locales, About + What's New + Discover reco modal
- Album view: classical "work" section grouping (PR #536 port) — new
  Track.work model field + run-length per-work headers in AlbumView.
- i18n: add Russian + Japanese locales (1365 msgids each); extend plural.rs
  (ru 3-form one/few/many, ja single-form) + tests; fix the language-selector
  reseed (main.rs set_languages + ui_prefs index maps). Seven languages total.
- About modal: native port of the Tauri About (version, links, build info,
  one-paragraph acknowledgments, inline contributors with GitHub avatars,
  signature). Contributors credit hoyon (PR #536) and mxnix (PR #517, ru).
- What's New modal: renders the GitHub release-tag description (markdown
  sections + TOC); release fetch + build.rs emits build date/commit.
- Discover Recommendations: move Cache window + Refresh now into the Customize
  modal, gated to the reco tab; hide the section-config UI there; add an
  explainer paragraph. New strings translated to all six non-English locales.
- About modal width +120px; splash screen gains the legal disclaimer footer.

cc
2026-06-29 22:55:17 -06:00
vicrodh e0e6ca81a9 fix(slint): queue sleep-timer flyout closed on any click
The sleep-timer PopupWindow used the default close-policy (close-on-click),
so any click inside it — including on the preset picker — closed it before
the value could change. Set close-policy: close-on-click-outside. Also
replaced the QbzSelect (itself a PopupWindow, which Slint cannot open nested
inside another PopupWindow) with inline preset rows, so the presets are
actually pickable.

cc
2026-06-29 16:58:16 -06:00
vicrodh 6f6f2d5aec feat(slint): whole-row selection in multi-select mode
In multi-select mode the entire TrackRow body is a selection target (Tauri
whole-row selectable), not just the 13px checkbox. The body click routes the
same media-action("track", id, "toggle-select") the checkbox does, so the
central toggle arm reads the modifier — Shift+Click range and Ctrl+Click
work on the body too. The "..." context-menu button + the checkbox keep
their own handlers (the menu stays reachable); the other in-row controls
(artist/album link, favorite heart, offline button) are disabled in select
mode so the click passes through to the row. Double-click no longer plays in
select mode. Inert (blacklisted) rows never select.

cc
2026-06-29 16:58:16 -06:00
vicrodh acfd3d928d feat(slint): keyboard multi-select on the built-out surfaces (Mix, Label, LocalLibrary Albums)
Extend the keyboard multi-select feature to the surfaces that had no
working selection in the Slint port.

- Mix (DailyQ/WeeklyQ/FavQ/TopQ all render through one MixView): mode
  toggle + MultiSelectBar, per-row checkbox via the central toggle arm + a
  ContentView::Mix branch (model + recount), bulk play-next/queue/
  add-to-playlist. Shift-range, Ctrl+A and Escape inherited automatically.
- Label Popular Tracks: was a dead stub (checkboxes with no bar/recount/
  router arms). Converted the local mode flag to LabelState.multi-select,
  added a real MultiSelectBar + label.rs recount/select_all/clear/selected,
  and the missing select-toggle/bulk router arms.
- LocalLibrary Albums grid (a previously-missing surface): AlbumCardItem
  gains a `selected` field; a `select-mode` prop is threaded through the
  shared AlbumCard / AlbumListRow / AlbumGrid / AlbumCollectionView (default
  false, so Discover/Label/Favorites are untouched). Albums toolbar toggle
  + MultiSelectBar; local_library.rs album select fns (flat + grouped,
  id-resilient Shift-range over AlbumCardItem); album->tracks bulk resolve
  via fetch_album_tracks_blocking on spawn_blocking for queue/play-next/
  add-to-playlist/add-to-mixtape; Ctrl+A and Escape routed by active-tab.

cargo check -p qbz green (qbz_ui compiles clean).

cc
2026-06-29 14:53:31 -06:00
vicrodh 53e1e8cd1c feat(slint): keyboard-driven multi-select on existing surfaces
Bring back the Tauri Excel-style keyboard multi-select to the Slint port:
Shift+Click extends an additive range from the last-clicked anchor,
Ctrl/Cmd+A selects all (select-all-only, never toggles to clear), and
Escape leaves select mode (clear + mode off). Plain/Ctrl+Click stay a
single per-row toggle (1:1 Tauri, which reads only shiftKey at click).

- New selection.rs: per-surface anchor (thread-local, id-resilient across
  model re-sort/filter) + generic apply_shift_range and select_all over
  any VecModel via a setter closure.
- keybindings::mods() made pub; Escape branch in handle_escape.
- Central toggle-select arm (Album/Artist/Playlist/Favorites) + dedicated
  paths for LocalLibrary Tracks and Offline Manager (OfflineRow, range
  setter guarded to kind==track).
- Ctrl/Cmd+A routed by NavState.view to the active surface's select-all,
  gated on its multi-select flag; behind the existing text-input guard.

cc
2026-06-29 13:58:18 -06:00
vicrodh 99da4c381b feat(slint): pre-launch dev panel, queue controls, appearance persistence, reco cache settings
Sleep timer (queue footer): tokio monotonic deadline + 1Hz countdown that
fires core().pause() at expiry; presets + custom minutes via a popover.

Stop after this song: 4 QbzCore passthroughs + playback consume-before-advance
seam and the load-bearing gapless-prefetch gate, plus the queue marker/menu so
the Queue persists when playback halts.

qbz-log crate: TeeLogger wrapping env_logger (stderr + 5000-line ring +
~/.local/share/qbz/logs/qbz.log) with write-time secret redaction (regex +
live-secret registry) and a diagnostics bundle builder.

Dev panel logs: in-app viewer that shows content the moment it opens, advanced
controls behind a discreet [+], "Copy all" as the sole footer button, a copy
button on the upload URL, and a redacted public paste upload.

Diagnostics: qbz-app::diagnostics (extracted, src-tauri untouched) feeding a
7-section panel with the live active output device + sample rate/format (pactl)
and a complete redacted report for the upload.

Report an Issue: wires the header menu item to a 1:1 Tauri modal that opens the
GitHub bug-report template.

Appearance persistence: 6 toggles that were live-only now persist via ui_prefs
+ handler arm + startup seed (window-title-show, show-volume-steppers,
sidebar-playlist-collage, local-library-track-artwork, in-app-toasts,
theme-filter).

Recommendations cache (per-tab only): TTL select 24/36/48/72h + Refresh now in
ExternalRecoView; no change to the other Discover tabs.

Bump workspace version 0.1.0 -> 1.2.15 so app_version is correct in the
diagnostics report and the QConnect About string. i18n es/de/fr/pt for all new
strings.

cc
2026-06-29 13:19:06 -06:00
vicrodh 158d6416ce fix(tray): debounce double-click to avoid wgpu TextureView panic
A tray double-click fired two activate calls within milliseconds. On Wayland
each toggle destroys and recreates the winit surface and the wgpu shader
underlay, so a render still in flight then use-after-frees a TextureView
(wgpu-core panic "TextureView ... is no longer alive"). Collapse a double-click
into a single toggle with a 400ms debounce; the first click always passes.

cc
2026-06-29 13:18:51 -06:00
vicrodh b76e2b8137 feat(immersive): Liquid Spectrum visualizer POC (parked, disabled in menu)
Circular 'liquid' audio-spectrum shader scene (Motion-Array style): FFT bands wrapped around a ring, smoothed into an organic blob glowing in the album palette, with a rotating radial-rays backdrop and a beat-triggered burst.

Wired as shader-mode 6 (liquid_spectrum.wgsl + SHADER_SOURCES[4] + render_frame), but the first cut did not land visually, so the picker row is commented out and the g-cycle is left at 0..5 — the scene is unreachable but kept intact for a future pass.

cc
2026-06-29 03:22:53 -06:00
vicrodh 226c2118ef feat(immersive): three new visualizer scenes (Wave Bed, Spectral Ribbon, Line Bed) + polish
Wave Bed (focus mode 6): centered vertical-mirror waveform. Spectral Ribbon (shader-mode 4): paint-as-you-play GPU spectrogram with green axes + header overlay, bass-at-bottom orientation aligned to the axes, and a real-time ceiling line tracking the highest active frequency. Line Bed (shader-mode 5): wgpu line-strip waterfall with a leveled camera, B-spline-smoothed continuous curves, a primary->accent height gradient, a bottom cutoff above the player, 2x supersampling, and a sampled-texture heights feed (no vertex storage buffer, so it runs on Slint's downlevel wgpu device).

Also: Plasma reworked to be far more reactive (4 ink emitters, beat splats, faster turbulence, quicker decay); immersive song card no longer truncates the album/artist line; player heart reflects favorite state; player bar narrowed; sample-rate and bit-depth wired through for the spectral header.

cc
2026-06-28 23:55:17 -06:00
vicrodh 2a8d01e686 feat(immersive): GPU shader scenes with album-art palette, audio uniforms, and song card
Expand the shader Uniforms to 144 bytes (phase/beat/level/level_smooth + 8 log
bands + 5 energy bands + the album-art palette triad), feed them per frame from
the 30 fps visualizer drain (new FrameAudio + a forward-motion phase accumulator)
and push the palette on track change. Move plasma and aurora onto the new
uniforms and add a bottom-right ImmersiveSongCard shown over any shader scene.
Pairs with the faster tunnel from the previous commit.

cc
2026-06-28 15:36:07 -06:00
vicrodh 705b735393 feat(immersive): faster tunnel shader with winding path and vanishing-point speed-lines
Amplify the forward-flight clock (phase * 6, seam-safe at the 4096 wrap), add
radial speed-lines fanning out of the vanishing point (shimmer with treble and
the beat), and bend the corridor CENTERLINE as a function of depth so the path
winds while the rectangular cross-section stays undeformed. Stronger beat punch.
tunnel.wgsl only; plasma and aurora untouched.

cc
2026-06-28 15:32:51 -06:00
vicrodh 98d606f913 fix(playlist): follow/unfollow button reflects the true follow state
The detail toggle used sidebar::contains to decide "following", which is
true for owned playlists shown in the sidebar and false for followed ones
opened from Favorites > Followed (a followed playlist may live in a folder
or not be rendered in the sidebar at all). Being in the sidebar is a
consequence of following, not the determinant, so derive it authoritatively
from get_user_playlists (present but owner != me). Reflect the state in the
button: a check icon + "Unfollow on Qobuz" tooltip when following.

cc
2026-06-28 12:59:06 -06:00
vicrodh 1a4e3bd3d8 feat(sidebar): add search and import to the collapsed-sidebar playlists flyout
When the sidebar is fully collapsed, the header playlists flyout now has
a search box (sharing the sidebar's Rust-side filter, cleared on every
close so it never leaks into the expanded list) and an Import entry
(offline-gated, mirroring the sidebar "..." menu).

cc
2026-06-28 12:58:17 -06:00
vicrodh be97d899d9 fix(sidebar): open the playlists options flyout to the right
The "..." options menu opened leftward over the playlist list; anchor it
to the right of the button (overlaying the content area) instead.

cc
2026-06-28 12:58:17 -06:00
vicrodh 9521c7e03e feat(playlist): hide Copy button for playlists already copied to library
Persist the source playlist id when a Qobuz playlist is copied (new
copied_playlists table in library.db, mirroring Tauri's user-scoped
qbz_copied_playlists set) and seed is-copied on detail-view load, so the
Copy button stays hidden on later opens, surviving an app restart.

cc
2026-06-28 12:58:04 -06:00
vicrodh e0c921070c fix(npb-classic): "+" opens the grouped add flyout like the other layouts
Classic NPB's "+" fired a one-click add-to-playlist; now it opens the same
grouped add menu (library / playlist / queue / play-next / mixtape / collection)
as New and Large. Classic keeps its inline favorite toggle next to it so the
favorite state stays visible.

cc
2026-06-28 11:00:36 -06:00
vicrodh 59409822ab fix(npb-large): lift the cover 2px off the window bottom
The Large now-playing cover sat flush against the window bottom. Lift it 2px and
take those 2px from the cover<->spectrum gap (10 -> 8) so the dock height is
unchanged; the bottom pad applies only while the spectrum is shown so the
off-state height still matches large-dock-height.

cc
2026-06-28 11:00:36 -06:00
vicrodh de32c1687f fix(playlist): owner-aware remove (delete vs unfollow) + refresh-in-place nav
A followed/subscribed Qobuz playlist could not be removed: the port flagged it
owned (via sidebar membership, which includes followed playlists) and called
delete_playlist, which Qobuz accepts but no-ops on a playlist you don't own.

- Ownership now follows Tauri: owner.id == current user id. Owned => Delete;
  not-owned => the Follow/Unfollow toggle (previously hidden because the
  playlist looked owned). Carry owner_id on PlaylistData; reset is-owner /
  is-following / is-copied per navigation so state can't leak.
- The delete handler re-derives ownership server-side (get_playlist) as a
  backstop for the manager/sidebar entry points and routes owned => delete,
  followed => unsubscribe. delete_playlist never runs on a non-owned playlist.
- Post-removal nav only steps back in history when viewing that playlist's
  detail; otherwise it stays on the invoking surface (sidebar refreshed),
  instead of always routing to Favorites > Playlists.

cc
2026-06-28 11:00:36 -06:00
vicrodh 6a17c48f59 feat(settings): appearance/integrations polish, functional window title, link modal
- Appearance: drop the red 'unsupported' tint on Language; hide UI Scale and
  the Title Bar / Window Controls / Immersive blocks (commented, not deleted).
- Window title: "Show track in window title" now works -- the OS window title
  becomes "{track} - {artist} | qbz" via a reactive binding (editable template
  row hidden; format is fixed).
- Plex and Scrobblers collapse carets enlarged (28/18) and moved to row level so
  they are vertically centered with the master toggle.
- Hamburger: "Open Qobuz Link" -> "Open Music Link".
- Link modal: a long pasted URL no longer grows the input/modal (min-width 0 on
  the clip container caps content-width propagation).

cc
2026-06-27 23:15:45 -06:00
vicrodh 0f34a4280d fix(playlist): reserve back-to-top FAB clearance at the content end
The floating back-to-top button overlapped the last track rows / the docked
Suggested Songs. Reserve the FAB footprint as a bottom footer so content
scrolls clear of it. Constant (not scroll-gated) to avoid a layout binding
loop (footer height -> track-list height -> visible-height -> FAB condition).

cc
2026-06-27 23:15:45 -06:00
vicrodh b5fd478b7b fix(foryou): radio excludes local/Plex, Plex/local artwork, radio nav buttons
- Radio Stations seed only Qobuz-sourced recent albums; local/Plex albums
  carry ids the Qobuz /radio/album endpoint can't resolve, so they no longer
  generate radio tiles (they still appear in Recently Played Albums).
- Recently Played albums/tracks route artwork by scheme (spawn_search_loads):
  http -> Qobuz CDN, /library/ or /photo/ -> Plex thumb, else local file. The
  plain HTTP loader left Plex/local covers blank.
- Radio Stations carousel extracted to a RadioCarousel component with prev/next
  nav buttons (drag preserved), mirroring SlimCarousel.

cc
2026-06-27 23:15:45 -06:00