From dc7ea16b226f11e2dbf9018d17f10f1a9265c790 Mon Sep 17 00:00:00 2001
From: KeelrLeek <156356974+KeelrLeek@users.noreply.github.com>
Date: Wed, 26 Aug 2026 07:56:00 -0500
Subject: [PATCH] mini player + dark glass: eight shapes run by the hidden main
as the one brain, dissolving covers and accents, drag from anywhere, and the
space between the panes turned to real acrylic - with the webview flags that
keep rest frames honest
---
mini.html | 12 ++
src-tauri/capabilities/default.json | 5 +-
src-tauri/src/commands.rs | 113 +++++++++++++-
src-tauri/src/config.rs | 8 +
src-tauri/src/lib.rs | 36 +++++
src-tauri/tauri.conf.json | 17 ++-
src/App.svelte | 19 +++
src/components/Icon.svelte | 4 +
src/components/PlayerBar.svelte | 11 +-
src/lib/api.js | 4 +
src/lib/glass.js | 10 ++
src/lib/miniBridge.svelte.js | 175 +++++++++++++++++++++
src/lib/state.svelte.js | 9 ++
src/mini-main.js | 5 +
src/mini/Mini.svelte | 119 +++++++++++++++
src/mini/bits/Art.svelte | 54 +++++++
src/mini/bits/Chrome.svelte | 141 +++++++++++++++++
src/mini/bits/Lane.svelte | 150 ++++++++++++++++++
src/mini/bits/VolumeFly.svelte | 154 +++++++++++++++++++
src/mini/bits/Wash.svelte | 74 +++++++++
src/mini/mini.svelte.js | 130 ++++++++++++++++
src/mini/shapes.js | 25 +++
src/mini/shapes/Deck.svelte | 96 ++++++++++++
src/mini/shapes/Focus.svelte | 131 ++++++++++++++++
src/mini/shapes/OnDeck.svelte | 135 ++++++++++++++++
src/mini/shapes/Queue.svelte | 229 ++++++++++++++++++++++++++++
src/mini/shapes/Sleeve.svelte | 146 ++++++++++++++++++
src/mini/shapes/Sliver.svelte | 120 +++++++++++++++
src/mini/shapes/Strip.svelte | 92 +++++++++++
src/mini/shapes/Wave.svelte | 162 ++++++++++++++++++++
src/styles/global.css | 13 ++
src/views/Settings.svelte | 28 ++++
vite.config.js | 10 ++
33 files changed, 2432 insertions(+), 5 deletions(-)
create mode 100644 mini.html
create mode 100644 src/lib/glass.js
create mode 100644 src/lib/miniBridge.svelte.js
create mode 100644 src/mini-main.js
create mode 100644 src/mini/Mini.svelte
create mode 100644 src/mini/bits/Art.svelte
create mode 100644 src/mini/bits/Chrome.svelte
create mode 100644 src/mini/bits/Lane.svelte
create mode 100644 src/mini/bits/VolumeFly.svelte
create mode 100644 src/mini/bits/Wash.svelte
create mode 100644 src/mini/mini.svelte.js
create mode 100644 src/mini/shapes.js
create mode 100644 src/mini/shapes/Deck.svelte
create mode 100644 src/mini/shapes/Focus.svelte
create mode 100644 src/mini/shapes/OnDeck.svelte
create mode 100644 src/mini/shapes/Queue.svelte
create mode 100644 src/mini/shapes/Sleeve.svelte
create mode 100644 src/mini/shapes/Sliver.svelte
create mode 100644 src/mini/shapes/Strip.svelte
create mode 100644 src/mini/shapes/Wave.svelte
diff --git a/mini.html b/mini.html
new file mode 100644
index 0000000..ad086c7
--- /dev/null
+++ b/mini.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ SoulStream Mini
+
+
+
+
+
+
diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json
index 88063b8..dc123fc 100644
--- a/src-tauri/capabilities/default.json
+++ b/src-tauri/capabilities/default.json
@@ -3,7 +3,8 @@
"identifier": "default",
"description": "enables the default permissions",
"windows": [
- "main"
+ "main",
+ "mini"
],
"permissions": [
"core:default",
@@ -16,6 +17,8 @@
"core:window:allow-is-maximized",
"core:window:allow-show",
"core:window:allow-set-focus",
+ "core:window:allow-set-size",
+ "core:window:allow-set-always-on-top",
"dialog:allow-open"
]
}
diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs
index 7162abc..bbb0890 100644
--- a/src-tauri/src/commands.rs
+++ b/src-tauri/src/commands.rs
@@ -1,4 +1,4 @@
-use serde::{Deserialize, Serialize};
+use serde::{Deserialize, Serialize};
use tauri::{Emitter, Manager, State};
use soulseek_client::{Client, Config, DownloadState, DownloadUpdate, Event};
@@ -78,6 +78,8 @@ pub struct Settings {
pub volume_normalization: bool,
/// Word-by-word lyric fill — experimental, default off.
pub word_timing: bool,
+ /// Dark-glass acrylic backdrop, default off.
+ pub glass_mode: bool,
}
#[tauri::command]
@@ -117,6 +119,7 @@ pub async fn get_settings(
download_source_prefs: cfg.download_source_prefs,
volume_normalization: config::volume_normalization(&cfg),
word_timing: config::word_timing(&cfg),
+ glass_mode: config::glass_mode(&cfg),
username: cfg.username,
})
}
@@ -145,6 +148,7 @@ pub async fn set_settings(
download_source_prefs: Option,
volume_normalization: Option,
word_timing: Option,
+ glass_mode: Option,
) -> Result<(), String> {
let mut cfg = config::load(&app);
let library_folder_moved = download_dir
@@ -216,6 +220,9 @@ pub async fn set_settings(
if word_timing.is_some() {
cfg.word_timing = word_timing;
}
+ if glass_mode.is_some() {
+ cfg.glass_mode = glass_mode;
+ }
config::save(&app, &cfg);
// A new library folder is a new library. Re-base immediately rather than
@@ -4857,6 +4864,108 @@ pub fn player_control(
Ok(())
}
+/// Enter mini mode: show the pre-built mini window (declared in
+/// tauri.conf.json, created hidden at startup — runtime window creation
+/// from a command half-initialises on Windows) and hide the main one. The
+/// main window is HIDDEN, never closed: its frontend stays alive as the one
+/// brain the mini remote-controls (see src/lib/miniBridge.svelte.js).
+#[tauri::command]
+pub async fn mini_open(app: tauri::AppHandle, width: f64, height: f64) -> Result<(), String> {
+ let mini = app
+ .get_webview_window("mini")
+ .ok_or_else(|| "mini window missing from config".to_string())?;
+ let _ = mini.set_size(tauri::LogicalSize::new(width, height));
+ mini.show().map_err(|e| e.to_string())?;
+ let _ = mini.set_focus();
+ if let Some(main) = app.get_webview_window("main") {
+ let _ = main.hide();
+ }
+ Ok(())
+}
+
+/// Leave mini mode: bring the full app back and tuck the mini away warm
+/// (hidden, not closed — reopening is instant and its mirror stays hot).
+#[tauri::command]
+pub async fn mini_close(app: tauri::AppHandle) -> Result<(), String> {
+ if let Some(main) = app.get_webview_window("main") {
+ let _ = main.show();
+ let _ = main.set_focus();
+ }
+ if let Some(mini) = app.get_webview_window("mini") {
+ let _ = mini.hide();
+ }
+ Ok(())
+}
+
+/// The OS half of dark-glass mode: acrylic blur behind the calling window
+/// (the window itself is transparent; the CSS half — :root.glass — thins the
+/// window canvas so the blur shows through).
+///
+/// NOT tauri's Effect::Acrylic: on Win11 22H2+ that picks the DWMSBT system
+/// backdrop, which Windows SUSPENDS whenever the maximized webview gets
+/// promoted to a hardware overlay — every hover animation or compositor
+/// scroll blinked the glass dark for a beat (Kyle, 2026-08-26). The legacy
+/// accent-policy acrylic below is composed with the window itself and holds
+/// through interaction.
+#[tauri::command]
+pub fn window_glass(window: tauri::WebviewWindow, on: bool) -> Result<(), String> {
+ // Clear any system backdrop a previous toggle/build may have applied.
+ let _ = window.set_effects(None::);
+ let hwnd = window.hwnd().map_err(|e| e.to_string())?.0 as *mut std::ffi::c_void;
+
+ #[repr(C)]
+ struct AccentPolicy {
+ state: u32,
+ flags: u32,
+ gradient: u32, // 0xAABBGGRR
+ animation: u32,
+ }
+ #[repr(C)]
+ struct AttribData {
+ attrib: u32,
+ data: *mut std::ffi::c_void,
+ size: usize,
+ }
+ #[allow(non_snake_case)]
+ #[link(name = "kernel32")]
+ extern "system" {
+ fn GetModuleHandleA(name: *const u8) -> *mut std::ffi::c_void;
+ fn GetProcAddress(
+ module: *mut std::ffi::c_void,
+ name: *const u8,
+ ) -> *mut std::ffi::c_void;
+ }
+ type Swca = unsafe extern "system" fn(*mut std::ffi::c_void, *mut AttribData) -> i32;
+
+ unsafe {
+ let user32 = GetModuleHandleA(b"user32.dll\0".as_ptr());
+ let f = if user32.is_null() {
+ std::ptr::null_mut()
+ } else {
+ GetProcAddress(user32, b"SetWindowCompositionAttribute\0".as_ptr())
+ };
+ if f.is_null() {
+ return Err("SetWindowCompositionAttribute unavailable".into());
+ }
+ let f: Swca = std::mem::transmute(f);
+ let mut policy = AccentPolicy {
+ state: if on { 4 } else { 0 }, // ACCENT_ENABLE_ACRYLICBLURBEHIND / ACCENT_DISABLED
+ flags: 0,
+ // A whisper of black in the OS layer (acrylic treats alpha 0 as
+ // "don't draw"); the real darkening is the CSS canvas tint.
+ gradient: if on { 0x2E00_0000 } else { 0 },
+ animation: 0,
+ };
+ let mut data = AttribData {
+ attrib: 0x13, // WCA_ACCENT_POLICY
+ data: &mut policy as *mut _ as _,
+ size: std::mem::size_of::(),
+ };
+ f(hwnd, &mut data);
+ }
+ Ok(())
+}
+
#[cfg(test)]
mod hero_processing_tests {
use super::*;
@@ -4937,3 +5046,5 @@ mod hero_processing_tests {
assert_eq!(trimmed.dimensions(), (256, 120));
}
}
+
+
diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs
index 0010c55..c1a8c31 100644
--- a/src-tauri/src/config.rs
+++ b/src-tauri/src/config.rs
@@ -140,6 +140,10 @@ pub struct AppConfig {
#[serde(default)]
pub volume_normalization: Option,
pub word_timing: Option,
+ /// Dark-glass mode: acrylic blur behind a transparent window, with the
+ /// base surfaces thinned in CSS so the desktop glows through. None → false.
+ #[serde(default)]
+ pub glass_mode: Option,
}
/// Default longest edge for embedded covers when the user hasn't chosen.
@@ -185,6 +189,10 @@ pub fn volume_normalization(cfg: &AppConfig) -> bool {
/// default: generated word spans still drift on dense or echo-heavy mixes,
/// and the solid line-level sync must be what users live with until the
/// feature earns its keep (Kyle, 2026-08-19).
+pub fn glass_mode(cfg: &AppConfig) -> bool {
+ cfg.glass_mode.unwrap_or(false)
+}
+
pub fn word_timing(cfg: &AppConfig) -> bool {
cfg.word_timing.unwrap_or(false)
}
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 8054e30..2cd443a 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -27,6 +27,39 @@ mod ytdlp;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
+ // Two WebView2 presentation fixes, both delivered by env var because
+ // tauri.conf's additionalBrowserArgs never reaches the browser process
+ // (verified via its command line). Merge, don't clobber — dev launches
+ // carry the CDP port here. Both found 2026-08-26 via the dark-glass work:
+ //
+ // 1. NO delegated compositing: with it, DWM composes the webview's layers
+ // at rest and blends semi-transparent darks in the wrong colorspace —
+ // the app rested washed-out LIGHT and looked correct only while an
+ // animation forced Chromium to compose in-process.
+ // 2. Force the sRGB color profile: on an HDR display the MAXIMIZED
+ // window's rest path takes a different SDR-white tone mapping than
+ // the animated path — covers blew out near-white at rest and snapped
+ // correct on hover. One profile, one mapping, both paths agree.
+ #[cfg(windows)]
+ {
+ const KEY: &str = "WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS";
+ let mut args = std::env::var(KEY).unwrap_or_default();
+ let mut ensure = |flag: &str, key: &str| {
+ if !args.contains(key) {
+ if !args.is_empty() {
+ args.push(' ');
+ }
+ args.push_str(flag);
+ }
+ };
+ ensure(
+ "--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection,DelegatedCompositing",
+ "DelegatedCompositing",
+ );
+ ensure("--force-color-profile=srgb", "force-color-profile");
+ std::env::set_var(KEY, args);
+ }
+
tauri::Builder::default()
.manage(state::AppState::default())
.setup(|app| {
@@ -108,6 +141,9 @@ pub fn run() {
commands::unfollow_artist,
commands::home_artist_news,
commands::home_warm,
+ commands::mini_open,
+ commands::mini_close,
+ commands::window_glass,
playlists::list_playlists,
playlists::create_playlist,
playlists::rename_playlist,
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 530c62d..bf8283a 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -19,8 +19,23 @@
"height": 820,
"minWidth": 960,
"minHeight": 600,
- "backgroundColor": "#121212",
+ "transparent": true,
"decorations": false
+ },
+ {
+ "label": "mini",
+ "title": "SoulStream Mini",
+ "url": "mini.html",
+ "width": 484,
+ "height": 96,
+ "visible": false,
+ "decorations": false,
+ "resizable": false,
+ "maximizable": false,
+ "minimizable": false,
+ "alwaysOnTop": true,
+ "shadow": true,
+ "backgroundColor": "#181818"
}
],
"security": {
diff --git a/src/App.svelte b/src/App.svelte
index b9dc6e1..65cbb72 100644
--- a/src/App.svelte
+++ b/src/App.svelte
@@ -36,6 +36,9 @@
restoreSession,
} from "./lib/state.svelte.js";
import { floatScroll } from "./lib/scrollbar.js";
+ import { initMiniBridge } from "./lib/miniBridge.svelte.js";
+ import { applyGlass } from "./lib/glass.js";
+ import { getSettings } from "./lib/api.js";
const views = {
home: Home,
@@ -97,6 +100,14 @@
]);
initEvents();
+ // The mini player's channel to this window — this window stays the one
+ // brain while the mini is up (see miniBridge.svelte.js).
+ initMiniBridge();
+ // Dark-glass mode is persisted in config; the window boots solid and
+ // glasses up as soon as the setting arrives (no-op when off).
+ void getSettings()
+ .then((s) => s?.glass_mode && applyGlass(true))
+ .catch(() => {});
autoConnect();
refreshLibrary();
refreshPlaylists();
@@ -208,6 +219,14 @@
transition: none;
}
+ /* Dark-glass mode: the CANVAS alone — the black the panes float on (grid
+ gaps, edge padding, the titlebar strip) — goes translucent over the OS
+ acrylic. The panes above it all paint their own opaque surfaces and
+ must stay untouched. */
+ :global(:root.glass) .app {
+ background: rgba(0, 0, 0, 0.7);
+ }
+
/* Collapsed left nav: narrow the sidebar column to an icon rail. The
grid-template-columns transition animates the reflow. */
.app.sidebar-collapsed {
diff --git a/src/components/Icon.svelte b/src/components/Icon.svelte
index 948fcac..dc51c8b 100644
--- a/src/components/Icon.svelte
+++ b/src/components/Icon.svelte
@@ -25,6 +25,10 @@
volume: '',
"volume-low": '',
"volume-mute": '',
+ // The mini player's chrome (Lucide pin / shapes / maximize-2).
+ pin: '',
+ shapes: '',
+ restore: '',
music: '',
disc: '',
folder: '',
diff --git a/src/components/PlayerBar.svelte b/src/components/PlayerBar.svelte
index 758a329..ef99e1c 100644
--- a/src/components/PlayerBar.svelte
+++ b/src/components/PlayerBar.svelte
@@ -36,6 +36,7 @@
PENDING_ALPHA,
} from "../lib/waveshape.js";
import Icon from "./Icon.svelte";
+ import { shapeById, storedShapeId } from "../mini/shapes.js";
const pct = $derived(
player.duration ? Math.min(100, (player.position / player.duration) * 100) : 0,
@@ -265,6 +266,12 @@
menuOpen = false;
showNowPlaying(); // the queue lives in the panel; always reveal it
}
+ function openMini() {
+ menuOpen = false;
+ // Open at the stored shape's size; the mini asserts it again on boot.
+ const s = shapeById(storedShapeId());
+ void api.miniOpen(s.w, s.h);
+ }
function openLyrics() {
menuOpen = false;
// The lyrics PAGE (views/Lyrics.svelte) — the overlay it used to open
@@ -1324,8 +1331,8 @@
-