mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
feat(themes): warn about animated themes on high-CPU setups (#1020)
* feat(themes): warn about animated themes on high-CPU setups Show a warning icon + tooltip on animated themes (those defining @keyframes) in the store and in Your Themes, on Linux setups where animation is costly — the Nvidia WebKit quirk is active or compositing is forced off. The Nvidia detection already runs once at startup; its result is recorded (theme_animation.rs) and read via a new theme_animation_risk command — no GPU re-probe. Display-only; never shown off Linux. The animated flag comes from the registry (store) or the theme CSS (Your Themes). * docs(themes): note the animated-theme warning PR in the Theme Store entry
This commit is contained in:
@@ -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
|
||||
</span>
|
||||
)}
|
||||
{animRisk && c.animated && (
|
||||
<span
|
||||
role="img"
|
||||
aria-label={t('settings.themeAnimationWarning')}
|
||||
data-tooltip={t('settings.themeAnimationWarning')}
|
||||
data-tooltip-pos="top"
|
||||
style={{ display: 'inline-flex', color: 'var(--warning)', marginTop: 2 }}
|
||||
>
|
||||
<AlertTriangle size={12} />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{!c.fixed && (
|
||||
<button
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Check, ChevronLeft, ChevronRight, Download, RefreshCw, Trash2, WifiOff } from 'lucide-react';
|
||||
import { AlertTriangle, Check, ChevronLeft, ChevronRight, Download, RefreshCw, Trash2, WifiOff } from 'lucide-react';
|
||||
import { open as openUrl } from '@tauri-apps/plugin-shell';
|
||||
import CoverLightbox from '../CoverLightbox';
|
||||
import { useThemeAnimationRisk } from '../../hooks/useThemeAnimationRisk';
|
||||
import { useThemeStore } from '../../store/themeStore';
|
||||
import { useInstalledThemesStore, type InstalledTheme } from '../../store/installedThemesStore';
|
||||
import {
|
||||
@@ -33,6 +34,7 @@ export function ThemeStoreSection() {
|
||||
const setTheme = useThemeStore(s => s.setTheme);
|
||||
const installed = useInstalledThemesStore(s => s.themes);
|
||||
const install = useInstalledThemesStore(s => s.install);
|
||||
const animRisk = useThemeAnimationRisk();
|
||||
|
||||
const [themes, setThemes] = useState<RegistryTheme[] | null>(null);
|
||||
const [generatedAt, setGeneratedAt] = useState('');
|
||||
@@ -293,6 +295,17 @@ export function ThemeStoreSection() {
|
||||
<Check size={12} /> {t('settings.themeStoreActive')}
|
||||
</span>
|
||||
)}
|
||||
{animRisk && th.animated && (
|
||||
<span
|
||||
role="img"
|
||||
aria-label={t('settings.themeAnimationWarning')}
|
||||
data-tooltip={t('settings.themeAnimationWarning')}
|
||||
data-tooltip-pos="top"
|
||||
style={{ display: 'inline-flex', color: 'var(--warning)' }}
|
||||
>
|
||||
<AlertTriangle size={14} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>
|
||||
{t('settings.themeStoreByAuthor', { author: th.author })}
|
||||
|
||||
@@ -357,7 +357,7 @@ const CONTRIBUTOR_ENTRIES = [
|
||||
'Performance Probe: Monitor/Toggles redesign, live CPU/RSS/thread metrics, overlay pins and sparklines, macOS snapshots (PR #890)',
|
||||
'Performance Probe: opt-in thread-group CPU poll toggle and includeThreadGroups IPC fix (PR #891)',
|
||||
'Queue: switchable display mode — Queue (upcoming only) vs Playlist (full list), with header toggle and settings entry (PR #922)',
|
||||
'Community Theme Store: semantic-token refactor, dedicated Themes tab with day/night scheduler, install/update/uninstall, local .zip import, and free-form community themes (safety floor + state-reactive styling); 80+ palettes moved to an on-demand CDN repo (PR #1009, #1011, #1012, #1013, #1014, #1015, #1016, #1018)',
|
||||
'Community Theme Store: semantic-token refactor, dedicated Themes tab with day/night scheduler, install/update/uninstall, local .zip import, and free-form community themes (safety floor + state-reactive styling); 80+ palettes moved to an on-demand CDN repo (PR #1009, #1011, #1012, #1013, #1014, #1015, #1016, #1018, #1020)',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { IS_LINUX } from '../utils/platform';
|
||||
|
||||
/**
|
||||
* Whether animated themes may raise CPU load on this setup — Linux with the
|
||||
* Nvidia WebKit quirk active (recorded at startup) or compositing forced off.
|
||||
* The store / theme cards show a warning on animated themes when this is true.
|
||||
*
|
||||
* Fetched once and cached for the process (read-once, per the Tauri-boundary
|
||||
* rule). Always false off Linux, so the `invoke` is skipped there.
|
||||
*/
|
||||
let cached: boolean | null = null;
|
||||
|
||||
export function useThemeAnimationRisk(): boolean {
|
||||
const [risk, setRisk] = useState(cached ?? false);
|
||||
|
||||
useEffect(() => {
|
||||
if (cached !== null) {
|
||||
setRisk(cached);
|
||||
return;
|
||||
}
|
||||
if (!IS_LINUX) {
|
||||
cached = false;
|
||||
return;
|
||||
}
|
||||
let alive = true;
|
||||
const p = invoke<boolean>('theme_animation_risk');
|
||||
// Guard the mocked-in-tests case where invoke isn't a real promise.
|
||||
if (p && typeof (p as Promise<boolean>).then === 'function') {
|
||||
(p as Promise<boolean>)
|
||||
.then((v) => {
|
||||
cached = !!v;
|
||||
if (alive) setRisk(cached);
|
||||
})
|
||||
.catch(() => {
|
||||
cached = false;
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return risk;
|
||||
}
|
||||
@@ -324,6 +324,7 @@ export const settings = {
|
||||
tabThemes: 'Themes',
|
||||
themesYourThemesTitle: 'Deine Themes',
|
||||
themesCvdTooltip: 'Farbfehlsichtigkeits-sicher – Deuteranopie, Protanopie, Tritanopie',
|
||||
themeAnimationWarning: 'Dieses Theme nutzt Animationen, die auf deinem Setup (Nvidia/Linux oder Compositing aus) die CPU-Last erhöhen können.',
|
||||
themeStoreTitle: 'Theme-Store',
|
||||
themeStoreSubmitText: 'Eigenes Theme gebaut? Teil es mit der Community — mehr Infos im Themes-Repository.',
|
||||
themeStoreSubmitLink: 'Themes-Repository öffnen',
|
||||
|
||||
@@ -391,6 +391,7 @@ export const settings = {
|
||||
tabThemes: 'Themes',
|
||||
themesYourThemesTitle: 'Your Themes',
|
||||
themesCvdTooltip: 'Colour-blind safe — deuteranopia, protanopia, tritanopia',
|
||||
themeAnimationWarning: 'This theme uses animations that may raise CPU usage on your setup (Nvidia/Linux or compositing off).',
|
||||
themeStoreTitle: 'Theme Store',
|
||||
themeStoreSubmitText: 'Made your own theme? Share it with the community — more info in the themes repository.',
|
||||
themeStoreSubmitLink: 'Open the themes repository',
|
||||
|
||||
@@ -322,6 +322,7 @@ export const settings = {
|
||||
tabThemes: 'Temas',
|
||||
themesYourThemesTitle: 'Tus temas',
|
||||
themesCvdTooltip: 'Apto para daltonismo: deuteranopía, protanopía, tritanopía',
|
||||
themeAnimationWarning: 'Este tema usa animaciones que pueden aumentar el uso de CPU en tu sistema (Nvidia/Linux o compositing desactivado).',
|
||||
themeStoreTitle: 'Tienda de temas',
|
||||
themeStoreSubmitText: '¿Has creado tu propio tema? Compártelo con la comunidad — más información en el repositorio de temas.',
|
||||
themeStoreSubmitLink: 'Abrir el repositorio de temas',
|
||||
|
||||
@@ -320,6 +320,7 @@ export const settings = {
|
||||
tabThemes: 'Thèmes',
|
||||
themesYourThemesTitle: 'Vos thèmes',
|
||||
themesCvdTooltip: 'Adapté au daltonisme — deutéranopie, protanopie, tritanopie',
|
||||
themeAnimationWarning: 'Ce thème utilise des animations qui peuvent augmenter l’utilisation du processeur sur votre configuration (Nvidia/Linux ou compositing désactivé).',
|
||||
themeStoreTitle: 'Boutique de thèmes',
|
||||
themeStoreSubmitText: "Vous avez créé votre propre thème ? Partagez-le avec la communauté — plus d'infos dans le dépôt de thèmes.",
|
||||
themeStoreSubmitLink: 'Ouvrir le dépôt de thèmes',
|
||||
|
||||
@@ -323,6 +323,7 @@ export const settings = {
|
||||
tabThemes: 'Temaer',
|
||||
themesYourThemesTitle: 'Dine temaer',
|
||||
themesCvdTooltip: 'Fargeblind-sikker – deuteranopi, protanopi, tritanopi',
|
||||
themeAnimationWarning: 'Dette temaet bruker animasjoner som kan øke CPU-bruken på oppsettet ditt (Nvidia/Linux eller compositing av).',
|
||||
themeStoreTitle: 'Temabutikk',
|
||||
themeStoreSubmitText: 'Laget ditt eget tema? Del det med fellesskapet — mer info i tema-repoet.',
|
||||
themeStoreSubmitLink: 'Åpne tema-repoet',
|
||||
|
||||
@@ -320,6 +320,7 @@ export const settings = {
|
||||
tabThemes: "Thema's",
|
||||
themesYourThemesTitle: "Jouw thema's",
|
||||
themesCvdTooltip: 'Kleurenblind-veilig – deuteranopie, protanopie, tritanopie',
|
||||
themeAnimationWarning: 'Dit thema gebruikt animaties die het CPU-gebruik op jouw systeem kunnen verhogen (Nvidia/Linux of compositing uit).',
|
||||
themeStoreTitle: 'Themawinkel',
|
||||
themeStoreSubmitText: 'Eigen thema gemaakt? Deel het met de community — meer info in de themarepository.',
|
||||
themeStoreSubmitLink: 'Themarepository openen',
|
||||
|
||||
@@ -326,6 +326,7 @@ export const settings = {
|
||||
tabThemes: 'Teme',
|
||||
themesYourThemesTitle: 'Temele tale',
|
||||
themesCvdTooltip: 'Sigur pentru daltonism – deuteranopie, protanopie, tritanopie',
|
||||
themeAnimationWarning: 'Această temă folosește animații care pot crește utilizarea procesorului pe configurația ta (Nvidia/Linux sau compositing dezactivat).',
|
||||
themeStoreTitle: 'Magazin de teme',
|
||||
themeStoreSubmitText: 'Ți-ai creat propria temă? Împărtășește-o cu comunitatea — mai multe informații în depozitul de teme.',
|
||||
themeStoreSubmitLink: 'Deschide depozitul de teme',
|
||||
|
||||
@@ -402,6 +402,7 @@ export const settings = {
|
||||
tabThemes: 'Темы',
|
||||
themesYourThemesTitle: 'Ваши темы',
|
||||
themesCvdTooltip: 'Безопасно при дальтонизме — дейтеранопия, протанопия, тританопия',
|
||||
themeAnimationWarning: 'Эта тема использует анимации, которые могут повысить нагрузку на процессор в вашей конфигурации (Nvidia/Linux или композитинг выключен).',
|
||||
themeStoreTitle: 'Магазин тем',
|
||||
themeStoreSubmitText: 'Создали свою тему? Поделитесь с сообществом — подробности в репозитории тем.',
|
||||
themeStoreSubmitLink: 'Открыть репозиторий тем',
|
||||
|
||||
@@ -319,6 +319,7 @@ export const settings = {
|
||||
tabThemes: '主题',
|
||||
themesYourThemesTitle: '你的主题',
|
||||
themesCvdTooltip: '色觉障碍友好 — 绿色盲、红色盲、蓝色盲',
|
||||
themeAnimationWarning: '此主题使用动画,在你的环境(Nvidia/Linux 或关闭合成)下可能会增加 CPU 占用。',
|
||||
themeStoreTitle: '主题商店',
|
||||
themeStoreSubmitText: '做了自己的主题?与社区分享——更多信息见主题仓库。',
|
||||
themeStoreSubmitLink: '打开主题仓库',
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface RegistryTheme {
|
||||
description: string;
|
||||
mode: 'dark' | 'light';
|
||||
tags?: string[];
|
||||
/** True when the theme defines @keyframes (used for the CPU-load warning). */
|
||||
animated?: boolean;
|
||||
/** Repo-relative path to the theme's CSS. */
|
||||
css: string;
|
||||
/** Repo-relative path to the thumbnail. */
|
||||
|
||||
Reference in New Issue
Block a user