diff --git a/src/core/i18n/default.json b/src/core/i18n/default.json index f080e5a..d0efbe0 100644 --- a/src/core/i18n/default.json +++ b/src/core/i18n/default.json @@ -70,6 +70,7 @@ "MANUFACTURER": "Manufacturer", "MESSAGE": "Message", "MISCELLANEOUS": "Miscellaneous", + "MODAL_RELOAD_THEME_CHANGE": "Reload the app to apply the theme change?", "MODAL_RELOAD_REQUIRED": "Reload app?", "MODAL_RELOAD_REQUIRED_DESC": "A reload is required to see the changes. Do you want to reload now?", "MODAL_THEME_REFETCHED": "Theme refetched", diff --git a/src/core/plugins/index.ts b/src/core/plugins/index.ts index 9dacb44..845cabd 100644 --- a/src/core/plugins/index.ts +++ b/src/core/plugins/index.ts @@ -3,16 +3,40 @@ import { PluginInstanceInternal } from "@lib/addons/plugins/types"; interface CorePlugin { default: PluginInstanceInternal; preenabled: boolean; + loader: () => PluginInstanceInternal; +} + +type CorePluginRecord = Record; + +function makeLazyPlugin(loaderFn: () => any, preenabled = true): CorePlugin { + const container: any = { + loader: () => { + // loaderFn should synchronously require the module + const mod = loaderFn(); + // module may be an object with `.default` or the instance directly + const inst = mod && (mod.default ?? mod); + return inst; + }, + preenabled, + get default() { + // load and cache the instance on first access + const inst = container.loader(); + Object.defineProperty(container, "default", { value: inst, writable: false, enumerable: true }); + return inst; + }, + }; + + return container as CorePlugin; } // Called from @lib/plugins -export const getCorePlugins = (): Record => ({ - "bunny.quickinstall": { ...require("./quickinstall"), preenabled: true }, - "bunny.badges": require("./badges"), - "bunny.notrack": require("./notrack"), - "bunny.messagefix": require("./messagefix"), - "bunny.fixembed": require("./fixembed"), - "bunny.enhancements": require("./shiggyenhancements") +export const getCorePlugins = (): CorePluginRecord => ({ + "bunny.quickinstall": makeLazyPlugin(() => require("./quickinstall"), true), + "bunny.badges": makeLazyPlugin(() => require("./badges")), + "bunny.notrack": makeLazyPlugin(() => require("./notrack")), + "bunny.messagefix": makeLazyPlugin(() => require("./messagefix")), + "bunny.fixembed": makeLazyPlugin(() => require("./fixembed")), + "bunny.enhancements": makeLazyPlugin(() => require("./shiggyenhancements")), }); /** diff --git a/src/core/ui/settings/pages/Themes/index.tsx b/src/core/ui/settings/pages/Themes/index.tsx index 9af6fed..5489644 100644 --- a/src/core/ui/settings/pages/Themes/index.tsx +++ b/src/core/ui/settings/pages/Themes/index.tsx @@ -33,6 +33,8 @@ import { Text, } from "@metro/common/components"; import { View, Image } from "react-native"; +import { showConfirmationAlert } from "@core/vendetta/alerts"; +import { BundleUpdaterManager } from "@lib/api/native/modules"; const { useReducer } = React; @@ -361,6 +363,47 @@ export default function Themes() { const navigation = NavigationNative.useNavigation(); const [mode, setMode] = React.useState<"installed" | "browse">("installed"); + + // Track the theme that was active when the user entered the Themes page. + // If it changes while the user stays on this page, we'll prompt to reload when they leave. + const initialThemeIdRef = React.useRef(getCurrentTheme()?.id ?? null); + + React.useEffect(() => { + const beforeRemoveHandler = (e: any) => { + try { + const currentId = getCurrentTheme()?.id ?? null; + if (currentId !== initialThemeIdRef.current) { + // Notify the user that a reload is required for full application of theme changes + showConfirmationAlert({ + title: Strings.MODAL_RELOAD_THEME_CHANGE, + content: Strings.MODAL_THEME_REFETCHED_DESC, + confirmText: Strings.RELOAD, + cancelText: Strings.CANCEL, + confirmColor: "red", + onConfirm: () => { + try { + BundleUpdaterManager.reload(); + } catch (err) { + try { + (global as any).NativeModules?.BundleUpdaterManager?.reload?.(); + } catch (_) {} + } + }, + }); + } + } catch (_) { + // swallow any unexpected errors — we don't want to block navigation + } + }; + + const unsub = navigation?.addListener ? navigation.addListener("beforeRemove", beforeRemoveHandler) : undefined; + return () => { + try { + if (unsub && typeof unsub === "function") unsub(); + else if (unsub && typeof unsub.remove === "function") unsub.remove(); + } catch (_) {} + }; + }, [navigation]); const [themesList, setThemesList] = React.useState([]); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(null); diff --git a/src/lib/addons/themes/colors/index.ts b/src/lib/addons/themes/colors/index.ts index 1da31dc..5d4ade2 100644 --- a/src/lib/addons/themes/colors/index.ts +++ b/src/lib/addons/themes/colors/index.ts @@ -3,6 +3,7 @@ import patchDefinitionAndResolver from "./patches/resolver"; import patchStorage from "./patches/storage"; import { ColorManifest } from "./types"; import { updateBunnyColor } from "./updater"; +import fixStatusBar from "./statusbar"; /** @internal */ export default function initColors(manifest: ColorManifest | null) { @@ -11,8 +12,9 @@ export default function initColors(manifest: ColorManifest | null) { const patches = [ patchStorage(), patchDefinitionAndResolver(), - patchChatBackground() + patchChatBackground(), + fixStatusBar() ]; - return () => patches.forEach(p => p()); -} \ No newline at end of file + return () => patches.forEach(p => p && (typeof p === "function" ? p() : null)); +} diff --git a/src/lib/addons/themes/colors/patches/resolver.ts b/src/lib/addons/themes/colors/patches/resolver.ts index c6411f2..93ee2a2 100644 --- a/src/lib/addons/themes/colors/patches/resolver.ts +++ b/src/lib/addons/themes/colors/patches/resolver.ts @@ -25,7 +25,10 @@ const SEMANTIC_FALLBACK_MAP: Record = { const origRawColor = { ...tokenReference.RawColor }; export default function patchDefinitionAndResolver() { - const callback = ([theme]: any[]) => theme === _colorRef.key ? [_colorRef.current!.reference] : void 0; + const callback = ([theme]: any[]) => { + if (!_colorRef.current) return void 0; + return theme === _colorRef.key ? [_colorRef.current.reference] : void 0; + }; Object.keys(tokenReference.RawColor).forEach(key => { Object.defineProperty(tokenReference.RawColor, key, { @@ -34,7 +37,7 @@ export default function patchDefinitionAndResolver() { get: () => { const ret = _colorRef.current?.raw[key]; if (ret) return ret; - + return origRawColor[key]; } }); @@ -46,15 +49,20 @@ export default function patchDefinitionAndResolver() { before("updateTheme", NativeThemeModule, callback), instead("resolveSemanticColor", tokenReference.default.meta ?? tokenReference.default.internal, (args: any[], orig: any) => { if (!_colorRef.current) return orig(...args); + const currentRef = _colorRef.current; if (args[0] !== _colorRef.key) return orig(...args); - args[0] = _colorRef.current.reference; + // Use the captured reference value + args[0] = currentRef.reference; - const [name, colorDef] = extractInfo(_colorRef.current!.reference, args[1]); + const [name, colorDef] = extractInfo(currentRef.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 extractInfo couldn't resolve a color definition, fall back to the original resolver + if (!name || !colorDef) return orig(...args); + + let semanticDef = currentRef.semantic[name]; + if (!semanticDef && currentRef.spec === 2 && name in SEMANTIC_FALLBACK_MAP) { + semanticDef = currentRef.semantic[SEMANTIC_FALLBACK_MAP[name]]; } if (semanticDef?.value) { @@ -62,10 +70,11 @@ export default function patchDefinitionAndResolver() { return chroma(semanticDef.value).alpha(semanticDef.opacity).hex(); } - const rawValue = _colorRef.current.raw[colorDef.raw]; + const rawKey = colorDef.raw; + const rawValue = rawKey ? currentRef.raw[rawKey] : undefined; if (rawValue) { // Set opacity if needed - return colorDef.opacity === 1 ? rawValue : chroma(rawValue).alpha(colorDef.opacity).hex(); + return (colorDef.opacity === 1) ? rawValue : chroma(rawValue).alpha(colorDef.opacity).hex(); } // Fallback to default @@ -86,7 +95,13 @@ export default function patchDefinitionAndResolver() { 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]; + const entry = tokenReference.SemanticColor[propName]; - return [propName, colorDef[themeName]]; -} \ No newline at end of file + if (!entry || typeof entry !== "object") { + return [propName, undefined]; + } + + const colorDef = Object.prototype.hasOwnProperty.call(entry, themeName) ? entry[themeName] : undefined; + + return [propName, colorDef]; +} diff --git a/src/lib/addons/themes/colors/statusbar.ts b/src/lib/addons/themes/colors/statusbar.ts index 22f0f38..614e141 100644 --- a/src/lib/addons/themes/colors/statusbar.ts +++ b/src/lib/addons/themes/colors/statusbar.ts @@ -1,4 +1,4 @@ -import { StatusBar, TextInput, Platform } from "react-native"; +import { StatusBar, Platform } from "react-native"; import { findByStoreNameLazy } from "@metro/wrappers"; import { _colorRef } from "./updater"; @@ -7,15 +7,8 @@ const ThemeStore = findByStoreNameLazy("ThemeStore"); function getBarStyle() { // Check for custom theme const custom = _colorRef.current; - if (custom) { - // Bunny theme (spec 3) - if (custom.spec === 3 && custom.reference) { - return custom.reference === "light" ? "dark-content" : "light-content"; - } - // Vendetta theme (spec 2) - if (custom.spec === 2 && custom.reference) { - return custom.reference === "light" ? "dark-content" : "light-content"; - } + if (custom?.reference) { + return custom.reference === "light" ? "dark-content" : "light-content"; } // Fallback to Discord theme string @@ -25,33 +18,35 @@ function getBarStyle() { return (lowerTheme.includes("dark") || lowerTheme.includes("midnight")) ? "light-content" : "dark-content"; } - return "dark-content"; + return "light-content"; } export default function fixStatusBar() { + function applyStatusBar() { + const style = getBarStyle(); + StatusBar.setBarStyle(style, true); - if (Platform.OS == "android") { + if (Platform.OS === "android") { + StatusBar.setTranslucent?.(true); + StatusBar.setBackgroundColor?.("#00000000", true); + } + } + + if (Platform.OS === "android") { const origSetBarStyle = StatusBar.setBarStyle; StatusBar.setBarStyle = function (_style: any, ...args: any[]) { return origSetBarStyle.call(this, getBarStyle(), ...args); }; } - if (Platform.OS == "ios") { - function applyStatusBar() { - const style = getBarStyle(); - StatusBar.setBarStyle(style, true); - } + const unsubscribe = ThemeStore?.addChangeListener?.(applyStatusBar); - const unsubscribe = ThemeStore?.addChangeListener?.(applyStatusBar); + applyStatusBar(); - StatusBar.setBarStyle(getBarStyle()); - - let delay = 200; - for (let i = 0; i < 5; i++, delay *= 2) { - setTimeout(applyStatusBar, delay); - } - - return unsubscribe; + let delay = 200; + for (let i = 0; i < 5; i++, delay *= 2) { + setTimeout(applyStatusBar, delay); } + + return unsubscribe; } diff --git a/src/lib/addons/themes/colors/updater.ts b/src/lib/addons/themes/colors/updater.ts index 8c3d85b..46cd2f4 100644 --- a/src/lib/addons/themes/colors/updater.ts +++ b/src/lib/addons/themes/colors/updater.ts @@ -9,7 +9,6 @@ const origRawColor = { ...tokenRef.RawColor }; const AppearanceManager = findByPropsLazy("updateTheme"); const ThemeStore = findByStoreNameLazy("ThemeStore"); const FormDivider = findByPropsLazy("DIVIDER_COLORS"); -import { colorsPref } from "./preferences"; let _inc = 1; @@ -25,32 +24,28 @@ export const _colorRef: InternalColorRef = { current: null, key: `bn-theme-${_inc}`, origRaw: origRawColor, - lastSetDiscordTheme: "darker" + lastSetDiscordTheme: "dark" }; export function updateBunnyColor(colorManifest: ColorManifest | null, { update = true } ) { if (settings.safeMode?.enabled) return; - const resolveType = (type = "dark") => (colorsPref.type ?? type) === "dark" ? "darker" : "light"; const internalDef = colorManifest ? parseColorManifest(colorManifest) : null; - if (resolveType() == "light" || update) { - var ref = Object.assign(_colorRef, { - current: internalDef, - key: `bn-theme-${++_inc}`, - lastSetDiscordTheme: !ThemeStore.theme.startsWith("bn-theme-") || !ThemeStore.theme.startsWith("darker") - ? ThemeStore.theme - : _colorRef.lastSetDiscordTheme - }); - } else { - var ref = Object.assign(_colorRef, { - current: internalDef, - key: `dark`, - lastSetDiscordTheme: !ThemeStore.theme.startsWith("bn-theme-") || !ThemeStore.theme.startsWith("darker") - ? ThemeStore.theme - : _colorRef.lastSetDiscordTheme - }); - } + const keyName = internalDef && internalDef.reference === "light" ? `bn-theme-${++_inc}` : `dark`; + + const tsTheme = typeof ThemeStore.theme === "string" ? ThemeStore.theme : undefined; + const currentDiscordTheme = tsTheme === "darker" ? "dark" : (tsTheme ?? ""); + + const lastSet = (currentDiscordTheme && !currentDiscordTheme.startsWith("bn-theme-") && currentDiscordTheme !== keyName) + ? currentDiscordTheme + : _colorRef.lastSetDiscordTheme; + + var ref = Object.assign(_colorRef, { + current: internalDef, + key: keyName, + lastSetDiscordTheme: lastSet + }); if (internalDef != null) { tokenRef.Theme[ref.key.toUpperCase()] = ref.key; @@ -66,7 +61,9 @@ export function updateBunnyColor(colorManifest: ColorManifest | null, { update = if (update) { AppearanceManager.setShouldSyncAppearanceSettings(false); - AppearanceManager.updateTheme(internalDef != null ? ref.key : "darker"); + const fallback = (_colorRef.lastSetDiscordTheme && !_colorRef.lastSetDiscordTheme.startsWith("bn-theme-")) + ? _colorRef.lastSetDiscordTheme + : "dark"; + AppearanceManager.updateTheme(internalDef != null ? ref.key : fallback); } } -