[UI > Settings] Complete overhaul, support You tab

You tab support has a few issues, namely:
* Text inputs automatically close the keyboard
* The Developer toggle is not reactive
* Plugins will need to update if they set navigation options in their settings, otherwise an infinite re-render occurs

Co-authored-by: Jack Matthews <jm5112356@gmail.com>
This commit is contained in:
Beef 2023-05-21 00:58:46 +01:00
parent bc03464da3
commit fda8e31bb1
8 changed files with 222 additions and 112 deletions

View file

@ -45,5 +45,5 @@ export default async (unloads: any[]): Promise<VendettaObject> => ({
unloads.filter(i => typeof i === "function").forEach(p => p()); unloads.filter(i => typeof i === "function").forEach(p => p());
// @ts-expect-error explode // @ts-expect-error explode
delete window.vendetta; delete window.vendetta;
} },
}); });

View file

@ -1,6 +1,7 @@
import { NavigationNative } from "@metro/common"; import { NavigationNative } from "@metro/common";
import { useProxy } from "@lib/storage"; import { useProxy } from "@lib/storage";
import { getAssetIDByName } from "@ui/assets"; import { getAssetIDByName } from "@ui/assets";
import { getScreens } from "@ui/settings/data";
import { ErrorBoundary, Forms } from "@ui/components"; import { ErrorBoundary, Forms } from "@ui/components";
import settings from "@lib/settings"; import settings from "@lib/settings";
@ -10,44 +11,22 @@ export default function SettingsSection() {
const navigation = NavigationNative.useNavigation(); const navigation = NavigationNative.useNavigation();
useProxy(settings); useProxy(settings);
const screens = getScreens();
return ( return (
<ErrorBoundary> <ErrorBoundary>
<FormSection key="Vendetta" title={`Vendetta${settings.safeMode?.enabled ? " (Safe Mode)" : ""}`}> <FormSection key="Vendetta" title={`Vendetta${settings.safeMode?.enabled ? " (Safe Mode)" : ""}`}>
<FormRow {screens.filter(s => s.shouldRender ?? true).map((s, i) => (
label="General"
leading={<FormRow.Icon source={getAssetIDByName("settings")} />}
trailing={FormRow.Arrow}
onPress={() => navigation.push("VendettaSettings")}
/>
<FormDivider />
<FormRow
label="Plugins"
leading={<FormRow.Icon source={getAssetIDByName("debug")} />}
trailing={FormRow.Arrow}
onPress={() => navigation.push("VendettaPlugins")}
/>
{window.__vendetta_loader?.features.themes && (
<> <>
<FormDivider />
<FormRow <FormRow
label="Themes" label={s.title}
leading={<FormRow.Icon source={getAssetIDByName("ic_theme_24px")} />} leading={<FormRow.Icon source={getAssetIDByName(s.icon!)} />}
trailing={FormRow.Arrow} trailing={FormRow.Arrow}
onPress={() => navigation.push("VendettaThemes")} onPress={() => navigation.push(s.key)}
/> />
{i !== screens.length - 1 && <FormDivider />}
</> </>
)} ))}
{settings.developerSettings && (
<>
<FormDivider />
<FormRow
label="Developer"
leading={<FormRow.Icon source={getAssetIDByName("ic_progress_wrench_24px")} />}
trailing={FormRow.Arrow}
onPress={() => navigation.push("VendettaDeveloper")}
/>
</>
)}
</FormSection> </FormSection>
</ErrorBoundary> </ErrorBoundary>
) )

102
src/ui/settings/data.tsx Normal file
View file

@ -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<string, any>;
render: React.ComponentType<any>;
}
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: () => <InstallButton alertTitle="Install Plugin" installFunction={installPlugin} />,
},
render: Plugins,
},
{
key: formatKey("VendettaThemes", youKeys),
title: "Themes",
icon: "ic_theme_24px",
shouldRender: window.__vendetta_loader?.features.hasOwnProperty("themes"),
options: {
headerRight: () => !settings.safeMode?.enabled && <InstallButton alertTitle="Install Theme" installFunction={installTheme} />,
},
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<string, object>) => {
const navigation = NavigationNative.useNavigation();
navigation.addListener("focus", () => navigation.setOptions(without(options, "render", "noErrorBoundary")));
return noErrorBoundary ? <PageView /> : <ErrorBoundary><PageView /></ErrorBoundary>;
}
}
]
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 <RN.View style={styles.container}><s.render {...route.params} /></RN.View>;
}
}
}]))
}
}

12
src/ui/settings/index.ts Normal file
View file

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

View file

@ -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<Function>;
patches.push(after("default", screensModule, (args, existingScreens) => {
return {
...existingScreens,
VendettaSettings: {
title: "Vendetta",
render: General,
},
VendettaPlugins: {
title: "Plugins",
render: Plugins,
headerRight: () => <InstallButton alertTitle="Install Plugin" installFunction={installPlugin} />,
},
VendettaThemes: {
title: "Themes",
render: Themes,
headerRight: !settings.safeMode?.enabled && (() => <InstallButton alertTitle="Install Theme" installFunction={installTheme} />),
},
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<string, object>) => {
const navigation = NavigationNative.useNavigation();
React.useEffect(() => options && navigation.setOptions(without(options, "render", "noErrorBoundary")), []);
return noErrorBoundary ? <PageView /> : <ErrorBoundary><PageView /></ErrorBoundary>;
}
}
}
}));
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, <SettingsSection />);
}));
}, true);
return () => patches.forEach(p => p());
}

View file

@ -5,6 +5,7 @@ import { useProxy } from "@lib/storage";
import { getAssetIDByName } from "@ui/assets"; import { getAssetIDByName } from "@ui/assets";
import { Forms, ErrorBoundary } from "@ui/components"; import { Forms, ErrorBoundary } from "@ui/components";
import settings, { loaderConfig } from "@lib/settings"; import settings, { loaderConfig } from "@lib/settings";
import AssetBrowser from "@ui/settings/pages/AssetBrowser";
const { FormSection, FormRow, FormSwitchRow, FormInput, FormDivider } = Forms; const { FormSection, FormRow, FormSwitchRow, FormInput, FormDivider } = Forms;
const { hideActionSheet } = findByProps("openLazy", "hideActionSheet"); const { hideActionSheet } = findByProps("openLazy", "hideActionSheet");
@ -79,7 +80,10 @@ export default function Developer() {
label="Asset Browser" label="Asset Browser"
leading={<FormRow.Icon source={getAssetIDByName("ic_image")} />} leading={<FormRow.Icon source={getAssetIDByName("ic_image")} />}
trailing={FormRow.Arrow} trailing={FormRow.Arrow}
onPress={() => navigation.push("VendettaAssetBrowser")} onPress={() => navigation.push("VendettaCustomPage", {
title: "Asset Browser",
render: AssetBrowser,
})}
/> />
<FormDivider /> <FormDivider />
<FormRow <FormRow

View file

@ -0,0 +1,39 @@
import { i18n } from "@metro/common";
import { findByName } from "@metro/filters";
import { after } from "@lib/patcher";
import { findInReactTree } from "@lib/utils";
import { getPanelsScreens } from "@ui/settings/data";
import SettingsSection from "@ui/settings/components/SettingsSection";
const screensModule = findByName("getScreens", false);
const settingsModule = findByName("UserSettingsOverviewWrapper", false);
export default function patchPanels() {
const patches = new Array<Function>;
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, <SettingsSection />);
}));
}, true);
return () => patches.forEach(p => p());
}

View file

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