diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e5edb54..58604bbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/components/settings/InstalledThemes.tsx b/src/components/settings/InstalledThemes.tsx index 2405a041..8b06bd6a 100644 --- a/src/components/settings/InstalledThemes.tsx +++ b/src/components/settings/InstalledThemes.tsx @@ -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() { {c.label} + {c.version && ( + + v{c.version} + + )} {c.accessibility && ( ('all'); const [sortMode, setSortMode] = useState('newest'); + const [animFilter, setAnimFilter] = useState('all'); const [page, setPage] = useState(1); const [busyId, setBusyId] = useState(null); const [failedId, setFailedId] = useState(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 (
{/* Submit-your-own-theme hint */} @@ -202,6 +212,23 @@ export function ThemeStoreSection() { lineHeight: 1, }} /> + 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, + }} + />
{modeBtns.map(b => (