diff --git a/src/background/cloudSettingsAutoSync.ts b/src/background/cloudSettingsAutoSync.ts index a75de192..94218d1c 100644 --- a/src/background/cloudSettingsAutoSync.ts +++ b/src/background/cloudSettingsAutoSync.ts @@ -3,8 +3,10 @@ import { applyDownloadedEnvelope, buildUploadPayload, BSPLUS_CLOUD_KNOWN_REMOTE_UPDATED_AT_KEY, + BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY, CLOUD_SETTINGS_SYNC_SCHEMA_VERSION, isKeyIncludedInCloudUploadPayload, + resolveThemeIdForPostSyncDownload, setKnownRemoteUpdatedAt, } from "@/seqta/utils/cloudSettingsSync"; @@ -220,7 +222,15 @@ async function getSettingsAndApplyOnce(token: string): Promise { error: data?.error ?? `Download failed (${r.status})`, }; } + const themeIdToEnsure = resolveThemeIdForPostSyncDownload(data); await applyDownloadedEnvelope(data); + if (themeIdToEnsure) { + await browser.storage.local.set({ + [BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY]: themeIdToEnsure, + }); + } else { + await browser.storage.local.remove(BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY); + } reloadSeqtaPagesFn?.(); const updated_at = data?.updated_at as string | undefined; await setKnownRemoteUpdatedAt(updated_at); diff --git a/src/plugins/built-in/themes/index.ts b/src/plugins/built-in/themes/index.ts index b31abd86..9127d739 100644 --- a/src/plugins/built-in/themes/index.ts +++ b/src/plugins/built-in/themes/index.ts @@ -10,6 +10,7 @@ const themesPlugin: Plugin = { run: async (_) => { const themeManager = ThemeManager.getInstance(); + await themeManager.prepareThemeAfterCloudSync(); await themeManager.initialize(); }, }; diff --git a/src/plugins/built-in/themes/theme-manager.ts b/src/plugins/built-in/themes/theme-manager.ts index e37f90e5..0de365a0 100644 --- a/src/plugins/built-in/themes/theme-manager.ts +++ b/src/plugins/built-in/themes/theme-manager.ts @@ -6,6 +6,7 @@ import { type LoadedCustomTheme, shouldForceThemeAppearance, } from "@/types/CustomThemes"; +import { BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY } from "@/seqta/utils/cloudSettingsSync"; import { settingsState } from "@/seqta/utils/listeners/SettingsState"; import debounce from "@/seqta/utils/debounce"; import { themeUpdates } from "@/interface/hooks/ThemeUpdates"; @@ -166,6 +167,31 @@ export class ThemeManager { } } + /** + * After cloud restore, IndexedDB/theme storage is only reachable from page context (not MV3 SW). + * Background sets BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY; we fetch the store JSON here before setTheme(). + */ + public async prepareThemeAfterCloudSync(): Promise { + try { + const snap = await browser.storage.local.get(BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY); + const pending = snap[BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY]; + if (pending === undefined) return; + + await browser.storage.local.remove(BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY); + + if (typeof pending !== "string") return; + const id = pending.trim(); + if (!id) return; + + const existing = (await localforage.getItem(id)) as CustomTheme | null; + if (existing) return; + + await this.downloadAndInstallStoreTheme({ id, name: id }); + } catch (e) { + console.warn("[ThemeManager] prepareThemeAfterCloudSync:", e); + } + } + /** * Initialize the theme system and restore previous state */ diff --git a/src/seqta/utils/cloudSettingsSync.ts b/src/seqta/utils/cloudSettingsSync.ts index 8918cd80..cb9c7d59 100644 --- a/src/seqta/utils/cloudSettingsSync.ts +++ b/src/seqta/utils/cloudSettingsSync.ts @@ -10,6 +10,13 @@ export const CLOUD_SETTINGS_SYNC_SCHEMA_VERSION = 1; export const BSPLUS_CLOUD_KNOWN_REMOTE_UPDATED_AT_KEY = "bsplus_cloud_settings_known_remote_updated_at"; +/** + * Written by the service worker after applying a cloud settings envelope; the SEQTA page’s + * ThemeManager reads and clears it (SW cannot share localforage/IndexedDB with the page). + */ +export const BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY = + "bsplus_pending_theme_ensure_after_cloud"; + /** * Never uploaded to the cloud backup (OAuth and legacy keys). * IndexedDB (e.g. Global Search’s `betterseqta-index` database) is not part of @@ -39,6 +46,7 @@ export const SENSITIVE_DEVICE_STORAGE_KEY_PREFIXES = ["plugin.global-search.stor const CLIENT_ONLY_CLOUD_KEYS_EXACT = [ BSPLUS_CLOUD_KNOWN_REMOTE_UPDATED_AT_KEY, "bsplus_lastCloudPoll", + BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY, ] as const; /** After restoring from cloud, keep local session so the user stays signed in. */ @@ -101,8 +109,15 @@ function stripExcludedKeysFromRemoteData(remote: Record): Recor return out; } +/** Stored theme id (`selectedTheme`); trims whitespace; empty string clears. */ +export function normalizeThemeIdForSync(raw: unknown): string { + if (typeof raw !== "string") return ""; + return raw.trim(); +} + export function buildUploadPayload(all: Record): { schemaVersion: number; + themeId: string; data: Record; } { const filtered: Record = {}; @@ -111,17 +126,54 @@ export function buildUploadPayload(all: Record): { filtered[k] = v; } const data = migrateLegacyToPluginSettings(filtered); - return { schemaVersion: CLOUD_SETTINGS_SYNC_SCHEMA_VERSION, data }; + const themeId = normalizeThemeIdForSync(all.selectedTheme); + return { + schemaVersion: CLOUD_SETTINGS_SYNC_SCHEMA_VERSION, + themeId, + data, + }; } export async function getSnapshotForUpload(): Promise<{ schemaVersion: number; + themeId: string; data: Record; }> { const all = await browser.storage.local.get(); return buildUploadPayload(all as Record); } +/** Theme to ensure is installed locally after a downloaded envelope (explicit field overrides `data.selectedTheme`). */ +export function resolveThemeIdForPostSyncDownload(envelope: unknown): string | undefined { + if (envelope && typeof envelope === "object" && "themeId" in envelope) { + const top = normalizeThemeIdForSync( + (envelope as Record).themeId, + ); + if (top) return top; + } + + let remoteFlat: Record; + if ( + envelope && + typeof envelope === "object" && + "data" in envelope && + (envelope as { data?: unknown }).data !== undefined && + typeof (envelope as { data?: unknown }).data === "object" && + (envelope as { data?: unknown }).data !== null && + !Array.isArray((envelope as { data?: unknown }).data) + ) { + remoteFlat = (envelope as { data: Record }).data; + } else if (envelope && typeof envelope === "object" && !Array.isArray(envelope)) { + remoteFlat = envelope as Record; + } else { + return undefined; + } + + const migrated = migrateLegacyToPluginSettings(remoteFlat); + const fromData = normalizeThemeIdForSync(migrated.selectedTheme); + return fromData === "" ? undefined : fromData; +} + export async function setKnownRemoteUpdatedAt(iso: string | undefined): Promise { if (!iso || typeof iso !== "string") return; await browser.storage.local.set({ [BSPLUS_CLOUD_KNOWN_REMOTE_UPDATED_AT_KEY]: iso });