diff --git a/README.md b/README.md index 9e57fc1..b820af4 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,10 @@ the toast. That's it. instead of dumping the last 90 days on you. - **Starts with your session**, minimized to the tray, if you want it to. - **English and Spanish.** Follows your system language, or pick one in settings. +- **Glass.** The window is translucent and asks the compositor to blur what's + behind it: KDE Plasma (both the KDE and the new `ext_background_effect` + protocols), Windows 11 acrylic, macOS vibrancy. A slider in settings sets + the opacity; 100% is opaque. GNOME shows the tint without blur. - **Three-step first run.** Token, what to watch (your busiest repos suggested), and a real notification from your own repo. Skippable, replayable from settings. - **Tiny.** Built with Tauri v2: a few megabytes, not a bundled browser. diff --git a/index.html b/index.html index 0f60e25..502a6c8 100644 --- a/index.html +++ b/index.html @@ -237,6 +237,12 @@

+
+ + + +

+
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 50aa5f5..9b39b37 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -893,6 +893,15 @@ dependencies = [ "syn 3.0.5", ] +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + [[package]] name = "dlopen2" version = "0.8.2" @@ -931,6 +940,12 @@ dependencies = [ "tendril", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dpi" version = "0.1.2" @@ -1429,6 +1444,10 @@ dependencies = [ name = "gitbell" version = "0.1.0" dependencies = [ + "gdk", + "gdkwayland-sys", + "glib", + "gtk", "keyring", "notify-rust", "serde", @@ -1442,6 +1461,11 @@ dependencies = [ "tauri-plugin-opener", "tauri-plugin-single-instance", "tauri-plugin-store", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "window-vibrancy 0.8.0", ] [[package]] @@ -2493,6 +2517,7 @@ dependencies = [ "objc2", "objc2-core-foundation", "objc2-foundation", + "objc2-quartz-core", ] [[package]] @@ -2845,7 +2870,7 @@ checksum = "2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb" dependencies = [ "base64 0.23.1", "indexmap 2.14.2", - "quick-xml", + "quick-xml 0.42.0", "serde", "time", ] @@ -3003,6 +3028,15 @@ version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.42.0" @@ -3330,6 +3364,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -3938,7 +3978,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "window-vibrancy", + "window-vibrancy 0.6.0", "windows", ] @@ -4893,6 +4933,79 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.2", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml 0.41.0", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.105" @@ -5041,6 +5154,22 @@ dependencies = [ "windows-version", ] +[[package]] +name = "window-vibrancy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6461139557c2245c3b9b34de2092e7af39460bf97ef4723bc5feb0cf66f831" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "windows-sys 0.60.2", + "windows-version", +] + [[package]] name = "windows" version = "0.61.3" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9f2fa56..629b589 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -42,6 +42,17 @@ tauri-plugin-autostart = "2" tauri-plugin-single-instance = "2" [target.'cfg(target_os = "linux")'.dependencies] +gdk = "0.18" +gdkwayland-sys = "0.18" +glib = "0.18" +gtk = "0.18" # direct access to freedesktop hints (app name, desktop-entry, click actions) # that tauri-plugin-notification doesn't expose notify-rust = "4" +wayland-backend = { version = "0.3", features = ["client_system"] } +wayland-client = "0.31" +wayland-protocols = { version = "0.32", features = ["client", "staging"] } +wayland-protocols-plasma = { version = "0.3", features = ["client"] } + +[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies] +window-vibrancy = "0.8" diff --git a/src-tauri/src/glass.rs b/src-tauri/src/glass.rs new file mode 100644 index 0000000..8672629 --- /dev/null +++ b/src-tauri/src/glass.rs @@ -0,0 +1,133 @@ +//! Real glass: ask the compositor to blur whatever is behind our window. +//! +//! Tauri can make the window transparent, but "blur what's behind" is a +//! per-compositor favor an app has to request. On Wayland we speak two +//! protocols: KDE's `org_kde_kwin_blur_manager` (Plasma <= 6.6) and the +//! standard `ext_background_effect_v1` (Plasma 6.7+, Niri). We reuse the +//! wl_display/wl_surface GTK already owns, so no second connection. +//! Windows/macOS use Tauri's native vibrancy crate instead (todo). + +#[cfg(target_os = "linux")] +mod linux { + use std::sync::Mutex; + + use gtk::prelude::*; + use glib::translate::ToGlibPtr; + use wayland_client::{ + backend::{Backend, ObjectId}, + globals::registry_queue_init, + protocol::{wl_registry, wl_surface::WlSurface}, + Connection, Dispatch, Proxy, QueueHandle, + }; + use wayland_protocols::ext::background_effect::v1::client::{ + ext_background_effect_manager_v1::ExtBackgroundEffectManagerV1, + ext_background_effect_surface_v1::ExtBackgroundEffectSurfaceV1, + }; + use wayland_protocols_plasma::blur::client::{ + org_kde_kwin_blur::OrgKdeKwinBlur, org_kde_kwin_blur_manager::OrgKdeKwinBlurManager, + }; + + /// One wrapper around GTK's display for the whole process. Wrapping the + /// same foreign display twice is asking for trouble. + static CONN: Mutex> = Mutex::new(None); + /// The effect object must stay alive or the compositor drops it. GTK + /// destroys the wl_surface when the window hides, so we make a fresh one + /// on every show and let the old proxy go. + static EFFECT: Mutex> = Mutex::new(None); + #[allow(dead_code)] // held only to keep the compositor object alive + enum Effect { + Ext(ExtBackgroundEffectSurfaceV1), + Kde(OrgKdeKwinBlur), + } + + struct State; + impl Dispatch for State { + fn event( + _: &mut Self, + _: &wl_registry::WlRegistry, + _: wl_registry::Event, + _: &wayland_client::globals::GlobalListContents, + _: &Connection, + _: &QueueHandle, + ) { + } + } + wayland_client::delegate_noop!(State: ignore ExtBackgroundEffectManagerV1); + wayland_client::delegate_noop!(State: ignore ExtBackgroundEffectSurfaceV1); + wayland_client::delegate_noop!(State: ignore OrgKdeKwinBlurManager); + wayland_client::delegate_noop!(State: ignore OrgKdeKwinBlur); + wayland_client::delegate_noop!(State: ignore WlSurface); + + /// Must run on the main thread after the window is shown (realized). + /// Safe to call on every show. + pub fn enable(window: &tauri::WebviewWindow) -> Result<&'static str, String> { + let gtk_win = window.gtk_window().map_err(|e| e.to_string())?; + let gdk_win = gtk_win.window().ok_or("window not realized yet")?; + let display = gdk_win.display(); + + // GTK hands us raw libwayland pointers; null means X11/XWayland + let display_ptr: *mut gdk::ffi::GdkDisplay = display.to_glib_none().0; + let window_ptr: *mut gdk::ffi::GdkWindow = gdk_win.to_glib_none().0; + let wl_display = unsafe { + gdk_wayland_sys::gdk_wayland_display_get_wl_display(display_ptr as *mut _) + }; + let wl_surface = unsafe { + gdk_wayland_sys::gdk_wayland_window_get_wl_surface(window_ptr as *mut _) + }; + if wl_display.is_null() || wl_surface.is_null() { + return Err("not a native Wayland window".into()); + } + + let conn = { + let mut slot = CONN.lock().unwrap(); + if slot.is_none() { + let backend = unsafe { Backend::from_foreign_display(wl_display as *mut _) }; + *slot = Some(Connection::from_backend(backend)); + } + slot.as_ref().unwrap().clone() + }; + let (globals, queue) = registry_queue_init::(&conn).map_err(|e| e.to_string())?; + let qh = queue.handle(); + + let surface_id = unsafe { ObjectId::from_ptr(WlSurface::interface(), wl_surface as *mut _) } + .map_err(|e| e.to_string())?; + let surface = WlSurface::from_id(&conn, surface_id).map_err(|e| e.to_string())?; + + // standard protocol first, KDE's legacy one as fallback + let (effect, which) = if let Ok(mgr) = globals.bind::(&qh, 1..=1, ()) { + let eff = mgr.get_background_effect(&surface, &qh, ()); + eff.set_blur_region(None); // null region = the whole surface + (Effect::Ext(eff), "ext_background_effect_v1") + } else if let Ok(mgr) = globals.bind::(&qh, 1..=1, ()) { + let blur = mgr.create(&surface, &qh, ()); + blur.commit(); + (Effect::Kde(blur), "org_kde_kwin_blur_manager") + } else { + return Err("compositor offers no blur protocol".into()); + }; + // GTK commits the surface on its next frame; nudge one now so the + // effect shows without waiting for a repaint + surface.commit(); + conn.flush().map_err(|e| e.to_string())?; + + *EFFECT.lock().unwrap() = Some(effect); + Ok(which) + } +} + +#[cfg(target_os = "linux")] +pub use linux::enable; + +#[cfg(target_os = "windows")] +pub fn enable(window: &tauri::WebviewWindow) -> Result<&'static str, String> { + // acrylic on 10/11; the tint comes from CSS, keep the native one faint + window_vibrancy::apply_acrylic(window, Some((0, 0, 0, 10))).map_err(|e| e.to_string())?; + Ok("acrylic") +} + +#[cfg(target_os = "macos")] +pub fn enable(window: &tauri::WebviewWindow) -> Result<&'static str, String> { + use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial}; + apply_vibrancy(window, NSVisualEffectMaterial::HudWindow, None, None).map_err(|e| e.to_string())?; + Ok("vibrancy") +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index cd635ff..0ae4364 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,6 +9,7 @@ //! sounds - import user audio files into the app data dir mod cli; +mod glass; mod i18n; mod notify; mod secrets; diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index f89e3bb..814af3d 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -40,6 +40,30 @@ pub fn show_main_window(app: &AppHandle) { let _ = win.show(); let _ = win.unminimize(); let _ = win.set_focus(); + request_glass(&win); + } +} + +/// Ask the compositor to blur behind the window. GTK realizes the surface +/// asynchronously after show(), so we wait a beat on the GTK main loop. +#[cfg(target_os = "linux")] +fn request_glass(win: &tauri::WebviewWindow) { + let w = win.clone(); + let _ = win.run_on_main_thread(move || { + glib::timeout_add_local_once(std::time::Duration::from_millis(250), move || { + match crate::glass::enable(&w) { + Ok(which) => eprintln!("glass: blur via {which}"), + Err(e) => eprintln!("glass: {e}"), + } + }); + }); +} + +#[cfg(not(target_os = "linux"))] +fn request_glass(win: &tauri::WebviewWindow) { + match crate::glass::enable(win) { + Ok(which) => eprintln!("glass: {which}"), + Err(e) => eprintln!("glass: {e}"), } } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 24c8340..f14718f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -20,7 +20,8 @@ "minWidth": 560, "minHeight": 480, "visible": false, - "center": true + "center": true, + "transparent": true } ], "security": { diff --git a/src/core/i18n.ts b/src/core/i18n.ts index fe2c15f..e190e4d 100644 --- a/src/core/i18n.ts +++ b/src/core/i18n.ts @@ -61,6 +61,8 @@ const dict = { "options.ignoreOwn": "Ignorar lo que hago yo", "options.autostart": "Iniciar con el sistema", "options.language": "Idioma", + "options.glass": "Vidrio", + "options.glassHint": "Opacidad de la ventana. En KDE, Windows 11 y macOS lo de atrás se ve desenfocado.", "lang.auto": "Automático", "lang.es": "Español", "lang.en": "English", @@ -201,6 +203,8 @@ const dict = { "options.ignoreOwn": "Ignore my own activity", "options.autostart": "Start with the system", "options.language": "Language", + "options.glass": "Glass", + "options.glassHint": "Window opacity. On KDE, Windows 11 and macOS what's behind shows up blurred.", "lang.auto": "Automatic", "lang.es": "Español", "lang.en": "English", diff --git a/src/core/types.ts b/src/core/types.ts index 76b043a..c4077cc 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -52,6 +52,8 @@ export interface Settings { soundsEnabled: boolean; /** 0..1 */ volume: number; + /** window tint opacity, 0.3..1. 1 = opaque, no glass */ + glass: number; /** which sound each category plays */ sounds: Record; } @@ -78,6 +80,7 @@ export const DEFAULT_SETTINGS: Settings = { ignoreOwn: true, soundsEnabled: true, volume: 0.8, + glass: 0.7, sounds: { push: "sound1", pr: "sound2", diff --git a/src/main.ts b/src/main.ts index 0a5c0a0..7ec4473 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,7 +9,7 @@ import { playSound } from "./platform/sounds"; import { getToken } from "./platform/secrets"; import { loadRecent, loadSettings, saveRecent, saveSettings } from "./platform/settings"; import { Onboarding } from "./ui/onboarding"; -import { Ui } from "./ui/ui"; +import { Ui, applyGlass } from "./ui/ui"; async function boot(): Promise { // `pnpm dev` opened in a plain browser: no Tauri, no keyring, no store. @@ -21,6 +21,7 @@ async function boot(): Promise { } const settings = await loadSettings(); const recent = await loadRecent(); + applyGlass(settings.glass); // language first: everything rendered after this reads the dictionary const lang = initLang(settings.language, navigator.language); invoke("set_language", { lang }).catch((e) => console.warn("set_language", e)); diff --git a/src/ui/preview.ts b/src/ui/preview.ts index c8fb1b4..68a635a 100644 --- a/src/ui/preview.ts +++ b/src/ui/preview.ts @@ -14,6 +14,10 @@ import { Ui } from "./ui"; export function preview(): void { initLang("auto", navigator.language); const hash = new URLSearchParams(location.hash.slice(1)); + // stand-in for the blurred desktop the compositor would show behind us + document.documentElement.style.background = + "radial-gradient(60% 50% at 15% 20%, #1ec8c8 0%, transparent 60%), radial-gradient(50% 60% at 85% 80%, #3b5bff 0%, transparent 60%), #0b0d12"; + if (hash.has("glass")) document.documentElement.style.setProperty("--glass", String(Number(hash.get("glass")) / 100)); const ob = hash.get("ob"); const connected = ob?.endsWith("c") ?? false; const step = Number((ob ?? "1").replace("c", "")) || 1; @@ -43,6 +47,7 @@ export function preview(): void { token: () => token, }); ui.mount(); + if (hash.has("glass")) document.documentElement.style.setProperty("--glass", String(Number(hash.get("glass")) / 100)); ui.setStatus(noToken ? { kind: "error", message: "Falta el token de GitHub" } : { kind: "ok", at: new Date(), remaining: 4987 }); if (hash.get("view") === "settings") ui.setView("settings"); diff --git a/src/ui/styles.css b/src/ui/styles.css index 8abc962..3c96657 100644 --- a/src/ui/styles.css +++ b/src/ui/styles.css @@ -10,6 +10,8 @@ --ok: #16a34a; --warn: #d97706; --err: #dc2626; + --glass-line: rgba(0, 0, 0, 0.08); + --glass-highlight: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans", sans-serif; font-size: 14px; line-height: 1.45; @@ -22,6 +24,8 @@ --muted: #9aa3b2; --line: #2a2f3a; --accent: #3b82f6; + --glass-line: rgba(255, 255, 255, 0.1); + --glass-highlight: rgba(255, 255, 255, 0.08); } } @@ -30,7 +34,9 @@ [hidden] { display: none !important; } body { margin: 0; - background: var(--bg); + /* the window is transparent; --glass (0.3..1) is how much of our own tint + sits over whatever the compositor blurs behind us */ + background: color-mix(in srgb, var(--bg) calc(var(--glass, 0.7) * 100%), transparent); color: var(--text); user-select: none; } @@ -43,8 +49,9 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0 position: sticky; top: 0; z-index: 1; display: flex; align-items: center; justify-content: space-between; padding: 14px 20px; - background: var(--card); - border-bottom: 1px solid var(--line); + background: color-mix(in srgb, var(--card) calc(var(--glass, 0.7) * 100%), transparent); + border-bottom: 1px solid var(--glass-line); + -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px); } #status { color: var(--muted); font-size: 12px; } #status::before { @@ -57,14 +64,19 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0 #status[data-kind="error"]::before { background: var(--err); } main { padding: 16px 20px 32px; display: grid; gap: 14px; max-width: 760px; margin: 0 auto; } -.card { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 16px; } +.card { + background: color-mix(in srgb, var(--card) calc(var(--glass, 0.7) * 100%), transparent); + border: 1px solid var(--glass-line); + border-radius: 12px; padding: 16px; + box-shadow: inset 0 1px 0 var(--glass-highlight), 0 8px 24px rgba(0, 0, 0, 0.14); +} .hint { color: var(--muted); font-size: 12.5px; margin-bottom: 10px; } .row { display: flex; gap: 8px; } .row input { flex: 1; } input[type="text"], input[type="password"], input:not([type]), input[type="number"] { font: inherit; color: inherit; - background: var(--bg); border: 1px solid var(--line); border-radius: 7px; + background: color-mix(in srgb, var(--bg) 70%, transparent); border: 1px solid var(--line); border-radius: 7px; padding: 8px 10px; min-width: 0; } input[type="number"] { width: 84px; } @@ -105,7 +117,7 @@ button:active { transform: translateY(1px); } .sound-row span { color: var(--text); } select { font: inherit; color: inherit; - background: var(--bg); border: 1px solid var(--line); border-radius: 7px; + background: color-mix(in srgb, var(--bg) 70%, transparent); border: 1px solid var(--line); border-radius: 7px; padding: 6px 8px; } button.icon { padding: 4px 0; width: 34px; text-align: center; } @@ -288,3 +300,15 @@ h3.sub { font-size: 12px; font-weight: 600; color: var(--muted); margin: 10px 0 .notify-row .toggle { justify-content: center; padding: 0; } .notify-row[data-off="true"] > span:first-child, .notify-row[data-off="true"] select, .notify-row[data-off="true"] .icon { opacity: 0.5; } .options.actions { margin-top: 12px; } + +/* ===== glass slider */ +.glass-row { display: grid; grid-template-columns: auto 1fr auto; gap: 10px 12px; align-items: center; margin-top: 14px; } +.glass-row input[type="range"] { width: 100%; } +.glass-row output { min-width: 44px; text-align: right; color: var(--muted); font-variant-numeric: tabular-nums; } +.glass-row .hint { grid-column: 1 / -1; margin: 0; } +/* the onboarding surface and callouts are glass too */ +.ob { background: color-mix(in srgb, var(--bg) calc(var(--glass, 0.7) * 100%), transparent); } +.ob-say, .ob-latest, .callout { + background: color-mix(in srgb, var(--card) calc(var(--glass, 0.7) * 100%), transparent); + border-color: var(--glass-line); +} diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 8ac1dfb..ec5be96 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -24,6 +24,11 @@ import { export type View = "activity" | "settings"; +/** Push the tint opacity into CSS. 1 = opaque. */ +export function applyGlass(opacity: number): void { + document.documentElement.style.setProperty("--glass", String(Math.min(1, Math.max(0.3, opacity)))); +} + const $ = (sel: string) => { const el = document.querySelector(sel); if (!el) throw new Error(`missing element ${sel}`); @@ -382,6 +387,16 @@ export class Ui { await this.syncAutostart(); }); + const glass = $("#glass"); + const glassValue = $("#glass-value"); + const showGlass = (v: number) => { glassValue.value = `${v}%`; }; + glass.value = String(Math.round(this.settings.glass * 100)); + showGlass(Number(glass.value)); + applyGlass(this.settings.glass); + // live while dragging, persist on release + glass.addEventListener("input", () => { applyGlass(Number(glass.value) / 100); showGlass(Number(glass.value)); }); + glass.addEventListener("change", () => void this.commit({ ...this.settings, glass: Number(glass.value) / 100 })); + const language = $("#language"); language.value = this.settings.language; language.addEventListener("change", async () => {