mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
@@ -85,7 +85,7 @@
|
|||||||
try {
|
try {
|
||||||
const result = JSON.parse(event.target?.result as string);
|
const result = JSON.parse(event.target?.result as string);
|
||||||
tempTheme = result;
|
tempTheme = result;
|
||||||
await themeManager.installTheme(result);
|
await themeManager.installTheme(result, { fromStore: false });
|
||||||
await fetchThemes();
|
await fetchThemes();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error parsing file:', error);
|
console.error('Error parsing file:', error);
|
||||||
|
|||||||
@@ -125,6 +125,7 @@
|
|||||||
blob: image.blob
|
blob: image.blob
|
||||||
}))
|
}))
|
||||||
themeClone.coverImage = theme.coverImage
|
themeClone.coverImage = theme.coverImage
|
||||||
|
themeClone.userEdited = true
|
||||||
|
|
||||||
if (shouldForceThemeAppearance(themeClone)) {
|
if (shouldForceThemeAppearance(themeClone)) {
|
||||||
themeClone.forceTheme = true;
|
themeClone.forceTheme = true;
|
||||||
|
|||||||
@@ -8,4 +8,6 @@ export type Theme = {
|
|||||||
is_favorited?: boolean;
|
is_favorited?: boolean;
|
||||||
favorite_count?: number;
|
favorite_count?: number;
|
||||||
download_count?: number;
|
download_count?: number;
|
||||||
|
/** Unix seconds — last server update (GET /api/themes). */
|
||||||
|
updated_at?: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ export default defineLazyPlugin({
|
|||||||
settings,
|
settings,
|
||||||
disableToggle: true,
|
disableToggle: true,
|
||||||
defaultEnabled: false,
|
defaultEnabled: false,
|
||||||
beta: true,
|
|
||||||
styles: styles,
|
styles: styles,
|
||||||
|
|
||||||
// Lazy loader - only imports the heavy plugin when actually needed
|
// Lazy loader - only imports the heavy plugin when actually needed
|
||||||
|
|||||||
@@ -145,7 +145,6 @@ const globalSearchPlugin: Plugin<typeof settings> = {
|
|||||||
settings: settingsInstance.settings,
|
settings: settingsInstance.settings,
|
||||||
disableToggle: true,
|
disableToggle: true,
|
||||||
defaultEnabled: false,
|
defaultEnabled: false,
|
||||||
beta: true,
|
|
||||||
styles: styles,
|
styles: styles,
|
||||||
|
|
||||||
run: async (api) => {
|
run: async (api) => {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
} from "@/types/CustomThemes";
|
} 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";
|
||||||
|
import { themeUpdates } from "@/interface/hooks/ThemeUpdates";
|
||||||
|
import { cloudAuth } from "@/seqta/utils/CloudAuth";
|
||||||
import { updateAllColors } from "@/seqta/ui/colors/Manager";
|
import { updateAllColors } from "@/seqta/ui/colors/Manager";
|
||||||
import {
|
import {
|
||||||
clearCustomThemeAdaptiveCssVariables,
|
clearCustomThemeAdaptiveCssVariables,
|
||||||
@@ -24,6 +26,13 @@ type ThemeContent = {
|
|||||||
CustomCSS?: string;
|
CustomCSS?: string;
|
||||||
hideThemeName?: boolean;
|
hideThemeName?: boolean;
|
||||||
forceDark?: boolean;
|
forceDark?: boolean;
|
||||||
|
images?: { id: string; variableName: string; data: string }[]; // data: base64
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InstallThemeMeta = {
|
||||||
|
fromStore: boolean;
|
||||||
|
/** Server list `updated_at` (Unix seconds); set when installing from store. */
|
||||||
|
serverUpdatedAtSec?: number;
|
||||||
forceTheme?: boolean;
|
forceTheme?: boolean;
|
||||||
adaptiveCssVariables?: string[];
|
adaptiveCssVariables?: string[];
|
||||||
images: { id: string; variableName: string; data: string }[]; // data: base64
|
images: { id: string; variableName: string; data: string }[]; // data: base64
|
||||||
@@ -39,6 +48,7 @@ export class ThemeManager {
|
|||||||
private originalPreviewTheme: boolean | null = null;
|
private originalPreviewTheme: boolean | null = null;
|
||||||
private imageUrlCache: Map<string, string> = new Map();
|
private imageUrlCache: Map<string, string> = new Map();
|
||||||
private lastTransitionPoint: { x: number; y: number } = { x: 0, y: 0 };
|
private lastTransitionPoint: { x: number; y: number } = { x: 0, y: 0 };
|
||||||
|
private storeUpdateCheckRunning = false;
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
console.debug("[ThemeManager] Initializing...");
|
console.debug("[ThemeManager] Initializing...");
|
||||||
@@ -187,6 +197,8 @@ export class ThemeManager {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[ThemeManager] Error during initialization:", error);
|
console.error("[ThemeManager] Error during initialization:", error);
|
||||||
|
} finally {
|
||||||
|
void this.checkStoreThemeUpdates();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,9 +373,18 @@ export class ThemeManager {
|
|||||||
|
|
||||||
if (this.currentTheme) {
|
if (this.currentTheme) {
|
||||||
// Store the current color with the theme before removing it
|
// Store the current color with the theme before removing it
|
||||||
|
const selectedColor = settingsState.selectedColor;
|
||||||
|
const markUserEditedForColor =
|
||||||
|
this.currentTheme.userEdited !== true &&
|
||||||
|
this.currentTheme.installedFromStore !== false &&
|
||||||
|
selectedColor &&
|
||||||
|
this.currentTheme.defaultColour &&
|
||||||
|
selectedColor.trim().toLowerCase() !==
|
||||||
|
this.currentTheme.defaultColour.trim().toLowerCase();
|
||||||
await localforage.setItem(this.currentTheme.id, {
|
await localforage.setItem(this.currentTheme.id, {
|
||||||
...this.currentTheme,
|
...this.currentTheme,
|
||||||
selectedColor: settingsState.selectedColor,
|
selectedColor,
|
||||||
|
...(markUserEditedForColor ? { userEdited: true } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,18 +465,24 @@ export class ThemeManager {
|
|||||||
public async saveTheme(theme: LoadedCustomTheme): Promise<void> {
|
public async saveTheme(theme: LoadedCustomTheme): Promise<void> {
|
||||||
console.debug("[ThemeManager] Saving theme:", theme.name);
|
console.debug("[ThemeManager] Saving theme:", theme.name);
|
||||||
try {
|
try {
|
||||||
await localforage.setItem(theme.id, theme);
|
const existing = (await localforage.getItem(theme.id)) as CustomTheme | null;
|
||||||
|
let toSave = theme;
|
||||||
|
if (existing?.userEdited === true && theme.userEdited !== false) {
|
||||||
|
toSave = { ...theme, userEdited: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
await localforage.setItem(toSave.id, toSave);
|
||||||
const themeIds = (await localforage.getItem("customThemes")) as
|
const themeIds = (await localforage.getItem("customThemes")) as
|
||||||
| string[]
|
| string[]
|
||||||
| null;
|
| null;
|
||||||
|
|
||||||
if (themeIds) {
|
if (themeIds) {
|
||||||
if (!themeIds.includes(theme.id)) {
|
if (!themeIds.includes(toSave.id)) {
|
||||||
themeIds.push(theme.id);
|
themeIds.push(toSave.id);
|
||||||
await localforage.setItem("customThemes", themeIds);
|
await localforage.setItem("customThemes", themeIds);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await localforage.setItem("customThemes", [theme.id]);
|
await localforage.setItem("customThemes", [toSave.id]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[ThemeManager] Error saving theme:", error);
|
console.error("[ThemeManager] Error saving theme:", error);
|
||||||
@@ -513,39 +540,62 @@ export class ThemeManager {
|
|||||||
description?: string;
|
description?: string;
|
||||||
coverImage?: string;
|
coverImage?: string;
|
||||||
theme_json_url?: string;
|
theme_json_url?: string;
|
||||||
|
updated_at?: number;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
console.debug("[ThemeManager] Downloading theme:", themeContent.name);
|
|
||||||
try {
|
try {
|
||||||
if (!themeContent.id) return;
|
await this.downloadAndInstallStoreTheme(themeContent);
|
||||||
|
|
||||||
let themeData: ThemeContent;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Try API first (increments download_count)
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
themeData = (await this.fetchFromUrl(downloadData.data.theme_json_url)) as ThemeContent;
|
|
||||||
} catch (apiError) {
|
|
||||||
// Fallback to GitHub if API is unreachable
|
|
||||||
console.warn("[ThemeManager] API failed, trying GitHub fallback:", apiError);
|
|
||||||
const githubUrl = `${this.GITHUB_THEMES_BASE}/${themeContent.id}/theme.json`;
|
|
||||||
themeData = (await this.fetchFromUrl(githubUrl)) as ThemeContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.installTheme(themeData);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[ThemeManager] Error downloading theme:", error);
|
console.error("[ThemeManager] Error downloading theme:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch theme.json from the store and install (throws on failure).
|
||||||
|
*/
|
||||||
|
private async downloadAndInstallStoreTheme(themeContent: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
coverImage?: string;
|
||||||
|
theme_json_url?: string;
|
||||||
|
updated_at?: number;
|
||||||
|
}): Promise<void> {
|
||||||
|
console.debug("[ThemeManager] Downloading theme:", themeContent.name);
|
||||||
|
if (!themeContent.id) {
|
||||||
|
throw new Error("Missing theme id");
|
||||||
|
}
|
||||||
|
|
||||||
|
let themeData: ThemeContent;
|
||||||
|
|
||||||
|
try {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
themeData = (await this.fetchFromUrl(
|
||||||
|
downloadData.data.theme_json_url,
|
||||||
|
)) as ThemeContent;
|
||||||
|
} catch (apiError) {
|
||||||
|
console.warn("[ThemeManager] API failed, trying GitHub fallback:", apiError);
|
||||||
|
const githubUrl = `${this.GITHUB_THEMES_BASE}/${themeContent.id}/theme.json`;
|
||||||
|
themeData = (await this.fetchFromUrl(githubUrl)) as ThemeContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.installTheme(themeData, {
|
||||||
|
fromStore: true,
|
||||||
|
serverUpdatedAtSec: themeContent.updated_at,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install a theme from theme data
|
* Install a theme from theme data
|
||||||
*/
|
*/
|
||||||
public async installTheme(themeData: ThemeContent): Promise<void> {
|
public async installTheme(
|
||||||
|
themeData: ThemeContent,
|
||||||
|
meta?: InstallThemeMeta,
|
||||||
|
): Promise<void> {
|
||||||
console.debug("[ThemeManager] Installing theme:", themeData.name);
|
console.debug("[ThemeManager] Installing theme:", themeData.name);
|
||||||
try {
|
try {
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
@@ -553,6 +603,9 @@ export class ThemeManager {
|
|||||||
throw new Error("Theme is missing required fields (id or name)");
|
throw new Error("Theme is missing required fields (id or name)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fromStore = meta?.fromStore ?? false;
|
||||||
|
const serverUpdatedAtSec = meta?.serverUpdatedAtSec;
|
||||||
|
|
||||||
// Handle cover image (optional)
|
// Handle cover image (optional)
|
||||||
let coverImageBlob = null;
|
let coverImageBlob = null;
|
||||||
if (themeData.coverImage) {
|
if (themeData.coverImage) {
|
||||||
@@ -587,7 +640,6 @@ export class ThemeManager {
|
|||||||
})
|
})
|
||||||
.filter((img) => img !== null) ?? [];
|
.filter((img) => img !== null) ?? [];
|
||||||
|
|
||||||
// Create theme with defaults for optional fields
|
|
||||||
const theme: LoadedCustomTheme = {
|
const theme: LoadedCustomTheme = {
|
||||||
id: themeData.id,
|
id: themeData.id,
|
||||||
name: themeData.name,
|
name: themeData.name,
|
||||||
@@ -602,6 +654,12 @@ export class ThemeManager {
|
|||||||
isEditable: false,
|
isEditable: false,
|
||||||
hideThemeName: themeData.hideThemeName ?? false,
|
hideThemeName: themeData.hideThemeName ?? false,
|
||||||
forceDark: themeData.forceDark,
|
forceDark: themeData.forceDark,
|
||||||
|
installedFromStore: fromStore,
|
||||||
|
userEdited: fromStore ? false : undefined,
|
||||||
|
storeSyncedAtSec:
|
||||||
|
fromStore && serverUpdatedAtSec != null
|
||||||
|
? serverUpdatedAtSec
|
||||||
|
: undefined,
|
||||||
forceTheme:
|
forceTheme:
|
||||||
themeData.forceTheme !== undefined
|
themeData.forceTheme !== undefined
|
||||||
? themeData.forceTheme
|
? themeData.forceTheme
|
||||||
@@ -618,6 +676,107 @@ export class ThemeManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare installed store themes to GET /api/themes and refresh when the server is newer.
|
||||||
|
* Skips themes with userEdited: true (theme creator / popup save, or custom accent vs default).
|
||||||
|
*/
|
||||||
|
public async checkStoreThemeUpdates(): Promise<void> {
|
||||||
|
if (this.storeUpdateCheckRunning) return;
|
||||||
|
this.storeUpdateCheckRunning = true;
|
||||||
|
try {
|
||||||
|
const token = await cloudAuth.getStoredToken();
|
||||||
|
const res = (await browser.runtime.sendMessage({
|
||||||
|
type: "fetchThemes",
|
||||||
|
token: token ?? undefined,
|
||||||
|
})) as {
|
||||||
|
success?: boolean;
|
||||||
|
data?: { themes?: Array<{ id: string; updated_at?: number }> };
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!res?.success || !res.data?.themes?.length) return;
|
||||||
|
|
||||||
|
const serverById = new Map<string, number>();
|
||||||
|
for (const t of res.data.themes) {
|
||||||
|
if (
|
||||||
|
typeof t.id === "string" &&
|
||||||
|
typeof t.updated_at === "number"
|
||||||
|
) {
|
||||||
|
serverById.set(t.id, t.updated_at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (serverById.size === 0) return;
|
||||||
|
|
||||||
|
const themeIds = (await localforage.getItem("customThemes")) as
|
||||||
|
| string[]
|
||||||
|
| null;
|
||||||
|
if (!themeIds?.length) return;
|
||||||
|
|
||||||
|
for (const id of themeIds) {
|
||||||
|
const theme = (await localforage.getItem(id)) as CustomTheme | null;
|
||||||
|
if (!theme || theme.userEdited === true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// File imports explicitly set installedFromStore: false — never auto-sync.
|
||||||
|
if (theme.installedFromStore === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverUpdated = serverById.get(id);
|
||||||
|
if (serverUpdated == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
theme.installedFromStore === true &&
|
||||||
|
theme.storeSyncedAtSec != null
|
||||||
|
) {
|
||||||
|
if (serverUpdated <= theme.storeSyncedAtSec) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Else: legacy installs from before store metadata (installedFromStore/storeSyncedAtSec
|
||||||
|
// unset) or incomplete rows — still listed on the server, so sync to latest once.
|
||||||
|
|
||||||
|
const wasSelected = settingsState.selectedTheme === id;
|
||||||
|
try {
|
||||||
|
if (wasSelected) {
|
||||||
|
await this.disableTheme();
|
||||||
|
}
|
||||||
|
await this.downloadAndInstallStoreTheme({
|
||||||
|
id: theme.id,
|
||||||
|
name: theme.name,
|
||||||
|
updated_at: serverUpdated,
|
||||||
|
});
|
||||||
|
console.log(
|
||||||
|
"[ThemeManager] Theme auto-updated from store:",
|
||||||
|
theme.name,
|
||||||
|
);
|
||||||
|
if (wasSelected) {
|
||||||
|
await this.setTheme(id, false);
|
||||||
|
}
|
||||||
|
themeUpdates.triggerUpdate();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[ThemeManager] Store theme auto-update failed:", id, err);
|
||||||
|
if (wasSelected) {
|
||||||
|
try {
|
||||||
|
await this.setTheme(id, false);
|
||||||
|
} catch (restoreErr) {
|
||||||
|
console.error(
|
||||||
|
"[ThemeManager] Failed to restore theme after update error:",
|
||||||
|
restoreErr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[ThemeManager] checkStoreThemeUpdates error:", e);
|
||||||
|
} finally {
|
||||||
|
this.storeUpdateCheckRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Share a theme by exporting it
|
* Share a theme by exporting it
|
||||||
*/
|
*/
|
||||||
@@ -638,6 +797,9 @@ export class ThemeManager {
|
|||||||
isEditable,
|
isEditable,
|
||||||
selectedColor,
|
selectedColor,
|
||||||
allowBackgrounds,
|
allowBackgrounds,
|
||||||
|
installedFromStore,
|
||||||
|
storeSyncedAtSec,
|
||||||
|
userEdited,
|
||||||
...themeBasics
|
...themeBasics
|
||||||
} = theme;
|
} = theme;
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ export class MessageHandler {
|
|||||||
case "UpdateThemePreview":
|
case "UpdateThemePreview":
|
||||||
if (request?.save == true) {
|
if (request?.save == true) {
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
await themeManager.saveTheme(request.body);
|
await themeManager.saveTheme({
|
||||||
|
...request.body,
|
||||||
|
userEdited: true,
|
||||||
|
});
|
||||||
if (request.body.enableTheme) {
|
if (request.body.enableTheme) {
|
||||||
await themeManager.setTheme(request.body.id);
|
await themeManager.setTheme(request.body.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ export type CustomTheme = {
|
|||||||
*/
|
*/
|
||||||
forceTheme?: boolean;
|
forceTheme?: boolean;
|
||||||
forceDark?: boolean;
|
forceDark?: boolean;
|
||||||
|
/** True if installed from the BetterSEQTA theme store (not file import). */
|
||||||
|
installedFromStore?: boolean;
|
||||||
|
/** Server `updated_at` (Unix seconds) when this copy was installed or last auto-updated. */
|
||||||
|
storeSyncedAtSec?: number;
|
||||||
|
/** User saved edits in theme creator or popup; blocks store auto-update. */
|
||||||
|
userEdited?: boolean;
|
||||||
/** CSS custom property names (e.g. `--my-accent`) that receive the same value as `--better-main` when adaptive colours apply. */
|
/** CSS custom property names (e.g. `--my-accent`) that receive the same value as `--better-main` when adaptive colours apply. */
|
||||||
adaptiveCssVariables?: string[];
|
adaptiveCssVariables?: string[];
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user