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();
});