Compare commits

..

1 commit

6 changed files with 199 additions and 123 deletions

View file

@ -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);
},
});

View file

@ -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();

View file

@ -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"] = {};

View file

@ -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];
}

View file

@ -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) {

View file

@ -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!;