mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 14:55:43 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -428,6 +428,9 @@ export const settings = {
|
||||
themeStoreEnlarge: 'Увеличить превью',
|
||||
themeStoreSortNewest: 'Новые',
|
||||
themeStoreSortName: 'По алфавиту',
|
||||
themeStoreAnimAll: 'Все',
|
||||
themeStoreAnimAnimated: 'Анимированные',
|
||||
themeStoreAnimStatic: 'Статичные',
|
||||
themeStoreByAuthor: 'от {{author}}',
|
||||
themeStoreLastChanged: 'Изменён',
|
||||
themeMigrationNoticeTitle: 'Тема перенесена в магазин',
|
||||
|
||||
@@ -359,6 +359,9 @@ export const settings = {
|
||||
themeStoreEnlarge: '放大预览',
|
||||
themeStoreSortNewest: '最新',
|
||||
themeStoreSortName: '按字母',
|
||||
themeStoreAnimAll: '全部',
|
||||
themeStoreAnimAnimated: '动态',
|
||||
themeStoreAnimStatic: '静态',
|
||||
themeStoreByAuthor: '作者:{{author}}',
|
||||
themeStoreLastChanged: '最后更新',
|
||||
themeMigrationNoticeTitle: '主题已移至商店',
|
||||
|
||||
Reference in New Issue
Block a user