diff --git a/CHANGELOG.md b/CHANGELOG.md index 3880386c..b2f45eea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,7 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Themes — community Theme Store -**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1009](https://github.com/Psychotoxical/psysonic/pull/1009), [#1011](https://github.com/Psychotoxical/psysonic/pull/1011), [#1012](https://github.com/Psychotoxical/psysonic/pull/1012), [#1013](https://github.com/Psychotoxical/psysonic/pull/1013), [#1014](https://github.com/Psychotoxical/psysonic/pull/1014), [#1015](https://github.com/Psychotoxical/psysonic/pull/1015), [#1016](https://github.com/Psychotoxical/psysonic/pull/1016), [#1018](https://github.com/Psychotoxical/psysonic/pull/1018)** +**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1009](https://github.com/Psychotoxical/psysonic/pull/1009), [#1011](https://github.com/Psychotoxical/psysonic/pull/1011), [#1012](https://github.com/Psychotoxical/psysonic/pull/1012), [#1013](https://github.com/Psychotoxical/psysonic/pull/1013), [#1014](https://github.com/Psychotoxical/psysonic/pull/1014), [#1015](https://github.com/Psychotoxical/psysonic/pull/1015), [#1016](https://github.com/Psychotoxical/psysonic/pull/1016), [#1018](https://github.com/Psychotoxical/psysonic/pull/1018), [#1020](https://github.com/Psychotoxical/psysonic/pull/1020)** * New **Settings → Themes** tab: pick a theme, set the day/night scheduler, and browse a built-in **Theme Store** to install, update and uninstall community themes — with search, a dark/light filter, and full-size thumbnail previews. * The app now bundles six core themes (Catppuccin Mocha & Latte, Kanagawa Wave, Stark HUD, and the colour-blind-safe Vision Dark / Vision Navy); every other palette installs on demand from the [psysonic-themes](https://github.com/Psysonic/psysonic-themes) repo. Installed themes are saved locally and apply instantly at startup, even offline. diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b9b8f1d2..80b137bc 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6,6 +6,7 @@ mod cover_cache; mod library_analysis_backfill; mod lib_commands; mod theme_import; +pub mod theme_animation; pub use psysonic_integration::discord; @@ -653,6 +654,7 @@ pub fn run() { performance_cpu_snapshot, set_subsonic_wire_user_agent, no_compositing_mode, + theme_animation_risk, linux_xdg_session_type, is_tiling_wm_cmd, open_mini_player, diff --git a/src-tauri/src/lib_commands/app_api/mod.rs b/src-tauri/src/lib_commands/app_api/mod.rs index 29cdcbfd..869127a3 100644 --- a/src-tauri/src/lib_commands/app_api/mod.rs +++ b/src-tauri/src/lib_commands/app_api/mod.rs @@ -23,6 +23,7 @@ pub(crate) use perf::performance_cpu_snapshot; pub(crate) use platform::{ linux_wayland_gpu_font_tuning_active, linux_wayland_text_render_settings_available, set_linux_wayland_text_render_profile, set_linux_webkit_smooth_scrolling, set_window_decorations, + theme_animation_risk, }; #[cfg(target_os = "linux")] pub(crate) use platform::{ diff --git a/src-tauri/src/lib_commands/app_api/platform.rs b/src-tauri/src/lib_commands/app_api/platform.rs index 486b8db7..1bb3ea61 100644 --- a/src-tauri/src/lib_commands/app_api/platform.rs +++ b/src-tauri/src/lib_commands/app_api/platform.rs @@ -137,6 +137,29 @@ pub(crate) fn linux_webkit_apply_wayland_gpu_font_tuning(win: &tauri::WebviewWin } /// Toggle native window decorations at runtime (Linux custom title bar opt-out). +/// Tauri command: true when theme animations may be costly on this setup — +/// Linux with the Nvidia WebKit quirk active (recorded once at startup) or +/// compositing forced off. The frontend warns on animated themes when true. +/// Always false off Linux. +#[tauri::command] +pub(crate) fn theme_animation_risk() -> bool { + #[cfg(target_os = "linux")] + { + // Compositing forced off → GPU-accelerated effects/animation are costly. + if std::env::var("WEBKIT_DISABLE_COMPOSITING_MODE") + .map(|v| v == "1") + .unwrap_or(false) + { + return true; + } + crate::theme_animation::nvidia_quirk_active() + } + #[cfg(not(target_os = "linux"))] + { + false + } +} + #[tauri::command] pub(crate) fn set_window_decorations(enabled: bool, app_handle: tauri::AppHandle) { if let Some(win) = app_handle.get_webview_window("main") { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5115de5f..72de8962 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -17,11 +17,16 @@ fn apply_linux_webkit_nvidia_quirk() { // may still be `XDG_SESSION_TYPE=wayland`. The quirk maps that to `__NV_DISABLE_EXPLICIT_SYNC`, // which mismatches a real X11 EGL stack and can leave the webview gray — mirror the native-X11 // branch (`WEBKIT_DISABLE_DMABUF_RENDERER` only) whenever GDK is pinned to x11 first in the list. + // Detect once and record it for the UI's animated-theme CPU-load warning, + // so the theme_animation_risk command never has to re-probe the GPU. + let kind = needs_workaround(); + psysonic_lib::theme_animation::set_nvidia_quirk_active(!matches!(kind, WorkaroundKind::None)); + let forced_x11_gdk = std::env::var("GDK_BACKEND").ok().is_some_and(|s| { matches!(s.split(',').next().map(str::trim), Some("x11")) }); if forced_x11_gdk { - match needs_workaround() { + match kind { WorkaroundKind::None => {} WorkaroundKind::DisableWebkitDmabufRenderer | WorkaroundKind::DisableNvExplicitSync => { set_webkit_disable_dmabuf_renderer(); diff --git a/src-tauri/src/theme_animation.rs b/src-tauri/src/theme_animation.rs new file mode 100644 index 00000000..6e165e02 --- /dev/null +++ b/src-tauri/src/theme_animation.rs @@ -0,0 +1,22 @@ +//! Startup-recorded display hint for the theme system. +//! +//! The Nvidia/WebKit GPU quirk is detected once at process start (in `main()`, +//! before the webview/GTK init). We record whether it was needed so the UI can +//! warn that animated themes may raise CPU load on this setup — without +//! re-probing the GPU later. Read via the `theme_animation_risk` command +//! (`lib_commands::app_api::platform`). + +use std::sync::OnceLock; + +static NVIDIA_QUIRK_ACTIVE: OnceLock = OnceLock::new(); + +/// Record the startup Nvidia-WebKit-quirk detection. Called once from `main()`. +pub fn set_nvidia_quirk_active(active: bool) { + let _ = NVIDIA_QUIRK_ACTIVE.set(active); +} + +/// Whether the Nvidia WebKit quirk was needed at startup. False when unrecorded +/// (non-Linux, or GPU acceleration opted in via `PSYSONIC_WEBKIT_GPU_ACCEL`). +pub(crate) fn nvidia_quirk_active() -> bool { + NVIDIA_QUIRK_ACTIVE.get().copied().unwrap_or(false) +} diff --git a/src/components/settings/InstalledThemes.tsx b/src/components/settings/InstalledThemes.tsx index ca8c564a..ccb33635 100644 --- a/src/components/settings/InstalledThemes.tsx +++ b/src/components/settings/InstalledThemes.tsx @@ -1,8 +1,9 @@ -import { Check, X } from 'lucide-react'; +import { AlertTriangle, Check, X } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useThemeStore } from '../../store/themeStore'; import { useInstalledThemesStore } from '../../store/installedThemesStore'; import { uninstallTheme } from '../../utils/themes/uninstallTheme'; +import { useThemeAnimationRisk } from '../../hooks/useThemeAnimationRisk'; import { FIXED_THEMES } from './fixedThemes'; /** Pull a 3-band swatch (bg / card / accent) out of an installed theme's CSS. */ @@ -26,6 +27,7 @@ interface Card { accent: string; fixed: boolean; accessibility: boolean; + animated: boolean; } /** @@ -39,12 +41,13 @@ export function InstalledThemes() { const active = useThemeStore(s => s.theme); const setTheme = useThemeStore(s => s.setTheme); const installed = useInstalledThemesStore(s => s.themes); + const animRisk = useThemeAnimationRisk(); const cards: Card[] = [ - ...FIXED_THEMES.map(f => ({ id: f.id, label: f.label, bg: f.bg, card: f.card, accent: f.accent, fixed: true, accessibility: !!f.accessibility })), + ...FIXED_THEMES.map(f => ({ id: f.id, label: f.label, bg: f.bg, card: f.card, accent: f.accent, fixed: true, accessibility: !!f.accessibility, animated: false })), ...installed.map(it => { const s = swatch(it.css); - return { id: it.id, label: it.name, bg: s.bg, card: s.card, accent: s.accent, fixed: false, accessibility: (it.tags || []).includes('accessibility') }; + return { id: it.id, label: it.name, bg: s.bg, card: s.card, accent: s.accent, fixed: false, accessibility: (it.tags || []).includes('accessibility'), animated: /@(?:-[a-z]+-)?keyframes\b/i.test(it.css) }; }), ]; @@ -101,6 +104,17 @@ export function InstalledThemes() { CVD-safe )} + {animRisk && c.animated && ( + + + + )} {!c.fixed && (