Theme store: version display + animated/static filter (#1104)

* feat(themes): show theme version in the store and installed list

* feat(themes): filter the theme store by animated / static

* docs(changelog): add 1.49.0 entry for theme store version + animated filter
This commit is contained in:
Psychotoxical
2026-06-16 20:52:02 +02:00
committed by GitHub
parent 6d404fdc2d
commit 15fb0f6c56
12 changed files with 83 additions and 3 deletions
+8 -1
View File
@@ -33,6 +33,8 @@ interface Card {
fixed: boolean;
accessibility: boolean;
animated: boolean;
/** Community themes carry a manifest version; fixed cores do not. */
version?: string;
}
/**
@@ -64,7 +66,7 @@ export function InstalledThemes() {
...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'), animated: /@(?:-[a-z]+-)?keyframes\b/i.test(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'), animated: /@(?:-[a-z]+-)?keyframes\b/i.test(it.css), version: it.version };
}),
];
@@ -102,6 +104,11 @@ export function InstalledThemes() {
<span className={`theme-card-label${isActive ? ' is-active' : ''}`}>
{c.label}
</span>
{c.version && (
<span style={{ fontSize: 10, color: 'var(--text-muted)', display: 'block', textAlign: 'center', lineHeight: 1.2 }}>
v{c.version}
</span>
)}
{c.accessibility && (
<span
aria-label={t('settings.themesCvdTooltip')}
+35 -2
View File
@@ -20,6 +20,7 @@ import { isNewer } from '../../utils/componentHelpers/appUpdaterHelpers';
type ModeFilter = 'all' | 'dark' | 'light';
type SortMode = 'newest' | 'name';
type AnimFilter = 'all' | 'animated' | 'static';
const THEMES_REPO_URL = 'https://github.com/Psysonic/psysonic-themes';
@@ -61,6 +62,7 @@ export function ThemeStoreSection() {
const [query, setQuery] = useState('');
const [mode, setMode] = useState<ModeFilter>('all');
const [sortMode, setSortMode] = useState<SortMode>('newest');
const [animFilter, setAnimFilter] = useState<AnimFilter>('all');
const [page, setPage] = useState(1);
const [busyId, setBusyId] = useState<string | null>(null);
const [failedId, setFailedId] = useState<string | null>(null);
@@ -102,6 +104,8 @@ export function ThemeStoreSection() {
const q = query.trim().toLowerCase();
const matched = themes.filter(th => {
if (mode !== 'all' && th.mode !== mode) return false;
if (animFilter === 'animated' && !th.animated) return false;
if (animFilter === 'static' && th.animated) return false;
if (!q) return true;
return (
th.name.toLowerCase().includes(q) ||
@@ -115,11 +119,11 @@ export function ThemeStoreSection() {
const byName = (a: RegistryTheme, b: RegistryTheme) => a.name.localeCompare(b.name);
if (sortMode === 'name') return matched.sort(byName);
return matched.sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || '') || byName(a, b));
}, [themes, query, mode, sortMode]);
}, [themes, query, mode, sortMode, animFilter]);
// A changed filter can shrink the result set below the current page; reset to
// the first page whenever the query or mode filter changes.
useEffect(() => { setPage(1); }, [query, mode, sortMode]);
useEffect(() => { setPage(1); }, [query, mode, sortMode, animFilter]);
const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
// Clamp defensively so a stale `page` (e.g. after the registry shrank) never
@@ -153,6 +157,12 @@ export function ThemeStoreSection() {
{ value: 'name', label: t('settings.themeStoreSortName') },
];
const animFilterOptions = [
{ value: 'all', label: t('settings.themeStoreAnimAll') },
{ value: 'animated', label: t('settings.themeStoreAnimAnimated') },
{ value: 'static', label: t('settings.themeStoreAnimStatic') },
];
return (
<div className="settings-card">
{/* Submit-your-own-theme hint */}
@@ -202,6 +212,23 @@ export function ThemeStoreSection() {
lineHeight: 1,
}}
/>
<CustomSelect
value={animFilter}
options={animFilterOptions}
onChange={v => setAnimFilter(v as AnimFilter)}
style={{
width: 150,
flexShrink: 0,
alignSelf: 'stretch',
boxSizing: 'border-box',
padding: '0 var(--space-4)',
background: 'var(--input-bg)',
border: '1px solid var(--input-border)',
borderRadius: 'var(--radius-md)',
fontSize: 14,
lineHeight: 1,
}}
/>
<div style={{ display: 'flex', gap: 4 }} role="group" aria-label={t('settings.themeStoreFilterMode')}>
{modeBtns.map(b => (
<button
@@ -324,6 +351,12 @@ export function ThemeStoreSection() {
</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>
{t('settings.themeStoreByAuthor', { author: th.author })}
{' · '}
{updateAvailable ? (
<>v{inst!.version} <span style={{ color: 'var(--accent)' }}> v{th.version}</span></>
) : (
<>v{th.version}</>
)}
</div>
<div style={{ fontSize: 12.5, color: 'var(--text-secondary)', lineHeight: 1.4, marginTop: 10 }}>
{th.description}