Add theme color fallback helpers and improve updater logic
- Add fallbacks.ts with centralized fallback lists and helpers - Refactor updater to use fallback helpers for missing tokens - Enhance semantic and raw color population for theme injection - Improve theme manager to canonicalize and augment themes on install - Update resolver to add debug logs and fallback handling - Bump bunny-build-info version to 1.1.0-2
This commit is contained in:
parent
c01d0c98a4
commit
3842884cd2
6 changed files with 1465 additions and 225 deletions
|
|
@ -74,7 +74,7 @@ const config = {
|
|||
constModules: {
|
||||
globals: {
|
||||
"bunny-build-info": {
|
||||
version: `"1.1.0-1"`,
|
||||
version: `"1.1.0-2"`,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
220
src/lib/addons/themes/colors/fallbacks.ts
Normal file
220
src/lib/addons/themes/colors/fallbacks.ts
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
/**
|
||||
* Fallback lists and maps for the theme updater.
|
||||
*
|
||||
* These are the centralized, exported constants and small helpers used to
|
||||
* decide which semantic/raw keys to ensure exist at runtime so components
|
||||
* that read legacy or nonstandard token names receive sensible values.
|
||||
*
|
||||
* The goal is to keep all "magic" name lists in one place to make maintenance
|
||||
* easier and to support the idea of pulling a single file into the updater.
|
||||
*
|
||||
* This file intentionally contains only data and tiny pure helpers (regexes).
|
||||
* Any logic that requires runtime tables (e.g. reading tokenRef.RawColor)
|
||||
* should remain in the updater and consume these lists.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
|
||||
export const ensureRawKeys: string[] = [
|
||||
// Autocomplete / quickswitcher legacy/raw keys
|
||||
"AUTOCOMPLETE_BACKGROUND",
|
||||
"AUTOCOMPLETE_BG",
|
||||
"AUTOCOMPLETE_TEXT",
|
||||
"DEPRECATED_QUICKSWITCHER_INPUT_BACKGROUND",
|
||||
"QUICKSWITCHER_INPUT_BACKGROUND",
|
||||
"deprecated-quickswitcher-input-background",
|
||||
"autocomplete-bg",
|
||||
// Emoji picker / popover keys (various clients use different names)
|
||||
"EMOJI_PICKER_BACKGROUND",
|
||||
"EMOJI_POPOVER_BACKGROUND",
|
||||
"EMOJI_POPUP_BACKGROUND",
|
||||
"EMOJI_PANEL_BG",
|
||||
"EMOJI_PICKER_PANEL_BG",
|
||||
"EMOJI_PICKER_FOOTER_BG",
|
||||
"EMOJI_PICKER_FOOTER_BACKGROUND",
|
||||
];
|
||||
|
||||
export const ensureSemanticKeys: string[] = [
|
||||
"AUTOCOMPLETE_BACKGROUND",
|
||||
"AUTOCOMPLETE_TEXT",
|
||||
"EMOJI_PICKER_BACKGROUND",
|
||||
"EMOJI_POPOVER_BACKGROUND",
|
||||
"EMOJI_PANEL_BG",
|
||||
"EMOJI_PICKER_PANEL_BG",
|
||||
];
|
||||
|
||||
/**
|
||||
* Mirror groups - these group related aliases together so the updater can
|
||||
* pick the best available candidate among them and mirror the value across
|
||||
* all aliases (both raw and semantic tables).
|
||||
*/
|
||||
export const mirrorGroups: Record<string, string[]> = {
|
||||
// Autocomplete aliases (cover many legacy and client variants)
|
||||
AUTOCOMPLETE: [
|
||||
"AUTOCOMPLETE_BACKGROUND",
|
||||
"AUTOCOMPLETE_BG",
|
||||
"autocomplete-bg",
|
||||
"AUTOCOMPLETE-BG",
|
||||
"QUICKSWITCHER_INPUT_BACKGROUND",
|
||||
"DEPRECATED_QUICKSWITCHER_INPUT_BACKGROUND",
|
||||
"deprecated-quickswitcher-input-background",
|
||||
"QUICKSWITCHER_INPUT_BG",
|
||||
"DEPRECATED_QUICKSWITCHER_INPUT_BG",
|
||||
"deprecated-quickswitcher-input-bg",
|
||||
"DEPRECATED_QUICKSWITCHER_INPUT_BACKGROUND",
|
||||
],
|
||||
// Emoji / expression picker aliases, include footer variants explicitly
|
||||
EMOJI_PICKER: [
|
||||
"EMOJI_PICKER_BACKGROUND",
|
||||
"EMOJI_POPOVER_BACKGROUND",
|
||||
"EMOJI_POPUP_BACKGROUND",
|
||||
"EMOJI_PANEL_BG",
|
||||
"EMOJI_PICKER_PANEL_BG",
|
||||
"EMOJI_PICKER_FOOTER_BG",
|
||||
"EMOJI_PICKER_FOOTER_BACKGROUND",
|
||||
"EXPRESSION_PICKER_BG",
|
||||
"EXPRESSION_PICKER_BACKGROUND",
|
||||
"expression-picker-bg",
|
||||
],
|
||||
// Emoji text / label color aliases
|
||||
EMOJI_TEXT: [
|
||||
"EMOJI_TEXT",
|
||||
"EMOJI_PICKER_TEXT",
|
||||
"EMOJI_POPOVER_TEXT",
|
||||
"EMOJI_TOOLTIP_TEXT",
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* A small explicit list of additional semantics that often are missing from themes
|
||||
* and frequently cause visible light fallbacks (embeds, command/autocomplete bars, replies).
|
||||
*/
|
||||
export const extraSemanticNames: string[] = [
|
||||
"TEXT_EMBED_TITLE",
|
||||
"TEXT_EMBED_AUTHOR",
|
||||
"TEXT_EMBED_CONTENT",
|
||||
"TEXT_EMBED_FOOTER",
|
||||
"TEXT_REPLY",
|
||||
"TEXT_REPLY_BYLINE",
|
||||
"INPUT_PRIMARY",
|
||||
"INPUT_SECONDARY",
|
||||
"AUTOCOMPLETE_BACKGROUND",
|
||||
"AUTOCOMPLETE_TEXT",
|
||||
];
|
||||
|
||||
/**
|
||||
* Common semantic fallback mapping for names that have different canonical forms
|
||||
* across various theme manifests or engine versions.
|
||||
*
|
||||
* When a semantic key is missing, the updater will try the mapped key as a fallback.
|
||||
*/
|
||||
export const SEMANTIC_FALLBACK_MAP: Record<string, string> = {
|
||||
BG_BACKDROP: "BACKGROUND_FLOATING",
|
||||
BG_BASE_PRIMARY: "BACKGROUND_PRIMARY",
|
||||
BG_BASE_SECONDARY: "BACKGROUND_SECONDARY",
|
||||
BG_BASE_TERTIARY: "BACKGROUND_SECONDARY_ALT",
|
||||
BG_MOD_FAINT: "BACKGROUND_MODIFIER_ACCENT",
|
||||
BG_MOD_STRONG: "BACKGROUND_MODIFIER_ACCENT",
|
||||
BG_MOD_SUBTLE: "BACKGROUND_MODIFIER_ACCENT",
|
||||
BG_SURFACE_OVERLAY: "BACKGROUND_FLOATING",
|
||||
BG_SURFACE_OVERLAY_TMP: "BACKGROUND_FLOATING",
|
||||
BG_SURFACE_RAISED: "BACKGROUND_MOBILE_PRIMARY",
|
||||
};
|
||||
|
||||
/**
|
||||
* Candidate lists used by the updater's specialized cases (autocomplete/text).
|
||||
* These lists are intentionally conservative and ordered by preference.
|
||||
*/
|
||||
export const AUTOCOMPLETE_FALLBACK_CANDIDATES: string[] = [
|
||||
// avatar/bot bar variants (mirrors "bot avatar bar" style)
|
||||
"AVATAR_BAR_BG",
|
||||
"BOT_AVATAR_BG",
|
||||
"AVATAR_BAR_BACKGROUND",
|
||||
// you-bar / personal bar variants
|
||||
"YOU_BAR_BG",
|
||||
"YOU_BAR_BACKGROUND",
|
||||
"YOU_BAR",
|
||||
// quickswitcher / autocomplete legacy variants
|
||||
"QUICKSWITCHER_INPUT_BACKGROUND",
|
||||
"DEPRECATED_QUICKSWITCHER_INPUT_BACKGROUND",
|
||||
"AUTOCOMPLETE_BG",
|
||||
// chat/input fallbacks
|
||||
"CHAT_INPUT_BACKGROUND",
|
||||
"CHANNELTEXTAREA_BACKGROUND",
|
||||
// general surface/background fallbacks
|
||||
"BACKGROUND_FLOATING",
|
||||
"BACKGROUND_PRIMARY",
|
||||
"CARD_PRIMARY_BG",
|
||||
];
|
||||
|
||||
export const TEXT_CANDIDATES: string[] = [
|
||||
"TEXT_NORMAL",
|
||||
"TEXT_PRIMARY",
|
||||
"TEXT_SECONDARY",
|
||||
"TEXT_PRIMARY_ON_BACKGROUND",
|
||||
"TEXT_MUTED",
|
||||
];
|
||||
|
||||
/**
|
||||
* Delays (in ms) that the updater will use when scheduling reapply attempts.
|
||||
* Exported so tests / orchestration can inspect or reuse these values.
|
||||
*/
|
||||
export const REAPPLY_DELAYS: number[] = [300, 700, 1500];
|
||||
|
||||
/**
|
||||
* Regex used to detect footer / popup / panel-like aliases where a lighter surface
|
||||
* fallback (e.g. BACKGROUND_FLOATING) is preferred instead of a pure raw candidate.
|
||||
*/
|
||||
export const FOOTER_KEY_REGEX = /FOOTER|footer|PANEL|panel|POPUP/i;
|
||||
|
||||
/**
|
||||
* Utility: choose a "softened" fallback color when no explicit mapping exists.
|
||||
* NOTE: This helper intentionally does not access runtime token tables - it only
|
||||
* performs very small, deterministic transformations. The updater should call
|
||||
* this with an already-determined candidate color string.
|
||||
*
|
||||
* Keep this simple to avoid importing heavy color libs here; the updater owns any
|
||||
* chroma/contrast logic that depends on runtime state.
|
||||
*/
|
||||
export function softenHardFallback(fallbackHex: string, preferHex = true): string {
|
||||
// If caller provides something falsy, return a safe default.
|
||||
if (!fallbackHex || typeof fallbackHex !== "string") return "#000000";
|
||||
|
||||
// Basic normalization: ensure starts with '#', otherwise return as-is.
|
||||
if (!fallbackHex.startsWith("#")) return fallbackHex;
|
||||
|
||||
// Very small, naive "soften" algorithm: when hex appears very dark (#000...),
|
||||
// nudge it to a slightly lighter shade. This is intentionally minimal to avoid
|
||||
// reintroducing heavy dependencies here.
|
||||
const normalized = fallbackHex.slice(1);
|
||||
if (normalized.length !== 3 && normalized.length !== 6) return fallbackHex;
|
||||
|
||||
// Expand short hex (#abc -> #aabbcc)
|
||||
const expanded =
|
||||
normalized.length === 3
|
||||
? normalized.split("").map((c) => c + c).join("")
|
||||
: normalized;
|
||||
|
||||
try {
|
||||
const r = parseInt(expanded.slice(0, 2), 16);
|
||||
const g = parseInt(expanded.slice(2, 4), 16);
|
||||
const b = parseInt(expanded.slice(4, 6), 16);
|
||||
|
||||
// If extremely dark, lighten by a fixed delta; otherwise return original.
|
||||
const lum = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
|
||||
if (lum < 0.05) {
|
||||
const delta = 30; // lighten by ~12%
|
||||
const nr = Math.min(255, r + delta);
|
||||
const ng = Math.min(255, g + delta);
|
||||
const nb = Math.min(255, b + delta);
|
||||
const hex = `#${nr.toString(16).padStart(2, "0")}${ng
|
||||
.toString(16)
|
||||
.padStart(2, "0")}${nb.toString(16).padStart(2, "0")}`;
|
||||
return preferHex ? hex : hex;
|
||||
}
|
||||
|
||||
return `#${expanded}`;
|
||||
} catch {
|
||||
return fallbackHex;
|
||||
}
|
||||
}
|
||||
|
|
@ -10,80 +10,164 @@ const tokenReference = findByProps("SemanticColor");
|
|||
const isThemeModule = createLazyModule(byMutableProp("isThemeDark"));
|
||||
|
||||
const SEMANTIC_FALLBACK_MAP: Record<string, string> = {
|
||||
"BG_BACKDROP": "BACKGROUND_FLOATING",
|
||||
"BG_BASE_PRIMARY": "BACKGROUND_PRIMARY",
|
||||
"BG_BASE_SECONDARY": "BACKGROUND_SECONDARY",
|
||||
"BG_BASE_TERTIARY": "BACKGROUND_SECONDARY_ALT",
|
||||
"BG_MOD_FAINT": "BACKGROUND_MODIFIER_ACCENT",
|
||||
"BG_MOD_STRONG": "BACKGROUND_MODIFIER_ACCENT",
|
||||
"BG_MOD_SUBTLE": "BACKGROUND_MODIFIER_ACCENT",
|
||||
"BG_SURFACE_OVERLAY": "BACKGROUND_FLOATING",
|
||||
"BG_SURFACE_OVERLAY_TMP": "BACKGROUND_FLOATING",
|
||||
"BG_SURFACE_RAISED": "BACKGROUND_MOBILE_PRIMARY"
|
||||
BG_BACKDROP: "BACKGROUND_FLOATING",
|
||||
BG_BASE_PRIMARY: "BACKGROUND_PRIMARY",
|
||||
BG_BASE_SECONDARY: "BACKGROUND_SECONDARY",
|
||||
BG_BASE_TERTIARY: "BACKGROUND_SECONDARY_ALT",
|
||||
BG_MOD_FAINT: "BACKGROUND_MODIFIER_ACCENT",
|
||||
BG_MOD_STRONG: "BACKGROUND_MODIFIER_ACCENT",
|
||||
BG_MOD_SUBTLE: "BACKGROUND_MODIFIER_ACCENT",
|
||||
BG_SURFACE_OVERLAY: "BACKGROUND_FLOATING",
|
||||
BG_SURFACE_OVERLAY_TMP: "BACKGROUND_FLOATING",
|
||||
BG_SURFACE_RAISED: "BACKGROUND_MOBILE_PRIMARY",
|
||||
};
|
||||
|
||||
export default function patchDefinitionAndResolver() {
|
||||
const callback = ([theme]: any[]) => theme === _colorRef.key ? [_colorRef.current!.reference] : void 0;
|
||||
const callback = ([theme]: any[]) =>
|
||||
theme === _colorRef.key ? [_colorRef.current!.reference] : void 0;
|
||||
|
||||
Object.keys(tokenReference.RawColor).forEach(key => {
|
||||
Object.defineProperty(tokenReference.RawColor, key, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => {
|
||||
const ret = _colorRef.current?.raw[key];
|
||||
return ret || _colorRef.origRaw[key];
|
||||
}
|
||||
});
|
||||
Object.keys(tokenReference.RawColor).forEach((key) => {
|
||||
Object.defineProperty(tokenReference.RawColor, key, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => {
|
||||
const ret = _colorRef.current?.raw[key];
|
||||
return ret || _colorRef.origRaw[key];
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const unpatches = [
|
||||
before("isThemeDark", isThemeModule, callback),
|
||||
before("isThemeLight", isThemeModule, callback),
|
||||
before("updateTheme", NativeThemeModule, callback),
|
||||
instead("resolveSemanticColor", tokenReference.default.meta ?? tokenReference.default.internal, (args: any[], orig: any) => {
|
||||
if (!_colorRef.current) return orig(...args);
|
||||
if (args[0] !== _colorRef.key) return orig(...args);
|
||||
|
||||
args[0] = _colorRef.current.reference;
|
||||
|
||||
const [name, colorDef] = extractInfo(_colorRef.current!.reference, args[1]);
|
||||
|
||||
let semanticDef = _colorRef.current.semantic[name];
|
||||
if (!semanticDef && _colorRef.current.spec === 2 && name in SEMANTIC_FALLBACK_MAP) {
|
||||
semanticDef = _colorRef.current.semantic[SEMANTIC_FALLBACK_MAP[name]];
|
||||
}
|
||||
|
||||
if (semanticDef?.value) {
|
||||
if (semanticDef.opacity === 1) return semanticDef.value;
|
||||
return chroma(semanticDef.value).alpha(semanticDef.opacity).hex();
|
||||
}
|
||||
|
||||
const rawValue = _colorRef.current.raw[colorDef.raw];
|
||||
if (rawValue) {
|
||||
// Set opacity if needed
|
||||
return colorDef.opacity === 1 ? rawValue : chroma(rawValue).alpha(colorDef.opacity).hex();
|
||||
}
|
||||
|
||||
// Fallback to default
|
||||
return orig(...args);
|
||||
}),
|
||||
() => {
|
||||
// Not the actual module but.. yeah.
|
||||
Object.defineProperty(tokenReference, "RawColor", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: _colorRef.origRaw
|
||||
});
|
||||
const unpatches = [
|
||||
before("isThemeDark", isThemeModule, callback),
|
||||
before("isThemeLight", isThemeModule, callback),
|
||||
before("updateTheme", NativeThemeModule, callback),
|
||||
// Prefer the active bn-theme for all resolveSemanticColor calls.
|
||||
// Add debug logs to help trace resolution.
|
||||
instead(
|
||||
"resolveSemanticColor",
|
||||
tokenReference.default.meta ?? tokenReference.default.internal,
|
||||
(args: any[], orig: any) => {
|
||||
// If we don't have an active bunny theme, fall back to original resolver
|
||||
if (!_colorRef.current) {
|
||||
try {
|
||||
console.debug(
|
||||
"[themesystem] no active _colorRef, delegating to original resolver",
|
||||
);
|
||||
} catch {}
|
||||
return orig(...args);
|
||||
}
|
||||
];
|
||||
|
||||
return () => unpatches.forEach(p => p());
|
||||
// Capture original theme arg for debugging / fallback usage
|
||||
const originalThemeArg = args[0];
|
||||
|
||||
// Force the resolver to use the active bn-theme key so all semantic requests
|
||||
// are resolved against our active theme.
|
||||
args[0] = _colorRef.key;
|
||||
|
||||
try {
|
||||
console.debug(
|
||||
`[themesystem] resolveSemanticColor called. originalTheme=${String(originalThemeArg)}, forcedTheme=${_colorRef.key}`,
|
||||
);
|
||||
} catch {}
|
||||
|
||||
// Use the internal reference (darker/light) to extract color defs
|
||||
const themeReference = _colorRef.current!.reference;
|
||||
const [name, colorDef] = extractInfo(themeReference, args[1]);
|
||||
|
||||
// Try to resolve semantic from the active theme, with fallback mapping for spec 2
|
||||
let semanticDef = _colorRef.current.semantic[name];
|
||||
if (
|
||||
!semanticDef &&
|
||||
_colorRef.current.spec === 2 &&
|
||||
name in SEMANTIC_FALLBACK_MAP
|
||||
) {
|
||||
semanticDef = _colorRef.current.semantic[SEMANTIC_FALLBACK_MAP[name]];
|
||||
}
|
||||
|
||||
if (semanticDef?.value) {
|
||||
try {
|
||||
const out =
|
||||
semanticDef.opacity === 1
|
||||
? semanticDef.value
|
||||
: chroma(semanticDef.value).alpha(semanticDef.opacity).hex();
|
||||
try {
|
||||
console.debug(
|
||||
`[themesystem] resolved semantic ${name} => ${out}`,
|
||||
);
|
||||
} catch {}
|
||||
return out;
|
||||
} catch (e) {
|
||||
// fallthrough to try raw or original
|
||||
try {
|
||||
console.warn(
|
||||
"[themesystem] semantic resolution chroma failed",
|
||||
e,
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
// Try raw value from theme
|
||||
const rawValue = _colorRef.current.raw[colorDef.raw];
|
||||
if (rawValue) {
|
||||
try {
|
||||
const out =
|
||||
colorDef.opacity === 1
|
||||
? rawValue
|
||||
: chroma(rawValue).alpha(colorDef.opacity).hex();
|
||||
try {
|
||||
console.debug(
|
||||
`[themesystem] resolved raw ${colorDef.raw} => ${out}`,
|
||||
);
|
||||
} catch {}
|
||||
return out;
|
||||
} catch (e) {
|
||||
try {
|
||||
console.warn("[themesystem] raw resolution chroma failed", e);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
// If all fails, delegate back to original resolver.
|
||||
// Attempt to call original resolver with the original theme argument to preserve expected behavior.
|
||||
try {
|
||||
try {
|
||||
console.debug("[themesystem] falling back to original resolver");
|
||||
} catch {}
|
||||
return orig(originalThemeArg, args[1]);
|
||||
} catch (origErr) {
|
||||
// As a last resort, call with the mutated args
|
||||
try {
|
||||
console.warn(
|
||||
"[themesystem] original resolver call with original arg failed, calling orig with mutated args",
|
||||
origErr,
|
||||
);
|
||||
} catch {}
|
||||
return orig(...args);
|
||||
}
|
||||
},
|
||||
),
|
||||
() => {
|
||||
// Not the actual module but.. yeah.
|
||||
Object.defineProperty(tokenReference, "RawColor", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: _colorRef.origRaw,
|
||||
});
|
||||
},
|
||||
];
|
||||
|
||||
return () => unpatches.forEach((p) => p());
|
||||
}
|
||||
|
||||
function extractInfo(themeName: string, colorObj: any): [name: string, colorDef: any] {
|
||||
// @ts-ignore - assigning to extractInfo._sym
|
||||
const propName = colorObj[extractInfo._sym ??= Object.getOwnPropertySymbols(colorObj)[0]];
|
||||
const colorDef = tokenReference.SemanticColor[propName];
|
||||
function extractInfo(
|
||||
themeName: string,
|
||||
colorObj: any,
|
||||
): [name: string, colorDef: any] {
|
||||
// @ts-ignore - assigning to extractInfo._sym
|
||||
const propName =
|
||||
colorObj[(extractInfo._sym ??= Object.getOwnPropertySymbols(colorObj)[0])];
|
||||
const colorDef = tokenReference.SemanticColor[propName];
|
||||
|
||||
return [propName, colorDef[themeName]];
|
||||
return [propName, colorDef[themeName]];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,19 @@ import { findByProps, findByPropsLazy, findByStoreNameLazy } from "@metro";
|
|||
|
||||
import { parseColorManifest } from "./parser";
|
||||
import { ColorManifest, InternalColorDefinition } from "./types";
|
||||
import chroma from "chroma-js";
|
||||
import {
|
||||
ensureRawKeys as FB_ensureRawKeys,
|
||||
ensureSemanticKeys as FB_ensureSemanticKeys,
|
||||
mirrorGroups as FB_mirrorGroups,
|
||||
extraSemanticNames as FB_extraSemanticNames,
|
||||
SEMANTIC_FALLBACK_MAP as FB_SEMANTIC_FALLBACK_MAP,
|
||||
AUTOCOMPLETE_FALLBACK_CANDIDATES as FB_AUTOCOMPLETE_FALLBACK_CANDIDATES,
|
||||
TEXT_CANDIDATES as FB_TEXT_CANDIDATES,
|
||||
REAPPLY_DELAYS as FB_REAPPLY_DELAYS,
|
||||
FOOTER_KEY_REGEX,
|
||||
softenHardFallback,
|
||||
} from "./fallbacks";
|
||||
|
||||
const tokenRef = findByProps("SemanticColor");
|
||||
const origRawColor = { ...tokenRef.RawColor };
|
||||
|
|
@ -13,47 +26,593 @@ const FormDivider = findByPropsLazy("DIVIDER_COLORS");
|
|||
let _inc = 1;
|
||||
|
||||
interface InternalColorRef {
|
||||
key: `bn-theme-${string}`;
|
||||
current: InternalColorDefinition | null;
|
||||
readonly origRaw: Record<string, string>;
|
||||
lastSetDiscordTheme: string;
|
||||
key: `bn-theme-${string}`;
|
||||
current: InternalColorDefinition | null;
|
||||
readonly origRaw: Record<string, string>;
|
||||
lastSetDiscordTheme: string;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export const _colorRef: InternalColorRef = {
|
||||
current: null,
|
||||
key: `bn-theme-${_inc}`,
|
||||
origRaw: origRawColor,
|
||||
lastSetDiscordTheme: "darker"
|
||||
current: null,
|
||||
key: `bn-theme-${_inc}`,
|
||||
origRaw: origRawColor,
|
||||
lastSetDiscordTheme: "darker",
|
||||
};
|
||||
|
||||
export function updateBunnyColor(colorManifest: ColorManifest | null, { update = true }) {
|
||||
if (settings.safeMode?.enabled) return;
|
||||
export function updateBunnyColor(
|
||||
colorManifest: ColorManifest | null,
|
||||
{ update = true },
|
||||
) {
|
||||
if (settings.safeMode?.enabled) return;
|
||||
|
||||
const internalDef = colorManifest ? parseColorManifest(colorManifest) : null;
|
||||
const ref = Object.assign(_colorRef, {
|
||||
current: internalDef,
|
||||
key: `bn-theme-${++_inc}`,
|
||||
lastSetDiscordTheme: !ThemeStore.theme.startsWith("bn-theme-")
|
||||
? ThemeStore.theme
|
||||
: _colorRef.lastSetDiscordTheme
|
||||
});
|
||||
const internalDef = colorManifest ? parseColorManifest(colorManifest) : null;
|
||||
const ref = Object.assign(_colorRef, {
|
||||
current: internalDef,
|
||||
key: `bn-theme-${++_inc}`,
|
||||
lastSetDiscordTheme: !ThemeStore.theme.startsWith("bn-theme-")
|
||||
? ThemeStore.theme
|
||||
: _colorRef.lastSetDiscordTheme,
|
||||
});
|
||||
|
||||
if (internalDef != null) {
|
||||
tokenRef.Theme[ref.key.toUpperCase()] = ref.key;
|
||||
FormDivider.DIVIDER_COLORS[ref.key] = FormDivider.DIVIDER_COLORS[ref.current!.reference];
|
||||
if (internalDef != null) {
|
||||
// Establish maps and canonical fallback helper at the top of the `internalDef` block so
|
||||
// pickRawFallback / fallback logic is available consistently for all subsequent code.
|
||||
const rawMap = internalDef.raw ?? {};
|
||||
const engineRaw =
|
||||
_colorRef.origRaw && typeof _colorRef.origRaw === "object"
|
||||
? _colorRef.origRaw
|
||||
: {};
|
||||
|
||||
Object.keys(tokenRef.Shadow).forEach(k => tokenRef.Shadow[k][ref.key] = tokenRef.Shadow[k][ref.current!.reference]);
|
||||
Object.keys(tokenRef.SemanticColor).forEach(k => {
|
||||
tokenRef.SemanticColor[k][ref.key] = {
|
||||
...tokenRef.SemanticColor[k][ref.current!.reference]
|
||||
// Centralized helper used everywhere in this block to pick a sensible raw fallback.
|
||||
// This prefers explicit theme raw entries, then engine/raw tables, then a surface-like key,
|
||||
// and finally a softened fallback using chroma when possible.
|
||||
const pickRawFallback = (semKey: string): string => {
|
||||
if (rawMap[semKey]) return rawMap[semKey];
|
||||
const alt = semKey.replace(/^BG_|^BACKGROUND_/, "");
|
||||
if (rawMap[alt]) return rawMap[alt];
|
||||
if (rawMap[alt.toLowerCase()]) return rawMap[alt.toLowerCase()];
|
||||
if (engineRaw[semKey]) return engineRaw[semKey];
|
||||
const bg = Object.keys(rawMap).find((k) =>
|
||||
/BG|BACKGROUND|SURFACE/i.test(k),
|
||||
);
|
||||
if (bg) return rawMap[bg];
|
||||
const fallback =
|
||||
Object.values(rawMap)[0] ||
|
||||
Object.values(engineRaw)[0] ||
|
||||
tokenRef.RawColor["BACKGROUND_PRIMARY"] ||
|
||||
tokenRef.RawColor["BACKGROUND_FLOATING"] ||
|
||||
"#000000";
|
||||
try {
|
||||
const c = chroma(String(fallback));
|
||||
const lum = c.luminance();
|
||||
if (lum > 0.6) return c.darken(1.4).hex();
|
||||
return c.hex();
|
||||
} catch {
|
||||
return String(fallback);
|
||||
}
|
||||
};
|
||||
|
||||
tokenRef.Theme[ref.key.toUpperCase()] = ref.key;
|
||||
FormDivider.DIVIDER_COLORS[ref.key] =
|
||||
FormDivider.DIVIDER_COLORS[ref.current!.reference];
|
||||
|
||||
// Write raw colors directly into the runtime RawColor table for the active theme.
|
||||
// This ensures lookups that access RawColor receive our theme's values.
|
||||
try {
|
||||
const rawMap = internalDef.raw ?? {};
|
||||
Object.keys(rawMap).forEach((rawKey) => {
|
||||
try {
|
||||
// Prefer theme-provided raw color; fall back to original raw color if missing.
|
||||
const val =
|
||||
rawMap[rawKey] ??
|
||||
_colorRef.origRaw[rawKey] ??
|
||||
tokenRef.RawColor[rawKey];
|
||||
if (typeof val === "string") {
|
||||
tokenRef.RawColor[rawKey] = val;
|
||||
}
|
||||
} catch {
|
||||
// best-effort: ignore individual key failures
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure important quick-switcher / autocomplete and emoji picker raw/semantic keys exist in runtime tables.
|
||||
// Some components read RawColor directly and some resolve SemanticColor entries — ensure both are filled.
|
||||
try {
|
||||
const ensureRawKeys = FB_ensureRawKeys;
|
||||
|
||||
// A set of semantic keys we also want to ensure exist for our theme key
|
||||
const ensureSemanticKeys = FB_ensureSemanticKeys;
|
||||
|
||||
// Candidate lookup helper tries theme raw, original engine raw, tokenRef.RawColor, then a general fallback.
|
||||
const candidateFor = (k: string) => {
|
||||
return (
|
||||
rawMap[k] ??
|
||||
_colorRef.origRaw[k] ??
|
||||
tokenRef.RawColor[k] ??
|
||||
Object.values(rawMap)[0] ??
|
||||
Object.values(_colorRef.origRaw || {})[0] ??
|
||||
tokenRef.RawColor["BACKGROUND_PRIMARY"] ??
|
||||
"#000000"
|
||||
);
|
||||
};
|
||||
|
||||
// pickRawFallback is defined at the start of the internalDef block (to ensure a single, canonical implementation).
|
||||
// See the internalDef block for the canonical implementation that prefers engine/raw values and uses `chroma`.
|
||||
|
||||
// Write raw keys so modules that read RawColor directly get a sensible value.
|
||||
for (const k of ensureRawKeys) {
|
||||
try {
|
||||
const cand = candidateFor(k);
|
||||
if (typeof cand === "string") tokenRef.RawColor[k] = cand;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Also populate SemanticColor runtime table entries for our injected theme key for important aliases.
|
||||
for (const sem of ensureSemanticKeys) {
|
||||
try {
|
||||
// If the semantic entry object doesn't exist at all, create a minimal structure so consumers don't crash.
|
||||
if (!tokenRef.SemanticColor[sem]) {
|
||||
tokenRef.SemanticColor[sem] = {};
|
||||
}
|
||||
|
||||
// If there's already a value for our key, skip.
|
||||
if (tokenRef.SemanticColor[sem][ref.key]?.value) continue;
|
||||
|
||||
// Prefer an existing semantic value for the engine reference (darker/light) if available.
|
||||
const baseFromEngine =
|
||||
tokenRef.SemanticColor[sem]?.[ref.current!.reference];
|
||||
|
||||
// fallback to a candidate raw color
|
||||
const cand = candidateFor(sem) ?? pickRawFallback(sem);
|
||||
|
||||
tokenRef.SemanticColor[sem][ref.key] = {
|
||||
...(baseFromEngine ?? {}),
|
||||
value: typeof cand === "string" ? cand : String(cand),
|
||||
opacity: 1,
|
||||
};
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Aggressively also mirror some common emoji/quickbar aliases so components reading different names get consistent values.
|
||||
try {
|
||||
const mirrorGroups: Record<string, string[]> = FB_mirrorGroups;
|
||||
|
||||
for (const groupName of Object.keys(mirrorGroups)) {
|
||||
const keys = mirrorGroups[groupName];
|
||||
// find the best candidate value (prefer semantic for current reference, then raw)
|
||||
let best: string | undefined;
|
||||
for (const k of keys) {
|
||||
try {
|
||||
const sem =
|
||||
tokenRef.SemanticColor[k]?.[ref.current!.reference]?.value;
|
||||
if (sem) {
|
||||
best = sem;
|
||||
break;
|
||||
}
|
||||
if (rawMap[k]) {
|
||||
best = rawMap[k];
|
||||
break;
|
||||
}
|
||||
if (tokenRef.RawColor[k]) {
|
||||
best = tokenRef.RawColor[k];
|
||||
break;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
if (!best)
|
||||
best =
|
||||
Object.values(rawMap)[0] ??
|
||||
Object.values(_colorRef.origRaw || {})[0];
|
||||
|
||||
if (!best) continue;
|
||||
|
||||
// Write best into RawColor and SemanticColor for each alias.
|
||||
// For footer-specific aliases (emoji picker footer), prefer a lighter surface fallback
|
||||
// (BACKGROUND_FLOATING or BACKGROUND_PRIMARY) when available to avoid an overly dark footer.
|
||||
for (const k of keys) {
|
||||
try {
|
||||
// Determine the final value to write for this alias.
|
||||
let chosen = best;
|
||||
try {
|
||||
if (
|
||||
/FOOTER|footer|PANEL|panel|POPUP/i.test(k) &&
|
||||
(tokenRef.RawColor["BACKGROUND_FLOATING"] ||
|
||||
tokenRef.RawColor["BACKGROUND_PRIMARY"])
|
||||
) {
|
||||
chosen =
|
||||
tokenRef.RawColor["BACKGROUND_FLOATING"] ||
|
||||
tokenRef.RawColor["BACKGROUND_PRIMARY"] ||
|
||||
best;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Raw
|
||||
tokenRef.RawColor[k] = chosen;
|
||||
} catch {}
|
||||
try {
|
||||
if (!tokenRef.SemanticColor[k]) tokenRef.SemanticColor[k] = {};
|
||||
// preserve engine base when available, but override value/opactiy for our key
|
||||
tokenRef.SemanticColor[k][ref.key] = {
|
||||
...(tokenRef.SemanticColor[k]?.[ref.current!.reference] ??
|
||||
{}),
|
||||
value: tokenRef.SemanticColor[k]?.[ref.current!.reference]
|
||||
?.value
|
||||
? tokenRef.SemanticColor[k][ref.current!.reference].value
|
||||
: (tokenRef.SemanticColor[k]?.[ref.key]?.value ?? best),
|
||||
// if default engine opacity exists use it, else default to 1
|
||||
opacity:
|
||||
tokenRef.SemanticColor[k]?.[ref.current!.reference]
|
||||
?.opacity ??
|
||||
tokenRef.SemanticColor[k]?.[ref.key]?.opacity ??
|
||||
1,
|
||||
};
|
||||
|
||||
// If semantic value for our injected key was just set to engine fallback or missing,
|
||||
// ensure it uses the chosen raw color so resolveSemanticColor reads a consistent value.
|
||||
try {
|
||||
tokenRef.SemanticColor[k][ref.key].value =
|
||||
tokenRef.SemanticColor[k][ref.key].value || best;
|
||||
} catch {}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch {}
|
||||
} catch {
|
||||
// ignore failures while writing raw colors
|
||||
}
|
||||
|
||||
if (update) {
|
||||
AppearanceManager.setShouldSyncAppearanceSettings(false);
|
||||
AppearanceManager.updateTheme(internalDef != null ? ref.key : ref.lastSetDiscordTheme);
|
||||
// Populate semantic tokens for our injected theme key so resolveSemanticColor
|
||||
// and direct token table reads will get the correct values for each semantic token.
|
||||
try {
|
||||
const themeSemantic = internalDef.semantic ?? {};
|
||||
const rawMap = internalDef.raw ?? {};
|
||||
const engineRaw =
|
||||
_colorRef.origRaw && typeof _colorRef.origRaw === "object"
|
||||
? _colorRef.origRaw
|
||||
: {};
|
||||
|
||||
// Debug: announce start of semantic population for this theme key so we can trace issues
|
||||
try {
|
||||
// Use console.debug where available; fallback to console.log for environments that don't surface debug
|
||||
if (typeof console !== "undefined" && console.debug) {
|
||||
console.debug(
|
||||
`[themes_v2] populating semantics for key=${String(ref.key)} reference=${String(
|
||||
ref.current?.reference,
|
||||
)} semanticCount=${Object.keys(themeSemantic).length} rawCount=${
|
||||
Object.keys(rawMap).length
|
||||
}`,
|
||||
);
|
||||
} else if (typeof console !== "undefined" && console.log) {
|
||||
console.log(
|
||||
`[themes_v2] populating semantics for key=${String(ref.key)} reference=${String(
|
||||
ref.current?.reference,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// A small explicit list of additional semantics that often are missing from themes
|
||||
// and frequently cause visible light fallbacks (embeds, command/autocomplete bars, replies).
|
||||
const extraSemanticNames = FB_extraSemanticNames;
|
||||
|
||||
// helper to set one semantic key into runtime token table
|
||||
const setSemanticKey = (semKey: string, value: string, opacity = 1) => {
|
||||
try {
|
||||
const base =
|
||||
tokenRef.SemanticColor[semKey]?.[ref.current!.reference] ?? {};
|
||||
tokenRef.SemanticColor[semKey][ref.key] = {
|
||||
...base,
|
||||
value,
|
||||
opacity,
|
||||
};
|
||||
} catch {
|
||||
// best-effort per-key
|
||||
}
|
||||
};
|
||||
|
||||
// populate all runtime semantic keys based on canonical token list
|
||||
const allSemanticKeys = Object.keys(tokenRef.SemanticColor || {});
|
||||
const canonicalKeys = new Set([
|
||||
...allSemanticKeys,
|
||||
...extraSemanticNames,
|
||||
]);
|
||||
|
||||
// pickRawFallback previously lived here but was moved earlier so it is available
|
||||
// before any callers. Keep a minimal comment so line offsets remain clear.
|
||||
// (Implementation moved above to avoid 'Cannot find name pickRawFallback' at use sites.)
|
||||
|
||||
// fill semantics
|
||||
for (const semKey of Array.from(canonicalKeys)) {
|
||||
try {
|
||||
// 1) exact semantic from theme (value/opactiy)
|
||||
let semEntry = themeSemantic[semKey];
|
||||
if (semEntry && semEntry.value) {
|
||||
setSemanticKey(semKey, semEntry.value, semEntry.opacity ?? 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Special-case: ensure autocomplete uses the same background as common UI bars
|
||||
// (for example the You-bar / bot/avatar bar) and its text uses a readable text token
|
||||
// when the theme doesn't provide them. Try multiple likely candidates (semantic then raw)
|
||||
// so modules that expect different keys receive a sensible fallback instead of white.
|
||||
if (!semEntry || !semEntry.value) {
|
||||
if (semKey === "AUTOCOMPLETE_BACKGROUND") {
|
||||
try {
|
||||
const fallbackCandidates = FB_AUTOCOMPLETE_FALLBACK_CANDIDATES;
|
||||
|
||||
let chosen: string | undefined;
|
||||
for (const cand of fallbackCandidates) {
|
||||
// 1) semantic table value for the current reference
|
||||
const semVal =
|
||||
tokenRef.SemanticColor[cand]?.[ref.current!.reference]
|
||||
?.value;
|
||||
if (semVal) {
|
||||
chosen = semVal;
|
||||
break;
|
||||
}
|
||||
|
||||
// 2) theme-provided raw map
|
||||
if (rawMap[cand]) {
|
||||
chosen = rawMap[cand];
|
||||
break;
|
||||
}
|
||||
|
||||
// 3) runtime RawColor table
|
||||
if (tokenRef.RawColor[cand]) {
|
||||
chosen = tokenRef.RawColor[cand];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!chosen) chosen = pickRawFallback(semKey);
|
||||
// Ensure we always provide a string to the semantic table; fall back to a safe color.
|
||||
setSemanticKey(
|
||||
semKey,
|
||||
String(chosen ?? pickRawFallback(semKey) ?? "#000000"),
|
||||
1,
|
||||
);
|
||||
} catch {
|
||||
// fallback to generic raw candidate if anything goes wrong
|
||||
const candidate = pickRawFallback(semKey);
|
||||
setSemanticKey(semKey, candidate, 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (semKey === "AUTOCOMPLETE_TEXT") {
|
||||
try {
|
||||
const textCandidates = FB_TEXT_CANDIDATES;
|
||||
|
||||
let chosenText: string | undefined;
|
||||
for (const cand of textCandidates) {
|
||||
const semVal =
|
||||
tokenRef.SemanticColor[cand]?.[ref.current!.reference]
|
||||
?.value;
|
||||
if (semVal) {
|
||||
chosenText = semVal;
|
||||
break;
|
||||
}
|
||||
if (rawMap[cand]) {
|
||||
chosenText = rawMap[cand];
|
||||
break;
|
||||
}
|
||||
if (tokenRef.RawColor[cand]) {
|
||||
chosenText = tokenRef.RawColor[cand];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!chosenText) chosenText = pickRawFallback("TEXT_NORMAL");
|
||||
const textOpacity = themeSemantic["TEXT_NORMAL"]?.opacity ?? 1;
|
||||
// Coerce to string and ensure a safe fallback if undefined.
|
||||
setSemanticKey(
|
||||
semKey,
|
||||
String(
|
||||
chosenText ?? pickRawFallback("TEXT_NORMAL") ?? "#000000",
|
||||
),
|
||||
textOpacity,
|
||||
);
|
||||
} catch {
|
||||
const candidate = pickRawFallback(semKey);
|
||||
setSemanticKey(semKey, candidate, 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) try some reasonable alternate names (strip common prefixes)
|
||||
const alternates = [
|
||||
semKey,
|
||||
semKey.replace(/^BACKGROUND_/, ""),
|
||||
semKey.replace(/^BG_/, ""),
|
||||
semKey.replace(/^TEXT_/, ""),
|
||||
semKey.replace(/_COLOR$/, ""),
|
||||
];
|
||||
let foundAlt = false;
|
||||
for (const alt of alternates) {
|
||||
if (themeSemantic[alt] && themeSemantic[alt].value) {
|
||||
const altEntry = themeSemantic[alt];
|
||||
setSemanticKey(semKey, altEntry.value, altEntry.opacity ?? 1);
|
||||
foundAlt = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (foundAlt) continue;
|
||||
|
||||
// 3) try fallback mapping (common name differences)
|
||||
const SEMANTIC_FALLBACK_MAP: Record<string, string> =
|
||||
FB_SEMANTIC_FALLBACK_MAP;
|
||||
if (semKey in SEMANTIC_FALLBACK_MAP) {
|
||||
const alt = SEMANTIC_FALLBACK_MAP[semKey];
|
||||
const altEntry = themeSemantic[alt];
|
||||
if (altEntry && altEntry.value) {
|
||||
setSemanticKey(semKey, altEntry.value, altEntry.opacity ?? 1);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 4) derive fallback from raw colors and bias for dark-friendly appearance
|
||||
let candidate = pickRawFallback(semKey);
|
||||
try {
|
||||
const c = chroma(candidate);
|
||||
const lum = c.luminance();
|
||||
if (lum > 0.6) candidate = c.darken(1.4).hex();
|
||||
else candidate = c.hex();
|
||||
} catch {
|
||||
// keep candidate as-is
|
||||
}
|
||||
setSemanticKey(semKey, candidate, 1);
|
||||
} catch (e) {
|
||||
// per-key should not break everything
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("themes_v2: failed to populate semantic", semKey, e);
|
||||
}
|
||||
}
|
||||
|
||||
// After initial population, schedule a reapply sequence to catch modules that initialize slightly later.
|
||||
// Use multiple attempts with backoff and debug logs so we can observe late-initializing modules.
|
||||
try {
|
||||
const reapplyDelays = FB_REAPPLY_DELAYS;
|
||||
let attempt = 0;
|
||||
|
||||
const reapply = () => {
|
||||
attempt++;
|
||||
try {
|
||||
if (typeof console !== "undefined" && console.debug) {
|
||||
console.debug(
|
||||
`[themes_v2] semantic reapply attempt ${attempt}/${reapplyDelays.length} for key=${String(
|
||||
ref.key,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
// Reapply raw values (some modules read RawColor directly after init)
|
||||
const rawMap = internalDef.raw ?? {};
|
||||
Object.keys(rawMap).forEach((rawKey) => {
|
||||
try {
|
||||
const val =
|
||||
rawMap[rawKey] ??
|
||||
_colorRef.origRaw[rawKey] ??
|
||||
tokenRef.RawColor[rawKey];
|
||||
if (typeof val === "string") tokenRef.RawColor[rawKey] = val;
|
||||
} catch {}
|
||||
});
|
||||
|
||||
// Reapply semantic table values for our key (idempotent)
|
||||
let appliedCount = 0;
|
||||
for (const semKey of Array.from(canonicalKeys)) {
|
||||
try {
|
||||
// If our key already has a value, keep it; otherwise re-run pickRawFallback
|
||||
const existing = tokenRef.SemanticColor[semKey]?.[ref.key];
|
||||
if (!existing || !existing.value) {
|
||||
let candidate = pickRawFallback(semKey);
|
||||
try {
|
||||
const c = chroma(candidate);
|
||||
const lum = c.luminance();
|
||||
if (lum > 0.6) candidate = c.darken(1.4).hex();
|
||||
else candidate = c.hex();
|
||||
} catch {}
|
||||
tokenRef.SemanticColor[semKey][ref.key] = {
|
||||
...(tokenRef.SemanticColor[semKey]?.[
|
||||
ref.current!.reference
|
||||
] ?? {}),
|
||||
value: candidate,
|
||||
opacity: 1,
|
||||
};
|
||||
appliedCount++;
|
||||
}
|
||||
} catch (e) {
|
||||
// per-key should not break everything; log debug if available
|
||||
try {
|
||||
if (typeof console !== "undefined" && console.debug) {
|
||||
console.debug(
|
||||
`[themes_v2] failed per-key reapply for ${semKey} on attempt ${attempt}`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof console !== "undefined" && console.debug) {
|
||||
console.debug(
|
||||
`[themes_v2] reapply attempt ${attempt} complete for key=${String(
|
||||
ref.key,
|
||||
)} applied=${appliedCount}`,
|
||||
);
|
||||
}
|
||||
} catch {}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("themes_v2: reapply semantics failed", e);
|
||||
}
|
||||
|
||||
// Schedule next attempt (if any)
|
||||
if (attempt < reapplyDelays.length) {
|
||||
try {
|
||||
setTimeout(reapply, reapplyDelays[attempt]);
|
||||
} catch {}
|
||||
}
|
||||
};
|
||||
|
||||
// Start first reapply after initial delay
|
||||
try {
|
||||
setTimeout(reapply, reapplyDelays[0]);
|
||||
} catch {
|
||||
// ignore setTimeout failures on weird runtimes
|
||||
}
|
||||
} catch {
|
||||
// ignore unexpected failures in reapply orchestration
|
||||
}
|
||||
} catch (e) {
|
||||
// overall failure shouldn't break
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("themes_v2: semantic population failed", e);
|
||||
}
|
||||
|
||||
// Also ensure Shadow tokens for our key are populated from the reference so shadow lookups don't fallback unexpectedly
|
||||
try {
|
||||
Object.keys(tokenRef.Shadow).forEach(
|
||||
(k) =>
|
||||
(tokenRef.Shadow[k][ref.key] =
|
||||
tokenRef.Shadow[k][ref.current!.reference]),
|
||||
);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (update) {
|
||||
AppearanceManager.setShouldSyncAppearanceSettings(false);
|
||||
AppearanceManager.updateTheme(
|
||||
internalDef != null ? ref.key : ref.lastSetDiscordTheme,
|
||||
);
|
||||
|
||||
// Force additional reapply(s) shortly after token population completes so modules that
|
||||
// read theme values slightly later will pick up our injected theme key.
|
||||
// These are best-effort and wrapped in try/catch to avoid crashing in odd runtimes.
|
||||
try {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
AppearanceManager.updateTheme(
|
||||
internalDef != null ? ref.key : ref.lastSetDiscordTheme,
|
||||
);
|
||||
} catch {}
|
||||
}, 350);
|
||||
|
||||
// Second reapply with longer delay for very late initializers
|
||||
setTimeout(() => {
|
||||
try {
|
||||
AppearanceManager.updateTheme(
|
||||
internalDef != null ? ref.key : ref.lastSetDiscordTheme,
|
||||
);
|
||||
} catch {}
|
||||
}, 1200);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,160 +1,537 @@
|
|||
import { awaitStorage, createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@core/vendetta/storage";
|
||||
import {
|
||||
awaitStorage,
|
||||
createFileBackend,
|
||||
createMMKVBackend,
|
||||
createStorage,
|
||||
wrapSync,
|
||||
} from "@core/vendetta/storage";
|
||||
import { writeFile } from "@lib/api/native/fs";
|
||||
import { getStoredTheme, getThemeFilePath, isPyonLoader, isThemeSupported } from "@lib/api/native/loader";
|
||||
import {
|
||||
getStoredTheme,
|
||||
getThemeFilePath,
|
||||
isPyonLoader,
|
||||
isThemeSupported,
|
||||
} from "@lib/api/native/loader";
|
||||
import { awaitStorage as newAwaitStorage } from "@lib/api/storage";
|
||||
import { safeFetch } from "@lib/utils";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
import initColors from "./colors";
|
||||
import { applyAndroidAlphaKeys, normalizeToHex } from "./colors/parser";
|
||||
import { colorsPref } from "./colors/preferences";
|
||||
import { VendettaThemeManifest } from "./colors/types";
|
||||
import { updateBunnyColor } from "./colors/updater";
|
||||
import { applyAndroidAlphaKeys, normalizeToHex } from "./colors/parser";
|
||||
import {
|
||||
VendettaThemeManifest,
|
||||
BunnyColorManifest,
|
||||
InternalColorDefinition,
|
||||
ColorManifest,
|
||||
} from "./colors/types";
|
||||
|
||||
export interface VdThemeInfo {
|
||||
id: string;
|
||||
selected: boolean;
|
||||
data: VendettaThemeManifest;
|
||||
}
|
||||
|
||||
export const themes = wrapSync(createStorage<Record<string, VdThemeInfo>>(createMMKVBackend("VENDETTA_THEMES")));
|
||||
import { findByProps } from "@metro";
|
||||
import chroma from "chroma-js";
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Theme v2 - canonical theme manager (full replacement)
|
||||
*
|
||||
* Goals:
|
||||
* - Provide a canonical internal representation for both spec v2 (Vendetta) and spec v3
|
||||
* - Always fill missing semantic tokens deterministically (augment) and persist augmented themes
|
||||
* - Replace the resolver/updater behavior so components always receive sensible tokens
|
||||
* - Keep public API backward-compatible with previous `@lib/addons/themes` exports
|
||||
*
|
||||
* This file implements the manager, parser/normalizer, augmentation and high-level API
|
||||
* as a single, self-contained replacement.
|
||||
*/
|
||||
export async function writeThemeToNative(theme: VdThemeInfo | {}) {
|
||||
if (typeof theme !== "object") throw new Error("Theme must be an object");
|
||||
|
||||
// Save the current theme as current-theme.json. When supported by loader,
|
||||
// this json will be written to appropriate path and be used to theme the native side.
|
||||
await createFileBackend(getThemeFilePath() || "theme.json").set(theme);
|
||||
/* Types */
|
||||
export interface VdThemeInfo {
|
||||
id: string;
|
||||
selected: boolean;
|
||||
// Store the original ColorManifest (spec 3 / vendetta-compatible manifest).
|
||||
// This ensures the rest of the codebase that expects a ColorManifest
|
||||
// (for example `initColors`) receives the correct shape.
|
||||
data: ColorManifest;
|
||||
}
|
||||
|
||||
// Process data for some compatiblity with native side
|
||||
function processData(data: VendettaThemeManifest) {
|
||||
if (data.semanticColors) {
|
||||
const { semanticColors } = data;
|
||||
/* Storage wrapper for installed themes */
|
||||
export const themes = wrapSync(
|
||||
createStorage<Record<string, VdThemeInfo>>(
|
||||
// Keep the original storage key so existing installations migrate in-place.
|
||||
createMMKVBackend("VENDETTA_THEMES"),
|
||||
),
|
||||
);
|
||||
|
||||
for (const key in semanticColors) {
|
||||
for (const index in semanticColors[key]) {
|
||||
semanticColors[key][index] &&= normalizeToHex(semanticColors[key][index] as string) || false;
|
||||
}
|
||||
/* Helpers */
|
||||
|
||||
/**
|
||||
* Normalize and canonicalize an incoming theme manifest (v2 or v3) into InternalColorDefinition.
|
||||
* This function also normalizes color strings to hex and applies android-specific keys when necessary.
|
||||
*/
|
||||
function canonicalizeManifest(
|
||||
manifest: ColorManifest,
|
||||
): InternalColorDefinition {
|
||||
// Helper to normalize color values
|
||||
function normColor(v: any) {
|
||||
if (typeof v !== "string") return undefined;
|
||||
return normalizeToHex(v);
|
||||
}
|
||||
|
||||
// For v3-like BunnyColorManifest
|
||||
if ((manifest as BunnyColorManifest).main) {
|
||||
const b = manifest as BunnyColorManifest;
|
||||
const main = b.main;
|
||||
|
||||
// Gather raw map
|
||||
const raw: Record<string, string> = {};
|
||||
for (const k of Object.keys(main.raw ?? {})) {
|
||||
const n = normColor(main.raw?.[k]);
|
||||
if (n) raw[k] = n;
|
||||
}
|
||||
|
||||
if (Platform.OS === "android") applyAndroidAlphaKeys(raw);
|
||||
|
||||
// Parse semantic
|
||||
const semantic: Record<string, { value: string; opacity: number }> = {};
|
||||
for (const k of Object.keys(main.semantic ?? {})) {
|
||||
const v = main.semantic![k];
|
||||
if (typeof v === "string") {
|
||||
semantic[k] = { value: normColor(v) ?? v, opacity: 1 };
|
||||
} else if (typeof v === "object" && v !== null) {
|
||||
if (v.type === "raw")
|
||||
semantic[k] = {
|
||||
value: normColor(v.value) ?? v.value,
|
||||
opacity: v.opacity ?? 1,
|
||||
};
|
||||
else
|
||||
semantic[k] = {
|
||||
value: normColor(tokenRaw(v.value)) ?? tokenRaw(v.value),
|
||||
opacity: v.opacity ?? 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
spec: 3,
|
||||
reference:
|
||||
(colorsPref.type ?? b.main.type) === "dark" ? "darker" : "light",
|
||||
semantic,
|
||||
raw,
|
||||
background: main.background,
|
||||
} as InternalColorDefinition;
|
||||
}
|
||||
|
||||
// For v2 VendettaThemeManifest
|
||||
const v = manifest as VendettaThemeManifest;
|
||||
const raw: Record<string, string> = {};
|
||||
for (const k of Object.keys(v.rawColors ?? {})) {
|
||||
const n = normColor(v.rawColors![k]);
|
||||
if (n) raw[k] = n;
|
||||
}
|
||||
|
||||
if (Platform.OS === "android") applyAndroidAlphaKeys(raw);
|
||||
|
||||
const semantic: Record<string, { value: string; opacity: number }> = {};
|
||||
if (v.semanticColors) {
|
||||
// Vendetta gives arrays of color choices [dark, light]
|
||||
const type = (colorsPref.type ?? "dark") === "dark" ? 0 : 1;
|
||||
for (const k of Object.keys(v.semanticColors)) {
|
||||
const arr = v.semanticColors[k] as (string | false)[];
|
||||
const chosen = arr[type] || arr[0] || undefined;
|
||||
if (chosen)
|
||||
semantic[k] = {
|
||||
value: normalizeToHex(chosen) ?? (chosen as any),
|
||||
opacity: 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
spec: 2,
|
||||
reference: (colorsPref.type ?? "dark") === "dark" ? "darker" : "light",
|
||||
semantic,
|
||||
raw,
|
||||
background: v.background
|
||||
? {
|
||||
url: v.background.url,
|
||||
opacity: v.background.alpha ?? 1,
|
||||
blur: v.background.blur,
|
||||
}
|
||||
}
|
||||
|
||||
if (data.rawColors) {
|
||||
const { rawColors } = data;
|
||||
|
||||
for (const key in rawColors) {
|
||||
const normalized = normalizeToHex(rawColors[key]);
|
||||
if (normalized) data.rawColors[key] = normalized;
|
||||
}
|
||||
|
||||
if (Platform.OS === "android") applyAndroidAlphaKeys(rawColors);
|
||||
}
|
||||
|
||||
// this field is required by the spec but vd seems to ignore this
|
||||
// so are most vd themes
|
||||
data.spec ??= 2;
|
||||
|
||||
return data;
|
||||
: undefined,
|
||||
} as InternalColorDefinition;
|
||||
}
|
||||
|
||||
function validateTheme(themeJSON: any): boolean {
|
||||
if (typeof themeJSON !== "object" || themeJSON === null) return false;
|
||||
if (themeJSON.spec !== 2 && themeJSON.spec !== 3) return false;
|
||||
if (themeJSON.spec === 3 && !themeJSON.main) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function fetchTheme(url: string, selected = false) {
|
||||
let themeJSON: any;
|
||||
|
||||
try {
|
||||
themeJSON = await (await safeFetch(url, { cache: "no-store" })).json();
|
||||
} catch {
|
||||
throw new Error(`Failed to fetch theme at ${url}`);
|
||||
}
|
||||
|
||||
// Validate theme
|
||||
if (!validateTheme(themeJSON)) throw new Error(`Invalid theme at ${url}`);
|
||||
|
||||
themes[url] = {
|
||||
id: url,
|
||||
selected: selected,
|
||||
data: processData(themeJSON),
|
||||
};
|
||||
|
||||
if (selected) {
|
||||
writeThemeToNative(themes[url]);
|
||||
updateBunnyColor(themes[url].data, { update: true });
|
||||
}
|
||||
}
|
||||
|
||||
export async function installTheme(url: string) {
|
||||
if (typeof url !== "string" || url in themes) throw new Error("Theme already installed");
|
||||
await fetchTheme(url);
|
||||
}
|
||||
|
||||
export function selectTheme(theme: VdThemeInfo | null, write = true) {
|
||||
if (theme) theme.selected = true;
|
||||
Object.keys(themes).forEach(
|
||||
k => themes[k].selected = themes[k].id === theme?.id
|
||||
);
|
||||
|
||||
if (theme == null && write) {
|
||||
updateBunnyColor(null, { update: true });
|
||||
return writeThemeToNative({});
|
||||
} else if (theme) {
|
||||
updateBunnyColor(theme.data, { update: true });
|
||||
return writeThemeToNative(theme);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeTheme(id: string) {
|
||||
const theme = themes[id];
|
||||
if (theme.selected) await selectTheme(null);
|
||||
delete themes[id];
|
||||
|
||||
return theme.selected;
|
||||
}
|
||||
|
||||
export async function updateThemes() {
|
||||
await awaitStorage(themes);
|
||||
const currentTheme = getThemeFromLoader();
|
||||
await Promise.allSettled(Object.keys(themes).map(id => fetchTheme(id, currentTheme?.id === id)));
|
||||
}
|
||||
|
||||
export function getCurrentTheme() {
|
||||
return Object.values(themes).find(t => t.selected) ?? null;
|
||||
/* tokenRaw: helper to get runtime token raw color if available */
|
||||
function tokenRaw(key: string): string | undefined {
|
||||
try {
|
||||
const tokenRef: any = findByProps("SemanticColor");
|
||||
return tokenRef?.RawColor?.[key];
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Augment a canonical theme's semantic map by filling any missing semantic entries.
|
||||
* Augmentation is deterministic: for each missing semantic token we select a raw fallback
|
||||
* and bias it towards dark mode if necessary.
|
||||
*
|
||||
* This function mutates the provided `data` object and returns it.
|
||||
*/
|
||||
function augmentMissingSemantics(data: InternalColorDefinition) {
|
||||
data.semantic = data.semantic ?? {};
|
||||
const rawMap = { ...(data.raw ?? {}) };
|
||||
|
||||
// merge any native RawColor reference as fallback candidates
|
||||
try {
|
||||
const tokenRef: any = findByProps("SemanticColor") ?? {};
|
||||
const engineRaw = tokenRef?.RawColor ?? {};
|
||||
for (const k of Object.keys(engineRaw)) {
|
||||
if (!(k in rawMap)) rawMap[k] = engineRaw[k];
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
const tokenRefAny: any = findByProps("SemanticColor") ?? {};
|
||||
const canonicalSemanticNames: string[] = tokenRefAny?.SemanticColor
|
||||
? Object.keys(tokenRefAny.SemanticColor)
|
||||
: Object.keys(data.semantic).slice(0, 50);
|
||||
|
||||
// Utility to choose fallback raw color for a semantic name
|
||||
const chooseFallbackRaw = (semanticName: string) => {
|
||||
// direct mapping
|
||||
if (rawMap[semanticName]) return rawMap[semanticName];
|
||||
|
||||
// try simplified forms
|
||||
const simplified = semanticName.replace(/^BG_|^BACKGROUND_/, "");
|
||||
if (rawMap[simplified]) return rawMap[simplified];
|
||||
const lower = simplified.toLowerCase();
|
||||
if (rawMap[lower]) return rawMap[lower];
|
||||
if (rawMap[simplified.toUpperCase()])
|
||||
return rawMap[simplified.toUpperCase()];
|
||||
|
||||
// pick a background-like key
|
||||
const bgKey = Object.keys(rawMap).find((k) =>
|
||||
/BG|BACKGROUND|SURFACE/i.test(k),
|
||||
);
|
||||
if (bgKey) return rawMap[bgKey];
|
||||
|
||||
// fallback to any raw color
|
||||
const any = Object.values(rawMap)[0];
|
||||
if (any) return any;
|
||||
|
||||
// last resort hardcoded
|
||||
return "#0f0f0f";
|
||||
};
|
||||
|
||||
// Chromatic adjustment function for safety in dark mode (ensure not too light)
|
||||
const adjustForDark = (hex: string) => {
|
||||
try {
|
||||
const c = chroma(hex);
|
||||
const lum = c.luminance();
|
||||
if (lum > 0.6) return c.darken(1.4).hex();
|
||||
return c.hex();
|
||||
} catch {
|
||||
return hex;
|
||||
}
|
||||
};
|
||||
|
||||
for (const sem of canonicalSemanticNames) {
|
||||
const existing = data.semantic?.[sem];
|
||||
if (existing && existing.value) continue; // don't overwrite existing semantics
|
||||
|
||||
const rawCandidate = chooseFallbackRaw(sem);
|
||||
const final = adjustForDark(rawCandidate);
|
||||
data.semantic[sem] = { value: final, opacity: 1 };
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist an internal theme object to storage (overwrites existing stored manifest).
|
||||
* This function converts InternalColorDefinition back to a safe, serializable manifest.
|
||||
*/
|
||||
async function persistAugmentedTheme(
|
||||
id: string,
|
||||
data: InternalColorDefinition,
|
||||
) {
|
||||
// We store the canonical internal structure as-is under the same id so callers read consistent data
|
||||
themes[id] = { id, selected: themes[id]?.selected ?? false, data };
|
||||
await awaitStorage(themes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the current theme to native loader path (best-effort).
|
||||
*
|
||||
* NOTE: This function now writes the canonical ColorManifest (spec-3-like)
|
||||
* structure so native side receives the manifest format expected by initColors.
|
||||
*/
|
||||
export async function writeThemeToNative(theme: ColorManifest | {}) {
|
||||
if (typeof theme !== "object") throw new Error("Theme must be an object");
|
||||
await createFileBackend(getThemeFilePath() || "theme.json").set(theme);
|
||||
}
|
||||
|
||||
/* Public API functions (compatible names and behavior) */
|
||||
|
||||
/**
|
||||
* Fetch a theme manifest from a URL and install it (canonicalize + persist).
|
||||
*/
|
||||
export async function fetchTheme(url: string, selected = false) {
|
||||
let themeJSON: any;
|
||||
try {
|
||||
themeJSON = await (await safeFetch(url, { cache: "no-store" })).json();
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to fetch theme at ${url}`);
|
||||
}
|
||||
|
||||
// Accept vendetta spec v2 or bunny spec v3; validate loosely
|
||||
if (
|
||||
!themeJSON ||
|
||||
typeof themeJSON !== "object" ||
|
||||
(themeJSON.spec !== 2 && themeJSON.spec !== 3)
|
||||
) {
|
||||
throw new Error(`Invalid theme at ${url}`);
|
||||
}
|
||||
|
||||
// Canonicalize and augment semantics (always run augmentation per your request)
|
||||
const canonical = canonicalizeManifest(themeJSON as ColorManifest);
|
||||
augmentMissingSemantics(canonical);
|
||||
|
||||
// Persist augmented canonical theme into storage
|
||||
themes[url] = { id: url, selected, data: canonical };
|
||||
await persistAugmentedTheme(url, canonical);
|
||||
|
||||
// If selected, apply immediately
|
||||
if (selected) {
|
||||
updateBunnyColor(canonical, { update: true });
|
||||
await writeThemeToNative(canonical);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install theme (safe guard)
|
||||
*/
|
||||
export async function installTheme(url: string) {
|
||||
if (typeof url !== "string" || url in themes)
|
||||
throw new Error("Theme already installed");
|
||||
await fetchTheme(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a theme to be active.
|
||||
* This will ensure augmentation exists, persist it, update updater and write native theme.
|
||||
*/
|
||||
export function selectTheme(theme: VdThemeInfo | null, write = true) {
|
||||
if (theme) theme.selected = true;
|
||||
Object.keys(themes).forEach(
|
||||
(k) => (themes[k].selected = themes[k].id === theme?.id),
|
||||
);
|
||||
|
||||
if (theme == null) {
|
||||
// Clear active theme
|
||||
updateBunnyColor(null, { update: true });
|
||||
if (write) return writeThemeToNative({});
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure canonical augmentation exists for this theme and persist
|
||||
const info = themes[theme.id];
|
||||
try {
|
||||
const data = info.data;
|
||||
augmentMissingSemantics(data); // idempotent
|
||||
// persist augmented data
|
||||
void persistAugmentedTheme(theme.id, data);
|
||||
// Apply to runtime
|
||||
updateBunnyColor(data, { update: true });
|
||||
if (write) return writeThemeToNative(data);
|
||||
} catch (e) {
|
||||
// On any error, fallback to applying raw theme data if available
|
||||
try {
|
||||
const fallback = theme.data;
|
||||
updateBunnyColor(fallback, { update: true });
|
||||
if (write) return writeThemeToNative(fallback);
|
||||
} catch {
|
||||
// swallow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a theme (and clear selection if it was active).
|
||||
*/
|
||||
export async function removeTheme(id: string) {
|
||||
const theme = themes[id];
|
||||
if (theme?.selected) await selectTheme(null);
|
||||
delete themes[id];
|
||||
await awaitStorage(themes);
|
||||
return theme?.selected ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update (re-fetch) all installed themes in-place, re-canonicalizing and re-augmenting.
|
||||
*/
|
||||
export async function updateThemes() {
|
||||
await awaitStorage(themes);
|
||||
const current = getThemeFromLoader();
|
||||
await Promise.allSettled(
|
||||
Object.keys(themes).map(async (id) => {
|
||||
try {
|
||||
// re-fetch original location (if it's a URL) to get fresh manifest; otherwise skip
|
||||
if (id.startsWith("http")) {
|
||||
await fetchTheme(id, current?.id === id);
|
||||
} else {
|
||||
// re-augment stored data in-place
|
||||
const entry = themes[id];
|
||||
if (entry && entry.data) {
|
||||
augmentMissingSemantics(entry.data);
|
||||
await persistAugmentedTheme(id, entry.data);
|
||||
if (current?.id === id) {
|
||||
updateBunnyColor(entry.data, { update: true });
|
||||
await writeThemeToNative(entry.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// log and continue
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("themes_v2: update failed for", id, e);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns currently selected theme or null
|
||||
*/
|
||||
export function getCurrentTheme() {
|
||||
return Object.values(themes).find((t) => t.selected) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read theme directly from loader (native-side stored theme)
|
||||
*/
|
||||
export function getThemeFromLoader(): VdThemeInfo | null {
|
||||
return getStoredTheme();
|
||||
const stored = getStoredTheme();
|
||||
if (!stored) return null;
|
||||
// stored may be a raw manifest - attempt to canonicalize
|
||||
try {
|
||||
const canonical = canonicalizeManifest(stored as ColorManifest);
|
||||
return { id: "loader-theme", selected: false, data: canonical };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Initialize themes at startup: canonicalize existing, init color patches and write native.
|
||||
*/
|
||||
export async function initThemes() {
|
||||
if (!isThemeSupported()) return;
|
||||
if (!isThemeSupported()) return;
|
||||
|
||||
try {
|
||||
if (isPyonLoader()) {
|
||||
writeFile("../vendetta_theme.json", "null");
|
||||
}
|
||||
|
||||
await newAwaitStorage(colorsPref);
|
||||
|
||||
const currentTheme = getThemeFromLoader();
|
||||
initColors(currentTheme?.data ?? null);
|
||||
|
||||
updateThemes().catch(e => console.error("Failed to update themes", e));
|
||||
} catch (e) {
|
||||
console.error("Failed to initialize themes", e);
|
||||
// Try to perform the minimal, fast startup steps first so the app isn't blocked.
|
||||
try {
|
||||
if (isPyonLoader()) {
|
||||
try {
|
||||
writeFile("../vendetta_theme.json", "null");
|
||||
} catch (e) {
|
||||
// non-fatal
|
||||
console.error("themes_v2: pyon write failed", e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("themes_v2: early init error", e);
|
||||
}
|
||||
|
||||
// Ensure colors preference backend is available (fast)
|
||||
try {
|
||||
await newAwaitStorage(colorsPref);
|
||||
} catch (e) {
|
||||
console.error("themes_v2: failed to await colorsPref storage", e);
|
||||
}
|
||||
|
||||
// Immediately apply whatever theme the user last selected (if any).
|
||||
// This makes the app show the correct theme on first load without waiting for background migration.
|
||||
try {
|
||||
const curEntry = getCurrentTheme();
|
||||
if (curEntry && curEntry.data) {
|
||||
try {
|
||||
// Ensure missing semantics are present (idempotent)
|
||||
augmentMissingSemantics(curEntry.data as any);
|
||||
} catch (e) {
|
||||
console.warn("themes_v2: augment on startup failed", e);
|
||||
}
|
||||
|
||||
try {
|
||||
// Apply to runtime token tables (fast) so UI reflects the theme immediately.
|
||||
updateBunnyColor(curEntry.data as any, { update: true });
|
||||
} catch (e) {
|
||||
console.error("themes_v2: updateBunnyColor on startup failed", e);
|
||||
}
|
||||
|
||||
// Try to write native theme but do not block startup on its completion.
|
||||
writeThemeToNative(curEntry.data as any).catch((e) => {
|
||||
console.error("themes_v2: writeThemeToNative (startup) failed", e);
|
||||
});
|
||||
} else {
|
||||
// If no selected entry, try to initialize colors from loader theme quickly.
|
||||
try {
|
||||
const loader = getThemeFromLoader();
|
||||
// initColors expects a ColorManifest | null - pass loader data if available.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
initColors(loader?.data ?? null);
|
||||
} catch (e) {
|
||||
console.error("themes_v2: initColors failed on startup", e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("themes_v2: apply selected theme failed", e);
|
||||
}
|
||||
|
||||
// Perform heavier work (migrations, full updates) in the background so startup isn't blocked.
|
||||
(async () => {
|
||||
try {
|
||||
// Canonicalize and persist all installed themes safely in background.
|
||||
const ids = Object.keys(themes);
|
||||
for (const id of ids) {
|
||||
try {
|
||||
const entry = themes[id];
|
||||
if (!entry || !entry.data) continue;
|
||||
augmentMissingSemantics(entry.data as any);
|
||||
await persistAugmentedTheme(id, entry.data as any);
|
||||
} catch (e) {
|
||||
console.error("themes_v2: background canonicalize failed for", id, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-fetch remote manifests and re-apply if current changed or needs refresh.
|
||||
try {
|
||||
await updateThemes();
|
||||
} catch (e) {
|
||||
console.error("themes_v2: background updateThemes failed", e);
|
||||
}
|
||||
|
||||
// Ensure the currently selected theme is fully applied after migrations finish.
|
||||
try {
|
||||
const finalCur = getCurrentTheme();
|
||||
if (finalCur && finalCur.data) {
|
||||
augmentMissingSemantics(finalCur.data as any);
|
||||
updateBunnyColor(finalCur.data as any, { update: true });
|
||||
try {
|
||||
await writeThemeToNative(finalCur.data as any);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"themes_v2: writeThemeToNative (background) failed",
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("themes_v2: final apply after background work failed", e);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("themes_v2: background migration task failed", e);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
|
|
|||
2
src/modules.d.ts
vendored
2
src/modules.d.ts
vendored
|
|
@ -1,5 +1,5 @@
|
|||
declare module "bunny-build-info" {
|
||||
const version: "1.1.0-1";
|
||||
const version: "1.1.0-2";
|
||||
}
|
||||
|
||||
declare module "*.png" {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue