diff --git a/docs/features/ui.md b/docs/features/ui.md index a8b29eb..7e47188 100644 --- a/docs/features/ui.md +++ b/docs/features/ui.md @@ -50,6 +50,12 @@ Two entry points in the [`PlayerBar`](../../src/components/player/PlayerBar.tsx) - **Interactive seek bar** — slim white bar at the bottom, click/drag to scrub. Same `pointer capture` + local `dragMs` pattern as the main `ProgressBar`. Thumb + timestamps fade in on hover so the idle widget stays minimal. - **Capabilities** — the mini-player's window label is added to [`capabilities/default.json`](../../src-tauri/capabilities/default.json) so it inherits every command the main window has access to (no duplicated capability file, no per-window permission pruning). +## Splash screen + +To hide the cold-start delay (Windows SmartScreen / Defender scanning every freshly-extracted DLL on the very first launch after install, plus the `setup()` chain in [`lib.rs`](../../src-tauri/src/lib.rs) — opening `app.db` + running migrations, creating the default profile, cold-initialising cpal/WASAPI), the main window is created with `"visible": false` and a small secondary window (`label: "splashscreen"`, 360×240, transparent, decorations off, always-on-top, off the taskbar) shows a WaveFlow logo + indeterminate progress bar while the backend boots and the React bundle parses. + +The static HTML lives in [`public/splash.html`](../../public/splash.html) (no JS, inline SVG logo, single CSS animation) so it paints the instant the WebView2 process spawns. [`main.tsx`](../../src/main.tsx) runs the takeover after the first React frame: show the main window first, then close the splash so the desktop is never visible between the two. The mini-player webview branches out via `?mini=1` and skips the dance. + ## System tray Quick playback controls (Play/Pause, Previous, Next, Quitter). Close-to-tray is the default close behaviour — the `WindowEvent::CloseRequested` handler hides the window unless the tray "Quitter" item armed `QuitGate`. Tray ID is `waveflow`. diff --git a/public/splash.html b/public/splash.html new file mode 100644 index 0000000..81df78e --- /dev/null +++ b/public/splash.html @@ -0,0 +1,164 @@ + + + + + + WaveFlow + + + +
+ +
WaveFlow
+ +
+ + diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index d464a17..ce57794 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -12,12 +12,29 @@ "app": { "windows": [ { + "label": "main", "title": "WaveFlow", "width": 1440, "height": 900, "minWidth": 1000, "minHeight": 650, - "center": true + "center": true, + "visible": false + }, + { + "label": "splashscreen", + "url": "splash.html", + "title": "WaveFlow", + "width": 360, + "height": 240, + "center": true, + "resizable": false, + "decorations": false, + "transparent": true, + "alwaysOnTop": true, + "skipTaskbar": true, + "focus": false, + "shadow": false } ], "security": { diff --git a/src/main.tsx b/src/main.tsx index 760b51c..1b243ef 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,5 +1,6 @@ import React from "react"; import ReactDOM from "react-dom/client"; +import { getCurrentWindow, Window as TauriWindow } from "@tauri-apps/api/window"; import App from "./App"; import { MiniPlayerApp } from "./MiniPlayerApp"; import "./app.css"; @@ -10,6 +11,39 @@ 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. +// +// 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() { + if (isMini) return; + 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); + } + })(); + }); + }); +} + i18nReady .catch((err) => { console.error("[i18n] initialization failed", err); @@ -20,4 +54,5 @@ i18nReady {isMini ? : } , ); + revealMainWindow(); });