mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
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:
@@ -143,4 +143,14 @@ describe('ThemeStoreSection — pagination & refresh', () => {
|
||||
await waitFor(() => expect(container.querySelector('.animate-spin')).toBeNull());
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
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 CoverLightbox from '../CoverLightbox';
|
||||
import { useThemeStore } from '../../store/themeStore';
|
||||
@@ -159,7 +159,9 @@ export function ThemeStoreSection() {
|
||||
</button>
|
||||
</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 }}>
|
||||
<input
|
||||
type="search"
|
||||
@@ -195,6 +197,7 @@ export function ThemeStoreSection() {
|
||||
<RefreshCw size={15} className={refreshing ? 'animate-spin' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && stale && (
|
||||
<div className="settings-hint settings-hint-info" role="status" style={{ marginBottom: '0.75rem' }}>
|
||||
@@ -207,9 +210,28 @@ export function ThemeStoreSection() {
|
||||
)}
|
||||
|
||||
{!loading && error && (
|
||||
<div role="alert" style={{ fontSize: 13, color: 'var(--text-muted)' }}>
|
||||
<p style={{ marginBottom: 8 }}>{t('settings.themeStoreError')}</p>
|
||||
<button className="btn btn-ghost" onClick={() => load(true)}>{t('settings.themeStoreRetry')}</button>
|
||||
<div
|
||||
role="alert"
|
||||
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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user