diff --git a/src/lib/windowObject.ts b/src/lib/windowObject.ts index cb780d5..38e01c3 100644 --- a/src/lib/windowObject.ts +++ b/src/lib/windowObject.ts @@ -45,5 +45,5 @@ export default async (unloads: any[]): Promise => ({ unloads.filter(i => typeof i === "function").forEach(p => p()); // @ts-expect-error explode delete window.vendetta; - } + }, }); diff --git a/src/ui/settings/components/SettingsSection.tsx b/src/ui/settings/components/SettingsSection.tsx index 58325e0..c0993a6 100644 --- a/src/ui/settings/components/SettingsSection.tsx +++ b/src/ui/settings/components/SettingsSection.tsx @@ -1,6 +1,7 @@ import { NavigationNative } from "@metro/common"; import { useProxy } from "@lib/storage"; import { getAssetIDByName } from "@ui/assets"; +import { getScreens } from "@ui/settings/data"; import { ErrorBoundary, Forms } from "@ui/components"; import settings from "@lib/settings"; @@ -10,44 +11,22 @@ export default function SettingsSection() { const navigation = NavigationNative.useNavigation(); useProxy(settings); + const screens = getScreens(); + return ( - } - trailing={FormRow.Arrow} - onPress={() => navigation.push("VendettaSettings")} - /> - - } - trailing={FormRow.Arrow} - onPress={() => navigation.push("VendettaPlugins")} - /> - {window.__vendetta_loader?.features.themes && ( + {screens.filter(s => s.shouldRender ?? true).map((s, i) => ( <> - } + label={s.title} + leading={} trailing={FormRow.Arrow} - onPress={() => navigation.push("VendettaThemes")} + onPress={() => navigation.push(s.key)} /> + {i !== screens.length - 1 && } - )} - {settings.developerSettings && ( - <> - - } - trailing={FormRow.Arrow} - onPress={() => navigation.push("VendettaDeveloper")} - /> - - )} + ))} ) diff --git a/src/ui/settings/data.tsx b/src/ui/settings/data.tsx new file mode 100644 index 0000000..e39bd0c --- /dev/null +++ b/src/ui/settings/data.tsx @@ -0,0 +1,102 @@ +import { ReactNative as RN, NavigationNative, stylesheet, lodash } from "@metro/common"; +import { installPlugin } from "@lib/plugins"; +import { installTheme } from "@lib/themes"; +import { without } from "@lib/utils"; +import { semanticColors } from "@ui/color"; +import { getAssetIDByName } from "@ui/assets"; +import settings from "@lib/settings"; +import ErrorBoundary from "@ui/components/ErrorBoundary"; +import InstallButton from "@ui/settings/components/InstallButton"; +import General from "@ui/settings/pages/General"; +import Plugins from "@ui/settings/pages/Plugins"; +import Themes from "@ui/settings/pages/Themes"; +import Developer from "@ui/settings/pages/Developer"; + +interface Screen { + [index: string]: any; + key: string, + title: string; + icon?: string; + shouldRender?: boolean; + options?: Record; + render: React.ComponentType; +} + +const styles = stylesheet.createThemedStyleSheet({ container: { flex: 1, backgroundColor: semanticColors.BACKGROUND_MOBILE_PRIMARY } }); +const formatKey = (key: string, youKeys: boolean) => youKeys ? lodash.snakeCase(key).toUpperCase() : key; + +export const getScreens = (youKeys = false): Screen[] => [ + { + key: formatKey("VendettaSettings", youKeys), + title: "General", + icon: "settings", + render: General, + }, + { + key: formatKey("VendettaPlugins", youKeys), + title: "Plugins", + icon: "debug", + options: { + headerRight: () => , + }, + render: Plugins, + }, + { + key: formatKey("VendettaThemes", youKeys), + title: "Themes", + icon: "ic_theme_24px", + shouldRender: window.__vendetta_loader?.features.hasOwnProperty("themes"), + options: { + headerRight: () => !settings.safeMode?.enabled && , + }, + render: Themes, + }, + { + key: formatKey("VendettaDeveloper", youKeys), + title: "Developer", + icon: "ic_progress_wrench_24px", + shouldRender: settings.developerSettings, + render: Developer, + }, + { + key: formatKey("VendettaCustomPage", youKeys), + 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 = () => Object.fromEntries(getScreens().map(s => [s.key, { + title: s.title, + render: s.render, + ...s.options, +}])); + +export const getYouData = () => { + const screens = getScreens(true); + + return { + layout: { title: "Vendetta", settings: screens.filter(s => s.shouldRender ?? true).map(s => s.key) }, + titleConfig: Object.fromEntries(screens.map(s => [s.key, s.title])), + relationships: Object.fromEntries(screens.map(s => [s.key, null])), + rendererConfigs: Object.fromEntries(screens.map(s => [s.key, { + type: "route", + icon: s.icon ? getAssetIDByName(s.icon) : null, + screen: { + // TODO: This is bad, we should not re-convert the key casing + // For some context, just using the key here would make the route key be VENDETTA_CUSTOM_PAGE in you tab, which breaks compat with panels UI navigation + route: lodash.chain(s.key).camelCase().upperFirst().value(), + getComponent: () => ({ navigation, route }: any) => { + navigation.addListener("focus", () => navigation.setOptions(s.options)); + // TODO: Some ungodly issue causes the keyboard to automatically close in TextInputs. Why?! + return ; + } + } + }])) + } +} \ No newline at end of file diff --git a/src/ui/settings/index.ts b/src/ui/settings/index.ts new file mode 100644 index 0000000..d8bcd32 --- /dev/null +++ b/src/ui/settings/index.ts @@ -0,0 +1,12 @@ +import { findByProps } from "@metro/filters"; +import patchPanels from "@ui/settings/patches/panels"; +import patchYou from "@ui/settings/patches/you"; + +export default function initSettings() { + const patches = [ + patchPanels(), + ...(findByProps("useOverviewSettings") ? [patchYou()] : []), + ] + + return () => patches.forEach(p => p()); +} diff --git a/src/ui/settings/index.tsx b/src/ui/settings/index.tsx deleted file mode 100644 index bc6b842..0000000 --- a/src/ui/settings/index.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { NavigationNative, i18n } from "@metro/common"; -import { findByName } from "@metro/filters"; -import { after } from "@lib/patcher"; -import { installPlugin } from "@lib/plugins"; -import { installTheme } from "@lib/themes"; -import { Forms } from "@ui/components"; -import findInReactTree from "@lib/utils/findInReactTree"; -import without from "@lib/utils/without"; -import ErrorBoundary from "@ui/components/ErrorBoundary"; -import SettingsSection from "@ui/settings/components/SettingsSection"; -import InstallButton from "@ui/settings/components/InstallButton"; -import General from "@ui/settings/pages/General"; -import Plugins from "@ui/settings/pages/Plugins"; -import Themes from "@ui/settings/pages/Themes"; -import Developer from "@ui/settings/pages/Developer"; -import AssetBrowser from "@ui/settings/pages/AssetBrowser"; -import settings from "@lib/settings"; - -const screensModule = findByName("getScreens", false); -const settingsModule = findByName("UserSettingsOverviewWrapper", false); - -export default function initSettings() { - const patches = new Array; - - patches.push(after("default", screensModule, (args, existingScreens) => { - return { - ...existingScreens, - VendettaSettings: { - title: "Vendetta", - render: General, - }, - VendettaPlugins: { - title: "Plugins", - render: Plugins, - headerRight: () => , - }, - VendettaThemes: { - title: "Themes", - render: Themes, - headerRight: !settings.safeMode?.enabled && (() => ), - }, - VendettaDeveloper: { - title: "Developer", - render: Developer, - }, - VendettaAssetBrowser: { - title: "Asset Browser", - render: AssetBrowser, - }, - VendettaCustomPage: { - title: "Vendetta Page", - render: ({ render: PageView, noErrorBoundary, ...options }: { render: React.ComponentType, noErrorBoundary: boolean } & Record) => { - const navigation = NavigationNative.useNavigation(); - React.useEffect(() => options && navigation.setOptions(without(options, "render", "noErrorBoundary")), []); - return noErrorBoundary ? : ; - } - } - } - })); - - 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); - })); - - 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, (tree) => tree.children[1].type === Forms.FormSection).children; - 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/ui/settings/pages/Developer.tsx b/src/ui/settings/pages/Developer.tsx index 5b87b9d..bcceb62 100644 --- a/src/ui/settings/pages/Developer.tsx +++ b/src/ui/settings/pages/Developer.tsx @@ -5,6 +5,7 @@ import { useProxy } from "@lib/storage"; import { getAssetIDByName } from "@ui/assets"; import { Forms, ErrorBoundary } from "@ui/components"; import settings, { loaderConfig } from "@lib/settings"; +import AssetBrowser from "@ui/settings/pages/AssetBrowser"; const { FormSection, FormRow, FormSwitchRow, FormInput, FormDivider } = Forms; const { hideActionSheet } = findByProps("openLazy", "hideActionSheet"); @@ -79,7 +80,10 @@ export default function Developer() { label="Asset Browser" leading={} trailing={FormRow.Arrow} - onPress={() => navigation.push("VendettaAssetBrowser")} + onPress={() => navigation.push("VendettaCustomPage", { + title: "Asset Browser", + render: AssetBrowser, + })} /> ; + + 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").children; + 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/ui/settings/patches/you.tsx b/src/ui/settings/patches/you.tsx new file mode 100644 index 0000000..faef78a --- /dev/null +++ b/src/ui/settings/patches/you.tsx @@ -0,0 +1,54 @@ +import { i18n } from "@metro/common"; +import { findByProps } from "@metro/filters"; +import { after } from "@lib/patcher"; +import { getScreens, getYouData } from "@ui/settings/data"; + +const layoutModule = findByProps("useOverviewSettings"); +const titleConfigModule = findByProps("getSettingTitleConfig"); +const gettersModule = findByProps("getSettingSearchListItems"); +const miscModule = findByProps("SETTING_RELATIONSHIPS", "SETTING_RENDERER_CONFIGS"); + +export default function patchYou() { + const patches = new Array; + const screens = getScreens(true); + const data = getYouData(); + + patches.push(after("useOverviewSettings", layoutModule, (_, ret) => { + // Add our settings + const accountSettingsIndex = ret.findIndex((i: any) => i.title === i18n.Messages.ACCOUNT_SETTINGS); + ret.splice(accountSettingsIndex + 1, 0, data.layout); + + // Upload Logs button be gone + const supportCategory = ret.find((i: any) => i.title === i18n.Messages.SUPPORT); + supportCategory.settings = supportCategory.settings.filter((s: string) => s !== "UPLOAD_DEBUG_LOGS"); + })); + + patches.push(after("getSettingTitleConfig", titleConfigModule, (_, ret) => ({ + ...ret, + ...data.titleConfig, + }))); + + patches.push(after("getSettingSearchListItems", 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: ["Vendetta"], + icon: data.rendererConfigs[s.key].icon, + })), + ...ret.filter((i: any) => !screens.map(s => s.key).includes(i.setting)), + ].map((item, index, parent) => ({ ...item, index, total: parent.length })))); + + // TODO: We could use a proxy for these + const oldRelationships = miscModule.SETTING_RELATIONSHIPS; + const oldRendererConfigs = miscModule.SETTING_RENDERER_CONFIGS; + miscModule.SETTING_RELATIONSHIPS = { ...oldRelationships, ...data.relationships }; + miscModule.SETTING_RENDERER_CONFIGS = { ...oldRendererConfigs, ...data.rendererConfigs }; + + return () => { + miscModule.SETTING_RELATIONSHIPS = oldRelationships; + miscModule.SETTING_RENDERER_CONFIGS = oldRendererConfigs; + patches.forEach(p => p()); + }; +}