Commit Graph
100 Commits
Author SHA1 Message Date
Afonso Jorge Ramos 032021de44 feat(lyrics): drag region on sidebar header and selectable lyrics panel 2026-05-13 20:42:27 +01:00
Afonso Jorge Ramos e54c5de08b feat(views): drag regions on top-bars across remaining views 2026-05-13 20:40:43 +01:00
Afonso Jorge Ramos 47f3a9f335 feat(views): drag regions on home, library, and favorites top-bars 2026-05-13 20:40:39 +01:00
Afonso Jorge Ramos 897f6ac862 feat(detail-views): drag regions and selectable metadata blocks 2026-05-13 20:40:34 +01:00
Afonso Jorge Ramos eb57c70ef9 feat(modals): mark info modals and legal notice selectable 2026-05-13 20:40:29 +01:00
Afonso Jorge Ramos 418bf371e8 refactor(styles): drop now-redundant local user-select: none 2026-05-13 20:40:23 +01:00
Afonso Jorge Ramos a34e418530 feat(sidebar): drag window from aside top padding band on macos 2026-05-13 20:39:58 +01:00
Afonso Jorge Ramos 69d7ee0212 refactor(layout): remove macos overlay drag and main-content padding 2026-05-13 20:39:57 +01:00
Afonso Jorge Ramos 5610861ed8 refactor(titlebar): drag on spacer divs not full bar 2026-05-13 20:39:56 +01:00
Afonso Jorge Ramos c1c151ee25 refactor(css): user-select none on body with .selectable opt-in 2026-05-13 20:39:50 +01:00
Afonso Jorge Ramos 5865c16624 docs(player): explain set_stream_error(false) message swallow
Clearing the error flag also drops any pending stream_error_message.
A fast record_stream_error -> set_stream_error(false) -> polling-tick
sequence will silently swallow the toast — that is the intent (a
recovered error is not user-actionable), but it surprises future
readers, so document it inline.
2026-05-13 18:07:54 +01:00
Afonso Jorge Ramos 9231ea2bd4 fix(mixtapes): replace click-swallowing backdrop with document click-outside 2026-05-13 17:54:48 +01:00
Afonso Jorge Ramos b3b0fce535 fix(purchases): replace click-swallowing backdrop with document click-outside 2026-05-13 17:54:48 +01:00
Afonso Jorge Ramos 3382a36ede fix(mixtape-collection): replace click-swallowing backdrop with document click-outside 2026-05-13 17:54:48 +01:00
Afonso Jorge Ramos 29598747fb fix(collections): replace click-swallowing backdrop with document click-outside 2026-05-13 17:54:48 +01:00
Afonso Jorge Ramos ff11dea869 fix(label): drop tracks context menu backdrop in favor of click-outside listener 2026-05-13 17:54:48 +01:00
Afonso Jorge Ramos ddf0ed4175 fix(artist-detail): consolidate section sort menus and drop tracks context backdrop 2026-05-13 17:54:48 +01:00
Afonso Jorge Ramos 126dff1297 fix(library): consolidate toolbar dropdowns into single openMenu state 2026-05-13 17:54:47 +01:00
Afonso Jorge Ramos 7fccf9bd01 fix(favorites): consolidate toolbar and section dropdowns into single openMenu state 2026-05-13 17:54:47 +01:00
Afonso Jorge Ramos fc56fb4b2f feat(genre-filter): make isOpen bindable for parent dropdown coordination 2026-05-13 17:54:08 +01:00
Afonso Jorge Ramos 84dcd7ebbe perf(+page): guard lyrics state writes against subscriber churn
subscribeLyrics fires per rAF tick (~60Hz with extrapolation). The
previous version wrote all six $state slots unconditionally, which
re-triggered the lyricsSidebarLines $derived (a .map allocating N
objects per call) every tick even though lyricsLines itself only
changes per-track. Adding reference/value guards short-circuits the
fan-out for unchanged fields — the per-tick path now only writes
lyricsActiveProgress (and sometimes lyricsActiveIndex).
2026-05-13 14:31:19 +01:00
Afonso Jorge Ramos 640dead393 fix(lyricsStore): claim rAF handle pre-tick and notify on every change
Two related fixes to the active-line rAF loop:

