diff --git a/CHANGELOG.md b/CHANGELOG.md index c2fd2d8c..ab64d5a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,7 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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), [#1013](https://github.com/Psychotoxical/psysonic/pull/1013), [#1014](https://github.com/Psychotoxical/psysonic/pull/1014), [#1015](https://github.com/Psychotoxical/psysonic/pull/1015)** +**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), [#1014](https://github.com/Psychotoxical/psysonic/pull/1014), [#1015](https://github.com/Psychotoxical/psysonic/pull/1015), [#1016](https://github.com/Psychotoxical/psysonic/pull/1016)** * 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. diff --git a/src/components/settings/ThemeStoreSection.tsx b/src/components/settings/ThemeStoreSection.tsx index 708b27ab..9691980b 100644 --- a/src/components/settings/ThemeStoreSection.tsx +++ b/src/components/settings/ThemeStoreSection.tsx @@ -159,6 +159,11 @@ export function ThemeStoreSection() { + {/* Network disclosure — the store reaches external services. */} +

+ {t('settings.themeStoreNetworkNotice')} +

+ {/* Toolbar: search + mode filter + refresh. Hidden when offline with no catalogue to browse — the offline banner below stands in for it. */} {!error && ( diff --git a/src/config/settingsCredits.ts b/src/config/settingsCredits.ts index 6d1a0fa2..6dc90410 100644 --- a/src/config/settingsCredits.ts +++ b/src/config/settingsCredits.ts @@ -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: 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)', - 'Community Theme Store: semantic-token refactor, dedicated Themes tab with day/night scheduler, install/update/uninstall, local .zip import, and free-form community themes (safety floor + state-reactive styling); 80+ palettes moved to an on-demand CDN repo (PR #1009, #1011, #1012, #1013, #1014, #1015)', + 'Community Theme Store: semantic-token refactor, dedicated Themes tab with day/night scheduler, install/update/uninstall, local .zip import, and free-form community themes (safety floor + state-reactive styling); 80+ palettes moved to an on-demand CDN repo (PR #1009, #1011, #1012, #1013, #1014, #1015, #1016)', ], }, { diff --git a/src/locales/de/settings.ts b/src/locales/de/settings.ts index 55a8d891..63ff68be 100644 --- a/src/locales/de/settings.ts +++ b/src/locales/de/settings.ts @@ -327,6 +327,7 @@ export const settings = { themeStoreTitle: 'Theme-Store', themeStoreSubmitText: 'Eigenes Theme gebaut? Teil es mit der Community — mehr Infos im Themes-Repository.', themeStoreSubmitLink: 'Themes-Repository öffnen', + themeStoreNetworkNotice: 'Der Theme-Store lädt Katalog und Vorschauen von externen Diensten (jsDelivr-CDN und GitHub). Es werden keine persönlichen Daten gesendet.', themeStoreSearchPlaceholder: 'Themes suchen…', themeStoreFilterMode: 'Nach Modus filtern', themeStoreModeAll: 'Alle', diff --git a/src/locales/en/settings.ts b/src/locales/en/settings.ts index 45a20cce..1a8bc1f6 100644 --- a/src/locales/en/settings.ts +++ b/src/locales/en/settings.ts @@ -394,6 +394,7 @@ export const settings = { themeStoreTitle: 'Theme Store', themeStoreSubmitText: 'Made your own theme? Share it with the community — more info in the themes repository.', themeStoreSubmitLink: 'Open the themes repository', + themeStoreNetworkNotice: 'The Theme Store loads its catalogue and previews from external services (the jsDelivr CDN and GitHub). No personal data is sent.', themeStoreSearchPlaceholder: 'Search themes…', themeStoreFilterMode: 'Filter by mode', themeStoreModeAll: 'All', diff --git a/src/locales/es/settings.ts b/src/locales/es/settings.ts index 9044f7d0..9cf8a0cf 100644 --- a/src/locales/es/settings.ts +++ b/src/locales/es/settings.ts @@ -325,6 +325,7 @@ export const settings = { themeStoreTitle: 'Tienda de temas', themeStoreSubmitText: '¿Has creado tu propio tema? Compártelo con la comunidad — más información en el repositorio de temas.', themeStoreSubmitLink: 'Abrir el repositorio de temas', + themeStoreNetworkNotice: 'La tienda de temas carga el catálogo y las vistas previas desde servicios externos (la CDN de jsDelivr y GitHub). No se envían datos personales.', themeStoreSearchPlaceholder: 'Buscar temas…', themeStoreFilterMode: 'Filtrar por modo', themeStoreModeAll: 'Todos', diff --git a/src/locales/fr/settings.ts b/src/locales/fr/settings.ts index 0de97390..694374cf 100644 --- a/src/locales/fr/settings.ts +++ b/src/locales/fr/settings.ts @@ -323,6 +323,7 @@ export const settings = { themeStoreTitle: 'Boutique de thèmes', themeStoreSubmitText: "Vous avez créé votre propre thème ? Partagez-le avec la communauté — plus d'infos dans le dépôt de thèmes.", themeStoreSubmitLink: 'Ouvrir le dépôt de thèmes', + themeStoreNetworkNotice: 'Le Theme Store charge son catalogue et ses aperçus depuis des services externes (le CDN jsDelivr et GitHub). Aucune donnée personnelle n’est envoyée.', themeStoreSearchPlaceholder: 'Rechercher des thèmes…', themeStoreFilterMode: 'Filtrer par mode', themeStoreModeAll: 'Tous', diff --git a/src/locales/nb/settings.ts b/src/locales/nb/settings.ts index 24c0e094..2990b076 100644 --- a/src/locales/nb/settings.ts +++ b/src/locales/nb/settings.ts @@ -326,6 +326,7 @@ export const settings = { themeStoreTitle: 'Temabutikk', themeStoreSubmitText: 'Laget ditt eget tema? Del det med fellesskapet — mer info i tema-repoet.', themeStoreSubmitLink: 'Åpne tema-repoet', + themeStoreNetworkNotice: 'Theme Store laster katalogen og forhåndsvisninger fra eksterne tjenester (jsDelivr-CDN og GitHub). Ingen personopplysninger sendes.', themeStoreSearchPlaceholder: 'Søk i temaer…', themeStoreFilterMode: 'Filtrer etter modus', themeStoreModeAll: 'Alle', diff --git a/src/locales/nl/settings.ts b/src/locales/nl/settings.ts index 5df8d875..5270f581 100644 --- a/src/locales/nl/settings.ts +++ b/src/locales/nl/settings.ts @@ -323,6 +323,7 @@ export const settings = { themeStoreTitle: 'Themawinkel', themeStoreSubmitText: 'Eigen thema gemaakt? Deel het met de community — meer info in de themarepository.', themeStoreSubmitLink: 'Themarepository openen', + themeStoreNetworkNotice: 'De Theme Store laadt de catalogus en previews van externe diensten (de jsDelivr-CDN en GitHub). Er worden geen persoonlijke gegevens verzonden.', themeStoreSearchPlaceholder: "Thema's zoeken…", themeStoreFilterMode: 'Filteren op modus', themeStoreModeAll: 'Alle', diff --git a/src/locales/ro/settings.ts b/src/locales/ro/settings.ts index 6f09b27f..7f2f436a 100644 --- a/src/locales/ro/settings.ts +++ b/src/locales/ro/settings.ts @@ -329,6 +329,7 @@ export const settings = { themeStoreTitle: 'Magazin de teme', themeStoreSubmitText: 'Ți-ai creat propria temă? Împărtășește-o cu comunitatea — mai multe informații în depozitul de teme.', themeStoreSubmitLink: 'Deschide depozitul de teme', + themeStoreNetworkNotice: 'Theme Store încarcă catalogul și previzualizările de la servicii externe (CDN-ul jsDelivr și GitHub). Nu se trimit date personale.', themeStoreSearchPlaceholder: 'Caută teme…', themeStoreFilterMode: 'Filtrează după mod', themeStoreModeAll: 'Toate', diff --git a/src/locales/ru/settings.ts b/src/locales/ru/settings.ts index 2ee2d99f..a882aff5 100644 --- a/src/locales/ru/settings.ts +++ b/src/locales/ru/settings.ts @@ -405,6 +405,7 @@ export const settings = { themeStoreTitle: 'Магазин тем', themeStoreSubmitText: 'Создали свою тему? Поделитесь с сообществом — подробности в репозитории тем.', themeStoreSubmitLink: 'Открыть репозиторий тем', + themeStoreNetworkNotice: 'Магазин тем загружает каталог и превью с внешних сервисов (CDN jsDelivr и GitHub). Личные данные не отправляются.', themeStoreSearchPlaceholder: 'Поиск тем…', themeStoreFilterMode: 'Фильтр по режиму', themeStoreModeAll: 'Все', diff --git a/src/locales/zh/settings.ts b/src/locales/zh/settings.ts index d1151058..e950f2b7 100644 --- a/src/locales/zh/settings.ts +++ b/src/locales/zh/settings.ts @@ -322,6 +322,7 @@ export const settings = { themeStoreTitle: '主题商店', themeStoreSubmitText: '做了自己的主题?与社区分享——更多信息见主题仓库。', themeStoreSubmitLink: '打开主题仓库', + themeStoreNetworkNotice: '主题商店从外部服务(jsDelivr CDN 和 GitHub)加载目录和预览。不会发送任何个人数据。', themeStoreSearchPlaceholder: '搜索主题…', themeStoreFilterMode: '按模式筛选', themeStoreModeAll: '全部', diff --git a/src/utils/themes/themeRegistry.test.ts b/src/utils/themes/themeRegistry.test.ts index dcd11ec6..62754924 100644 --- a/src/utils/themes/themeRegistry.test.ts +++ b/src/utils/themes/themeRegistry.test.ts @@ -84,4 +84,31 @@ describe('fetchRegistry', () => { expect(r.registry.generatedAt).toBe('fresh2'); expect(fetchMock).toHaveBeenCalledTimes(1); }); + + it('force-refresh tries GitHub raw first', async () => { + const calls: string[] = []; + vi.stubGlobal('fetch', vi.fn(async (url: string) => { calls.push(url); return okRes(reg('forced')); })); + + await fetchRegistry({ force: true }); + expect(calls[0]).toContain('raw.githubusercontent.com'); + }); + + it('force-refresh falls back to jsDelivr when raw fails', async () => { + const fetchMock = vi.fn(async (url: string) => + url.includes('raw.githubusercontent.com') ? failRes() : okRes(reg('cdn'))); + vi.stubGlobal('fetch', fetchMock); + + const r = await fetchRegistry({ force: true }); + expect(r.registry.generatedAt).toBe('cdn'); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); + + it('non-force load uses the CDN only (not raw)', async () => { + const calls: string[] = []; + vi.stubGlobal('fetch', vi.fn(async (url: string) => { calls.push(url); return okRes(reg('cdn')); })); + + await fetchRegistry(); + expect(calls).toHaveLength(1); + expect(calls[0]).not.toContain('raw.githubusercontent.com'); + }); }); diff --git a/src/utils/themes/themeRegistry.ts b/src/utils/themes/themeRegistry.ts index 2a49fd26..2f808bec 100644 --- a/src/utils/themes/themeRegistry.ts +++ b/src/utils/themes/themeRegistry.ts @@ -7,6 +7,10 @@ const CDN_BASE = 'https://cdn.jsdelivr.net/gh/Psysonic/psysonic-themes@main'; const REGISTRY_URL = `${CDN_BASE}/registry.json`; +// GitHub raw serves with a ~5-minute cache (vs jsDelivr's up-to-12h @main edge) +// and permissive CORS. Used only on a manual refresh so freshly merged themes +// appear without waiting on — or purging — the shared CDN edge. +const RAW_REGISTRY_URL = 'https://raw.githubusercontent.com/Psysonic/psysonic-themes/main/registry.json'; const CACHE_KEY = 'psysonic_theme_registry_cache'; const TTL_MS = 12 * 60 * 60 * 1000; // 12h — matches jsDelivr's @main edge cache @@ -81,17 +85,23 @@ export async function fetchRegistry(opts?: { force?: boolean }): Promise