Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/features/ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
164 changes: 164 additions & 0 deletions public/splash.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WaveFlow</title>
<style>
:root {
color-scheme: dark;
}
html,
body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
background: transparent;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif;
-webkit-font-smoothing: antialiased;
user-select: none;
cursor: default;
}
.card {
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 22px;
background: radial-gradient(
120% 80% at 50% 0%,
rgba(16, 185, 129, 0.18) 0%,
rgba(16, 185, 129, 0) 60%
),
#121212;
border-radius: 14px;
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.55);
}
.logo {
width: 96px;
height: 96px;
display: block;
filter: drop-shadow(0 6px 18px rgba(16, 185, 129, 0.35));
}
.wordmark {
font-size: 22px;
font-weight: 600;
letter-spacing: 0.4px;
color: #f4f4f5;
}
.bar {
position: relative;
width: 180px;
height: 3px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
overflow: hidden;
}
.bar::after {
content: "";
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 40%;
background: linear-gradient(
90deg,
rgba(16, 185, 129, 0) 0%,
#10b981 50%,
rgba(16, 185, 129, 0) 100%
);
animation: slide 1.4s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
@keyframes slide {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(350%);
}
}
@media (prefers-reduced-motion: reduce) {
.bar::after {
animation: none;
width: 100%;
opacity: 0.6;
}
}
</style>
</head>
<body>
<div class="card" role="status" aria-label="Loading WaveFlow">
<svg
class="logo"
width="256"
height="256"
viewBox="0 0 256 256"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<defs>
<linearGradient
id="waveflowGrad"
x1="0"
y1="0"
x2="256"
y2="256"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#34D399" />
<stop offset="0.5" stop-color="#10B981" />
<stop offset="1" stop-color="#059669" />
</linearGradient>
</defs>
<rect
x="36"
y="58"
width="24"
height="140"
rx="12"
fill="url(#waveflowGrad)"
/>
<rect
x="76"
y="88"
width="24"
height="80"
rx="12"
fill="url(#waveflowGrad)"
/>
<rect
x="116"
y="108"
width="24"
height="40"
rx="12"
fill="url(#waveflowGrad)"
/>
<rect
x="156"
y="88"
width="24"
height="80"
rx="12"
fill="url(#waveflowGrad)"
/>
<rect
x="196"
y="58"
width="24"
height="140"
rx="12"
fill="url(#waveflowGrad)"
/>
</svg>
<div class="wordmark">WaveFlow</div>
<div class="bar" aria-hidden="true"></div>
</div>
</body>
</html>
19 changes: 18 additions & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
35 changes: 35 additions & 0 deletions src/main.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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);
Expand All @@ -20,4 +54,5 @@ i18nReady
{isMini ? <MiniPlayerApp /> : <App />}
</React.StrictMode>,
);
revealMainWindow();
});
Loading