- The previous guard reassigned updateRafHandle *after* updateActiveLine
  returned. Any listener that synchronously re-entered
  startActiveLineUpdates during notifyListeners() saw a null handle and
  scheduled a second parallel rAF chain. Now the handle is claimed
  before the work runs, so re-entry is a no-op.

- Drops the inter-tick progress threshold. With playerStore extrapolating
  audio time between backend events, every rAF tick has a freshly-changed
  progress value. Threshold-throttling would skip the edge samples where
  the snap-to-1 and indexChanged transitions need to land — re-introducing
  the "starts midway"/"ends before 100%" bugs. Downstream consumers
  equality-guard their $state writes, so fan-out only fires on real
  changes.
2026-05-13 14:31:11 +01:00
Afonso Jorge Ramos 6166584ee8 perf(playerStore): extrapolate audio time between backend position events
The Rust backend emits playback:state at 250ms cadence. Frontend
getCurrentTime() was returning that staircase value — for the karaoke
rAF loop running at 60Hz, every 15 frames got the same answer, then a
250ms jump. This caused two visible artifacts:

- Active line appears to "start midway" when detection of the new index
  lags by up to 250ms past the line's timeMs (audio is already that far
  into the line by the time we see the boundary).
- Active line gets demoted before its highlight reaches the end, because
  the last in-line notification reports progress ~0.92 (one tick before
  bound) and the next backend tick has already crossed into the next
  line — the snap-to-1 never gets a chance to fire.

Audio position is wall-clock-based on the backend (see the 250ms emit
loop comment in lib.rs); we now do the same math in JS: anchor
(positionSecs, performance.now()) on every event-driven update, then
return anchor + elapsed while playing. Cap extrapolation at +1s so a
stalled backend doesn't run away. Existing 33 playerStore tests pass
unchanged — they read currentTime directly, not getCurrentTime().
2026-05-13 14:31:03 +01:00
Afonso Jorge Ramos 64511c4fa2 feat(lyrics): segment active-line gradient by visual line via pretext
Pre-pretext, the karaoke gradient applied to a single span shared the same
horizontal cut across every wrapped visual line — line 2 appeared partially
sung when only line 1's worth of vocals had played. Pretext lays out the
active lyric into visual lines; each renders as its own block-level span
with its own 0->100% gradient, so progress fills sequentially.

Also adds race/perf safeguards layered on top:
- Cancellation token on tick().then(layoutActiveLine) so rapid activeIndex
  changes (seek-scrub, fast songs) don't queue stale microtasks against
  the current DOM.
- ResizeObserver coalesces via rAF and ignores sub-pixel width deltas, so
  window-drag and the active-line scale animation don't trigger Pretext
  re-measurement every callback.
- segmentBounds prefix array memoizes cumulative widths, dropping per-
  segment progress lookup from O(i) to O(1) inside the each-block.
- EMA writes equality-guard measuredBlockMs/Delta and lookaheadEnabled so
  Svelte signals don't fan out no-op updates 60 times per second.
- Lookahead clamped at 1/30 and disabled on backward dp so a coarse audio
  jump or in-line seek can't poison the visual lead.
- measuredBlockMs seed lowered (>=20ms, dur * 0.01) since rAF-rate data
  converges the EMA within 2-3 ticks; the prior 80ms floor visibly lagged
  the gradient on the first frame of every new line.
