fix(themes): offline banner for the Theme Store (#1013)

* fix(themes): show an offline banner when the Theme Store is unavailable

When the registry can't be fetched and no catalogue is cached, the store
now shows a clear offline banner (icon, message, retry) in place of the
list, and hides the search/filter toolbar — there is nothing to browse.
The cached-catalogue fallback with its offline indicator is unchanged, so
the store still works offline when a catalogue was previously cached.

* docs(themes): note the offline-banner PR in the Theme Store entry

Add PR #1013 to the still-unreleased Theme Store changelog and credits
entry, alongside the other follow-ups.
This commit is contained in:
Psychotoxical
2026-06-07 11:48:23 +02:00
committed by GitHub
parent b3573e7107
commit 10fc489ce9
13 changed files with 48 additions and 7 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Themes — community Theme Store ### Themes — community Theme Store
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1009](https://github.com/Psychotoxical/psysonic/pull/1009), [#1011](https://github.com/Psychotoxical/psysonic/pull/1011), [#1012](https://github.com/Psychotoxical/psysonic/pull/1012)** **By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1009](https://github.com/Psychotoxical/psysonic/pull/1009), [#1011](https://github.com/Psychotoxical/psysonic/pull/1011), [#1012](https://github.com/Psychotoxical/psysonic/pull/1012), [#1013](https://github.com/Psychotoxical/psysonic/pull/1013)**
* New **Settings → Themes** tab: pick a theme, set the day/night scheduler, and browse a built-in **Theme Store** to install, update and uninstall community themes — with search, a dark/light filter, and full-size thumbnail previews. * New **Settings → Themes** tab: pick a theme, set the day/night scheduler, and browse a built-in **Theme Store** to install, update and uninstall community themes — with search, a dark/light filter, and full-size thumbnail previews.
* The app now bundles six core themes (Catppuccin Mocha & Latte, Kanagawa Wave, Stark HUD, and the colour-blind-safe Vision Dark / Vision Navy); every other palette installs on demand from the [psysonic-themes](https://github.com/Psysonic/psysonic-themes) repo. Installed themes are saved locally and apply instantly at startup, even offline. * The app now bundles six core themes (Catppuccin Mocha & Latte, Kanagawa Wave, Stark HUD, and the colour-blind-safe Vision Dark / Vision Navy); every other palette installs on demand from the [psysonic-themes](https://github.com/Psysonic/psysonic-themes) repo. Installed themes are saved locally and apply instantly at startup, even offline.
@@ -143,4 +143,14 @@ describe('ThemeStoreSection — pagination & refresh', () => {
await waitFor(() => expect(container.querySelector('.animate-spin')).toBeNull()); await waitFor(() => expect(container.querySelector('.animate-spin')).toBeNull());
expect(rows(container)).toHaveLength(12); expect(rows(container)).toHaveLength(12);
}); });
it('shows an offline banner and hides the toolbar when the registry is unavailable', async () => {
fetchRegistryMock.mockRejectedValue(new Error('offline'));
renderWithProviders(<ThemeStoreSection />);
expect(await screen.findByText('The Theme Store is offline')).toBeInTheDocument();
// No catalogue to browse → the search/filter toolbar is not rendered.
expect(screen.queryByRole('searchbox')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Retry' })).toBeInTheDocument();
});
}); });
+27 -5
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Check, ChevronLeft, ChevronRight, Download, RefreshCw, Trash2 } from 'lucide-react'; import { Check, ChevronLeft, ChevronRight, Download, RefreshCw, Trash2, WifiOff } from 'lucide-react';
import { open as openUrl } from '@tauri-apps/plugin-shell'; import { open as openUrl } from '@tauri-apps/plugin-shell';
import CoverLightbox from '../CoverLightbox'; import CoverLightbox from '../CoverLightbox';
import { useThemeStore } from '../../store/themeStore'; import { useThemeStore } from '../../store/themeStore';
@@ -159,7 +159,9 @@ export function ThemeStoreSection() {
</button> </button>
</div> </div>
{/* Toolbar: search + mode filter + refresh */} {/* Toolbar: search + mode filter + refresh. Hidden when offline with no
catalogue to browse — the offline banner below stands in for it. */}
{!error && (
<div ref={topRef} style={{ display: 'flex', flexWrap: 'wrap', gap: 10, alignItems: 'center', marginBottom: '1rem', scrollMarginTop: 8 }}> <div ref={topRef} style={{ display: 'flex', flexWrap: 'wrap', gap: 10, alignItems: 'center', marginBottom: '1rem', scrollMarginTop: 8 }}>
<input <input
type="search" type="search"
@@ -195,6 +197,7 @@ export function ThemeStoreSection() {
<RefreshCw size={15} className={refreshing ? 'animate-spin' : ''} /> <RefreshCw size={15} className={refreshing ? 'animate-spin' : ''} />
</button> </button>
</div> </div>
)}
{!loading && stale && ( {!loading && stale && (
<div className="settings-hint settings-hint-info" role="status" style={{ marginBottom: '0.75rem' }}> <div className="settings-hint settings-hint-info" role="status" style={{ marginBottom: '0.75rem' }}>
@@ -207,9 +210,28 @@ export function ThemeStoreSection() {
)} )}
{!loading && error && ( {!loading && error && (
<div role="alert" style={{ fontSize: 13, color: 'var(--text-muted)' }}> <div
<p style={{ marginBottom: 8 }}>{t('settings.themeStoreError')}</p> role="alert"
<button className="btn btn-ghost" onClick={() => load(true)}>{t('settings.themeStoreRetry')}</button> style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center',
gap: 12,
padding: '28px 16px',
border: '1px dashed var(--border)',
borderRadius: 'var(--radius-md, 10px)',
background: 'var(--bg-elevated)',
}}
>
<WifiOff size={28} style={{ color: 'var(--text-muted)' }} aria-hidden="true" />
<div>
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 4 }}>{t('settings.themeStoreOfflineTitle')}</div>
<div style={{ fontSize: 13, color: 'var(--text-muted)' }}>{t('settings.themeStoreError')}</div>
</div>
<button className="btn btn-ghost" onClick={() => load(true)} disabled={refreshing}>
{t('settings.themeStoreRetry')}
</button>
</div> </div>
)} )}
+1 -1
View File
@@ -356,7 +356,7 @@ const CONTRIBUTOR_ENTRIES = [
'Performance Probe: Monitor/Toggles redesign, live CPU/RSS/thread metrics, overlay pins and sparklines, macOS snapshots (PR #890)', 'Performance Probe: Monitor/Toggles redesign, live CPU/RSS/thread metrics, overlay pins and sparklines, macOS snapshots (PR #890)',
'Performance Probe: opt-in thread-group CPU poll toggle and includeThreadGroups IPC fix (PR #891)', 'Performance Probe: opt-in thread-group CPU poll toggle and includeThreadGroups IPC fix (PR #891)',
'Queue: switchable display mode — Queue (upcoming only) vs Playlist (full list), with header toggle and settings entry (PR #922)', 'Queue: switchable display mode — Queue (upcoming only) vs Playlist (full list), with header toggle and settings entry (PR #922)',
'Community Theme Store: semantic-token refactor, dedicated Themes tab with day/night scheduler, install/update/uninstall, local .zip import with full contract validation, and 80+ palettes moved to an on-demand CDN repo (PR #1009, #1011, #1012)', 'Community Theme Store: semantic-token refactor, dedicated Themes tab with day/night scheduler, install/update/uninstall, local .zip import with full contract validation, and 80+ palettes moved to an on-demand CDN repo (PR #1009, #1011, #1012, #1013)',
], ],
}, },
{ {
+1
View File
@@ -336,6 +336,7 @@ export const settings = {
themeStoreLoading: 'Themes werden geladen…', themeStoreLoading: 'Themes werden geladen…',
themeStoreError: 'Theme-Store konnte nicht geladen werden. Prüfe deine Verbindung.', themeStoreError: 'Theme-Store konnte nicht geladen werden. Prüfe deine Verbindung.',
themeStoreRetry: 'Erneut versuchen', themeStoreRetry: 'Erneut versuchen',
themeStoreOfflineTitle: 'Der Theme-Store ist offline',
themeStoreEmpty: 'Keine Themes passen zu deinen Filtern.', themeStoreEmpty: 'Keine Themes passen zu deinen Filtern.',
themeStoreActive: 'Aktiv', themeStoreActive: 'Aktiv',
themeStoreEnlarge: 'Vorschau vergrößern', themeStoreEnlarge: 'Vorschau vergrößern',
+1
View File
@@ -403,6 +403,7 @@ export const settings = {
themeStoreLoading: 'Loading themes…', themeStoreLoading: 'Loading themes…',
themeStoreError: 'Could not load the theme store. Check your connection.', themeStoreError: 'Could not load the theme store. Check your connection.',
themeStoreRetry: 'Retry', themeStoreRetry: 'Retry',
themeStoreOfflineTitle: 'The Theme Store is offline',
themeStoreEmpty: 'No themes match your filters.', themeStoreEmpty: 'No themes match your filters.',
themeStoreActive: 'Active', themeStoreActive: 'Active',
themeStoreEnlarge: 'Enlarge preview', themeStoreEnlarge: 'Enlarge preview',
+1
View File
@@ -334,6 +334,7 @@ export const settings = {
themeStoreLoading: 'Cargando temas…', themeStoreLoading: 'Cargando temas…',
themeStoreError: 'No se pudo cargar la tienda de temas. Comprueba tu conexión.', themeStoreError: 'No se pudo cargar la tienda de temas. Comprueba tu conexión.',
themeStoreRetry: 'Reintentar', themeStoreRetry: 'Reintentar',
themeStoreOfflineTitle: 'La tienda de temas está sin conexión',
themeStoreEmpty: 'Ningún tema coincide con tus filtros.', themeStoreEmpty: 'Ningún tema coincide con tus filtros.',
themeStoreActive: 'Activo', themeStoreActive: 'Activo',
themeStoreEnlarge: 'Ampliar vista previa', themeStoreEnlarge: 'Ampliar vista previa',
+1
View File
@@ -332,6 +332,7 @@ export const settings = {
themeStoreLoading: 'Chargement des thèmes…', themeStoreLoading: 'Chargement des thèmes…',
themeStoreError: 'Impossible de charger la boutique de thèmes. Vérifiez votre connexion.', themeStoreError: 'Impossible de charger la boutique de thèmes. Vérifiez votre connexion.',
themeStoreRetry: 'Réessayer', themeStoreRetry: 'Réessayer',
themeStoreOfflineTitle: 'Le Theme Store est hors ligne',
themeStoreEmpty: 'Aucun thème ne correspond à vos filtres.', themeStoreEmpty: 'Aucun thème ne correspond à vos filtres.',
themeStoreActive: 'Actif', themeStoreActive: 'Actif',
themeStoreEnlarge: "Agrandir l'aperçu", themeStoreEnlarge: "Agrandir l'aperçu",
+1
View File
@@ -335,6 +335,7 @@ export const settings = {
themeStoreLoading: 'Laster temaer…', themeStoreLoading: 'Laster temaer…',
themeStoreError: 'Kunne ikke laste temabutikken. Sjekk tilkoblingen din.', themeStoreError: 'Kunne ikke laste temabutikken. Sjekk tilkoblingen din.',
themeStoreRetry: 'Prøv igjen', themeStoreRetry: 'Prøv igjen',
themeStoreOfflineTitle: 'Theme Store er frakoblet',
themeStoreEmpty: 'Ingen temaer samsvarer med filtrene dine.', themeStoreEmpty: 'Ingen temaer samsvarer med filtrene dine.',
themeStoreActive: 'Aktiv', themeStoreActive: 'Aktiv',
themeStoreEnlarge: 'Forstørr forhåndsvisning', themeStoreEnlarge: 'Forstørr forhåndsvisning',
+1
View File
@@ -332,6 +332,7 @@ export const settings = {
themeStoreLoading: "Thema's laden…", themeStoreLoading: "Thema's laden…",
themeStoreError: 'Kan de themawinkel niet laden. Controleer je verbinding.', themeStoreError: 'Kan de themawinkel niet laden. Controleer je verbinding.',
themeStoreRetry: 'Opnieuw proberen', themeStoreRetry: 'Opnieuw proberen',
themeStoreOfflineTitle: 'De Theme Store is offline',
themeStoreEmpty: "Geen thema's komen overeen met je filters.", themeStoreEmpty: "Geen thema's komen overeen met je filters.",
themeStoreActive: 'Actief', themeStoreActive: 'Actief',
themeStoreEnlarge: 'Voorbeeld vergroten', themeStoreEnlarge: 'Voorbeeld vergroten',
+1
View File
@@ -338,6 +338,7 @@ export const settings = {
themeStoreLoading: 'Se încarcă temele…', themeStoreLoading: 'Se încarcă temele…',
themeStoreError: 'Magazinul de teme nu a putut fi încărcat. Verifică conexiunea.', themeStoreError: 'Magazinul de teme nu a putut fi încărcat. Verifică conexiunea.',
themeStoreRetry: 'Reîncearcă', themeStoreRetry: 'Reîncearcă',
themeStoreOfflineTitle: 'Theme Store este offline',
themeStoreEmpty: 'Nicio temă nu corespunde filtrelor tale.', themeStoreEmpty: 'Nicio temă nu corespunde filtrelor tale.',
themeStoreActive: 'Activă', themeStoreActive: 'Activă',
themeStoreEnlarge: 'Mărește previzualizarea', themeStoreEnlarge: 'Mărește previzualizarea',
+1
View File
@@ -414,6 +414,7 @@ export const settings = {
themeStoreLoading: 'Загрузка тем…', themeStoreLoading: 'Загрузка тем…',
themeStoreError: 'Не удалось загрузить магазин тем. Проверьте подключение.', themeStoreError: 'Не удалось загрузить магазин тем. Проверьте подключение.',
themeStoreRetry: 'Повторить', themeStoreRetry: 'Повторить',
themeStoreOfflineTitle: 'Магазин тем недоступен',
themeStoreEmpty: 'Нет тем, соответствующих фильтрам.', themeStoreEmpty: 'Нет тем, соответствующих фильтрам.',
themeStoreActive: 'Активна', themeStoreActive: 'Активна',
themeStoreEnlarge: 'Увеличить превью', themeStoreEnlarge: 'Увеличить превью',
+1
View File
@@ -331,6 +331,7 @@ export const settings = {
themeStoreLoading: '正在加载主题…', themeStoreLoading: '正在加载主题…',
themeStoreError: '无法加载主题商店。请检查网络连接。', themeStoreError: '无法加载主题商店。请检查网络连接。',
themeStoreRetry: '重试', themeStoreRetry: '重试',
themeStoreOfflineTitle: '主题商店已离线',
themeStoreEmpty: '没有符合筛选条件的主题。', themeStoreEmpty: '没有符合筛选条件的主题。',
themeStoreActive: '使用中', themeStoreActive: '使用中',
themeStoreEnlarge: '放大预览', themeStoreEnlarge: '放大预览',