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 @@ - + + + +{#if picker} +
+ {#each SHAPES as s (s.id)} + + {/each} +
+{/if} + + diff --git a/src/mini/bits/Lane.svelte b/src/mini/bits/Lane.svelte new file mode 100644 index 0000000..7edc3a6 --- /dev/null +++ b/src/mini/bits/Lane.svelte @@ -0,0 +1,150 @@ + + +
+ {#if showTimes}{fmt(mini.position)}{/if} +
+ +
+ {#if showTimes}{fmt(mini.duration)}{/if} +
+ + diff --git a/src/mini/bits/VolumeFly.svelte b/src/mini/bits/VolumeFly.svelte new file mode 100644 index 0000000..47b6f21 --- /dev/null +++ b/src/mini/bits/VolumeFly.svelte @@ -0,0 +1,154 @@ + + + + + + + {#if open} +
+
+
+
+
+
+ {/if} +
+ + diff --git a/src/mini/bits/Wash.svelte b/src/mini/bits/Wash.svelte new file mode 100644 index 0000000..000184d --- /dev/null +++ b/src/mini/bits/Wash.svelte @@ -0,0 +1,74 @@ + + +{#if shown || prev} + +{/if} + + diff --git a/src/mini/mini.svelte.js b/src/mini/mini.svelte.js new file mode 100644 index 0000000..8cf9fb5 --- /dev/null +++ b/src/mini/mini.svelte.js @@ -0,0 +1,130 @@ +// The mini window's whole world: a mirror of what the main window streams +// over the mini:* channels, and intent senders that go back to it. This file +// deliberately does NOT import state.svelte.js — the main window is the one +// brain (see miniBridge.svelte.js for the channel contract and the why). +import { emit } from "@tauri-apps/api/event"; +import { onEvent } from "../lib/api.js"; +import { invoke } from "@tauri-apps/api/core"; +import { getCurrentWindow, LogicalSize } from "@tauri-apps/api/window"; + +export const mini = $state({ + // mini:track + title: null, + artist: null, + album: null, + artUrl: null, + duration: null, + waveform: [], + coverage: 1, + // mini:tick + position: 0, + status: "idle", + volume: 0.7, + muted: false, + shuffle: false, + repeat: "off", + // mini:queue + rows: [], + index: -1, + next: null, // { title, artUrl } + // local + pinned: true, +}); + +export const playing = () => mini.status === "playing"; + +// ---- shapes --------------------------------------------------------------- +import { SHAPES, storedShapeId } from "./shapes.js"; +export { SHAPES }; +export const shapeState = $state({ id: storedShapeId() }); + +export async function setShape(id) { + const s = SHAPES.find((x) => x.id === id); + if (!s) return; + shapeState.id = id; + localStorage.setItem("miniShape", id); + try { + await getCurrentWindow().setSize(new LogicalSize(s.w, s.h)); + } catch {} +} + +// ---- intents -------------------------------------------------------------- +export const cmd = (op, value) => void emit("mini:cmd", { op, value }); +export const seekTo = (secs) => cmd("seek", secs); +export function setVolume(frac) { + mini.volume = Math.max(0, Math.min(1, frac)); + mini.muted = mini.volume === 0; + cmd("volume", mini.volume); +} + +export async function togglePin() { + mini.pinned = !mini.pinned; + try { + await getCurrentWindow().setAlwaysOnTop(mini.pinned); + } catch {} +} + +export const backToApp = () => void invoke("mini_close"); + +// ---- boot ----------------------------------------------------------------- +let booted = false; +export function initMini() { + if (booted) return; + booted = true; + // Dev-only probe handle, the mini's __soulstream: CDP drives the LIVE + // module through this — a bare-URL import can be a phantom second copy. + globalThis.__mini = { mini, shapeState, setShape, cmd, setVolume }; + onEvent("mini:track", (p) => { + p = p ?? {}; + mini.title = p.title; + mini.artist = p.artist; + mini.album = p.album; + mini.artUrl = p.artUrl; + mini.duration = p.duration; + mini.waveform = p.waveform ?? []; + mini.coverage = p.coverage ?? 1; + if (p.accent) { + document.documentElement.style.setProperty("--accent", p.accent); + document.documentElement.style.setProperty("--accent-hover", p.hover ?? p.accent); + } + }); + onEvent("mini:tick", (p) => { + p = p ?? {}; + mini.position = p.position ?? mini.position; + mini.status = p.status ?? mini.status; + if (typeof p.volume === "number") mini.volume = p.volume; + mini.muted = !!p.muted; + mini.shuffle = !!p.shuffle; + mini.repeat = p.repeat ?? mini.repeat; + }); + onEvent("mini:queue", (p) => { + p = p ?? {}; + mini.rows = p.rows ?? []; + mini.index = p.index ?? -1; + mini.next = p.next ?? null; + }); + // Ask the brain for a full snapshot; retry briefly in case the mini won + // the race against the bridge's listener registration. + void emit("mini:hello", {}); + setTimeout(() => { + if (mini.title == null) void emit("mini:hello", {}); + }, 600); + // Smooth position between ticks while playing (the brain ticks ~5/s). + let last = performance.now(); + const step = (now) => { + const dt = (now - last) / 1000; + last = now; + if (playing() && mini.duration) { + mini.position = Math.min(mini.duration, mini.position + dt); + } + requestAnimationFrame(step); + }; + requestAnimationFrame(step); +} + +export const fmt = (s) => { + if (s == null || !isFinite(s)) return "–:––"; + s = Math.max(0, Math.floor(s)); + return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; +}; + diff --git a/src/mini/shapes.js b/src/mini/shapes.js new file mode 100644 index 0000000..0f45708 --- /dev/null +++ b/src/mini/shapes.js @@ -0,0 +1,25 @@ +// The mini player's shape catalogue — shared between the mini window (which +// renders them) and the main window (which needs a size to open the window +// at). Logical sizes; the card IS the window, the OS draws the shadow. +export const SHAPES = [ + { id: "strip", label: "Strip", w: 484, h: 96 }, + { id: "sleeve", label: "Sleeve", w: 300, h: 452 }, + { id: "sliver", label: "Sliver", w: 356, h: 68 }, + { id: "wave", label: "Wave", w: 396, h: 184 }, + { id: "focus", label: "Focus", w: 288, h: 330 }, + { id: "deck", label: "Deck", w: 600, h: 72 }, + { id: "ondeck", label: "On Deck", w: 416, h: 104 }, + { id: "queue", label: "Queue", w: 344, h: 262 }, +]; + +export function shapeById(id) { + return SHAPES.find((s) => s.id === id) ?? SHAPES[0]; +} + +export function storedShapeId() { + try { + return localStorage.getItem("miniShape") || "strip"; + } catch { + return "strip"; + } +} diff --git a/src/mini/shapes/Deck.svelte b/src/mini/shapes/Deck.svelte new file mode 100644 index 0000000..06e70ee --- /dev/null +++ b/src/mini/shapes/Deck.svelte @@ -0,0 +1,96 @@ + + +
+ + +
+
+ +
+
+ + + +
+
+
{mini.artist ?? "Nothing playing"}{mini.title ? ` - ${mini.title}` : ""}
+ +
+
+ + + +
+
+
+ + diff --git a/src/mini/shapes/Focus.svelte b/src/mini/shapes/Focus.svelte new file mode 100644 index 0000000..7b92290 --- /dev/null +++ b/src/mini/shapes/Focus.svelte @@ -0,0 +1,131 @@ + + +
+ + +
+
+ +
+ + +
+ +
+
+
{mini.title ?? "Nothing playing"}
+
{mini.artist ?? ""}
+
{mini.album ?? ""}
+
+ +
+ + + +
+
+
+ + diff --git a/src/mini/shapes/OnDeck.svelte b/src/mini/shapes/OnDeck.svelte new file mode 100644 index 0000000..f7869f1 --- /dev/null +++ b/src/mini/shapes/OnDeck.svelte @@ -0,0 +1,135 @@ + + +
+ + +
+
+
+ +
+
+
{mini.title ?? "Nothing playing"}
+
{mini.artist ?? ""}
+
+ + + {#if mini.next} + + {/if} +
+ +
+
+ + diff --git a/src/mini/shapes/Queue.svelte b/src/mini/shapes/Queue.svelte new file mode 100644 index 0000000..161501d --- /dev/null +++ b/src/mini/shapes/Queue.svelte @@ -0,0 +1,229 @@ + + +
+ + +
+
+ +
+
+
+
+ {mini.title ?? "Nothing playing"}{mini.artist ? ` — ${mini.artist}` : ""} +
+ + + +
+
+ + + + +
+
+
+
+
Queue{mini.rows.length} tracks
+
+ {#each mini.rows as row, i (i)} + + {/each} +
+
+
+ + diff --git a/src/mini/shapes/Sleeve.svelte b/src/mini/shapes/Sleeve.svelte new file mode 100644 index 0000000..5a5a484 --- /dev/null +++ b/src/mini/shapes/Sleeve.svelte @@ -0,0 +1,146 @@ + + +
+ + +
+ +
+
+
+ +
+
{mini.title ?? "Nothing playing"}
+
{mini.artist ?? ""}
+
+ +
+ +
+ + + + + + + + + + + + +
+
+
+ + diff --git a/src/mini/shapes/Sliver.svelte b/src/mini/shapes/Sliver.svelte new file mode 100644 index 0000000..a8fb633 --- /dev/null +++ b/src/mini/shapes/Sliver.svelte @@ -0,0 +1,120 @@ + + +
+ + +
+
+ +
+
+
0} class:gliding={over > 0} style="--glide: {over + 12}px" bind:this={box}> +
+ {mini.title ?? "Nothing playing"}{mini.artist ?? ""} +
+
+ +
+ + + +
+
+ + diff --git a/src/mini/shapes/Strip.svelte b/src/mini/shapes/Strip.svelte new file mode 100644 index 0000000..db82f95 --- /dev/null +++ b/src/mini/shapes/Strip.svelte @@ -0,0 +1,92 @@ + + +
+ + +
+
+ +
+
+
+
+
{mini.title ?? "Nothing playing"}
+
{mini.artist ?? ""}
+
+ + + + +
+ +
+
+
+ + diff --git a/src/mini/shapes/Wave.svelte b/src/mini/shapes/Wave.svelte new file mode 100644 index 0000000..2fd4630 --- /dev/null +++ b/src/mini/shapes/Wave.svelte @@ -0,0 +1,162 @@ + + +
+ + +
+ +
+
+ +
+
+ {mini.artist ?? "Nothing playing"} - {mini.title ?? ""} +
+ + +
+ +
+
+ + diff --git a/src/styles/global.css b/src/styles/global.css index 8e70a26..68ccf4c 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -94,6 +94,19 @@ --nowplaying-rail: 64px; } +/* Dark-glass mode (Settings → Appearance): the window is transparent and the + OS puts acrylic blur behind it (window_glass command). ONLY the window + canvas thins over it — the black frame the panes float on, painted by .app + in App.svelte. Every pane keeps its own opaque surface; do NOT thin the + shared --bg vars here — they paint the sidebar, the content pane and the + artist page, and going translucent washed the whole app out (2026-08-26). + html and body must both stop painting, or their solid base blocks the + blur (html alone held a solid #121212 and ate the first attempt). */ +:root.glass, +:root.glass body { + background: transparent; +} + /* Floating overlay scrollbar (see src/lib/scrollbar.js). The native bar is hidden so it never reserves width or reflows content; a thin thumb floats in the container's right padding, fading in on scroll/hover, out when idle. */ diff --git a/src/views/Settings.svelte b/src/views/Settings.svelte index 606a7d5..8b70468 100644 --- a/src/views/Settings.svelte +++ b/src/views/Settings.svelte @@ -1,6 +1,7 @@