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]
|
## [1.48.0]
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ interface Card {
|
|||||||
fixed: boolean;
|
fixed: boolean;
|
||||||
accessibility: boolean;
|
accessibility: boolean;
|
||||||
animated: 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 })),
|
...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 => {
|
...installed.map(it => {
|
||||||
const s = swatch(it.css);
|
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' : ''}`}>
|
<span className={`theme-card-label${isActive ? ' is-active' : ''}`}>
|
||||||
{c.label}
|
{c.label}
|
||||||
</span>
|
</span>
|
||||||
|
{c.version && (
|
||||||
|
<span style={{ fontSize: 10, color: 'var(--text-muted)', display: 'block', textAlign: 'center', lineHeight: 1.2 }}>
|
||||||
|
v{c.version}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{c.accessibility && (
|
{c.accessibility && (
|
||||||
<span
|
<span
|
||||||
aria-label={t('settings.themesCvdTooltip')}
|
aria-label={t('settings.themesCvdTooltip')}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { isNewer } from '../../utils/componentHelpers/appUpdaterHelpers';
|
|||||||
|
|
||||||
type ModeFilter = 'all' | 'dark' | 'light';
|
type ModeFilter = 'all' | 'dark' | 'light';
|
||||||
type SortMode = 'newest' | 'name';
|
type SortMode = 'newest' | 'name';
|
||||||
|
type AnimFilter = 'all' | 'animated' | 'static';
|
||||||
|
|
||||||
const THEMES_REPO_URL = 'https://github.com/Psysonic/psysonic-themes';
|
const THEMES_REPO_URL = 'https://github.com/Psysonic/psysonic-themes';
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ export function ThemeStoreSection() {
|
|||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const [mode, setMode] = useState<ModeFilter>('all');
|
const [mode, setMode] = useState<ModeFilter>('all');
|
||||||
const [sortMode, setSortMode] = useState<SortMode>('newest');
|
const [sortMode, setSortMode] = useState<SortMode>('newest');
|
||||||
|
const [animFilter, setAnimFilter] = useState<AnimFilter>('all');
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [busyId, setBusyId] = useState<string | null>(null);
|
const [busyId, setBusyId] = useState<string | null>(null);
|
||||||
const [failedId, setFailedId] = useState<string | null>(null);
|
const [failedId, setFailedId] = useState<string | null>(null);
|
||||||
@@ -102,6 +104,8 @@ export function ThemeStoreSection() {
|
|||||||
const q = query.trim().toLowerCase();
|
const q = query.trim().toLowerCase();
|
||||||
const matched = themes.filter(th => {
|
const matched = themes.filter(th => {
|
||||||
if (mode !== 'all' && th.mode !== mode) return false;
|
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;
|
if (!q) return true;
|
||||||
return (
|
return (
|
||||||
th.name.toLowerCase().includes(q) ||
|
th.name.toLowerCase().includes(q) ||
|
||||||
@@ -115,11 +119,11 @@ export function ThemeStoreSection() {
|
|||||||
const byName = (a: RegistryTheme, b: RegistryTheme) => a.name.localeCompare(b.name);
|
const byName = (a: RegistryTheme, b: RegistryTheme) => a.name.localeCompare(b.name);
|
||||||
if (sortMode === 'name') return matched.sort(byName);
|
if (sortMode === 'name') return matched.sort(byName);
|
||||||
return matched.sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || '') || byName(a, b));
|
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
|
// A changed filter can shrink the result set below the current page; reset to
|
||||||
// the first page whenever the query or mode filter changes.
|
// 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));
|
const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
|
||||||
// Clamp defensively so a stale `page` (e.g. after the registry shrank) never
|
// 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') },
|
{ 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 (
|
return (
|
||||||
<div className="settings-card">
|
<div className="settings-card">
|
||||||
{/* Submit-your-own-theme hint */}
|
{/* Submit-your-own-theme hint */}
|
||||||
@@ -202,6 +212,23 @@ export function ThemeStoreSection() {
|
|||||||
lineHeight: 1,
|
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')}>
|
<div style={{ display: 'flex', gap: 4 }} role="group" aria-label={t('settings.themeStoreFilterMode')}>
|
||||||
{modeBtns.map(b => (
|
{modeBtns.map(b => (
|
||||||
<button
|
<button
|
||||||
@@ -324,6 +351,12 @@ export function ThemeStoreSection() {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>
|
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>
|
||||||
{t('settings.themeStoreByAuthor', { author: th.author })}
|
{t('settings.themeStoreByAuthor', { author: th.author })}
|
||||||
|
{' · '}
|
||||||
|
{updateAvailable ? (
|
||||||
|
<>v{inst!.version} <span style={{ color: 'var(--accent)' }}>→ v{th.version}</span></>
|
||||||
|
) : (
|
||||||
|
<>v{th.version}</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 12.5, color: 'var(--text-secondary)', lineHeight: 1.4, marginTop: 10 }}>
|
<div style={{ fontSize: 12.5, color: 'var(--text-secondary)', lineHeight: 1.4, marginTop: 10 }}>
|
||||||
{th.description}
|
{th.description}
|
||||||
|
|||||||
@@ -374,6 +374,9 @@ export const settings = {
|
|||||||
themeStoreEnlarge: 'Vorschau vergrößern',
|
themeStoreEnlarge: 'Vorschau vergrößern',
|
||||||
themeStoreSortNewest: 'Neueste',
|
themeStoreSortNewest: 'Neueste',
|
||||||
themeStoreSortName: 'Alphabetisch',
|
themeStoreSortName: 'Alphabetisch',
|
||||||
|
themeStoreAnimAll: 'Alle',
|
||||||
|
themeStoreAnimAnimated: 'Animiert',
|
||||||
|
themeStoreAnimStatic: 'Statisch',
|
||||||
themeStoreByAuthor: 'von {{author}}',
|
themeStoreByAuthor: 'von {{author}}',
|
||||||
themeStoreLastChanged: 'Zuletzt geändert',
|
themeStoreLastChanged: 'Zuletzt geändert',
|
||||||
themeMigrationNoticeTitle: 'Theme in den Store gezogen',
|
themeMigrationNoticeTitle: 'Theme in den Store gezogen',
|
||||||
|
|||||||
@@ -418,6 +418,9 @@ export const settings = {
|
|||||||
themeStoreEnlarge: 'Enlarge preview',
|
themeStoreEnlarge: 'Enlarge preview',
|
||||||
themeStoreSortNewest: 'Newest',
|
themeStoreSortNewest: 'Newest',
|
||||||
themeStoreSortName: 'Alphabetical',
|
themeStoreSortName: 'Alphabetical',
|
||||||
|
themeStoreAnimAll: 'All',
|
||||||
|
themeStoreAnimAnimated: 'Animated',
|
||||||
|
themeStoreAnimStatic: 'Static',
|
||||||
themeStoreByAuthor: 'by {{author}}',
|
themeStoreByAuthor: 'by {{author}}',
|
||||||
themeStoreLastChanged: 'Last changed',
|
themeStoreLastChanged: 'Last changed',
|
||||||
themeMigrationNoticeTitle: 'Theme moved to the Store',
|
themeMigrationNoticeTitle: 'Theme moved to the Store',
|
||||||
|
|||||||
@@ -372,6 +372,9 @@ export const settings = {
|
|||||||
themeStoreEnlarge: 'Ampliar vista previa',
|
themeStoreEnlarge: 'Ampliar vista previa',
|
||||||
themeStoreSortNewest: 'Más recientes',
|
themeStoreSortNewest: 'Más recientes',
|
||||||
themeStoreSortName: 'Alfabético',
|
themeStoreSortName: 'Alfabético',
|
||||||
|
themeStoreAnimAll: 'Todos',
|
||||||
|
themeStoreAnimAnimated: 'Animados',
|
||||||
|
themeStoreAnimStatic: 'Estáticos',
|
||||||
themeStoreByAuthor: 'por {{author}}',
|
themeStoreByAuthor: 'por {{author}}',
|
||||||
themeStoreLastChanged: 'Última modificación',
|
themeStoreLastChanged: 'Última modificación',
|
||||||
themeMigrationNoticeTitle: 'Tema movido a la tienda',
|
themeMigrationNoticeTitle: 'Tema movido a la tienda',
|
||||||
|
|||||||
@@ -360,6 +360,9 @@ export const settings = {
|
|||||||
themeStoreEnlarge: "Agrandir l'aperçu",
|
themeStoreEnlarge: "Agrandir l'aperçu",
|
||||||
themeStoreSortNewest: 'Plus récents',
|
themeStoreSortNewest: 'Plus récents',
|
||||||
themeStoreSortName: 'Alphabétique',
|
themeStoreSortName: 'Alphabétique',
|
||||||
|
themeStoreAnimAll: 'Tous',
|
||||||
|
themeStoreAnimAnimated: 'Animés',
|
||||||
|
themeStoreAnimStatic: 'Statiques',
|
||||||
themeStoreByAuthor: 'par {{author}}',
|
themeStoreByAuthor: 'par {{author}}',
|
||||||
themeStoreLastChanged: 'Dernière modification',
|
themeStoreLastChanged: 'Dernière modification',
|
||||||
themeMigrationNoticeTitle: 'Thème déplacé vers la boutique',
|
themeMigrationNoticeTitle: 'Thème déplacé vers la boutique',
|
||||||
|
|||||||
@@ -363,6 +363,9 @@ export const settings = {
|
|||||||
themeStoreEnlarge: 'Forstørr forhåndsvisning',
|
themeStoreEnlarge: 'Forstørr forhåndsvisning',
|
||||||
themeStoreSortNewest: 'Nyeste',
|
themeStoreSortNewest: 'Nyeste',
|
||||||
themeStoreSortName: 'Alfabetisk',
|
themeStoreSortName: 'Alfabetisk',
|
||||||
|
themeStoreAnimAll: 'Alle',
|
||||||
|
themeStoreAnimAnimated: 'Animerte',
|
||||||
|
themeStoreAnimStatic: 'Statiske',
|
||||||
themeStoreByAuthor: 'av {{author}}',
|
themeStoreByAuthor: 'av {{author}}',
|
||||||
themeStoreLastChanged: 'Sist endret',
|
themeStoreLastChanged: 'Sist endret',
|
||||||
themeMigrationNoticeTitle: 'Temaet er flyttet til butikken',
|
themeMigrationNoticeTitle: 'Temaet er flyttet til butikken',
|
||||||
|
|||||||
@@ -360,6 +360,9 @@ export const settings = {
|
|||||||
themeStoreEnlarge: 'Voorbeeld vergroten',
|
themeStoreEnlarge: 'Voorbeeld vergroten',
|
||||||
themeStoreSortNewest: 'Nieuwste',
|
themeStoreSortNewest: 'Nieuwste',
|
||||||
themeStoreSortName: 'Alfabetisch',
|
themeStoreSortName: 'Alfabetisch',
|
||||||
|
themeStoreAnimAll: 'Alle',
|
||||||
|
themeStoreAnimAnimated: 'Geanimeerd',
|
||||||
|
themeStoreAnimStatic: 'Statisch',
|
||||||
themeStoreByAuthor: 'door {{author}}',
|
themeStoreByAuthor: 'door {{author}}',
|
||||||
themeStoreLastChanged: 'Laatst gewijzigd',
|
themeStoreLastChanged: 'Laatst gewijzigd',
|
||||||
themeMigrationNoticeTitle: 'Thema verplaatst naar de store',
|
themeMigrationNoticeTitle: 'Thema verplaatst naar de store',
|
||||||
|
|||||||
@@ -376,6 +376,9 @@ export const settings = {
|
|||||||
themeStoreEnlarge: 'Mărește previzualizarea',
|
themeStoreEnlarge: 'Mărește previzualizarea',
|
||||||
themeStoreSortNewest: 'Cele mai noi',
|
themeStoreSortNewest: 'Cele mai noi',
|
||||||
themeStoreSortName: 'Alfabetic',
|
themeStoreSortName: 'Alfabetic',
|
||||||
|
themeStoreAnimAll: 'Toate',
|
||||||
|
themeStoreAnimAnimated: 'Animate',
|
||||||
|
themeStoreAnimStatic: 'Statice',
|
||||||
themeStoreByAuthor: 'de {{author}}',
|
themeStoreByAuthor: 'de {{author}}',
|
||||||
themeStoreLastChanged: 'Ultima modificare',
|
themeStoreLastChanged: 'Ultima modificare',
|
||||||
themeMigrationNoticeTitle: 'Tema a fost mutată în magazin',
|
themeMigrationNoticeTitle: 'Tema a fost mutată în magazin',
|
||||||
|
|||||||
@@ -428,6 +428,9 @@ export const settings = {
|
|||||||
themeStoreEnlarge: 'Увеличить превью',
|
themeStoreEnlarge: 'Увеличить превью',
|
||||||
themeStoreSortNewest: 'Новые',
|
themeStoreSortNewest: 'Новые',
|
||||||
themeStoreSortName: 'По алфавиту',
|
themeStoreSortName: 'По алфавиту',
|
||||||
|
themeStoreAnimAll: 'Все',
|
||||||
|
themeStoreAnimAnimated: 'Анимированные',
|
||||||
|
themeStoreAnimStatic: 'Статичные',
|
||||||
themeStoreByAuthor: 'от {{author}}',
|
themeStoreByAuthor: 'от {{author}}',
|
||||||
themeStoreLastChanged: 'Изменён',
|
themeStoreLastChanged: 'Изменён',
|
||||||
themeMigrationNoticeTitle: 'Тема перенесена в магазин',
|
themeMigrationNoticeTitle: 'Тема перенесена в магазин',
|
||||||
|
|||||||
@@ -359,6 +359,9 @@ export const settings = {
|
|||||||
themeStoreEnlarge: '放大预览',
|
themeStoreEnlarge: '放大预览',
|
||||||
themeStoreSortNewest: '最新',
|
themeStoreSortNewest: '最新',
|
||||||
themeStoreSortName: '按字母',
|
themeStoreSortName: '按字母',
|
||||||
|
themeStoreAnimAll: '全部',
|
||||||
|
themeStoreAnimAnimated: '动态',
|
||||||
|
themeStoreAnimStatic: '静态',
|
||||||
themeStoreByAuthor: '作者:{{author}}',
|
themeStoreByAuthor: '作者:{{author}}',
|
||||||
themeStoreLastChanged: '最后更新',
|
themeStoreLastChanged: '最后更新',
|
||||||
themeMigrationNoticeTitle: '主题已移至商店',
|
themeMigrationNoticeTitle: '主题已移至商店',
|
||||||
|
|||||||
Reference in New Issue
Block a user