fix: modern Discord themes compatibility, storage async resilience, and startup safety
This commit is contained in:
parent
1547421074
commit
ad7d405b7c
6 changed files with 199 additions and 123 deletions
|
|
@ -13,8 +13,9 @@ export function createProxy(target: any = {}): { proxy: any; emitter: Emitter; }
|
|||
|
||||
function createProxy(target: any, path: string[]): any {
|
||||
return new Proxy(target, {
|
||||
get(target, prop: string) {
|
||||
get(target, prop: string | symbol) {
|
||||
if ((prop as unknown) === emitterSymbol) return emitter;
|
||||
if (typeof prop === "symbol") return target[prop];
|
||||
|
||||
const newPath = [...path, prop];
|
||||
const value: any = target[prop];
|
||||
|
|
@ -82,25 +83,45 @@ export function createProxy(target: any = {}): { proxy: any; emitter: Emitter; }
|
|||
}
|
||||
|
||||
export function useProxy<T>(storage: T): T {
|
||||
const emitter = (storage as any)?.[emitterSymbol] as Emitter;
|
||||
if (!emitter) throw new Error("storage?.[emitterSymbol] is undefined");
|
||||
|
||||
const [, forceUpdate] = React.useReducer(n => ~n, 0);
|
||||
|
||||
React.useEffect(() => {
|
||||
const listener: EmitterListener = (event: EmitterEvent, data: EmitterListenerData) => {
|
||||
if (event === "DEL" && data.value === storage) return;
|
||||
forceUpdate();
|
||||
if (!storage) return;
|
||||
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
|
||||
const attach = (s: any) => {
|
||||
const emitter = s?.[emitterSymbol] as Emitter | undefined;
|
||||
if (!emitter) return;
|
||||
|
||||
const listener: EmitterListener = (event: EmitterEvent, data: EmitterListenerData) => {
|
||||
if (event === "DEL" && data.value === s) return;
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
emitter.on("SET", listener);
|
||||
emitter.on("DEL", listener);
|
||||
|
||||
unsubscribe = () => {
|
||||
emitter.off("SET", listener);
|
||||
emitter.off("DEL", listener);
|
||||
};
|
||||
};
|
||||
|
||||
emitter.on("SET", listener);
|
||||
emitter.on("DEL", listener);
|
||||
const currentEmitter = (storage as any)?.[emitterSymbol];
|
||||
if (currentEmitter) {
|
||||
attach(storage);
|
||||
} else if (typeof (storage as any)?.[syncAwaitSymbol] === "function") {
|
||||
(storage as any)[syncAwaitSymbol](() => {
|
||||
attach(storage);
|
||||
forceUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
emitter.off("SET", listener);
|
||||
emitter.off("DEL", listener);
|
||||
unsubscribe?.();
|
||||
};
|
||||
}, []);
|
||||
}, [storage]);
|
||||
|
||||
return storage;
|
||||
}
|
||||
|
|
@ -135,6 +156,7 @@ export function wrapSync<T extends Promise<any>>(store: T): Awaited<T> {
|
|||
),
|
||||
get(target, prop, recv) {
|
||||
if (prop === syncAwaitSymbol) return awaitInit;
|
||||
if (prop === emitterSymbol) return (awaited as any)?.[emitterSymbol];
|
||||
return Reflect.get(awaited ?? target, prop, recv);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
46
src/entry.ts
46
src/entry.ts
|
|
@ -10,21 +10,15 @@ async function initializeShiggyCord() {
|
|||
// Make 'freeze' and 'seal' do nothing
|
||||
Object.freeze = Object.seal = Object;
|
||||
|
||||
await require("@metro/internals/caches").initMetroCache();
|
||||
try {
|
||||
await require("@metro/internals/caches").initMetroCache();
|
||||
} catch (e) {
|
||||
console.warn("Failed to initialize metro cache, proceeding anyway:", e);
|
||||
}
|
||||
require(".").default();
|
||||
} catch (e) {
|
||||
const { ClientInfoManager } = require("@lib/api/native/modules");
|
||||
const stack = e instanceof Error ? e.stack : undefined;
|
||||
|
||||
console.log(stack ?? e?.toString?.() ?? e);
|
||||
alert(
|
||||
[
|
||||
"Failed to load ShiggyCord!\n",
|
||||
`Build Number: ${ClientInfoManager.getConstants().Build}`,
|
||||
`ShiggyCord: ${version}`,
|
||||
stack || e?.toString?.(),
|
||||
].join("\n"),
|
||||
);
|
||||
console.error("Failed to load ShiggyCord:", stack ?? e?.toString?.() ?? e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -109,13 +103,29 @@ if (typeof window.__r === "undefined") {
|
|||
}
|
||||
|
||||
const startDiscord = async () => {
|
||||
await initializeShiggyCord();
|
||||
try {
|
||||
await initializeShiggyCord();
|
||||
} catch (err) {
|
||||
console.error("Critical error during initializeShiggyCord:", err);
|
||||
} finally {
|
||||
for (const unpatch of unpatches) {
|
||||
try {
|
||||
unpatch();
|
||||
} catch {}
|
||||
}
|
||||
unpatches.length = 0;
|
||||
|
||||
for (const unpatch of unpatches) unpatch();
|
||||
unpatches.length = 0;
|
||||
|
||||
originalRequire(0);
|
||||
resumeDeferred();
|
||||
try {
|
||||
originalRequire(0);
|
||||
} catch (e) {
|
||||
console.error("originalRequire(0) error:", e);
|
||||
}
|
||||
try {
|
||||
resumeDeferred();
|
||||
} catch (e) {
|
||||
console.error("resumeDeferred error:", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
startDiscord();
|
||||
|
|
|
|||
|
|
@ -10,7 +10,20 @@ import { ColorManifest, InternalColorDefinition } from "./types";
|
|||
const tokenRef = findByProps("SemanticColor");
|
||||
|
||||
export function parseColorManifest(manifest: ColorManifest): InternalColorDefinition {
|
||||
const resolveType = (type = "dark") => (colorsPref.type ?? type) === "dark" ? "darker" : "light";
|
||||
const resolveType = (type = "dark") => {
|
||||
const pref = colorsPref.type ?? type;
|
||||
if (pref === "light") return "light";
|
||||
if (tokenRef?.SemanticColor) {
|
||||
const sample = Object.values(tokenRef.SemanticColor)[0] as Record<string, any> | undefined;
|
||||
if (sample) {
|
||||
if ("darker" in sample) return "darker";
|
||||
if ("midnight" in sample) return "midnight";
|
||||
if ("dark" in sample) return "dark";
|
||||
if ("onyx" in sample) return "onyx";
|
||||
}
|
||||
}
|
||||
return "dark";
|
||||
};
|
||||
|
||||
if (manifest.spec === 3) {
|
||||
const semanticColorDefinitions: InternalColorDefinition["semantic"] = {};
|
||||
|
|
|
|||
|
|
@ -2,14 +2,11 @@ import { _colorRef } from "@lib/addons/themes/colors/updater";
|
|||
import { NativeThemeModule } from "@lib/api/native/modules";
|
||||
import { before, instead } from "@lib/api/patcher";
|
||||
import { findByProps } from "@metro";
|
||||
import { byMutableProp } from "@metro/filters";
|
||||
import { createLazyModule } from "@metro/lazy";
|
||||
import chroma from "chroma-js";
|
||||
|
||||
const tokenReference = findByProps("SemanticColor");
|
||||
const themeTypes = findByProps("ThemeTypes")?.ThemeTypes;
|
||||
|
||||
const origRawColor = { ...tokenReference.RawColor };
|
||||
const origDarker = themeTypes.DARKER as string;
|
||||
const origLight = themeTypes.LIGHT as string;
|
||||
const isThemeModule = createLazyModule(byMutableProp("isThemeDark"));
|
||||
|
||||
const SEMANTIC_FALLBACK_MAP: Record<string, string> = {
|
||||
"BG_BACKDROP": "BACKGROUND_FLOATING",
|
||||
|
|
@ -24,41 +21,57 @@ const SEMANTIC_FALLBACK_MAP: Record<string, string> = {
|
|||
"BG_SURFACE_RAISED": "BACKGROUND_MOBILE_PRIMARY"
|
||||
};
|
||||
|
||||
const alphaCache = new Map<string, string>();
|
||||
let alphaCacheKey: string | null = null;
|
||||
|
||||
function cachedAlphaHex(hex: string, opacity: number): string {
|
||||
if (alphaCacheKey !== _colorRef.key) {
|
||||
alphaCache.clear();
|
||||
alphaCacheKey = _colorRef.key;
|
||||
}
|
||||
const cacheKey = `${hex}|${opacity}`;
|
||||
let result = alphaCache.get(cacheKey);
|
||||
if (result === undefined) {
|
||||
result = chroma(hex).alpha(opacity).hex();
|
||||
alphaCache.set(cacheKey, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export default function patchDefinitionAndResolver() {
|
||||
const callback = ([theme]: any[]) => theme === _colorRef.key ? [_colorRef.current!.reference] : void 0;
|
||||
const tokenReference = findByProps("SemanticColor");
|
||||
if (!tokenReference) return () => {};
|
||||
|
||||
Object.defineProperty(themeTypes, "DARKER", {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => _colorRef.current?.reference === "darker" ? _colorRef.key : origDarker,
|
||||
});
|
||||
Object.defineProperty(themeTypes, "LIGHT", {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => _colorRef.current?.reference === "light" ? _colorRef.key : origLight,
|
||||
});
|
||||
const origRawColor = { ...tokenReference.RawColor };
|
||||
|
||||
Object.keys(tokenReference.RawColor).forEach(key => {
|
||||
Object.defineProperty(tokenReference.RawColor, key, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => {
|
||||
const ret = _colorRef.current?.raw[key];
|
||||
if (ret) return ret;
|
||||
return origRawColor[key];
|
||||
}
|
||||
const callback = ([theme]: any[]) => theme === _colorRef.key ? [_colorRef.current?.reference ?? "dark"] : void 0;
|
||||
|
||||
if (tokenReference.RawColor) {
|
||||
Object.keys(tokenReference.RawColor).forEach(key => {
|
||||
Object.defineProperty(tokenReference.RawColor, key, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => {
|
||||
const ret = _colorRef.current?.raw[key];
|
||||
return ret || origRawColor[key];
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const resolverTarget = tokenReference.default?.meta ?? tokenReference.default?.internal ?? tokenReference.default;
|
||||
|
||||
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) => {
|
||||
resolverTarget ? instead("resolveSemanticColor", resolverTarget, (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]);
|
||||
const [name, colorDef] = extractInfo(_colorRef.current.reference, args[1], tokenReference);
|
||||
|
||||
let semanticDef = _colorRef.current.semantic[name];
|
||||
if (!semanticDef && _colorRef.current.spec === 2 && name in SEMANTIC_FALLBACK_MAP) {
|
||||
|
|
@ -66,42 +79,40 @@ export default function patchDefinitionAndResolver() {
|
|||
}
|
||||
|
||||
if (semanticDef?.value) {
|
||||
return semanticDef.opacity === 1
|
||||
? semanticDef.value
|
||||
: chroma(semanticDef.value).alpha(semanticDef.opacity).hex();
|
||||
if (semanticDef.opacity === 1) return semanticDef.value;
|
||||
return cachedAlphaHex(semanticDef.value, semanticDef.opacity);
|
||||
}
|
||||
|
||||
const rawValue = _colorRef.current.raw[colorDef.raw];
|
||||
if (rawValue) {
|
||||
// Set opacity if needed
|
||||
return colorDef.opacity === 1 ? rawValue : chroma(rawValue).alpha(colorDef.opacity).hex();
|
||||
if (colorDef?.raw) {
|
||||
const rawValue = _colorRef.current.raw[colorDef.raw];
|
||||
if (rawValue) {
|
||||
return colorDef.opacity === 1 ? rawValue : cachedAlphaHex(rawValue, colorDef.opacity);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default
|
||||
return orig(...args);
|
||||
}),
|
||||
}) : () => {},
|
||||
() => {
|
||||
Object.defineProperty(themeTypes, "DARKER", {
|
||||
configurable: true, writable: true, value: origDarker
|
||||
});
|
||||
Object.defineProperty(themeTypes, "LIGHT", {
|
||||
configurable: true, writable: true, value: origLight
|
||||
});
|
||||
Object.defineProperty(tokenReference, "RawColor", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: origRawColor
|
||||
});
|
||||
if (tokenReference.RawColor) {
|
||||
Object.defineProperty(tokenReference, "RawColor", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: origRawColor
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return () => unpatches.forEach(p => p());
|
||||
}
|
||||
|
||||
function extractInfo(themeName: string, colorObj: any): [name: string, colorDef: any] {
|
||||
function extractInfo(themeName: string, colorObj: any, tokenReference: any): [name: string, colorDef: any] {
|
||||
if (!colorObj || typeof colorObj !== "object") return ["", null];
|
||||
// @ts-ignore - assigning to extractInfo._sym
|
||||
const propName = colorObj[extractInfo._sym ??= Object.getOwnPropertySymbols(colorObj)[0]];
|
||||
const colorDef = tokenReference.SemanticColor[propName];
|
||||
const colorDef = tokenReference?.SemanticColor?.[propName];
|
||||
if (!colorDef) return [propName, null];
|
||||
|
||||
return [propName, colorDef[themeName]];
|
||||
const targetDef = colorDef[themeName] ?? colorDef.darker ?? colorDef.dark ?? colorDef.midnight ?? colorDef.onyx ?? Object.values(colorDef)[0];
|
||||
return [propName, targetDef];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import { findByProps, findByPropsLazy, findByStoreNameLazy } from "@metro";
|
|||
import { parseColorManifest } from "./parser";
|
||||
import { ColorManifest, InternalColorDefinition } from "./types";
|
||||
|
||||
const tokenRef = findByProps("SemanticColor");
|
||||
const origRawColor = { ...tokenRef.RawColor };
|
||||
const AppearanceManager = findByPropsLazy("updateTheme");
|
||||
const ThemeStore = findByStoreNameLazy("ThemeStore");
|
||||
const FormDivider = findByPropsLazy("DIVIDER_COLORS");
|
||||
|
|
@ -23,63 +21,85 @@ interface InternalColorRef {
|
|||
export const _colorRef: InternalColorRef = {
|
||||
current: null,
|
||||
key: `bn-theme-${_inc}`,
|
||||
origRaw: origRawColor,
|
||||
lastSetDiscordTheme: "darker"
|
||||
origRaw: {},
|
||||
lastSetDiscordTheme: "dark"
|
||||
};
|
||||
|
||||
export function updateBunnyColor(colorManifest: ColorManifest | null, { update = true }) {
|
||||
if (settings.safeMode?.enabled) return;
|
||||
|
||||
const tokenRef = findByProps("SemanticColor");
|
||||
if (tokenRef?.RawColor && Object.keys(_colorRef.origRaw).length === 0) {
|
||||
Object.assign(_colorRef.origRaw, tokenRef.RawColor);
|
||||
}
|
||||
|
||||
const internalDef = colorManifest ? parseColorManifest(colorManifest) : null;
|
||||
const currentThemeName = ThemeStore?.theme ?? "dark";
|
||||
const ref = Object.assign(_colorRef, {
|
||||
current: internalDef,
|
||||
key: `bn-theme-${++_inc}`,
|
||||
lastSetDiscordTheme: !ThemeStore.theme.startsWith("bn-theme-")
|
||||
? ThemeStore.theme
|
||||
key: `bn-theme-${++_inc}` as const,
|
||||
lastSetDiscordTheme: !currentThemeName.startsWith("bn-theme-")
|
||||
? currentThemeName
|
||||
: _colorRef.lastSetDiscordTheme
|
||||
});
|
||||
|
||||
if (internalDef != null) {
|
||||
// Register the synthetic key BEFORE calling native, and verify it actually
|
||||
// landed in every table native reads from. If any registration step fails,
|
||||
// bail out to a known-good theme instead of handing native an unknown key.
|
||||
if (internalDef != null && tokenRef) {
|
||||
try {
|
||||
tokenRef.Theme[ref.key.toUpperCase()] = ref.key;
|
||||
|
||||
if (!FormDivider.DIVIDER_COLORS[ref.current!.reference]) {
|
||||
throw new Error(`Missing divider color reference: ${ref.current!.reference}`);
|
||||
if (tokenRef.Theme) {
|
||||
tokenRef.Theme[ref.key.toUpperCase()] = ref.key;
|
||||
}
|
||||
FormDivider.DIVIDER_COLORS[ref.key] = FormDivider.DIVIDER_COLORS[ref.current!.reference];
|
||||
|
||||
Object.keys(tokenRef.Shadow).forEach(k => {
|
||||
if (!(ref.current!.reference in tokenRef.Shadow[k])) {
|
||||
throw new Error(`Missing shadow reference: ${ref.current!.reference}`);
|
||||
}
|
||||
tokenRef.Shadow[k][ref.key] = tokenRef.Shadow[k][ref.current!.reference];
|
||||
});
|
||||
const targetRef = ref.current?.reference ?? "dark";
|
||||
|
||||
Object.keys(tokenRef.SemanticColor).forEach(k => {
|
||||
if (!(ref.current!.reference in tokenRef.SemanticColor[k])) {
|
||||
throw new Error(`Missing semantic color reference: ${ref.current!.reference}`);
|
||||
}
|
||||
tokenRef.SemanticColor[k][ref.key] = {
|
||||
...tokenRef.SemanticColor[k][ref.current!.reference]
|
||||
};
|
||||
});
|
||||
if (FormDivider?.DIVIDER_COLORS) {
|
||||
const dividerFallback = FormDivider.DIVIDER_COLORS[targetRef] ??
|
||||
FormDivider.DIVIDER_COLORS.dark ??
|
||||
FormDivider.DIVIDER_COLORS.darker ??
|
||||
Object.values(FormDivider.DIVIDER_COLORS)[0];
|
||||
FormDivider.DIVIDER_COLORS[ref.key] = dividerFallback;
|
||||
}
|
||||
|
||||
if (tokenRef.Shadow) {
|
||||
Object.keys(tokenRef.Shadow).forEach(k => {
|
||||
const shadowGroup = tokenRef.Shadow[k];
|
||||
if (shadowGroup) {
|
||||
const shadowFallback = shadowGroup[targetRef] ??
|
||||
shadowGroup.dark ??
|
||||
shadowGroup.darker ??
|
||||
shadowGroup.midnight ??
|
||||
shadowGroup.onyx ??
|
||||
Object.values(shadowGroup)[0];
|
||||
shadowGroup[ref.key] = shadowFallback;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (tokenRef.SemanticColor) {
|
||||
Object.keys(tokenRef.SemanticColor).forEach(k => {
|
||||
const colorGroup = tokenRef.SemanticColor[k];
|
||||
if (colorGroup) {
|
||||
const colorFallback = colorGroup[targetRef] ??
|
||||
colorGroup.dark ??
|
||||
colorGroup.darker ??
|
||||
colorGroup.midnight ??
|
||||
colorGroup.onyx ??
|
||||
Object.values(colorGroup)[0];
|
||||
colorGroup[ref.key] = { ...colorFallback };
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to register custom theme key, falling back", e);
|
||||
ref.current = null; // force fallback path below
|
||||
console.error("Failed to register custom theme keys:", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (update) {
|
||||
AppearanceManager.setShouldSyncAppearanceSettings(false);
|
||||
if (update && AppearanceManager?.updateTheme) {
|
||||
try {
|
||||
AppearanceManager.setShouldSyncAppearanceSettings?.(false);
|
||||
} catch {}
|
||||
|
||||
const targetTheme = ref.current != null ? ref.key : ref.lastSetDiscordTheme;
|
||||
|
||||
// This is the actual crash site: a raw call into native code with no
|
||||
// guard. Wrap it so a native-side rejection can't take the whole
|
||||
// bridge/thread down — fall back to the last known-good Discord theme.
|
||||
try {
|
||||
AppearanceManager.updateTheme(targetTheme);
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -33,10 +33,10 @@ function buildInitCache() {
|
|||
|
||||
/** @internal */
|
||||
export async function initMetroCache() {
|
||||
if (!await fileExists(BUNNY_METRO_CACHE_PATH)) return void buildInitCache();
|
||||
const rawCache = await readFile(BUNNY_METRO_CACHE_PATH);
|
||||
|
||||
try {
|
||||
if (!await fileExists(BUNNY_METRO_CACHE_PATH)) return void buildInitCache();
|
||||
const rawCache = await readFile(BUNNY_METRO_CACHE_PATH);
|
||||
|
||||
_metroCache = JSON.parse(rawCache);
|
||||
if (_metroCache._v !== CACHE_VERSION) {
|
||||
_metroCache = null!;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue