diff --git a/packages/bunny-types/def.tt b/packages/bunny-types/def.tt index 67a7d38..f6f25c4 100644 --- a/packages/bunny-types/def.tt +++ b/packages/bunny-types/def.tt @@ -33,6 +33,7 @@ color toasts styles + settings } utils { constants diff --git a/src/assets/icons/pyon64.png b/src/assets/icons/pyon64.png new file mode 100644 index 0000000..9d44b7a Binary files /dev/null and b/src/assets/icons/pyon64.png differ diff --git a/src/core/ui/settings/data.tsx b/src/core/ui/settings/data.tsx deleted file mode 100644 index 1054845..0000000 --- a/src/core/ui/settings/data.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { Strings } from "@core/i18n"; -import InstallButton from "@core/ui/components/InstallButton"; -import Developer from "@core/ui/settings/pages/Developer"; -import General from "@core/ui/settings/pages/General"; -import Plugins from "@core/ui/settings/pages/Plugins"; -import Themes from "@core/ui/settings/pages/Themes"; -import { getAssetIDByName } from "@lib/api/assets"; -import { isThemeSupported } from "@lib/api/native/loader"; -import { useProxy } from "@lib/api/storage"; -import { installPlugin } from "@lib/managers/plugins"; -import { installTheme } from "@lib/managers/themes"; -import { findByProps } from "@lib/metro/filters"; -import { settings } from "@lib/settings"; -import { without } from "@lib/utils"; -import { PROXY_PREFIX } from "@lib/utils/constants"; -import { NavigationNative } from "@metro/common"; -import { showConfirmationAlert } from "@ui/alerts"; -import { semanticColors } from "@ui/color"; -import ErrorBoundary from "@ui/components/ErrorBoundary"; -import { createStyles } from "@ui/styles"; -import { showToast } from "@ui/toasts"; -import { View } from "react-native"; - -const { useSafeAreaInsets } = findByProps("useSafeAreaInsets"); - -interface Screen { - [index: string]: any; - key: string; - title: () => string; - icon?: string; - shouldRender?: () => boolean; - options?: Record; - render: React.ComponentType; -} - -const useStyles = createStyles({ - container: { - flex: 1, - backgroundColor: semanticColors.BACKGROUND_MOBILE_PRIMARY, - } -}); - -// If a function is passed, it is called with the screen object, and the return value is mapped. If a string is passed, we map to the value of the property with that name on the screen. Else, just map to the given data. -// Question: Isn't this overengineered? -// Answer: Maybe. -const keyMap = (screens: Screen[], data: string | ((s: Screen) => any) | null) => Object.fromEntries(screens.map(s => [s.key, typeof data === "function" ? data(s) : typeof data === "string" ? s[data] : data])); - -export const getScreens = (): Screen[] => [ - { - key: "VendettaSettings", - title: () => Strings.GENERAL, - icon: "settings", - render: General, - }, - { - key: "VendettaPlugins", - title: () => Strings.PLUGINS, - icon: "debug", - options: { - headerRight: () => ( - { - if (!input.startsWith(PROXY_PREFIX) && !settings.developerSettings) - setImmediate(() => showConfirmationAlert({ - title: Strings.MODAL_UNPROXIED_PLUGIN_HEADER, - content: Strings.MODAL_UNPROXIED_PLUGIN_DESC, - confirmText: Strings.INSTALL, - onConfirm: () => - installPlugin(input) - .then(() => showToast(Strings.TOASTS_INSTALLED_PLUGIN, getAssetIDByName("Check"))) - .catch((x: any) => showToast(x?.message ?? `${x}`, getAssetIDByName("Small"))), - cancelText: Strings.CANCEL, - })); - else return await installPlugin(input); - }} - /> - ), - }, - render: Plugins, - }, - { - key: "VendettaThemes", - title: () => Strings.THEMES, - icon: "ic_theme_24px", - // TODO: bad - shouldRender: () => isThemeSupported(), - options: { - headerRight: () => !settings.safeMode?.enabled && , - }, - render: Themes, - }, - { - key: "VendettaDeveloper", - title: () => Strings.DEVELOPER, - icon: "ic_progress_wrench_24px", - shouldRender: () => settings.developerSettings ?? false, - render: Developer, - }, - { - key: "VendettaCustomPage", - title: () => "Vendetta Page", - shouldRender: () => false, - render: ({ render: PageView, noErrorBoundary, ...options }: { render: React.ComponentType; noErrorBoundary: boolean; } & Record) => { - const navigation = NavigationNative.useNavigation(); - - navigation.addListener("focus", () => navigation.setOptions(without(options, "render", "noErrorBoundary"))); - return noErrorBoundary ? : ; - }, - }, -]; - -export const getPanelsScreens = () => keyMap(getScreens(), s => ({ - title: s.title(), - render: s.render, - ...s.options, -})); - -export const getYouData = () => { - const screens = getScreens(); - - return { - getLayout: () => ({ - // This is an old key, keeping it as "Vendetta" is good as some plugins actually checks - // to determine if this section is Vendetta's - title: "Vendetta", - label: Strings.BUNNY, - // We can't use our keyMap function here since `settings` is an array not an object - settings: screens.map(s => s.key) - }), - titleConfig: keyMap(screens, s => s.title()), - relationships: keyMap(screens, null), - rendererConfigs: keyMap(screens, s => { - const WrappedComponent = React.memo(({ navigation, route }: any) => { - const styles = useStyles(); - const { bottom: paddingBottom } = useSafeAreaInsets(); - navigation.addListener("focus", () => navigation.setOptions(s.options)); - return - - ; - }); - - return { - type: "route", - title: s.title, - icon: s.icon ? getAssetIDByName(s.icon) : null, - usePredicate: s.shouldRender && (() => useProxy(settings) && s.shouldRender!!()), - screen: { - route: s.key, - getComponent: () => WrappedComponent, - } - }; - }), - }; -}; diff --git a/src/core/ui/settings/index.ts b/src/core/ui/settings/index.ts index 8956c78..de6e792 100644 --- a/src/core/ui/settings/index.ts +++ b/src/core/ui/settings/index.ts @@ -1,12 +1,47 @@ -// TODO: make settings patches part of the api -import patchPanels from "@core/ui/settings/patches/panels"; -import patchYou from "@core/ui/settings/patches/you"; +import { Strings } from "@core/i18n"; +import { getAssetIDByName } from "@lib/api/assets"; +import { isThemeSupported } from "@lib/api/native/loader"; +import { useProxy } from "@lib/api/storage"; +import { settings } from "@lib/settings"; +import { registerSection } from "@lib/ui/settings"; +import { version } from "bunny-build"; + +// @ts-ignore +import PyoncordIcon from "../../../assets/icons/pyon64.png"; export default function initSettings() { - const patches = [ - patchPanels(), - patchYou(), - ]; - - return () => patches.forEach(p => p?.()); + registerSection({ + name: "Bunny", + items: [ + { + key: "BUNNY", + title: () => Strings.BUNNY, + icon: { uri: PyoncordIcon }, + render: () => import("@core/ui/settings/pages/General"), + rawTabsConfig: { + useTrailing: () => `(${version})` + } + }, + { + key: "BUNNY_PLUGINS", + title: () => Strings.PLUGINS, + icon: getAssetIDByName("ActivitiesIcon"), + render: () => import("@core/ui/settings/pages/Plugins") + }, + { + key: "BUNNY_THEMES", + title: () => Strings.THEMES, + icon: getAssetIDByName("PaintPaletteIcon"), + render: () => import("@core/ui/settings/pages/Themes"), + usePredicate: () => isThemeSupported() + }, + { + key: "BUNNY_DEVELOPER", + title: () => Strings.DEVELOPER, + icon: getAssetIDByName("WrenchIcon"), + render: () => import("@core/ui/settings/pages/Developer"), + usePredicate: () => useProxy(settings).developerSettings ?? false + } + ] + }); } diff --git a/src/core/ui/settings/pages/General.tsx b/src/core/ui/settings/pages/General.tsx index 93e1b0a..8e0ee64 100644 --- a/src/core/ui/settings/pages/General.tsx +++ b/src/core/ui/settings/pages/General.tsx @@ -2,7 +2,7 @@ import { Strings } from "@core/i18n"; import Version from "@core/ui/components/Version"; import { getAssetIDByName } from "@lib/api/assets"; import { useProxy } from "@lib/api/storage"; -import { getDebugInfo, _toggleSafeMode } from "@lib/debug"; +import { _toggleSafeMode, getDebugInfo } from "@lib/debug"; import { settings } from "@lib/settings"; import { Stack, TableRow, TableRowGroup, TableSwitchRow } from "@lib/ui/components/discord/Redesign"; import { DISCORD_SERVER, GITHUB } from "@lib/utils/constants"; diff --git a/src/core/ui/settings/patches/panels.tsx b/src/core/ui/settings/patches/panels.tsx deleted file mode 100644 index aa4f9df..0000000 --- a/src/core/ui/settings/patches/panels.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import SettingsSection from "@core/ui/components/SettingsSection"; -import { getPanelsScreens } from "@core/ui/settings/data"; -import { after } from "@lib/api/patcher"; -import { findInReactTree } from "@lib/utils"; -import { i18n } from "@metro/common"; -import { findByName } from "@metro/filters"; - -const screensModule = findByName("getScreens", false); -const settingsModule = findByName("UserSettingsOverviewWrapper", false); - -export default function patchPanels() { - const patches = new Array; - - patches.push(after("default", screensModule, (_, existingScreens) => ({ - ...existingScreens, - ...getPanelsScreens(), - }))); - - after("default", settingsModule, (_, ret) => { - const Overview = findInReactTree(ret.props.children, i => i.type && i.type.name === "UserSettingsOverview"); - - // Upload logs button gone - patches.push(after("renderSupportAndAcknowledgements", Overview.type.prototype, (_, { props: { children } }) => { - const index = children.findIndex((c: any) => c?.type?.name === "UploadLogsButton"); - if (index !== -1) children.splice(index, 1); - })); - - // TODO: Rewrite this whole patch, the index hasn't been properly found for months now - patches.push(after("render", Overview.type.prototype, (_, { props: { children } }) => { - const titles = [i18n.Messages.BILLING_SETTINGS, i18n.Messages.PREMIUM_SETTINGS]; - //! Fix for Android 174201 and iOS 42188 - ({ children } = findInReactTree(children, i => i.children?.[1].type?.name === "FormSection")); - const index = children.findIndex((c: any) => titles.includes(c?.props.label)); - children.splice(index === -1 ? 4 : index, 0, ); - })); - }, true); - - return () => patches.forEach(p => p()); -} diff --git a/src/core/ui/settings/patches/you.tsx b/src/core/ui/settings/patches/you.tsx deleted file mode 100644 index 10c2de6..0000000 --- a/src/core/ui/settings/patches/you.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { Strings } from "@core/i18n"; -import { getScreens, getYouData } from "@core/ui/settings/data"; -import { after, before } from "@lib/api/patcher"; -import { i18n } from "@lib/metro/common"; -import { findByProps } from "@metro/filters"; - -export default function patchYou() { - const patches = new Array; - - newYouPatch(patches) || oldYouPatch(patches); - return () => patches.forEach(p => p?.()); -} - -function oldYouPatch(patches: Function[]) { - const layoutModule = findByProps("useOverviewSettings"); - const titleConfigModule = findByProps("getSettingTitleConfig"); - const miscModule = findByProps("SETTING_RELATIONSHIPS", "SETTING_RENDERER_CONFIGS"); - - // Checks for 189.4 and above - // When dropping support for 189.3 and below, following can be done: (unless Discord changes things again) - // const gettersModule = findByProps("getSettingListItems"); - const OLD_GETTER_FUNCTION = "getSettingSearchListItems"; - const NEW_GETTER_FUNCTION = "getSettingListItems"; - const oldGettersModule = findByProps(OLD_GETTER_FUNCTION); - const usingNewGettersModule = !oldGettersModule; - const getterFunctionName = usingNewGettersModule ? NEW_GETTER_FUNCTION : OLD_GETTER_FUNCTION; - const gettersModule = oldGettersModule ?? findByProps(NEW_GETTER_FUNCTION); - - if (!gettersModule || !layoutModule) return; - - const screens = getScreens(); - const data = getYouData(); - - patches.push(after("useOverviewSettings", layoutModule, (_, ret) => manipulateSections(ret, data.getLayout()))); - - patches.push(after("getSettingTitleConfig", titleConfigModule, (_, ret) => ({ - ...ret, - ...data.titleConfig, - }))); - - patches.push(after(getterFunctionName, gettersModule, ([settings], ret) => [ - ...(screens.filter(s => settings.includes(s.key) && (s.shouldRender?.() ?? true))).map(s => ({ - type: "setting_search_result", - ancestorRendererData: data.rendererConfigs[s.key], - setting: s.key, - title: data.titleConfig[s.key], - breadcrumbs: [Strings.BUNNY], - icon: data.rendererConfigs[s.key].icon, - })), - // .filter can be removed when dropping support for 189.3 and below (unless Discord changes things again) - ...ret.filter((i: any) => (usingNewGettersModule || !screens.map(s => s.key).includes(i.setting))) - ].map((item, index, parent) => ({ ...item, index, total: parent.length })))); - - const oldRelationships = miscModule.SETTING_RELATIONSHIPS; - const oldRendererConfigs = miscModule.SETTING_RENDERER_CONFIGS; - - miscModule.SETTING_RELATIONSHIPS = { ...oldRelationships, ...data.relationships }; - miscModule.SETTING_RENDERER_CONFIGS = { ...oldRendererConfigs, ...data.rendererConfigs }; - - patches.push(() => { - miscModule.SETTING_RELATIONSHIPS = oldRelationships; - miscModule.SETTING_RENDERER_CONFIGS = oldRendererConfigs; - }); - - return true; -} - -function newYouPatch(patches: Function[]) { - const settingsListComponents = findByProps("SearchableSettingsList"); - const settingConstantsModule = findByProps("SETTING_RENDERER_CONFIG"); - const gettersModule = findByProps("getSettingListItems"); - - if (!gettersModule || !settingsListComponents || !settingConstantsModule) return false; - - const screens = getScreens(); - const data = getYouData(); - - patches.push(before("type", settingsListComponents.SearchableSettingsList, ([{ sections }]) => manipulateSections(sections, data.getLayout()))); - - patches.push(after("getSettingListSearchResultItems", gettersModule, (_, ret) => { - ret.forEach((s: any) => screens.some(b => b.key === s.setting) && (s.breadcrumbs = [Strings.BUNNY])); - })); - - const oldRendererConfig = settingConstantsModule.SETTING_RENDERER_CONFIG; - settingConstantsModule.SETTING_RENDERER_CONFIG = { ...oldRendererConfig, ...data.rendererConfigs }; - - patches.push(() => { - settingConstantsModule.SETTING_RENDERER_CONFIG = oldRendererConfig; - }); - - return true; -} - -const isLabel = (i: any, name: string) => i?.label === name || i?.title === name; - -function manipulateSections(sections: any[], layout: any) { - if (!Array.isArray(sections) || sections.find((i: any) => isLabel(i, Strings.BUNNY))) return; - - // Add our settings - const accountSettingsIndex = sections.findIndex((i: any) => isLabel(i, i18n.Messages.ACCOUNT_SETTINGS)); - sections.splice(accountSettingsIndex + 1, 0, layout); - - // Upload Logs button be gone - const supportCategory = sections.find((i: any) => isLabel(i, i18n.Messages.SUPPORT)); - if (supportCategory) supportCategory.settings = supportCategory.settings.filter((s: string) => s !== "UPLOAD_DEBUG_LOGS"); -} diff --git a/src/index.ts b/src/index.ts index fc368d8..cd2a142 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ import { isThemeSupported } from "@lib/api/native/loader"; import { _patchLogHook } from "@lib/debug"; import { _initPlugins } from "@lib/managers/plugins"; import { _initThemes, _patchChatBackground } from "@lib/managers/themes"; +import { _patchSettings } from "@lib/ui/settings"; import { logger } from "@lib/utils/logger"; import initWindowObject from "@lib/windowObject"; import initSafeMode from "@ui/safeMode"; @@ -25,13 +26,14 @@ export default async () => { // Load everything in parallel const unloads = await Promise.all([ _injectFluxInterceptor(), + _patchSettings(), _patchLogHook(), _patchAssets(), _patchCommands(), _patchChatBackground(), + initSettings(), initFixes(), initSafeMode(), - initSettings(), initCorePlugins(), ]); diff --git a/src/lib/ui/index.ts b/src/lib/ui/index.ts index 7595a3f..824de76 100644 --- a/src/lib/ui/index.ts +++ b/src/lib/ui/index.ts @@ -1,5 +1,6 @@ export * as alerts from "./alerts"; export * as color from "./color"; export * as components from "./components"; +export * as settings from "./settings"; export * as styles from "./styles"; export * as toasts from "./toasts"; diff --git a/src/lib/ui/settings/index.tsx b/src/lib/ui/settings/index.tsx new file mode 100644 index 0000000..f9e2480 --- /dev/null +++ b/src/lib/ui/settings/index.tsx @@ -0,0 +1,33 @@ +// Good luck reading this! +import type { ImageURISource } from "react-native"; + +import { patchPanelUI } from "./patches/panel"; +import { patchTabsUI } from "./patches/tabs"; + +export interface RowConfig { + key: string; + title: () => string; + onPress?: () => any; + render?: Parameters[0]; + icon?: ImageURISource | number; + usePredicate?: () => boolean, + rawTabsConfig?: Record; +} + +export const registeredSections = {} as { + [key: string]: RowConfig[]; +}; + +export function registerSection(section: { name: string; items: RowConfig[]; }) { + registeredSections[section.name] = section.items; + return () => delete registeredSections[section.name]; +} + +export function _patchSettings() { + const unpatches = new Array<() => boolean>; + + patchPanelUI(unpatches); + patchTabsUI(unpatches); + + return () => unpatches.forEach(u => u()); +} diff --git a/src/lib/ui/settings/patches/panel.tsx b/src/lib/ui/settings/patches/panel.tsx new file mode 100644 index 0000000..5af66f9 --- /dev/null +++ b/src/lib/ui/settings/patches/panel.tsx @@ -0,0 +1,62 @@ +import { findByName } from "@lib/metro"; +import { i18n, NavigationNative } from "@lib/metro/common"; +import { FormIcon, FormRow, FormSection } from "@lib/ui/components/discord/Forms"; +import { registeredSections } from "@lib/ui/settings"; +import { findInReactTree } from "@lib/utils"; +import { after } from "spitroast"; + +import { CustomPageRenderer, wrapOnPress } from "./shared"; + +function SettingsSection() { + const navigation = NavigationNative.useNavigation(); + + return <> + {Object.keys(registeredSections).map(sect => ( + + { /** Is usePredicate here safe? */} + {registeredSections[sect].filter(r => r.usePredicate?.() ?? true).map(row => ( + } + trailing={FormRow.Arrow} + onPress={wrapOnPress(row.onPress, navigation, row.render, row.title())} + /> + ))} + + ))} + ; +} + +export function patchPanelUI(unpatches: (() => void | boolean)[]) { + unpatches.push(after("default", findByName("getScreens", false), (_a, screens) => ({ + ...screens, + VendettaCustomPage: { + title: "Bnuuy", + render: () => + } + }))); + + const unpatch = after("default", findByName("UserSettingsOverviewWrapper", false), (_a, ret) => { + const UserSettingsOverview = findInReactTree(ret.props.children, n => n.type?.name === "UserSettingsOverview"); + + unpatches.push(after("renderSupportAndAcknowledgements", UserSettingsOverview.type.prototype, (_args, { props: { children } }) => { + const index = children.findIndex((c: any) => c?.type?.name === "UploadLogsButton"); + if (index !== -1) children.splice(index, 1); + })); + + unpatches.push(after("render", UserSettingsOverview.type.prototype, (_args, res) => { + const titles = [i18n.Messages.BILLING_SETTINGS, i18n.Messages.PREMIUM_SETTINGS]; + + const sections = findInReactTree( + res.props.children, + n => n?.children?.[1]?.type === FormSection + ).children; + + const index = sections.findIndex((c: any) => titles.includes(c?.props.label)); + sections.splice(-~index || 4, 0, ); + })); + }, true); + + unpatches.push(unpatch); +} + diff --git a/src/lib/ui/settings/patches/shared.tsx b/src/lib/ui/settings/patches/shared.tsx new file mode 100644 index 0000000..2f63abb --- /dev/null +++ b/src/lib/ui/settings/patches/shared.tsx @@ -0,0 +1,41 @@ +import { NavigationNative } from "@lib/metro/common"; +import { findByProps } from "@lib/metro/filters"; +import { RowConfig } from "@lib/ui/settings"; + +const tabsNavigationRef = findByProps("getRootNavigationRef"); + +export const CustomPageRenderer = React.memo(() => { + const navigation = NavigationNative.useNavigation(); + const route = NavigationNative.useRoute(); + + const { render: PageComponent, ...args } = route.params; + + React.useEffect(() => void navigation.setOptions({ ...args }), []); + + return ; +}); + +export function wrapOnPress( + onPress: (() => unknown) | undefined, + navigation?: any, + renderPromise?: RowConfig["render"], + screenOptions?: string | Record, + props?: any, +) { + return async () => { + if (onPress) return void onPress(); + + const Component = await renderPromise!!().then(m => m.default); + + if (typeof screenOptions === "string") { + screenOptions = { title: screenOptions }; + } + + navigation ??= tabsNavigationRef.getRootNavigationRef(); + navigation.navigate("VendettaCustomPage", { + ...screenOptions, + render: () => + }); + }; +} + diff --git a/src/lib/ui/settings/patches/tabs.tsx b/src/lib/ui/settings/patches/tabs.tsx new file mode 100644 index 0000000..9af415a --- /dev/null +++ b/src/lib/ui/settings/patches/tabs.tsx @@ -0,0 +1,67 @@ +import { findByName, findByProps } from "@lib/metro"; +import { i18n } from "@lib/metro/common"; +import { registeredSections } from "@lib/ui/settings"; +import { findInReactTree } from "@lib/utils"; +import { after } from "spitroast"; + +import { CustomPageRenderer, wrapOnPress } from "./shared"; + +const settingConstants = findByProps("SETTING_RENDERER_CONFIG"); +const SettingsOverviewScreen = findByName("SettingsOverviewScreen", false); + +function useIsFirstRender() { + let firstRender = false; + React.useEffect(() => void (firstRender = true), []); + return firstRender; +} + +export function patchTabsUI(unpatches: (() => void | boolean)[]) { + settingConstants.SETTING_RENDERER_CONFIG.VendettaCustomPage = { + type: "route", + title: () => "Bnuuy", + screen: { + route: "VendettaCustomPage", + getComponent: () => CustomPageRenderer + } + }; + + const getRows = () => Object.values(registeredSections) + .flatMap(sect => sect.map(row => ({ + [row.key]: { + type: "pressable", + title: row.title(), + icon: row.icon, + usePredicate: row.usePredicate, + onPress: wrapOnPress(row.onPress, null, row.render, row.title()), + withArrow: true, + ...row.rawTabsConfig + } + }))) + .reduce((a, c) => Object.assign(a, c)); + + const origRendererConfig = settingConstants.SETTING_RENDERER_CONFIG; + settingConstants.SETTING_RENDERER_CONFIG = new Proxy(origRendererConfig, { + get: (target, prop: string, reciever) => Reflect.get({ ...target, ...getRows() }, prop, reciever), + getOwnPropertyDescriptor: (target, prop: string) => Reflect.getOwnPropertyDescriptor({ ...target, ...getRows() }, prop), + ownKeys: target => [...Reflect.ownKeys(target), ...Object.keys(getRows())] + }); + + unpatches.push(() => { + settingConstants.SETTING_RENDERER_CONFIG = origRendererConfig; + delete settingConstants.SETTING_RENDERER_CONFIG.VendettaCustomPage; + }); + + unpatches.push(after("default", SettingsOverviewScreen, (_, ret) => { + if (useIsFirstRender()) return; // :shrug: + + const { sections } = findInReactTree(ret, i => i.props?.sections).props; + let index = -~sections.findIndex((i: any) => i.label === i18n.Messages.ACCOUNT_SETTINGS) || 1; + + Object.keys(registeredSections).forEach(sect => { + sections.splice(index++, 0, { + label: sect, + settings: registeredSections[sect].map(a => a.key) + }); + }); + })); +} diff --git a/src/lib/windowObject.ts b/src/lib/windowObject.ts index 3576515..8f99026 100644 --- a/src/lib/windowObject.ts +++ b/src/lib/windowObject.ts @@ -88,7 +88,8 @@ const _bunnyObject = () => ({ // this function is never used, only exists for ty alerts: { ...ui.alerts }, color: { ...ui.color }, toasts: { ...ui.toasts }, - styles: { ...ui.styles } + styles: { ...ui.styles }, + settings: { ...ui.settings } }, utils: { ...utils,