mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
feat: auto install themes if not present locally with BS Cloud
This commit is contained in:
@@ -3,8 +3,10 @@ import {
|
|||||||
applyDownloadedEnvelope,
|
applyDownloadedEnvelope,
|
||||||
buildUploadPayload,
|
buildUploadPayload,
|
||||||
BSPLUS_CLOUD_KNOWN_REMOTE_UPDATED_AT_KEY,
|
BSPLUS_CLOUD_KNOWN_REMOTE_UPDATED_AT_KEY,
|
||||||
|
BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY,
|
||||||
CLOUD_SETTINGS_SYNC_SCHEMA_VERSION,
|
CLOUD_SETTINGS_SYNC_SCHEMA_VERSION,
|
||||||
isKeyIncludedInCloudUploadPayload,
|
isKeyIncludedInCloudUploadPayload,
|
||||||
|
resolveThemeIdForPostSyncDownload,
|
||||||
setKnownRemoteUpdatedAt,
|
setKnownRemoteUpdatedAt,
|
||||||
} from "@/seqta/utils/cloudSettingsSync";
|
} from "@/seqta/utils/cloudSettingsSync";
|
||||||
|
|
||||||
@@ -220,7 +222,15 @@ async function getSettingsAndApplyOnce(token: string): Promise<GetResult> {
|
|||||||
error: data?.error ?? `Download failed (${r.status})`,
|
error: data?.error ?? `Download failed (${r.status})`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const themeIdToEnsure = resolveThemeIdForPostSyncDownload(data);
|
||||||
await applyDownloadedEnvelope(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?.();
|
reloadSeqtaPagesFn?.();
|
||||||
const updated_at = data?.updated_at as string | undefined;
|
const updated_at = data?.updated_at as string | undefined;
|
||||||
await setKnownRemoteUpdatedAt(updated_at);
|
await setKnownRemoteUpdatedAt(updated_at);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const themesPlugin: Plugin = {
|
|||||||
|
|
||||||
run: async (_) => {
|
run: async (_) => {
|
||||||
const themeManager = ThemeManager.getInstance();
|
const themeManager = ThemeManager.getInstance();
|
||||||
|
await themeManager.prepareThemeAfterCloudSync();
|
||||||
await themeManager.initialize();
|
await themeManager.initialize();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
type LoadedCustomTheme,
|
type LoadedCustomTheme,
|
||||||
shouldForceThemeAppearance,
|
shouldForceThemeAppearance,
|
||||||
} from "@/types/CustomThemes";
|
} from "@/types/CustomThemes";
|
||||||
|
import { BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY } from "@/seqta/utils/cloudSettingsSync";
|
||||||
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";
|
||||||
import { themeUpdates } from "@/interface/hooks/ThemeUpdates";
|
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<void> {
|
||||||
|
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
|
* Initialize the theme system and restore previous state
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ export const CLOUD_SETTINGS_SYNC_SCHEMA_VERSION = 1;
|
|||||||
export const BSPLUS_CLOUD_KNOWN_REMOTE_UPDATED_AT_KEY =
|
export const BSPLUS_CLOUD_KNOWN_REMOTE_UPDATED_AT_KEY =
|
||||||
"bsplus_cloud_settings_known_remote_updated_at";
|
"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).
|
* Never uploaded to the cloud backup (OAuth and legacy keys).
|
||||||
* IndexedDB (e.g. Global Search’s `betterseqta-index` database) is not part of
|
* 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 = [
|
const CLIENT_ONLY_CLOUD_KEYS_EXACT = [
|
||||||
BSPLUS_CLOUD_KNOWN_REMOTE_UPDATED_AT_KEY,
|
BSPLUS_CLOUD_KNOWN_REMOTE_UPDATED_AT_KEY,
|
||||||
"bsplus_lastCloudPoll",
|
"bsplus_lastCloudPoll",
|
||||||
|
BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY,
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/** After restoring from cloud, keep local session so the user stays signed in. */
|
/** After restoring from cloud, keep local session so the user stays signed in. */
|
||||||
@@ -101,8 +109,15 @@ function stripExcludedKeysFromRemoteData(remote: Record<string, unknown>): Recor
|
|||||||
return out;
|
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<string, unknown>): {
|
export function buildUploadPayload(all: Record<string, unknown>): {
|
||||||
schemaVersion: number;
|
schemaVersion: number;
|
||||||
|
themeId: string;
|
||||||
data: Record<string, unknown>;
|
data: Record<string, unknown>;
|
||||||
} {
|
} {
|
||||||
const filtered: Record<string, unknown> = {};
|
const filtered: Record<string, unknown> = {};
|
||||||
@@ -111,17 +126,54 @@ export function buildUploadPayload(all: Record<string, unknown>): {
|
|||||||
filtered[k] = v;
|
filtered[k] = v;
|
||||||
}
|
}
|
||||||
const data = migrateLegacyToPluginSettings(filtered);
|
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<{
|
export async function getSnapshotForUpload(): Promise<{
|
||||||
schemaVersion: number;
|
schemaVersion: number;
|
||||||
|
themeId: string;
|
||||||
data: Record<string, unknown>;
|
data: Record<string, unknown>;
|
||||||
}> {
|
}> {
|
||||||
const all = await browser.storage.local.get();
|
const all = await browser.storage.local.get();
|
||||||
return buildUploadPayload(all as Record<string, unknown>);
|
return buildUploadPayload(all as Record<string, unknown>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 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<string, unknown>).themeId,
|
||||||
|
);
|
||||||
|
if (top) return top;
|
||||||
|
}
|
||||||
|
|
||||||
|
let remoteFlat: Record<string, unknown>;
|
||||||
|
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<string, unknown> }).data;
|
||||||
|
} else if (envelope && typeof envelope === "object" && !Array.isArray(envelope)) {
|
||||||
|
remoteFlat = envelope as Record<string, unknown>;
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrated = migrateLegacyToPluginSettings(remoteFlat);
|
||||||
|
const fromData = normalizeThemeIdForSync(migrated.selectedTheme);
|
||||||
|
return fromData === "" ? undefined : fromData;
|
||||||
|
}
|
||||||
|
|
||||||
export async function setKnownRemoteUpdatedAt(iso: string | undefined): Promise<void> {
|
export async function setKnownRemoteUpdatedAt(iso: string | undefined): Promise<void> {
|
||||||
if (!iso || typeof iso !== "string") return;
|
if (!iso || typeof iso !== "string") return;
|
||||||
await browser.storage.local.set({ [BSPLUS_CLOUD_KNOWN_REMOTE_UPDATED_AT_KEY]: iso });
|
await browser.storage.local.set({ [BSPLUS_CLOUD_KNOWN_REMOTE_UPDATED_AT_KEY]: iso });
|
||||||
|
|||||||
Reference in New Issue
Block a user