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:
Psychotoxical
2026-06-07 20:38:36 +02:00
committed by GitHub
parent daa6fbbfd7
commit 88f7a7bc90
20 changed files with 144 additions and 7 deletions
+17 -3
View File
@@ -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
+14 -1
View File
@@ -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 })}
+1 -1
View File
@@ -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)',
],
},
{
+46
View File
@@ -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;
}
+1
View File
@@ -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',
+1
View File
@@ -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',
+1
View File
@@ -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',
+1
View File
@@ -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 lutilisation 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',
+1
View File
@@ -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',
+1
View File
@@ -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',
+1
View File
@@ -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',
+1
View File
@@ -402,6 +402,7 @@ export const settings = {
tabThemes: 'Темы',
themesYourThemesTitle: 'Ваши темы',
themesCvdTooltip: 'Безопасно при дальтонизме — дейтеранопия, протанопия, тританопия',
themeAnimationWarning: 'Эта тема использует анимации, которые могут повысить нагрузку на процессор в вашей конфигурации (Nvidia/Linux или композитинг выключен).',
themeStoreTitle: 'Магазин тем',
themeStoreSubmitText: 'Создали свою тему? Поделитесь с сообществом — подробности в репозитории тем.',
themeStoreSubmitLink: 'Открыть репозиторий тем',
+1
View File
@@ -319,6 +319,7 @@ export const settings = {
tabThemes: '主题',
themesYourThemesTitle: '你的主题',
themesCvdTooltip: '色觉障碍友好 — 绿色盲、红色盲、蓝色盲',
themeAnimationWarning: '此主题使用动画,在你的环境(Nvidia/Linux 或关闭合成)下可能会增加 CPU 占用。',
themeStoreTitle: '主题商店',
themeStoreSubmitText: '做了自己的主题?与社区分享——更多信息见主题仓库。',
themeStoreSubmitLink: '打开主题仓库',
+2
View File
@@ -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. */