From 0e5618aa1b1d6eb92a11c67abb853532c7220c11 Mon Sep 17 00:00:00 2001 From: InstaZDLL Date: Mon, 18 May 2026 01:16:55 +0200 Subject: [PATCH 1/7] fix(splash): move handoff to backend `app://ready` listener (closes #42) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v1.1.0's splash → main window handoff lived in the frontend (src/main.tsx): two rAFs after ReactDOM.render(), then `getCurrentWindow().show()` + `splash.close()` over IPC. On Linux WebKitGTK 2.52+ (Fedora 44, recent Arch), the heavy first-launch init (sqlx migrations + SQLite pool open + library scan) took longer than the rAF window, so the show() either failed silently or revealed an empty webview — splash hung forever, main window never appeared. Ubuntu showed the same race on first launch only (second launch cached the init, won the race). Move the handoff to native code: - src-tauri/src/lib.rs: setup() now installs an `app://ready` event listener that calls a new `reveal_main_close_splash` helper (show main → focus → close splash, same ordering as before). A 15 s fallback timer fires the same helper if the event never arrives (frontend crash before render). - src/main.tsx: revealMainWindow becomes signalReady — keep the double rAF to let React commit + the compositor paint at least one frame, then `emit("app://ready")`. No more IPC show/close. - src-tauri/tauri.conf.json + public/splash.html: drop `transparent: true` on the splash window (and switch the body background to opaque #121212). Transparency forced an alpha- capable EGL config that some WebKitGTK builds reject, adding a second EGL failure mode on top of the timing bug. Tested on Windows 11 (no regression), pending Linux confirmation on Fedora 44 + Ubuntu via CI-built bundles. --- public/splash.html | 2 +- src-tauri/src/lib.rs | 64 ++++++++++++++++++++++++++++++++++++++- src-tauri/tauri.conf.json | 1 - src/main.tsx | 53 ++++++++++++++------------------ 4 files changed, 87 insertions(+), 33 deletions(-) diff --git a/public/splash.html b/public/splash.html index bbf8ef6..755189d 100644 --- a/public/splash.html +++ b/public/splash.html @@ -14,7 +14,7 @@ padding: 0; height: 100%; overflow: hidden; - background: transparent; + background: #121212; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a596c2b..b17c1b6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -30,11 +30,12 @@ mod watcher; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::time::Duration; use tauri::{ menu::{Menu, MenuItem, PredefinedMenuItem}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, - AppHandle, Manager, WindowEvent, + AppHandle, Listener, Manager, WindowEvent, }; use audio::{AudioCmd, AudioEngine}; @@ -364,6 +365,43 @@ pub fn run() { }) .build(app)?; + // Splash → main handoff. + // + // The frontend emits `app://ready` once the React root has + // committed its first useful paint (see src/main.tsx). When + // we get that event we close the splash and reveal the + // main window from native code — more reliable than the + // previous IPC dance, which on Linux WebKitGTK 2.52 raced + // against the heavy first-launch init (migrations + DB + // pool + library scan) and left the splash hanging + // forever (issue #42). + // + // A 15 s safety-net timer force-reveals the main window if + // `app://ready` never fires — guards against a frontend + // crash leaving the user stuck on an eternal splash. + let handoff_done = Arc::new(AtomicBool::new(false)); + let handoff_handle = app.handle().clone(); + let handoff_done_for_event = handoff_done.clone(); + app.listen("app://ready", move |_event| { + if handoff_done_for_event.swap(true, Ordering::SeqCst) { + return; + } + reveal_main_close_splash(&handoff_handle); + }); + + let fallback_handle = app.handle().clone(); + let fallback_done = handoff_done.clone(); + tauri::async_runtime::spawn(async move { + tokio::time::sleep(Duration::from_secs(15)).await; + if fallback_done.swap(true, Ordering::SeqCst) { + return; + } + tracing::warn!( + "splash handoff fallback: `app://ready` never fired after 15s, force-revealing main window" + ); + reveal_main_close_splash(&fallback_handle); + }); + Ok(()) }) .invoke_handler(tauri::generate_handler![ @@ -647,6 +685,30 @@ fn show_main_window(app: &AppHandle) { } } +/// Reveal the main window and close the splash, in that order. +/// +/// Same ordering rule as the old frontend version: show main first, +/// then close splash, so there is never a moment where the desktop is +/// visible between the two on a multi-monitor / compositing setup. +/// Both calls are best-effort — failures are logged but don't propagate +/// because there is nothing useful the caller can do about them at this +/// point (the app is already running, just visually rough). +fn reveal_main_close_splash(app: &AppHandle) { + if let Some(main) = app.get_webview_window("main") { + if let Err(err) = main.show() { + tracing::warn!(?err, "splash handoff: main.show failed"); + } + let _ = main.set_focus(); + } else { + tracing::warn!("splash handoff: main window missing at reveal time"); + } + if let Some(splash) = app.get_webview_window("splashscreen") { + if let Err(err) = splash.close() { + tracing::warn!(?err, "splash handoff: splash.close failed"); + } + } +} + /// Toggle Pause / Resume from the tray. Looks at the engine's current /// state (atomic, no async needed) so the menu item works as a single /// "Lecture / Pause" entry instead of two stateful labels we'd have to diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6f4a459..d8aea5f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -30,7 +30,6 @@ "center": true, "resizable": false, "decorations": false, - "transparent": true, "alwaysOnTop": true, "skipTaskbar": true, "focus": false, diff --git a/src/main.tsx b/src/main.tsx index b489400..567370b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,9 +1,6 @@ import React from "react"; import ReactDOM from "react-dom/client"; -import { - getCurrentWindow, - Window as TauriWindow, -} from "@tauri-apps/api/window"; +import { emit } from "@tauri-apps/api/event"; import App from "./App"; import { MiniPlayerApp } from "./MiniPlayerApp"; import "./app.css"; @@ -14,35 +11,31 @@ import { i18nReady } from "./i18n"; // a stripped-down provider tree (no LibraryContext / sidebar / etc). const isMini = new URLSearchParams(window.location.search).get("mini") === "1"; -// The main window is created with `visible: false` in tauri.conf.json so -// the user never sees a white WebView while Rust setup + React mount run. -// A `splashscreen` window is created in its place (small, transparent, -// always-on-top) to give visual feedback during the cold-start delay -// — especially on the very first launch after install, when Windows -// SmartScreen / Defender scans every freshly-extracted DLL. +// The main window is created with `visible: false` in tauri.conf.json +// so the user never sees a white WebView while Rust setup + React mount +// run. A `splashscreen` window is shown in its place. The backend +// listens for `app://ready` and atomically reveals the main window + +// closes the splash from native code (see `reveal_main_close_splash` +// in src-tauri/src/lib.rs). // -// We reveal the main window after the first frame is painted, then -// close the splash. Order matters: show main BEFORE closing splash so -// there's never a moment where the desktop is visible between the two. -// The mini-player is its own window opened explicitly with visible: -// true, so skip the dance there. -function revealMainWindow() { +// Doing the handoff in native code rather than IPC avoids the race +// that bit issue #42 on Linux WebKitGTK 2.52: the previous frontend +// rAF-driven `window.show()` could fire before React had committed +// anything, leaving the user staring at an empty splash forever. +// +// The mini-player is opened with visible: true so it's already on +// screen by the time React mounts — skip the signal there. +function signalReady() { if (isMini) return; + // Two rAFs to let React commit + the compositor paint at least one + // useful frame before we reveal the main window. The backend has a + // 15 s safety-net timer so even if this never fires (e.g. a frontend + // crash before render) the user is not stuck on the splash. requestAnimationFrame(() => { requestAnimationFrame(() => { - void (async () => { - try { - await getCurrentWindow().show(); - } catch (err) { - console.error("[main] window.show failed", err); - } - try { - const splash = await TauriWindow.getByLabel("splashscreen"); - if (splash) await splash.close(); - } catch (err) { - console.error("[main] splash close failed", err); - } - })(); + void emit("app://ready").catch((err) => { + console.error("[main] emit(app://ready) failed", err); + }); }); }); } @@ -57,5 +50,5 @@ i18nReady {isMini ? : } , ); - revealMainWindow(); + signalReady(); }); From 446a153d9dbca7f602ecbbfcf3f3563317bb22f4 Mon Sep 17 00:00:00 2001 From: InstaZDLL Date: Mon, 18 May 2026 01:22:58 +0200 Subject: [PATCH 2/7] fix(splash): allow retry when reveal_main_close_splash fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously both the `app://ready` listener and the 15s fallback timer marked the handoff as done *before* calling the reveal helper. If main.show() or splash.close() failed transiently (e.g. a brief WebKitGTK glitch on first reveal), there was no path back to a working UI — the flag stayed `true`, the listener short-circuited, the fallback was already past, and the user was stuck on an eternal splash. Make `reveal_main_close_splash` return a `bool` (true iff both ops succeeded), and have both call-sites roll the `done` flag back on failure so the other path (or a subsequent ready re-emission) can retry. Per coderabbit review on PR #43. --- src-tauri/src/lib.rs | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b17c1b6..9257e57 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -386,7 +386,14 @@ pub fn run() { if handoff_done_for_event.swap(true, Ordering::SeqCst) { return; } - reveal_main_close_splash(&handoff_handle); + // Reset the flag if the reveal fails so the fallback + // timer (or a subsequent `app://ready` re-emission) can + // retry — otherwise a transient main.show() / splash.close() + // failure would leave the user stuck on an eternal splash + // with no recovery path. + if !reveal_main_close_splash(&handoff_handle) { + handoff_done_for_event.store(false, Ordering::SeqCst); + } }); let fallback_handle = app.handle().clone(); @@ -399,7 +406,9 @@ pub fn run() { tracing::warn!( "splash handoff fallback: `app://ready` never fired after 15s, force-revealing main window" ); - reveal_main_close_splash(&fallback_handle); + if !reveal_main_close_splash(&fallback_handle) { + fallback_done.store(false, Ordering::SeqCst); + } }); Ok(()) @@ -690,23 +699,32 @@ fn show_main_window(app: &AppHandle) { /// Same ordering rule as the old frontend version: show main first, /// then close splash, so there is never a moment where the desktop is /// visible between the two on a multi-monitor / compositing setup. -/// Both calls are best-effort — failures are logged but don't propagate -/// because there is nothing useful the caller can do about them at this -/// point (the app is already running, just visually rough). -fn reveal_main_close_splash(app: &AppHandle) { +/// +/// Returns `true` only when *both* operations succeed (main shown + +/// splash closed, or splash already absent). On any failure, returns +/// `false` so the caller can clear its "done" flag and let the other +/// path (event listener or fallback timer) retry — without this, +/// a transient failure would leave the user stuck on an eternal +/// splash. +fn reveal_main_close_splash(app: &AppHandle) -> bool { + let mut ok = true; if let Some(main) = app.get_webview_window("main") { if let Err(err) = main.show() { tracing::warn!(?err, "splash handoff: main.show failed"); + ok = false; } let _ = main.set_focus(); } else { tracing::warn!("splash handoff: main window missing at reveal time"); + ok = false; } if let Some(splash) = app.get_webview_window("splashscreen") { if let Err(err) = splash.close() { tracing::warn!(?err, "splash handoff: splash.close failed"); + ok = false; } } + ok } /// Toggle Pause / Resume from the tray. Looks at the engine's current From e7da1c41d81d89b66b41484588b9553bd55dbbc7 Mon Sep 17 00:00:00 2001 From: InstaZDLL Date: Mon, 18 May 2026 01:52:30 +0200 Subject: [PATCH 3/7] fix(splash): emit app://ready from useEffect instead of rAF MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WebKitGTK 2.52 suspends requestAnimationFrame callbacks while a window is hidden. Since the main window is created with `visible: false` until the backend reveals it, the 2-rAF dance in signalReady() never fired, deadlocking the handoff for the full 15 s safety-net window. useEffect runs after the first React commit, which is the real guarantee we needed — the compositor paints as part of the reveal itself. --- src/main.tsx | 45 +++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/main.tsx b/src/main.tsx index 567370b..d9dcd69 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect } from "react"; import ReactDOM from "react-dom/client"; import { emit } from "@tauri-apps/api/event"; import App from "./App"; @@ -23,21 +23,22 @@ const isMini = new URLSearchParams(window.location.search).get("mini") === "1"; // rAF-driven `window.show()` could fire before React had committed // anything, leaving the user staring at an empty splash forever. // -// The mini-player is opened with visible: true so it's already on -// screen by the time React mounts — skip the signal there. -function signalReady() { - if (isMini) return; - // Two rAFs to let React commit + the compositor paint at least one - // useful frame before we reveal the main window. The backend has a - // 15 s safety-net timer so even if this never fires (e.g. a frontend - // crash before render) the user is not stuck on the splash. - requestAnimationFrame(() => { - requestAnimationFrame(() => { - void emit("app://ready").catch((err) => { - console.error("[main] emit(app://ready) failed", err); - }); +// We emit from a useEffect at the React root rather than from a 2-rAF +// dance because WebKitGTK 2.52 suspends `requestAnimationFrame` +// callbacks while a window is hidden — `visible: false` means rAF +// never fires until the backend reveals the window, deadlocking the +// handoff until the 15 s safety-net timer trips. useEffect runs after +// the first React commit, which is the actual guarantee we care about +// (DOM is populated before reveal); the compositor will paint the +// first frame as part of the reveal itself, so we don't need to +// observe a paint to avoid a flash. +function ReadySignal({ children }: { children: React.ReactNode }) { + useEffect(() => { + void emit("app://ready").catch((err) => { + console.error("[main] emit(app://ready) failed", err); }); - }); + }, []); + return <>{children}; } i18nReady @@ -45,10 +46,18 @@ i18nReady console.error("[i18n] initialization failed", err); }) .finally(() => { - ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + const root = ReactDOM.createRoot( + document.getElementById("root") as HTMLElement, + ); + root.render( - {isMini ? : } + {isMini ? ( + + ) : ( + + + + )} , ); - signalReady(); }); From 8f29b4f07cea1a3055e32443f6d92ab7e0b1ec17 Mon Sep 17 00:00:00 2001 From: InstaZDLL Date: Mon, 18 May 2026 02:14:02 +0200 Subject: [PATCH 4/7] fix(splash): set native window backgroundColor to avoid white flash on windows Without `transparent: true`, the WebView2 control on Windows shows its default white background until splash.html is parsed and the CSS `background: #121212` applies, producing a brief white flash. Setting the native window backgroundColor paints the OS-level window dark before WebView2 emits its first frame. --- src-tauri/tauri.conf.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index d8aea5f..d65e546 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -33,7 +33,8 @@ "alwaysOnTop": true, "skipTaskbar": true, "focus": false, - "shadow": false + "shadow": false, + "backgroundColor": "#121212" } ], "security": { From f6d439df3752770e98728d788eedc579dd87fcce Mon Sep 17 00:00:00 2001 From: InstaZDLL Date: Mon, 18 May 2026 02:21:02 +0200 Subject: [PATCH 5/7] fix(share): use full-res artwork + real blur on Now Playing card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Now Playing share PNG looked blurry / pixelated because the canvas pulled `track.artwork_path_2x` first (a 128×128 thumbnail from src-tauri/src/thumbnails.rs). On the 1080×1080 card that meant upscaling 4.5× for the 580 px cover slot and ~10× for the 1320 px backdrop — both visibly soft, the backdrop especially mushy because the old code "faked" a blur by drawing the cover huge over the canvas + a dark wash. - Prefer `track.artwork_path` (the original cover the scanner extracted from the audio file, typically 500–1500 px) and fall back to the thumbnails only when the original is missing. - Set `imageSmoothingQuality = "high"` for the residual upscale to 580 px so even small originals stay sharp instead of bilinear-soft. - Switch the backdrop from the upscale + wash hack to a real `ctx.filter = "blur(60px) brightness(0.6)"`, which both Tauri's Windows WebView2 (Chromium) and Linux/macOS WebKitGTK 2.40+ support natively. Cleaner backdrop, no quality loss on the foreground cover. --- src/lib/nowPlayingCard.ts | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/lib/nowPlayingCard.ts b/src/lib/nowPlayingCard.ts index f519925..09636a1 100644 --- a/src/lib/nowPlayingCard.ts +++ b/src/lib/nowPlayingCard.ts @@ -39,10 +39,21 @@ export async function renderNowPlayingCard( canvas.height = SIZE; const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("Canvas 2D context unavailable"); + // Smoother image scaling for the inevitable cover upscale below. + // Browsers default to "low" which produces visibly soft cover art on + // a 1080×1080 card. + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = "high"; // ----- Load cover (if any) ------------------------------------------ + // Prefer the full-resolution artwork over the thumbnails — `_2x` is + // 128×128 and `_1x` is 64×64 (see src-tauri/src/thumbnails.rs), both + // far too small for the 580 px cover slot and 1320 px blurred + // backdrop on this 1080² card. The original `artwork_path` is the + // image the scanner extracted from the audio file (typically + // 500–1500 px square), which gives a crisp share card. const coverSrc = resolveRemoteImage( - track.artwork_path_2x ?? track.artwork_path, + track.artwork_path ?? track.artwork_path_2x ?? track.artwork_path_1x, null, ); let coverImg: HTMLImageElement | null = null; @@ -67,15 +78,22 @@ export async function renderNowPlayingCard( // ----- Backdrop ------------------------------------------------------ if (coverImg) { - // Draw the cover full-bleed, then blur via composite layers (the - // 2D context's filter API isn't reliable in WebKit/Edge — we fake - // it by drawing the cover scaled up and overlaying a heavy dark - // wash). - ctx.drawImage(coverImg, -120, -120, SIZE + 240, SIZE + 240); - // Heavy dark overlay so the foreground text + cover stay readable. + // Real CSS-style blur via the 2D context filter. Both + // Chromium-WebView2 (Windows) and WebKitGTK 2.40+ (Linux) support + // it; macOS WebKit has supported it for years. Falls back to a no- + // op string on the off chance an old engine doesn't recognise it, + // which gives a slightly less blurry but still readable backdrop. + ctx.save(); + ctx.filter = "blur(60px) brightness(0.6)"; + // Draw a bit past the edges so the blur's transparent halo doesn't + // bleed into the canvas border. + ctx.drawImage(coverImg, -80, -80, SIZE + 160, SIZE + 160); + ctx.restore(); + // Tinted gradient overlay so the foreground text + cover stay + // readable on bright artwork. const wash = ctx.createLinearGradient(0, 0, 0, SIZE); - wash.addColorStop(0, `rgba(0,0,0,0.55)`); - wash.addColorStop(1, `rgba(${darken(accent, 0.4)},0.85)`); + wash.addColorStop(0, `rgba(0,0,0,0.45)`); + wash.addColorStop(1, `rgba(${darken(accent, 0.4)},0.75)`); ctx.fillStyle = wash; ctx.fillRect(0, 0, SIZE, SIZE); } else { From 6c1f7accd851e121a26334e8bd47b695d191873b Mon Sep 17 00:00:00 2001 From: InstaZDLL Date: Mon, 18 May 2026 02:27:18 +0200 Subject: [PATCH 6/7] fix(splash): retry fallback in a loop + don't close splash on main reveal failure Two coderabbit findings on the splash handoff: 1. `reveal_main_close_splash` was still calling `splash.close()` even when the main window was missing or `main.show()` had failed. On that path the user ended up with zero visible windows. Early- return before touching the splash if the main reveal didn't actually succeed. 2. The fallback timer was one-shot: a single 15 s sleep, one reveal attempt, done. `ReadySignal` also emits `app://ready` only once at mount. If the timer narrowly won the race AND the reveal then failed, both triggers were spent and the user was stuck. Replace the one-shot sleep with a bounded retry loop: up to 10 attempts with a 250 ms backoff between failures. Permanent failure escalates to a `tracing::error!` so it shows up in diagnostics. --- src-tauri/src/lib.rs | 54 +++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9257e57..1fbc7bc 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -399,16 +399,32 @@ pub fn run() { let fallback_handle = app.handle().clone(); let fallback_done = handoff_done.clone(); tauri::async_runtime::spawn(async move { + // First attempt after 15 s; subsequent retries every + // 250 ms up to 10 total attempts. Bounded so a + // permanently missing main window doesn't spin forever + // (the warn! log surfaces it instead). The retry exists + // because `ReadySignal` only emits `app://ready` once + // at mount — if we lost the race with that single + // event AND the first reveal failed, without a retry + // the user would be stuck on the splash. tokio::time::sleep(Duration::from_secs(15)).await; - if fallback_done.swap(true, Ordering::SeqCst) { - return; - } - tracing::warn!( - "splash handoff fallback: `app://ready` never fired after 15s, force-revealing main window" - ); - if !reveal_main_close_splash(&fallback_handle) { + for attempt in 0..10 { + if fallback_done.swap(true, Ordering::SeqCst) { + return; + } + tracing::warn!( + attempt, + "splash handoff fallback: `app://ready` never fired in time, force-revealing main window" + ); + if reveal_main_close_splash(&fallback_handle) { + return; + } fallback_done.store(false, Ordering::SeqCst); + tokio::time::sleep(Duration::from_millis(250)).await; } + tracing::error!( + "splash handoff fallback: exhausted 10 reveal attempts, user is likely stuck on splash" + ); }); Ok(()) @@ -707,24 +723,26 @@ fn show_main_window(app: &AppHandle) { /// a transient failure would leave the user stuck on an eternal /// splash. fn reveal_main_close_splash(app: &AppHandle) -> bool { - let mut ok = true; - if let Some(main) = app.get_webview_window("main") { - if let Err(err) = main.show() { - tracing::warn!(?err, "splash handoff: main.show failed"); - ok = false; - } - let _ = main.set_focus(); - } else { + // Bail out *before* touching the splash if the main window isn't + // available or refuses to show — otherwise we'd close the only + // visible window the user has and leave them staring at the + // desktop with no way back into the app until they re-launch. + let Some(main) = app.get_webview_window("main") else { tracing::warn!("splash handoff: main window missing at reveal time"); - ok = false; + return false; + }; + if let Err(err) = main.show() { + tracing::warn!(?err, "splash handoff: main.show failed"); + return false; } + let _ = main.set_focus(); if let Some(splash) = app.get_webview_window("splashscreen") { if let Err(err) = splash.close() { tracing::warn!(?err, "splash handoff: splash.close failed"); - ok = false; + return false; } } - ok + true } /// Toggle Pause / Resume from the tray. Looks at the engine's current From bb13f037fc91e5adc3153d0aca2fb4fc6491bee4 Mon Sep 17 00:00:00 2001 From: InstaZDLL Date: Mon, 18 May 2026 02:28:41 +0200 Subject: [PATCH 7/7] refactor(splash): extract ReadySignal into its own file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inline ReadySignal component in src/main.tsx triggered react-refresh/only-export-components — Fast Refresh requires a file to export *only* components, and main.tsx has the ReactDOM.createRoot + i18n bootstrap side effects alongside. Move ReadySignal to src/components/common/ReadySignal.tsx with the same useEffect-based emit. Identical runtime behaviour, just cleaner module boundaries. main.tsx now imports it like any other component. --- .coderabbit.yaml | 1 - CHANGELOG.md | 141 +++++++++++++------------- docs/RELEASING.md | 2 +- src-tauri/src/backup.rs | 7 +- src-tauri/src/commands/profile_io.rs | 13 ++- src/components/common/ReadySignal.tsx | 31 ++++++ src/components/views/SettingsView.tsx | 4 +- src/contexts/ThemeContext.tsx | 105 +++++++++---------- src/main.tsx | 30 +----- 9 files changed, 172 insertions(+), 162 deletions(-) create mode 100644 src/components/common/ReadySignal.tsx diff --git a/.coderabbit.yaml b/.coderabbit.yaml index c4e8716..32a2c07 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -5,7 +5,6 @@ tone_instructions: >- utilisateur, les problèmes de sécurité, les erreurs de logique et les oublis de tests/validation. Évite les remarques purement cosmétiques. early_access: true - reviews: profile: assertive request_changes_workflow: true diff --git a/CHANGELOG.md b/CHANGELOG.md index bf5ec31..43c843b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,86 +2,83 @@ ## [1.1.0](https://github.com/InstaZDLL/WaveFlow/compare/v1.0.0...v1.1.0) (2026-05-17) - ### Features -* **apt:** publish .deb to Buildkite Packages registry on release ([328367f](https://github.com/InstaZDLL/WaveFlow/commit/328367f8fd2ae96dba34742c6efdcafd61cd47b9)) -* **apt:** publish .deb to Buildkite Packages registry on release ([cb9a4bc](https://github.com/InstaZDLL/WaveFlow/commit/cb9a4bccccb9373e00ab2927053a259ea882fff0)) -* **artist:** add UI to pick or remove the artist image ([d277519](https://github.com/InstaZDLL/WaveFlow/commit/d277519aef7ed0b999b1888f748c44b191384de4)) -* **backup:** optionally bundle shared Deezer artwork cache ([03365ca](https://github.com/InstaZDLL/WaveFlow/commit/03365ca3fc01e3b8aae9373cd33eb98efc93c88b)) -* **backup:** optionally bundle shared Deezer artwork cache ([932106e](https://github.com/InstaZDLL/WaveFlow/commit/932106e2cac4128170a7377a7e05c09bc9ff36fc)) -* **config:** add .coderabbit.yaml for project configuration and review instructions ([cc72976](https://github.com/InstaZDLL/WaveFlow/commit/cc729762a4e4ebe3d31daac1f274fc42d77c4762)) -* **distribution:** publish to fedora copr on every release ([8af4104](https://github.com/InstaZDLL/WaveFlow/commit/8af41043fde2ee502503dd8acf84ea3753495bee)) -* **distribution:** publish to winget-pkgs on every release ([e069679](https://github.com/InstaZDLL/WaveFlow/commit/e0696791076e6ad20d479cc0e4d067dc591807be)) -* **library:** enhance create playlist functionality with source tracking ([3b806bf](https://github.com/InstaZDLL/WaveFlow/commit/3b806bfefd782622d0ce515c640052296bcf5245)) -* **library:** local artist images + picker UI ([#33](https://github.com/InstaZDLL/WaveFlow/issues/33)) ([af719b6](https://github.com/InstaZDLL/WaveFlow/commit/af719b6841423b5080b88494dd9c2cc5f9ab0b03)) -* **library:** open genre detail page from tags grid ([#23](https://github.com/InstaZDLL/WaveFlow/issues/23)) ([0a5fbb9](https://github.com/InstaZDLL/WaveFlow/commit/0a5fbb9108be3ae6035f8751c846e671f8500628)) -* **library:** use local artist.jpg sidecar as artist photo ([e23ece4](https://github.com/InstaZDLL/WaveFlow/commit/e23ece4b5465cd962afb0b0537f0311c67735082)) -* **lyrics:** word-level karaoke (enhanced LRC + TTML) ([7aeb19e](https://github.com/InstaZDLL/WaveFlow/commit/7aeb19ee9d953e99591ceb4faca5d27d67d9539d)) -* **lyrics:** word-level karaoke (enhanced LRC + TTML) ([#25](https://github.com/InstaZDLL/WaveFlow/issues/25)) ([62701c3](https://github.com/InstaZDLL/WaveFlow/commit/62701c3c8fa6c85d968ae609efda253074c50bad)) -* **player-bar:** host A-B loop + sleep timer in overflow menu by default ([1a7aabb](https://github.com/InstaZDLL/WaveFlow/commit/1a7aabbf5dc552f1d5fec4ac34bd862e5b767f7e)) -* **player-bar:** move playback speed into the overflow menu ([c322e54](https://github.com/InstaZDLL/WaveFlow/commit/c322e54269f9682c355b3f177c2ee000a198556c)) -* **player-bar:** redesign right cluster (Spotify-style) + overflow defaults ([ade9f13](https://github.com/InstaZDLL/WaveFlow/commit/ade9f1379faefe3f1be61d6ccd60b70902131258)) -* **player-bar:** spotify-style mini-player + fullscreen icons after volume ([7d58651](https://github.com/InstaZDLL/WaveFlow/commit/7d58651ddcec4259014deb6465140dc5431eef34)) -* **playlist:** add filename sort mode ([83b5fda](https://github.com/InstaZDLL/WaveFlow/commit/83b5fda3abd054cbd337415c3276c144331866c7)) -* **playlist:** add filename sort mode ([525ff85](https://github.com/InstaZDLL/WaveFlow/commit/525ff85a21413eb14a8110fd6f7cc56ed32be384)) -* **playlist:** add Spotify-style sort modes with per-playlist memory ([685bfbf](https://github.com/InstaZDLL/WaveFlow/commit/685bfbff6082b55daa30bcb8587e78217a6d75b7)) -* **scan:** auto-merge implicit compilations into Various Artists ([1b24e7e](https://github.com/InstaZDLL/WaveFlow/commit/1b24e7e9b1cb4e6a7d5c908d3b270672c55c5227)) -* **ui:** add splash screen to mask cold-start delay ([#26](https://github.com/InstaZDLL/WaveFlow/issues/26)) ([813a38d](https://github.com/InstaZDLL/WaveFlow/commit/813a38d64ca6240e6d74bcd1d3acacf3292473a9)) -* **ui:** drop the beta badge from the sidebar brand ([e731834](https://github.com/InstaZDLL/WaveFlow/commit/e73183477f0ba778fbddb069dcf0c8ad5e2968d4)) -* **ui:** make album cell clickable in playlist + library track rows ([209b8be](https://github.com/InstaZDLL/WaveFlow/commit/209b8bebda8ad56f5346bed93e77422eb6e0e6fa)) - +- **apt:** publish .deb to Buildkite Packages registry on release ([328367f](https://github.com/InstaZDLL/WaveFlow/commit/328367f8fd2ae96dba34742c6efdcafd61cd47b9)) +- **apt:** publish .deb to Buildkite Packages registry on release ([cb9a4bc](https://github.com/InstaZDLL/WaveFlow/commit/cb9a4bccccb9373e00ab2927053a259ea882fff0)) +- **artist:** add UI to pick or remove the artist image ([d277519](https://github.com/InstaZDLL/WaveFlow/commit/d277519aef7ed0b999b1888f748c44b191384de4)) +- **backup:** optionally bundle shared Deezer artwork cache ([03365ca](https://github.com/InstaZDLL/WaveFlow/commit/03365ca3fc01e3b8aae9373cd33eb98efc93c88b)) +- **backup:** optionally bundle shared Deezer artwork cache ([932106e](https://github.com/InstaZDLL/WaveFlow/commit/932106e2cac4128170a7377a7e05c09bc9ff36fc)) +- **config:** add .coderabbit.yaml for project configuration and review instructions ([cc72976](https://github.com/InstaZDLL/WaveFlow/commit/cc729762a4e4ebe3d31daac1f274fc42d77c4762)) +- **distribution:** publish to fedora copr on every release ([8af4104](https://github.com/InstaZDLL/WaveFlow/commit/8af41043fde2ee502503dd8acf84ea3753495bee)) +- **distribution:** publish to winget-pkgs on every release ([e069679](https://github.com/InstaZDLL/WaveFlow/commit/e0696791076e6ad20d479cc0e4d067dc591807be)) +- **library:** enhance create playlist functionality with source tracking ([3b806bf](https://github.com/InstaZDLL/WaveFlow/commit/3b806bfefd782622d0ce515c640052296bcf5245)) +- **library:** local artist images + picker UI ([#33](https://github.com/InstaZDLL/WaveFlow/issues/33)) ([af719b6](https://github.com/InstaZDLL/WaveFlow/commit/af719b6841423b5080b88494dd9c2cc5f9ab0b03)) +- **library:** open genre detail page from tags grid ([#23](https://github.com/InstaZDLL/WaveFlow/issues/23)) ([0a5fbb9](https://github.com/InstaZDLL/WaveFlow/commit/0a5fbb9108be3ae6035f8751c846e671f8500628)) +- **library:** use local artist.jpg sidecar as artist photo ([e23ece4](https://github.com/InstaZDLL/WaveFlow/commit/e23ece4b5465cd962afb0b0537f0311c67735082)) +- **lyrics:** word-level karaoke (enhanced LRC + TTML) ([7aeb19e](https://github.com/InstaZDLL/WaveFlow/commit/7aeb19ee9d953e99591ceb4faca5d27d67d9539d)) +- **lyrics:** word-level karaoke (enhanced LRC + TTML) ([#25](https://github.com/InstaZDLL/WaveFlow/issues/25)) ([62701c3](https://github.com/InstaZDLL/WaveFlow/commit/62701c3c8fa6c85d968ae609efda253074c50bad)) +- **player-bar:** host A-B loop + sleep timer in overflow menu by default ([1a7aabb](https://github.com/InstaZDLL/WaveFlow/commit/1a7aabbf5dc552f1d5fec4ac34bd862e5b767f7e)) +- **player-bar:** move playback speed into the overflow menu ([c322e54](https://github.com/InstaZDLL/WaveFlow/commit/c322e54269f9682c355b3f177c2ee000a198556c)) +- **player-bar:** redesign right cluster (Spotify-style) + overflow defaults ([ade9f13](https://github.com/InstaZDLL/WaveFlow/commit/ade9f1379faefe3f1be61d6ccd60b70902131258)) +- **player-bar:** spotify-style mini-player + fullscreen icons after volume ([7d58651](https://github.com/InstaZDLL/WaveFlow/commit/7d58651ddcec4259014deb6465140dc5431eef34)) +- **playlist:** add filename sort mode ([83b5fda](https://github.com/InstaZDLL/WaveFlow/commit/83b5fda3abd054cbd337415c3276c144331866c7)) +- **playlist:** add filename sort mode ([525ff85](https://github.com/InstaZDLL/WaveFlow/commit/525ff85a21413eb14a8110fd6f7cc56ed32be384)) +- **playlist:** add Spotify-style sort modes with per-playlist memory ([685bfbf](https://github.com/InstaZDLL/WaveFlow/commit/685bfbff6082b55daa30bcb8587e78217a6d75b7)) +- **scan:** auto-merge implicit compilations into Various Artists ([1b24e7e](https://github.com/InstaZDLL/WaveFlow/commit/1b24e7e9b1cb4e6a7d5c908d3b270672c55c5227)) +- **ui:** add splash screen to mask cold-start delay ([#26](https://github.com/InstaZDLL/WaveFlow/issues/26)) ([813a38d](https://github.com/InstaZDLL/WaveFlow/commit/813a38d64ca6240e6d74bcd1d3acacf3292473a9)) +- **ui:** drop the beta badge from the sidebar brand ([e731834](https://github.com/InstaZDLL/WaveFlow/commit/e73183477f0ba778fbddb069dcf0c8ad5e2968d4)) +- **ui:** make album cell clickable in playlist + library track rows ([209b8be](https://github.com/InstaZDLL/WaveFlow/commit/209b8bebda8ad56f5346bed93e77422eb6e0e6fa)) ### Bug Fixes -* **about:** read version from Tauri at runtime + bump to 1.0.1 ([829ec89](https://github.com/InstaZDLL/WaveFlow/commit/829ec89f8f7196d9d69b28588eba42163b96e98c)) -* **about:** read version from Tauri at runtime instead of hardcoded 0.1.0 ([6bdc69d](https://github.com/InstaZDLL/WaveFlow/commit/6bdc69debbcd2b5fd25d88304de98536dc28fa3c)) -* **apt:** address coderabbit review ([285a5f1](https://github.com/InstaZDLL/WaveFlow/commit/285a5f104b5038b368bd70633dcf7a1fd5a86db5)) -* **audio:** handle panics in decoder thread and improve WASAPI exclusive mode management ([27cfdf7](https://github.com/InstaZDLL/WaveFlow/commit/27cfdf7a262ea7627abf828e0a14219ff8e83c62)) -* **audio:** keep splash close from stopping playback ([a4fdaa9](https://github.com/InstaZDLL/WaveFlow/commit/a4fdaa90cf6645d7e668a8ddc560d930f3e504a2)) -* **backup:** preserve metadata-artwork flag on archive failure + fr typo ([9b8030d](https://github.com/InstaZDLL/WaveFlow/commit/9b8030d931d19e0569fb41a2ffa6488392cd0081)) -* **ci:** copr project name is lowercase 'instazdll/waveflow' ([6b4235d](https://github.com/InstaZDLL/WaveFlow/commit/6b4235d0288de42e169c8a008ffdcce07e6d99c0)) -* **ci:** copr workflow checks out main instead of the release tag ([37e7dd1](https://github.com/InstaZDLL/WaveFlow/commit/37e7dd1397e9e5becc8d07586c9dcf2ae24114a6)) -* **ci:** plug script injection in lockfile-build (head.ref attacker-controlled) ([ac13cae](https://github.com/InstaZDLL/WaveFlow/commit/ac13cae1f33f3b532955a5076cc75aa6c1c8f544)) -* **ci:** split release-please lockfile pipeline (codeql untrusted-checkout/critical) ([9db6031](https://github.com/InstaZDLL/WaveFlow/commit/9db60314e81700bfd7c302e3cc2efd9a63b924e9)) -* **ci:** split release-please lockfile pipeline (codeql untrusted-checkout/critical) ([49645b0](https://github.com/InstaZDLL/WaveFlow/commit/49645b056069786178da6e21778ef8413dc5ae4d)) -* **copr:** match the upstream rpm's mixed-case file layout ([093a516](https://github.com/InstaZDLL/WaveFlow/commit/093a51651009f9f2671c3d6ecd8c854e13b650d9)) -* **decoder:** simplify parameter passing in decoder_loop function ([ebd4d85](https://github.com/InstaZDLL/WaveFlow/commit/ebd4d851d501e3f9f21a8b5efde30749e9e9a626)) -* **deezer:** surface result of missing-cover batch fetch ([4bb905b](https://github.com/InstaZDLL/WaveFlow/commit/4bb905ba7299fe4c8b476f3751c83746df5250ec)) -* **feedback:** correct contact email domain + wire mailto handler ([6cf797f](https://github.com/InstaZDLL/WaveFlow/commit/6cf797ff397d8369811ecc46f4877791eb18422c)) -* **home:** avoid nested button in recently played tile ([fb0eff4](https://github.com/InstaZDLL/WaveFlow/commit/fb0eff41b7dc6aac64858dbf615511518c73b8fe)) -* **locale:** add missing spotify.emptyPlaylist key to 15 locales ([fe3aa19](https://github.com/InstaZDLL/WaveFlow/commit/fe3aa197e8422724a7fca5b54fb7b382c68668a5)) -* **locale:** declare missing keys in playlist modal, spotify integration, and progress bar ([dd1d7a7](https://github.com/InstaZDLL/WaveFlow/commit/dd1d7a7bffab98600505bdfda218c53a8142f6ea)) -* **locale:** translate home daily mix section and tray menu ([3475280](https://github.com/InstaZDLL/WaveFlow/commit/34752801783815700422649b6a06930f55288716)) -* **locale:** translate home daily mix section and tray menu ([#32](https://github.com/InstaZDLL/WaveFlow/issues/32)) ([9bbdd64](https://github.com/InstaZDLL/WaveFlow/commit/9bbdd64b3facb6cbc310442a817fda2790c7b0b5)) -* **lyrics:** address coderabbit review ([ddd89ff](https://github.com/InstaZDLL/WaveFlow/commit/ddd89ff4ed3dbbd6438a17cee9f6dda83950563b)) -* **lyrics:** bind prefix word to each duplicated line stamp ([ecd87c1](https://github.com/InstaZDLL/WaveFlow/commit/ecd87c127f753d765db822383e4269739d438cf8)) -* **lyrics:** build enhanced LRC word stamps directly (codeql) ([3b80c8e](https://github.com/InstaZDLL/WaveFlow/commit/3b80c8e5e4fc3ba3f55b0b308f709d9e04d3e22c)) -* **lyrics:** keep unstamped text rows on word-mode save ([4240f9d](https://github.com/InstaZDLL/WaveFlow/commit/4240f9db8a2c4a9782306922f4a59f3f3eff77a4)) -* **lyrics:** keep word/ttml badge intact in the panel footer ([fb9c7bf](https://github.com/InstaZDLL/WaveFlow/commit/fb9c7bfa1b8419bf866649379021f79339292582)) -* **lyrics:** mirror fullscreen word animation in the side panel ([da44f8b](https://github.com/InstaZDLL/WaveFlow/commit/da44f8b3b05074d7201a12e460b1a2371a62dd71)) -* **migration:** force file_modified=0 so album_artist backfill triggers ([277871d](https://github.com/InstaZDLL/WaveFlow/commit/277871d35fbcac40beebcca0a27ee80485bc1297)) -* **nav:** preserve detail payloads in history + don't toggle off shuffle ([#24](https://github.com/InstaZDLL/WaveFlow/issues/24)) ([981144c](https://github.com/InstaZDLL/WaveFlow/commit/981144c50a972b42f70afa3875fd3d5fcf7f1bee)) -* **packaging:** drop duplicate ReleaseNotesUrl from winget installer manifest ([f62b242](https://github.com/InstaZDLL/WaveFlow/commit/f62b2429ffd6d008376ad0f533233c2aea8fc33e)) -* **player-bar:** address CodeRabbit review on MoreActionsMenu ([6458185](https://github.com/InstaZDLL/WaveFlow/commit/6458185a293f7148b473a27057559663a8a74512)) -* **playlist-cover:** dedupe artwork hashes before composing auto-cover ([b75e80c](https://github.com/InstaZDLL/WaveFlow/commit/b75e80c03d3f9054131b5feffb25569547f8c0e3)) -* **playlist:** drop dead i18n fallback for sort.filename ([a2e0467](https://github.com/InstaZDLL/WaveFlow/commit/a2e0467dad28bfa911d46d63a8a7cd505acd72d0)) -* **profile:** normalise sqlx migration checksums on import + pin sources to LF ([#27](https://github.com/InstaZDLL/WaveFlow/issues/27)) ([521e484](https://github.com/InstaZDLL/WaveFlow/commit/521e4842d4de7cd9537e50d15aa12cc3d85149b7)) -* **profile:** roll back partial import on checksum/migrate failure ([#28](https://github.com/InstaZDLL/WaveFlow/issues/28)) ([921678b](https://github.com/InstaZDLL/WaveFlow/commit/921678be8493c56d78248ab8a2f8487a67f7afe4)) -* **review:** address PR [#33](https://github.com/InstaZDLL/WaveFlow/issues/33) review feedback ([f67f496](https://github.com/InstaZDLL/WaveFlow/commit/f67f496bb8018c5af4b935937e747be2a8a0d6f9)) -* **review:** batch rescan tx + modal i18n and request-id cleanup ([a3429bc](https://github.com/InstaZDLL/WaveFlow/commit/a3429bce43e2edb1a8b406044570eb696068b82d)) -* **scan:** group albums by Album Artist tag + compilation flag ([d341e5f](https://github.com/InstaZDLL/WaveFlow/commit/d341e5f1ed9be6125336e6163f5b96daf0534ebe)) -* **settings:** persist autostart, close-to-tray and scan-on-start toggles ([35e3719](https://github.com/InstaZDLL/WaveFlow/commit/35e3719cf767d00943ae47f86f914896f3fe7491)) -* **theme:** persist preference before view transition ([415dbbe](https://github.com/InstaZDLL/WaveFlow/commit/415dbbe23ff88aa56eac9e083218636fa6971205)) -* **theme:** persist preference before view transition ([aaeeec7](https://github.com/InstaZDLL/WaveFlow/commit/aaeeec75ae297873244f9e3e623b5485a53a231c)), closes [#34](https://github.com/InstaZDLL/WaveFlow/issues/34) -* **ui:** cap grid item width on wide screens (auto-fill instead of fixed cols) ([36c44a1](https://github.com/InstaZDLL/WaveFlow/commit/36c44a19ab5e26258950e9ba6ae76feeb7c33b20)) -* **ui:** cap grid item width on wide screens (auto-fill instead of fixed cols) ([#22](https://github.com/InstaZDLL/WaveFlow/issues/22)) ([194d7ee](https://github.com/InstaZDLL/WaveFlow/commit/194d7ee8311cd53fcfc3c91dc59569e376b041ce)) -* **workflow:** enhance security by restricting PR execution to github-actions[bot] ([6b993df](https://github.com/InstaZDLL/WaveFlow/commit/6b993dfe5fd81b7d836703e04de275947f932e12)) - +- **about:** read version from Tauri at runtime + bump to 1.0.1 ([829ec89](https://github.com/InstaZDLL/WaveFlow/commit/829ec89f8f7196d9d69b28588eba42163b96e98c)) +- **about:** read version from Tauri at runtime instead of hardcoded 0.1.0 ([6bdc69d](https://github.com/InstaZDLL/WaveFlow/commit/6bdc69debbcd2b5fd25d88304de98536dc28fa3c)) +- **apt:** address coderabbit review ([285a5f1](https://github.com/InstaZDLL/WaveFlow/commit/285a5f104b5038b368bd70633dcf7a1fd5a86db5)) +- **audio:** handle panics in decoder thread and improve WASAPI exclusive mode management ([27cfdf7](https://github.com/InstaZDLL/WaveFlow/commit/27cfdf7a262ea7627abf828e0a14219ff8e83c62)) +- **audio:** keep splash close from stopping playback ([a4fdaa9](https://github.com/InstaZDLL/WaveFlow/commit/a4fdaa90cf6645d7e668a8ddc560d930f3e504a2)) +- **backup:** preserve metadata-artwork flag on archive failure + fr typo ([9b8030d](https://github.com/InstaZDLL/WaveFlow/commit/9b8030d931d19e0569fb41a2ffa6488392cd0081)) +- **ci:** copr project name is lowercase 'instazdll/waveflow' ([6b4235d](https://github.com/InstaZDLL/WaveFlow/commit/6b4235d0288de42e169c8a008ffdcce07e6d99c0)) +- **ci:** copr workflow checks out main instead of the release tag ([37e7dd1](https://github.com/InstaZDLL/WaveFlow/commit/37e7dd1397e9e5becc8d07586c9dcf2ae24114a6)) +- **ci:** plug script injection in lockfile-build (head.ref attacker-controlled) ([ac13cae](https://github.com/InstaZDLL/WaveFlow/commit/ac13cae1f33f3b532955a5076cc75aa6c1c8f544)) +- **ci:** split release-please lockfile pipeline (codeql untrusted-checkout/critical) ([9db6031](https://github.com/InstaZDLL/WaveFlow/commit/9db60314e81700bfd7c302e3cc2efd9a63b924e9)) +- **ci:** split release-please lockfile pipeline (codeql untrusted-checkout/critical) ([49645b0](https://github.com/InstaZDLL/WaveFlow/commit/49645b056069786178da6e21778ef8413dc5ae4d)) +- **copr:** match the upstream rpm's mixed-case file layout ([093a516](https://github.com/InstaZDLL/WaveFlow/commit/093a51651009f9f2671c3d6ecd8c854e13b650d9)) +- **decoder:** simplify parameter passing in decoder_loop function ([ebd4d85](https://github.com/InstaZDLL/WaveFlow/commit/ebd4d851d501e3f9f21a8b5efde30749e9e9a626)) +- **deezer:** surface result of missing-cover batch fetch ([4bb905b](https://github.com/InstaZDLL/WaveFlow/commit/4bb905ba7299fe4c8b476f3751c83746df5250ec)) +- **feedback:** correct contact email domain + wire mailto handler ([6cf797f](https://github.com/InstaZDLL/WaveFlow/commit/6cf797ff397d8369811ecc46f4877791eb18422c)) +- **home:** avoid nested button in recently played tile ([fb0eff4](https://github.com/InstaZDLL/WaveFlow/commit/fb0eff41b7dc6aac64858dbf615511518c73b8fe)) +- **locale:** add missing spotify.emptyPlaylist key to 15 locales ([fe3aa19](https://github.com/InstaZDLL/WaveFlow/commit/fe3aa197e8422724a7fca5b54fb7b382c68668a5)) +- **locale:** declare missing keys in playlist modal, spotify integration, and progress bar ([dd1d7a7](https://github.com/InstaZDLL/WaveFlow/commit/dd1d7a7bffab98600505bdfda218c53a8142f6ea)) +- **locale:** translate home daily mix section and tray menu ([3475280](https://github.com/InstaZDLL/WaveFlow/commit/34752801783815700422649b6a06930f55288716)) +- **locale:** translate home daily mix section and tray menu ([#32](https://github.com/InstaZDLL/WaveFlow/issues/32)) ([9bbdd64](https://github.com/InstaZDLL/WaveFlow/commit/9bbdd64b3facb6cbc310442a817fda2790c7b0b5)) +- **lyrics:** address coderabbit review ([ddd89ff](https://github.com/InstaZDLL/WaveFlow/commit/ddd89ff4ed3dbbd6438a17cee9f6dda83950563b)) +- **lyrics:** bind prefix word to each duplicated line stamp ([ecd87c1](https://github.com/InstaZDLL/WaveFlow/commit/ecd87c127f753d765db822383e4269739d438cf8)) +- **lyrics:** build enhanced LRC word stamps directly (codeql) ([3b80c8e](https://github.com/InstaZDLL/WaveFlow/commit/3b80c8e5e4fc3ba3f55b0b308f709d9e04d3e22c)) +- **lyrics:** keep unstamped text rows on word-mode save ([4240f9d](https://github.com/InstaZDLL/WaveFlow/commit/4240f9db8a2c4a9782306922f4a59f3f3eff77a4)) +- **lyrics:** keep word/ttml badge intact in the panel footer ([fb9c7bf](https://github.com/InstaZDLL/WaveFlow/commit/fb9c7bfa1b8419bf866649379021f79339292582)) +- **lyrics:** mirror fullscreen word animation in the side panel ([da44f8b](https://github.com/InstaZDLL/WaveFlow/commit/da44f8b3b05074d7201a12e460b1a2371a62dd71)) +- **migration:** force file_modified=0 so album_artist backfill triggers ([277871d](https://github.com/InstaZDLL/WaveFlow/commit/277871d35fbcac40beebcca0a27ee80485bc1297)) +- **nav:** preserve detail payloads in history + don't toggle off shuffle ([#24](https://github.com/InstaZDLL/WaveFlow/issues/24)) ([981144c](https://github.com/InstaZDLL/WaveFlow/commit/981144c50a972b42f70afa3875fd3d5fcf7f1bee)) +- **packaging:** drop duplicate ReleaseNotesUrl from winget installer manifest ([f62b242](https://github.com/InstaZDLL/WaveFlow/commit/f62b2429ffd6d008376ad0f533233c2aea8fc33e)) +- **player-bar:** address CodeRabbit review on MoreActionsMenu ([6458185](https://github.com/InstaZDLL/WaveFlow/commit/6458185a293f7148b473a27057559663a8a74512)) +- **playlist-cover:** dedupe artwork hashes before composing auto-cover ([b75e80c](https://github.com/InstaZDLL/WaveFlow/commit/b75e80c03d3f9054131b5feffb25569547f8c0e3)) +- **playlist:** drop dead i18n fallback for sort.filename ([a2e0467](https://github.com/InstaZDLL/WaveFlow/commit/a2e0467dad28bfa911d46d63a8a7cd505acd72d0)) +- **profile:** normalise sqlx migration checksums on import + pin sources to LF ([#27](https://github.com/InstaZDLL/WaveFlow/issues/27)) ([521e484](https://github.com/InstaZDLL/WaveFlow/commit/521e4842d4de7cd9537e50d15aa12cc3d85149b7)) +- **profile:** roll back partial import on checksum/migrate failure ([#28](https://github.com/InstaZDLL/WaveFlow/issues/28)) ([921678b](https://github.com/InstaZDLL/WaveFlow/commit/921678be8493c56d78248ab8a2f8487a67f7afe4)) +- **review:** address PR [#33](https://github.com/InstaZDLL/WaveFlow/issues/33) review feedback ([f67f496](https://github.com/InstaZDLL/WaveFlow/commit/f67f496bb8018c5af4b935937e747be2a8a0d6f9)) +- **review:** batch rescan tx + modal i18n and request-id cleanup ([a3429bc](https://github.com/InstaZDLL/WaveFlow/commit/a3429bce43e2edb1a8b406044570eb696068b82d)) +- **scan:** group albums by Album Artist tag + compilation flag ([d341e5f](https://github.com/InstaZDLL/WaveFlow/commit/d341e5f1ed9be6125336e6163f5b96daf0534ebe)) +- **settings:** persist autostart, close-to-tray and scan-on-start toggles ([35e3719](https://github.com/InstaZDLL/WaveFlow/commit/35e3719cf767d00943ae47f86f914896f3fe7491)) +- **theme:** persist preference before view transition ([415dbbe](https://github.com/InstaZDLL/WaveFlow/commit/415dbbe23ff88aa56eac9e083218636fa6971205)) +- **theme:** persist preference before view transition ([aaeeec7](https://github.com/InstaZDLL/WaveFlow/commit/aaeeec75ae297873244f9e3e623b5485a53a231c)), closes [#34](https://github.com/InstaZDLL/WaveFlow/issues/34) +- **ui:** cap grid item width on wide screens (auto-fill instead of fixed cols) ([36c44a1](https://github.com/InstaZDLL/WaveFlow/commit/36c44a19ab5e26258950e9ba6ae76feeb7c33b20)) +- **ui:** cap grid item width on wide screens (auto-fill instead of fixed cols) ([#22](https://github.com/InstaZDLL/WaveFlow/issues/22)) ([194d7ee](https://github.com/InstaZDLL/WaveFlow/commit/194d7ee8311cd53fcfc3c91dc59569e376b041ce)) +- **workflow:** enhance security by restricting PR execution to github-actions[bot] ([6b993df](https://github.com/InstaZDLL/WaveFlow/commit/6b993dfe5fd81b7d836703e04de275947f932e12)) ### Performance Improvements -* **scan:** skip album backfill UPDATE when scan has no new tag info ([1b44737](https://github.com/InstaZDLL/WaveFlow/commit/1b447375d32955868ef1608812cca23be1661e38)) +- **scan:** skip album backfill UPDATE when scan has no new tag info ([1b44737](https://github.com/InstaZDLL/WaveFlow/commit/1b447375d32955868ef1608812cca23be1661e38)) ## 1.0.0 (2026-05-12) diff --git a/docs/RELEASING.md b/docs/RELEASING.md index ce2808f..9b83b30 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -80,7 +80,7 @@ Actions): | `WINGET_PAT` | GitHub Personal Access Token (classic, `public_repo` scope) the `.github/workflows/winget.yml` action uses to fork microsoft/winget-pkgs and open the PR with the new manifest | | `COPR_LOGIN` | `login` field from — `.github/workflows/copr.yml` uses it to authenticate to Fedora COPR via `copr-cli` | | `COPR_TOKEN` | `token` field from the same COPR API page (paired with `COPR_LOGIN`). Token lifetime is 6 months — rotate when builds start returning `401 Unauthorized` | -| `BUILDKITE_PACKAGES_TOKEN` | Buildkite API token (`read_packages` + `write_packages`) for `.github/workflows/apt-publish.yml` to push the `.deb` to the `instazdll/waveflow` registry | +| `BUILDKITE_PACKAGES_TOKEN` | Buildkite API token (`read_packages` + `write_packages`) for `.github/workflows/apt-publish.yml` to push the `.deb` to the `instazdll/waveflow` registry | The AUR package itself (`waveflow-bin`) needs a one-off manual setup on the maintainer's box — see [`packaging/aur/README.md`](../packaging/aur/README.md). diff --git a/src-tauri/src/backup.rs b/src-tauri/src/backup.rs index 8154dd5..49f8f1d 100644 --- a/src-tauri/src/backup.rs +++ b/src-tauri/src/backup.rs @@ -169,7 +169,12 @@ pub async fn write_config( ("backup.retention", retention.to_string(), "int"), ( "backup.include_metadata_artwork", - if include_metadata_artwork { "true" } else { "false" }.to_string(), + if include_metadata_artwork { + "true" + } else { + "false" + } + .to_string(), "bool", ), ] { diff --git a/src-tauri/src/commands/profile_io.rs b/src-tauri/src/commands/profile_io.rs index b3c6d00..0abc2b6 100644 --- a/src-tauri/src/commands/profile_io.rs +++ b/src-tauri/src/commands/profile_io.rs @@ -140,17 +140,13 @@ pub async fn export_profile( /// Read the `backup.include_metadata_artwork` app setting. Defaults to /// `true` so a fresh install + first manual export produces a complete /// archive without the user having to opt in. -pub(crate) async fn read_include_metadata_artwork( - app_db: &SqlitePool, -) -> AppResult { +pub(crate) async fn read_include_metadata_artwork(app_db: &SqlitePool) -> AppResult { let row: Option = sqlx::query_scalar( "SELECT value FROM app_setting WHERE key = 'backup.include_metadata_artwork'", ) .fetch_optional(app_db) .await?; - Ok(row - .map(|v| v == "true" || v == "1") - .unwrap_or(true)) + Ok(row.map(|v| v == "true" || v == "1").unwrap_or(true)) } /// Import a `.waveflow` archive as a brand-new profile. The new @@ -353,7 +349,10 @@ pub(crate) fn write_archive( let rel = entry_path .strip_prefix(meta_dir) .map_err(|e| AppError::Other(format!("metadata_artwork rel: {e}")))?; - let zip_name = format!("metadata_artwork/{}", rel.to_string_lossy().replace('\\', "/")); + let zip_name = format!( + "metadata_artwork/{}", + rel.to_string_lossy().replace('\\', "/") + ); zip.start_file(&zip_name, opts)?; let mut src = File::open(entry_path)?; std::io::copy(&mut src, &mut zip)?; diff --git a/src/components/common/ReadySignal.tsx b/src/components/common/ReadySignal.tsx new file mode 100644 index 0000000..90fe16b --- /dev/null +++ b/src/components/common/ReadySignal.tsx @@ -0,0 +1,31 @@ +import { useEffect, type ReactNode } from "react"; +import { emit } from "@tauri-apps/api/event"; + +/** + * Emits `app://ready` once after the first React commit so the Rust + * backend can reveal the main window and close the splash from native + * code (see `reveal_main_close_splash` in src-tauri/src/lib.rs). + * + * We rely on a `useEffect` rather than a `requestAnimationFrame` dance + * because WebKitGTK 2.52 suspends rAF callbacks while a window is + * `visible: false` — rAF would never fire until the backend reveals + * the window, deadlocking the handoff until the 15 s safety-net timer + * trips. `useEffect` runs after React commits, which is the actual + * guarantee we care about (DOM is populated before reveal); the + * compositor will paint the first frame as part of the reveal itself, + * so we don't need to observe a paint to avoid a flash. + * + * Lives in its own file rather than in `main.tsx` so it satisfies + * the React Fast Refresh constraint ("a file must only export + * components"). Bundle entry points like `main.tsx` have non-component + * side effects (root.render, i18n init) that prevent HMR from + * extracting the component cleanly. + */ +export function ReadySignal({ children }: { children: ReactNode }) { + useEffect(() => { + void emit("app://ready").catch((err) => { + console.error("[ReadySignal] emit(app://ready) failed", err); + }); + }, []); + return <>{children}; +} diff --git a/src/components/views/SettingsView.tsx b/src/components/views/SettingsView.tsx index 8bff9f7..cf1796e 100644 --- a/src/components/views/SettingsView.tsx +++ b/src/components/views/SettingsView.tsx @@ -615,9 +615,7 @@ export function SettingsView({ onNavigate }: SettingsViewProps) { setCoverResultMsg(null); try { const fetched = await batchFetchMissingAlbumCovers(); - setCoverResultMsg( - t("library.fetchCoversResult", { count: fetched }), - ); + setCoverResultMsg(t("library.fetchCoversResult", { count: fetched })); } catch (err) { console.error("[SettingsView] fetch missing covers failed", err); setCoverResultMsg(t("library.fetchCoversFailed")); diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx index 95a248d..517bd99 100644 --- a/src/contexts/ThemeContext.tsx +++ b/src/contexts/ThemeContext.tsx @@ -34,63 +34,66 @@ const writeStoredTheme = (isDark: boolean) => { export function ThemeProvider({ children }: { children: ReactNode }) { const [isDark, setIsDark] = useState(readStoredTheme); - const toggleTheme = useCallback((event?: ReactMouseEvent) => { - let nextValue = false; - const flipTheme = () => - setIsDark((prev) => { - nextValue = !prev; - return nextValue; - }); + const toggleTheme = useCallback( + (event?: ReactMouseEvent) => { + let nextValue = false; + const flipTheme = () => + setIsDark((prev) => { + nextValue = !prev; + return nextValue; + }); - // Persist BEFORE triggering any animation. Some Linux WebKitGTK builds - // crash the webview during startViewTransition on certain GPU/Wayland - // stacks (issue #34) — writing first guarantees the next launch picks - // up the new theme even if this transition kills the process. - const persistNext = (value: boolean) => writeStoredTheme(value); + // Persist BEFORE triggering any animation. Some Linux WebKitGTK builds + // crash the webview during startViewTransition on certain GPU/Wayland + // stacks (issue #34) — writing first guarantees the next launch picks + // up the new theme even if this transition kills the process. + const persistNext = (value: boolean) => writeStoredTheme(value); - // Fallback: no View Transitions API support → instant swap - if (typeof document === "undefined" || !document.startViewTransition) { - flipTheme(); - persistNext(nextValue); - return; - } + // Fallback: no View Transitions API support → instant swap + if (typeof document === "undefined" || !document.startViewTransition) { + flipTheme(); + persistNext(nextValue); + return; + } - // Compute & persist the future value before the animation starts so the - // write lands even if the compositor crashes mid-transition. - persistNext(!isDark); + // Compute & persist the future value before the animation starts so the + // write lands even if the compositor crashes mid-transition. + persistNext(!isDark); - const transition = document.startViewTransition(flipTheme); + const transition = document.startViewTransition(flipTheme); - // Radial reveal from the click point - if (event) { - const x = event.clientX; - const y = event.clientY; - const endRadius = Math.hypot( - Math.max(x, window.innerWidth - x), - Math.max(y, window.innerHeight - y), - ); + // Radial reveal from the click point + if (event) { + const x = event.clientX; + const y = event.clientY; + const endRadius = Math.hypot( + Math.max(x, window.innerWidth - x), + Math.max(y, window.innerHeight - y), + ); - transition.ready - .then(() => { - document.documentElement.animate( - { - clipPath: [ - `circle(0px at ${x}px ${y}px)`, - `circle(${endRadius}px at ${x}px ${y}px)`, - ], - }, - { - duration: 600, - easing: "ease-in-out", - pseudoElement: "::view-transition-new(root)", - }, - ); - }) - .catch(() => { - // Animation failed — the theme has still toggled via flipTheme() - }); - } - }, [isDark]); + transition.ready + .then(() => { + document.documentElement.animate( + { + clipPath: [ + `circle(0px at ${x}px ${y}px)`, + `circle(${endRadius}px at ${x}px ${y}px)`, + ], + }, + { + duration: 600, + easing: "ease-in-out", + pseudoElement: "::view-transition-new(root)", + }, + ); + }) + .catch(() => { + // Animation failed — the theme has still toggled via flipTheme() + }); + } + }, + [isDark], + ); return ( diff --git a/src/main.tsx b/src/main.tsx index d9dcd69..002bed3 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,8 +1,8 @@ -import React, { useEffect } from "react"; +import React from "react"; import ReactDOM from "react-dom/client"; -import { emit } from "@tauri-apps/api/event"; import App from "./App"; import { MiniPlayerApp } from "./MiniPlayerApp"; +import { ReadySignal } from "./components/common/ReadySignal"; import "./app.css"; import { i18nReady } from "./i18n"; @@ -16,30 +16,8 @@ const isMini = new URLSearchParams(window.location.search).get("mini") === "1"; // run. A `splashscreen` window is shown in its place. The backend // listens for `app://ready` and atomically reveals the main window + // closes the splash from native code (see `reveal_main_close_splash` -// in src-tauri/src/lib.rs). -// -// Doing the handoff in native code rather than IPC avoids the race -// that bit issue #42 on Linux WebKitGTK 2.52: the previous frontend -// rAF-driven `window.show()` could fire before React had committed -// anything, leaving the user staring at an empty splash forever. -// -// We emit from a useEffect at the React root rather than from a 2-rAF -// dance because WebKitGTK 2.52 suspends `requestAnimationFrame` -// callbacks while a window is hidden — `visible: false` means rAF -// never fires until the backend reveals the window, deadlocking the -// handoff until the 15 s safety-net timer trips. useEffect runs after -// the first React commit, which is the actual guarantee we care about -// (DOM is populated before reveal); the compositor will paint the -// first frame as part of the reveal itself, so we don't need to -// observe a paint to avoid a flash. -function ReadySignal({ children }: { children: React.ReactNode }) { - useEffect(() => { - void emit("app://ready").catch((err) => { - console.error("[main] emit(app://ready) failed", err); - }); - }, []); - return <>{children}; -} +// in src-tauri/src/lib.rs). The actual event emission lives in +// `ReadySignal` so this entry-point file can stay HMR-friendly. i18nReady .catch((err) => {