feat: implement cloud store

This commit is contained in:
2026-02-20 10:27:17 +10:30
parent 170b1cf5c3
commit d64962147a
5 changed files with 80 additions and 17 deletions
+29 -1
View File
@@ -14,8 +14,8 @@ function reloadSeqtaPages() {
result.then(open, console.error); result.then(open, console.error);
} }
// @ts-ignore
browser.runtime.onMessage.addListener( browser.runtime.onMessage.addListener(
// @ts-ignore - OnMessageListener expects literal true for async, we return boolean
(request: any, _: any, sendResponse: (response?: any) => void) => { (request: any, _: any, sendResponse: (response?: any) => void) => {
switch (request.type) { switch (request.type) {
case "reloadTabs": case "reloadTabs":
@@ -56,6 +56,34 @@ browser.runtime.onMessage.addListener(
fetchNews(request.source ?? "australia", sendResponse); fetchNews(request.source ?? "australia", sendResponse);
return true; 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: default:
console.log("Unknown request type"); console.log("Unknown request type");
} }
+12 -6
View File
@@ -48,20 +48,26 @@
activeTab = tab; activeTab = tab;
}; };
// Fetch themes and initialize app // Fetch themes via background script (avoids CORS when store runs inside SEQTA page)
const fetchThemes = async () => { const fetchThemes = async () => {
try { 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 browser.runtime.sendMessage({ type: 'fetchThemes' })) as {
const data = await response.json(); success?: boolean;
themes = data.themes; 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 // Shuffle for cover themes
const shuffled = [...themes].sort(() => 0.5 - Math.random()); const shuffled = [...themes].sort(() => 0.5 - Math.random());
coverThemes = shuffled.slice(0, 3); coverThemes = shuffled.slice(0, 3);
loading = false; loading = false;
} catch (error) { } catch (err) {
console.error('Failed to fetch themes', error); console.error('Failed to fetch themes', err);
setTimeout(fetchThemes, 5000); // Retry after 5 seconds if failure occurs setTimeout(fetchThemes, 5000); // Retry after 5 seconds if failure occurs
} }
}; };
+3 -2
View File
@@ -1,7 +1,8 @@
export type Theme = { export type Theme = {
id: string;
name: string; name: string;
description: string; description: string;
coverImage: string; coverImage: string;
marqueeImage: string; marqueeImage?: string;
id: string; theme_json_url?: string;
}; };
+2 -2
View File
@@ -16,12 +16,12 @@
} }
}, },
"permissions": ["tabs", "notifications", "storage"], "permissions": ["tabs", "notifications", "storage"],
"host_permissions": ["https://newsapi.org/", "*://*/*"], "host_permissions": ["https://newsapi.org/", "https://betterseqta.org/", "*://*/*"],
"background": { "background": {
"service_worker": "background.ts" "service_worker": "background.ts"
}, },
"content_security_policy": { "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": [ "content_scripts": [
{ {
+34 -6
View File
@@ -1,4 +1,5 @@
import localforage from "localforage"; import localforage from "localforage";
import browser from "webextension-polyfill";
import type { CustomTheme, LoadedCustomTheme } from "@/types/CustomThemes"; import type { CustomTheme, LoadedCustomTheme } from "@/types/CustomThemes";
import { settingsState } from "@/seqta/utils/listeners/SettingsState"; import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import debounce from "@/seqta/utils/debounce"; 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<any> {
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 * Download and install a theme from the store
*/ */
public async downloadTheme(themeContent: { public async downloadTheme(themeContent: {
id: string; id: string;
name: string; name: string;
description: string; description?: string;
coverImage: string; coverImage?: string;
theme_json_url?: string;
}): Promise<void> { }): Promise<void> {
console.debug("[ThemeManager] Downloading theme:", themeContent.name); console.debug("[ThemeManager] Downloading theme:", themeContent.name);
try { try {
if (!themeContent.id) return; if (!themeContent.id) return;
const response = await fetch( let themeJsonUrl: string;
`https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/store/themes/${themeContent.id}/theme.json`,
); // Use theme_json_url if provided (from API list), otherwise call download endpoint
const themeData = (await response.json()) as ThemeContent; 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); await this.installTheme(themeData);
} catch (error) { } catch (error) {