- LyricsLine type unified from store (single source of truth).
2026-05-13 14:30:54 +01:00
Afonso Jorge Ramos 57f8307934 feat(ui): toast on audio:init-failed event
Listen for the backend audio:init-failed event and show a user-facing
error toast (8s) with the backend-provided cause. Mirrors the
audio:device-missing listener pattern alongside it. Surfaces the
silence when macOS CoreAudio init fails, instead of leaving the user
guessing why playback didn't start.
2026-05-13 13:09:35 +01:00
Afonso Jorge Ramos a251849094 i18n(toast): add audioInitFailed translation key
Add localized strings for the new backend audio init failure toast
(en, de, es, fr, pt). Reason placeholder receives the backend's
already user-friendly message (e.g. "macOS audio device changed its
sample rate during stream open...").
2026-05-13 13:09:31 +01:00
Afonso Jorge Ramos 6d91ec0e02 feat(audio): emit audio:init-failed Tauri event on backend init failure
The playback-event polling loop now drains
SharedState::take_stream_error_message and emits a Tauri
audio:init-failed event with the user-readable cause. Drain semantics
guarantee the toast fires exactly once per error instead of every
tick of the polling loop.
2026-05-13 13:09:26 +01:00
Afonso Jorge Ramos 04e17cc449 feat(player): expose user-readable stream_error_message channel
Pairs the existing stream_error AtomicBool with an Option<String> that
the audio thread can populate when a stream init failure has a
meaningful cause to surface to the user (currently: the macOS
CoreAudio rate-mismatch path).

set_stream_error(false) also clears the message; record_stream_error
sets both atomically; take_stream_error_message drains so the UI
fires its notification exactly once per error.
2026-05-13 13:09:18 +01:00
Afonso Jorge Ramos a7b40bf6bb perf(player): cache CoreAudio nominal rate query for 750ms
evaluate_stream_recreate runs on every track change on the audio
thread, and each call issued two HAL syscalls
(resolve_output_device_id + get_nominal_sample_rate). Cache the result
per-thread for 750ms — long enough to absorb the bulk of repeats
within an album, short enough to notice a system rate change.

Cache invalidates on device-name change, so switching the output
device still triggers a fresh query.
2026-05-13 09:23:39 +01:00
Afonso Jorge Ramos b5db0241a9 fix(LyricsLines): make line-text block to expand background-clip paint area 2026-05-12 18:24:37 +01:00
Afonso Jorge Ramos f9a9d103cb fix(LyricsLines): let EMA accept rAF-rate block measurements 2026-05-12 18:01:52 +01:00
Afonso Jorge Ramos f4f8fcf9bf fix(LyricsLines): use activeProgress as karaoke floor, lookahead as bonus 2026-05-12 17:58:16 +01:00
Afonso Jorge Ramos 4de657e2cd feat(LyricsLines): JS-driven karaoke with measured block-interval and lookahead 2026-05-12 17:57:35 +01:00
Afonso Jorge Ramos 6d25100133 refactor(LyricsLines): single-div template with class-toggle transitions 2026-05-12 17:55:30 +01:00
Afonso Jorge Ramos 4d426a80cd fix(lyrics): snap line progress to 1 when at least 99% through 2026-05-12 17:54:01 +01:00
Afonso Jorge Ramos 959f656866 refactor(lyrics): notify listeners on every progress change 2026-05-12 17:53:49 +01:00
Afonso Jorge Ramos 13d1d6919a perf(lyrics): drive active-line updates via requestAnimationFrame 2026-05-12 17:53:35 +01:00
Afonso Jorge Ramos a5100a329a perf(page): memoize lyrics sidebar lines projection 2026-05-12 17:53:06 +01:00
Afonso Jorge Ramos 9aacf1d7eb feat(lyrics): parse LRC gap markers and use endMs for line bounds 2026-05-12 17:52:48 +01:00
Afonso Jorge Ramos fdeb544c63 fix(lyrics): correct active-line color on macOS WebKit 2026-05-12 15:10:09 +01:00
Afonso Jorge Ramos 4fd85b5b2f fix(ci): suffix macOS .app.tar.gz bundles with arch to fix auto-updater 2026-05-11 16:17:07 +02:00
Afonso Jorge Ramos 1edde8a28a fix(audio): rephrase CoreAudio rate-mismatch errors for end users
The error strings flow up through stream_error and end up in user-
visible logs. Replace the internal-shaped messages ("opened at XHz
while device nominal rate is YHz") with text that names the cause,
the remediation, and the reason the fallback path is refused.
2026-05-11 15:19:30 +02:00
Afonso Jorge Ramos 9e632386b9 fix(audio): retry CoreAudio shared-mode open on rate mismatch
CPAL caches device configs and can briefly return a stale sample rate
right after CoreAudio changes the device nominal rate. Before this
change, opening at the stale rate failed the post-open validation and
returned Err, leaving the user with no audio on the first track after
a rate change.

Drop the bad sink, re-resolve the device, re-query nominal rate and
try once more with a 50ms backoff. Two attempts in total. Turns the
racy window into transparent recovery.
2026-05-11 15:16:46 +02:00
Afonso Jorge Ramos c408ea73cb test(player): cover compute_needs_new_stream decision rule
Extract the boolean decision out of evaluate_stream_recreate into a
pure function so the truth table can be tested without faking a
MixerDeviceSink or AudioSettings mutex. Eight cases cover the matrix:
no stream, unchanged format, format change with and without each
bit-perfect backend, and the CoreAudio shared-mode rate-mismatch path.
2026-05-11 15:15:57 +02:00
Afonso Jorge Ramos ac60113cdd refactor(player): extract StreamRecreateDecision helper
The rate-mismatch detection block was duplicated between the Play and
Stream handlers, and each handler acquired thread_settings.lock() three
times in a row. Extract into one helper that reads settings once and
returns every flag the recreate decision needs.

Also drops a dead else-branch that logged "Creating initial audio
stream" — unreachable inside if stream_opt.is_some() — and the
redundant info-log that re-announced the rate-mismatch reason at
recreate time (the warn-level log inside the helper already says
"Recreating stream to avoid wrong-speed playback").
2026-05-11 15:14:54 +02:00
Afonso Jorge Ramos 3b34a734b4 refactor(player): rename current_sample_rate to current_track_sample_rate
The field tracks the decoded source format, not the output stream's
hardware rate. The names are now explicit so future readers don't
conflate the two — on macOS shared mode these can legitimately differ
since Rodio resamples internally.
2026-05-11 15:13:29 +02:00
Afonso Jorge Ramos ac627d432b fix(home): unify sticky tab header to avoid pop-in animation 2026-05-09 17:52:39 +02:00
Afonso Jorge Ramos 8ed62676c2 fix(titlebar): tune macos drag band to 32px 2026-05-09 17:45:46 +02:00
Afonso Jorge Ramos ac5fc25944 fix(titlebar): match macos drag band height to sidebar top padding 2026-05-09 17:37:04 +02:00
Afonso Jorge Ramos be67fffb28 feat(titlebar): expose hide-title-bar toggle on macOS 2026-05-09 17:36:28 +02:00
Afonso Jorge Ramos 61bd1eb940 feat(settings): expose nav and search titlebar toggles on macOS 2026-05-09 17:23:40 +02:00
Afonso Jorge Ramos 3cb54dd06b style(sidebar): drop dead macos no-titlebar padding rule 2026-05-09 17:23:37 +02:00
Afonso Jorge Ramos fd6464fdf3 refactor(macos): drop overlay drag region superseded by qbz titlebar 2026-05-09 17:23:32 +02:00
Afonso Jorge Ramos 0ee2e429c8 feat(titlebar): mount qbz titlebar on macOS with traffic-light reservation 2026-05-09 17:23:23 +02:00
Afonso Jorge Ramos 88e072a4ac fix(macos): harden CoreAudio exclusive stream handling
Address review feedback from the macOS Exclusive Mode PR.

Pin the resolved CoreAudio output device before acquiring Hog Mode so
System Default cannot drift between Hog Mode acquisition, sample-rate
switching, and stream creation.

Fall back to software volume when CoreAudio hardware volume is unavailable,
which keeps the volume slider usable on knob-only DACs.

Snapshot and restore CoreAudio hardware volume around Exclusive Mode so
leaving Exclusive Mode does not leave the output device at QBZ's last
hardware volume.

Refuse shared-mode fallback when macOS Exclusive Mode stream creation
fails, so the app does not silently play in non-exclusive mode while the UI
still reports Exclusive Mode.

Clean up Linux unused-variable warnings from the macOS-only guard plumbing.
2026-05-07 09:36:20 -05:00
Afonso Jorge Ramos e93fcaec99 fix(audio): defer host-stream drop on Stop to reuse device across track changes 2026-05-07 02:32:39 +02:00
Afonso Jorge Ramos 468a421e26 fix(audio): gate 50ms post-stop sleep behind cfg(target_os = "linux") 2026-05-05 17:11:28 +02:00
Afonso Jorge Ramos 1e3f5ab981 fix(audio): populate macOS output device picker 2026-05-04 00:31:21 +02:00
Afonso Jorge Ramos 65792b3a9c fix: sync package-lock.json with package.json 2026-04-06 01:35:19 +02:00
Afonso Jorge Ramos e4462dfefd fix(macos): ad-hoc sign app bundle to fix Gatekeeper rejection 2026-04-06 01:35:08 +02:00
Afonso Jorge Ramos 787196db99 fix: keep exclusive mode and DAC passthrough visible on all platforms 2026-04-05 01:12:54 +02:00
Afonso Jorge Ramos 86d5286a29 fix: add Windows case to AboutModal platform label 2026-04-05 01:12:53 +02:00
Afonso Jorge Ramos 2e5b9368e4 fix: add windows to Platform type 2026-04-05 01:12:53 +02:00
Afonso Jorge Ramos adfc390009 fix(notifications): use released notify-rust 4.13.0 instead of fork 2026-04-04 15:31:58 +02:00
Afonso Jorge Ramos 85bef48a0a fix: use linux-only gates instead of not-macos for platform-specific settings 2026-04-03 14:01:54 +02:00
Afonso Jorge Ramos 7a1890ad57 fix(audio): use released coreaudio-rs 0.14.1 instead of fork 2026-04-03 14:01:25 +02:00
Afonso Jorge Ramos 94bd222c52 fix: gate Linux-only settings sections on macOS 2026-04-03 13:58:15 +02:00
Afonso Jorge Ramos b71284187b refactor: migrate platform detection to Tauri OS plugin utility 2026-04-03 13:58:11 +02:00
Afonso Jorge Ramos e089417ab5 feat: create platform utility using Tauri OS plugin 2026-04-03 13:58:08 +02:00
Afonso Jorge Ramos 5d115277a5 feat: add @tauri-apps/plugin-os for platform detection 2026-04-03 13:58:04 +02:00
Afonso Jorge Ramos b1000cb9f0 fix(macos): gate Linux graphics CLI flags and crash handler with cfg 2026-04-03 03:19:52 +02:00
Afonso Jorge Ramos ba3ccf9aaa feat(settings): hide tray sub-settings on macOS and use menu bar terminology 2026-04-01 15:44:07 +02:00
Afonso Jorge Ramos 7edffcf537 feat(i18n): add macOS-specific menu bar tray keys for all locales 2026-04-01 15:44:02 +02:00
Afonso Jorge Ramos 84b1f1a542 docs: remove Apple Silicon only note from macOS section 2026-04-01 14:51:26 +02:00
Afonso Jorge Ramos 833cc2b35c fix: boost immersive drag region z-index on macOS 2026-04-01 14:50:22 +02:00
Afonso Jorge Ramos 8283bec614 feat: add Cmd+, keyboard shortcut to open settings 2026-04-01 14:38:06 +02:00
Afonso Jorge Ramos b8af1763ef feat(macos): add album artwork to notifications via image_path 2026-04-01 03:35:34 +02:00
Afonso Jorge Ramos a4a591e73c feat(macos): switch to notify-rust fork with image_path support 2026-04-01 03:35:27 +02:00
Afonso Jorge Ramos 684dd55887 fix(macos): scope title bar padding to non-home views only 2026-04-01 03:27:25 +02:00
Afonso Jorge Ramos 9823dfb680 fix(macos): enable x86_64 cross-compilation
- Upgrade rust_cast 0.18.1 → 0.21 (switches from openssl to rustls,
  removing the openssl-sys dependency that blocked x86_64 cross-compile)
- Split mupdf sys-lib features to Linux-only (on macOS there are no GTK
  symbol conflicts, and system libs don't support x86_64 cross-compile
  from Apple Silicon)
- Update release-macos workflow to build both aarch64 and x86_64 DMGs
  using a matrix strategy
2026-04-01 02:40:55 +02:00
Afonso Jorge Ramos 071865e79b fix: replace removed Github icon with simple-icons
lucide-svelte 1.0 dropped brand icons, breaking the Github import in
AboutModal. Use @icons-pack/svelte-simple-icons (SiGithub) instead.
2026-04-01 02:32:27 +02:00
Afonso Jorge Ramos cd2472f5d9 fix(macos): add top padding to clear native overlay title bar 2026-04-01 02:29:33 +02:00
Afonso Jorge Ramos b6ad192658 fix(macos): hide Linux-only settings on macOS 2026-04-01 02:29:32 +02:00
Afonso Jorge Ramos 478353055c fix(macos): gate idle_inhibit module for Linux only 2026-04-01 02:27:21 +02:00
Afonso Jorge Ramos a2fb44511d feat(macos): add deep link support for qobuzapp:// URLs
Add tauri-plugin-deep-link to handle qobuzapp:// URLs on macOS, where
the OS delivers URLs via Apple Events instead of CLI arguments.

- Register qobuzapp scheme in tauri.conf.json (injected into Info.plist)
- Add on_open_url handler for Apple Event URL delivery
- Extract is_qobuz_link() and handle_qobuz_link() helpers to deduplicate
  URL matching across single-instance, first-launch, and deep-link paths
- Tighten URL matching from contains() to starts_with() for security
- Log emit errors instead of silently discarding them
2026-03-31 05:53:44 +02:00
Afonso Jorge Ramos 6bf397e0f3 feat(macos): auto-select SystemDefault backend on non-Linux
When backend_type is None (auto-detect), default to SystemDefault on
non-Linux platforms so macOS gets CoreAudio device probing and sample
rate switching instead of falling through to the legacy CPAL path.
2026-03-31 02:20:13 +02:00
Afonso Jorge Ramos 4c15a43200 feat(macos): enhance CpalDefaultBackend with device capabilities
On macOS, enumerate_devices() now populates supported_sample_rates,
max_sample_rate, device_bus, and is_hardware via CoreAudio probing.
create_output_stream() switches the device sample rate to match the
track before opening the CPAL stream.
2026-03-31 02:20:08 +02:00
Afonso Jorge Ramos c51f07fe65 feat(macos): add CoreAudio device probing and sample rate switching
Add coreaudio_direct module using coreaudio-rs safe wrappers for:
- Device capability probing (supported sample rates, transport type)
- Automatic sample rate switching per-track
- Device lookup by name for CoreAudio↔CPAL bridging

Uses a fork branch of coreaudio-rs that adds get_available_sample_rates
and get_device_transport_type helpers (PR RustAudio/coreaudio-rs#149).
2026-03-31 02:19:59 +02:00
Afonso Jorge Ramos 70c27f3772 fix(macos): log when DAC sample rate detection is skipped 2026-03-20 01:16:09 +01:00
Afonso Jorge Ramos ce33270535 refactor: gate KWin functions with cfg(target_os = "linux") 2026-03-20 01:15:52 +01:00
Afonso Jorge Ramos 379ba28cee refactor: simplify should_use_main_window_transparency 2026-03-20 01:15:28 +01:00
Afonso Jorge Ramos 6422ae8ff9 refactor: cache isMacOS at module scope in titleBarStore 2026-03-20 01:15:15 +01:00
Afonso Jorge Ramos 6017cc70de fix(macos): gate drag region to macOS only 2026-03-20 01:14:47 +01:00
Afonso Jorge Ramos 7dca962080 fix(macos): merge duplicate platform detection and remove non-passive wheel listener 2026-03-20 01:14:27 +01:00
Afonso Jorge Ramos ebe555b7ca fix(macos): make notification fire-and-forget 2026-03-20 01:14:08 +01:00
Afonso Jorge Ramos dad062d61b fix(audio): add docs and fix param names for non-Linux stubs 2026-03-20 01:13:55 +01:00
Afonso Jorge Ramos f854e09661 feat(audio): add SystemDefault variant to AudioBackendType 2026-03-20 01:13:29 +01:00
Afonso Jorge Ramos 77888fb310 fix: suppress unused_mut warning for cross-platform builder 2026-03-20 00:49:42 +01:00
Afonso Jorge Ramos 08367fe951 fix: convert 16-bit PNG icons to 8-bit for Tauri compatibility 2026-03-20 00:45:16 +01:00
Afonso Jorge Ramos 00147dc7fb feat(macos): adapt frontend for native title bar
- Add macOS platform detection (html.macos class)
- Prevent pinch-to-zoom gestures in WKWebView
- Hide custom title bar on macOS (uses native overlay title bar)
- Add 32px top padding to sidebar for traffic light buttons
- Add drag region for window movement
2026-03-20 00:45:16 +01:00