diff --git a/src/background.ts b/src/background.ts index 0aa331b8..911dbdf2 100644 --- a/src/background.ts +++ b/src/background.ts @@ -14,8 +14,8 @@ function reloadSeqtaPages() { result.then(open, console.error); } -// @ts-ignore browser.runtime.onMessage.addListener( + // @ts-ignore - OnMessageListener expects literal true for async, we return boolean (request: any, _: any, sendResponse: (response?: any) => void) => { switch (request.type) { case "reloadTabs": @@ -56,6 +56,34 @@ browser.runtime.onMessage.addListener( fetchNews(request.source ?? "australia", sendResponse); return true; + case "fetchThemes": { + const url = `https://betterseqta.org/api/themes?type=betterseqta&limit=100&nocache=${Date.now()}`; + fetch(url, { cache: "no-store" }) + .then((r) => r.json()) + .then(sendResponse) + .catch((err) => { + console.error("[Background] fetchThemes error:", err); + sendResponse({ success: false, error: err?.message }); + }); + return true; + } + + case "fetchFromUrl": { + const { url } = request; + if (!url || typeof url !== "string") { + sendResponse({ error: "Missing url" }); + return false; + } + fetch(url, { cache: "no-store" }) + .then((r) => r.json()) + .then((data) => sendResponse({ data })) + .catch((err) => { + console.error("[Background] fetchFromUrl error:", err); + sendResponse({ error: err?.message }); + }); + return true; + } + default: console.log("Unknown request type"); } diff --git a/src/interface/pages/store.svelte b/src/interface/pages/store.svelte index f3387af0..6d4c0345 100644 --- a/src/interface/pages/store.svelte +++ b/src/interface/pages/store.svelte @@ -48,20 +48,26 @@ activeTab = tab; }; - // Fetch themes and initialize app + // Fetch themes via background script (avoids CORS when store runs inside SEQTA page) const fetchThemes = async () => { try { - const response = await fetch(`https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/store/themes.json?nocache=${(new Date()).getTime()}`, { cache: 'no-store' }); - const data = await response.json(); - themes = data.themes; + const data = (await browser.runtime.sendMessage({ type: 'fetchThemes' })) as { + success?: boolean; + data?: { themes: Theme[] }; + error?: string; + }; + if (!data?.success || !data?.data?.themes) { + throw new Error(data?.error || 'Failed to fetch themes'); + } + themes = data.data.themes; // Shuffle for cover themes const shuffled = [...themes].sort(() => 0.5 - Math.random()); coverThemes = shuffled.slice(0, 3); loading = false; - } catch (error) { - console.error('Failed to fetch themes', error); + } catch (err) { + console.error('Failed to fetch themes', err); setTimeout(fetchThemes, 5000); // Retry after 5 seconds if failure occurs } }; diff --git a/src/interface/types/Theme.ts b/src/interface/types/Theme.ts index b68a5ba1..853760cb 100644 --- a/src/interface/types/Theme.ts +++ b/src/interface/types/Theme.ts @@ -1,7 +1,8 @@ export type Theme = { + id: string; name: string; description: string; coverImage: string; - marqueeImage: string; - id: string; + marqueeImage?: string; + theme_json_url?: string; }; diff --git a/src/manifests/manifest.json b/src/manifests/manifest.json index 92736dce..1ef79abb 100644 --- a/src/manifests/manifest.json +++ b/src/manifests/manifest.json @@ -16,12 +16,12 @@ } }, "permissions": ["tabs", "notifications", "storage"], - "host_permissions": ["https://newsapi.org/", "*://*/*"], + "host_permissions": ["https://newsapi.org/", "https://betterseqta.org/", "*://*/*"], "background": { "service_worker": "background.ts" }, "content_security_policy": { - "extension_pages": "script-src 'self'; object-src 'self'" + "extension_pages": "script-src 'self'; object-src 'self'; connect-src 'self' https://betterseqta.org" }, "content_scripts": [ { diff --git a/src/plugins/built-in/themes/theme-manager.ts b/src/plugins/built-in/themes/theme-manager.ts index 69e6ff23..0ac37ddb 100644 --- a/src/plugins/built-in/themes/theme-manager.ts +++ b/src/plugins/built-in/themes/theme-manager.ts @@ -1,4 +1,5 @@ import localforage from "localforage"; +import browser from "webextension-polyfill"; import type { CustomTheme, LoadedCustomTheme } from "@/types/CustomThemes"; import { settingsState } from "@/seqta/utils/listeners/SettingsState"; import debounce from "@/seqta/utils/debounce"; @@ -470,23 +471,50 @@ export class ThemeManager { } } + private readonly THEME_API_BASE = 'https://betterseqta.org/api'; + + /** + * Fetch JSON from a URL via background script (avoids CORS when running inside SEQTA page) + */ + private async fetchFromUrl(url: string): Promise { + const result = (await browser.runtime.sendMessage({ + type: 'fetchFromUrl', + url, + })) as { data?: unknown; error?: string }; + if (result?.error) throw new Error(result.error); + return result?.data; + } + /** * Download and install a theme from the store */ public async downloadTheme(themeContent: { id: string; name: string; - description: string; - coverImage: string; + description?: string; + coverImage?: string; + theme_json_url?: string; }): Promise { console.debug("[ThemeManager] Downloading theme:", themeContent.name); try { if (!themeContent.id) return; - const response = await fetch( - `https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/store/themes/${themeContent.id}/theme.json`, - ); - const themeData = (await response.json()) as ThemeContent; + let themeJsonUrl: string; + + // Use theme_json_url if provided (from API list), otherwise call download endpoint + if (themeContent.theme_json_url) { + themeJsonUrl = themeContent.theme_json_url; + } else { + const downloadData = await this.fetchFromUrl( + `${this.THEME_API_BASE}/themes/${themeContent.id}/download` + ) as { success?: boolean; data?: { theme_json_url: string } }; + if (!downloadData?.success || !downloadData?.data?.theme_json_url) { + throw new Error("Failed to get theme download URL"); + } + themeJsonUrl = downloadData.data.theme_json_url; + } + + const themeData = (await this.fetchFromUrl(themeJsonUrl)) as ThemeContent; await this.installTheme(themeData); } catch (error) {