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
+13
View File
@@ -9,6 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
>
## [1.49.0]
## Added
### Theme store — version numbers and an animated/static filter
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1104](https://github.com/Psychotoxical/psysonic/pull/1104)**
* Theme versions now show in the store (next to the author) and under each installed community theme; when an update is available, the store shows the installed → available version.
* New store filter to show only animated themes or only static ones, next to the existing mode and sort controls.
## [1.48.0]
## Added
+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}
+3
View File
@@ -374,6 +374,9 @@ export const settings = {
themeStoreEnlarge: 'Vorschau vergrößern',
themeStoreSortNewest: 'Neueste',
themeStoreSortName: 'Alphabetisch',
themeStoreAnimAll: 'Alle',
themeStoreAnimAnimated: 'Animiert',
themeStoreAnimStatic: 'Statisch',
themeStoreByAuthor: 'von {{author}}',
themeStoreLastChanged: 'Zuletzt geändert',
themeMigrationNoticeTitle: 'Theme in den Store gezogen',
+3
View File
@@ -418,6 +418,9 @@ export const settings = {
themeStoreEnlarge: 'Enlarge preview',
themeStoreSortNewest: 'Newest',
themeStoreSortName: 'Alphabetical',
themeStoreAnimAll: 'All',
themeStoreAnimAnimated: 'Animated',
themeStoreAnimStatic: 'Static',
themeStoreByAuthor: 'by {{author}}',
themeStoreLastChanged: 'Last changed',
themeMigrationNoticeTitle: 'Theme moved to the Store',
+3
View File
@@ -372,6 +372,9 @@ export const settings = {
themeStoreEnlarge: 'Ampliar vista previa',
themeStoreSortNewest: 'Más recientes',
themeStoreSortName: 'Alfabético',
themeStoreAnimAll: 'Todos',
themeStoreAnimAnimated: 'Animados',
themeStoreAnimStatic: 'Estáticos',
themeStoreByAuthor: 'por {{author}}',
themeStoreLastChanged: 'Última modificación',
themeMigrationNoticeTitle: 'Tema movido a la tienda',
+3
View File
@@ -360,6 +360,9 @@ export const settings = {
themeStoreEnlarge: "Agrandir l'aperçu",
themeStoreSortNewest: 'Plus récents',
themeStoreSortName: 'Alphabétique',
themeStoreAnimAll: 'Tous',
themeStoreAnimAnimated: 'Animés',
themeStoreAnimStatic: 'Statiques',
themeStoreByAuthor: 'par {{author}}',
themeStoreLastChanged: 'Dernière modification',
themeMigrationNoticeTitle: 'Thème déplacé vers la boutique',
+3
View File
@@ -363,6 +363,9 @@ export const settings = {
themeStoreEnlarge: 'Forstørr forhåndsvisning',
themeStoreSortNewest: 'Nyeste',
themeStoreSortName: 'Alfabetisk',
themeStoreAnimAll: 'Alle',
themeStoreAnimAnimated: 'Animerte',
themeStoreAnimStatic: 'Statiske',
themeStoreByAuthor: 'av {{author}}',
themeStoreLastChanged: 'Sist endret',
themeMigrationNoticeTitle: 'Temaet er flyttet til butikken',
+3
View File
@@ -360,6 +360,9 @@ export const settings = {
themeStoreEnlarge: 'Voorbeeld vergroten',
themeStoreSortNewest: 'Nieuwste',
themeStoreSortName: 'Alfabetisch',
themeStoreAnimAll: 'Alle',
themeStoreAnimAnimated: 'Geanimeerd',
themeStoreAnimStatic: 'Statisch',
themeStoreByAuthor: 'door {{author}}',
themeStoreLastChanged: 'Laatst gewijzigd',
themeMigrationNoticeTitle: 'Thema verplaatst naar de store',
+3
View File
@@ -376,6 +376,9 @@ export const settings = {
themeStoreEnlarge: 'Mărește previzualizarea',
themeStoreSortNewest: 'Cele mai noi',
themeStoreSortName: 'Alfabetic',
themeStoreAnimAll: 'Toate',
themeStoreAnimAnimated: 'Animate',
themeStoreAnimStatic: 'Statice',
themeStoreByAuthor: 'de {{author}}',
themeStoreLastChanged: 'Ultima modificare',
themeMigrationNoticeTitle: 'Tema a fost mutată în magazin',
+3
View File
@@ -428,6 +428,9 @@ export const settings = {
themeStoreEnlarge: 'Увеличить превью',
themeStoreSortNewest: 'Новые',
themeStoreSortName: 'По алфавиту',
themeStoreAnimAll: 'Все',
themeStoreAnimAnimated: 'Анимированные',
themeStoreAnimStatic: 'Статичные',
themeStoreByAuthor: 'от {{author}}',
themeStoreLastChanged: 'Изменён',
themeMigrationNoticeTitle: 'Тема перенесена в магазин',
+3
View File
@@ -359,6 +359,9 @@ export const settings = {
themeStoreEnlarge: '放大预览',
themeStoreSortNewest: '最新',
themeStoreSortName: '按字母',
themeStoreAnimAll: '全部',
themeStoreAnimAnimated: '动态',
themeStoreAnimStatic: '静态',
themeStoreByAuthor: '作者:{{author}}',
themeStoreLastChanged: '最后更新',
themeMigrationNoticeTitle: '主题已移至商店',