[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:
parent
bc03464da3
commit
fda8e31bb1
8 changed files with 222 additions and 112 deletions
|
@ -45,5 +45,5 @@ export default async (unloads: any[]): Promise<VendettaObject> => ({
|
|||
unloads.filter(i => typeof i === "function").forEach(p => p());
|
||||
// @ts-expect-error explode
|
||||
delete window.vendetta;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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 (
|
||||
<ErrorBoundary>
|
||||
<FormSection key="Vendetta" title={`Vendetta${settings.safeMode?.enabled ? " (Safe Mode)" : ""}`}>
|
||||
<FormRow
|
||||
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 && (
|
||||
{screens.filter(s => s.shouldRender ?? true).map((s, i) => (
|
||||
<>
|
||||
<FormDivider />
|
||||
<FormRow
|
||||
label="Themes"
|
||||
leading={<FormRow.Icon source={getAssetIDByName("ic_theme_24px")} />}
|
||||
label={s.title}
|
||||
leading={<FormRow.Icon source={getAssetIDByName(s.icon!)} />}
|
||||
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>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
|
|
102
src/ui/settings/data.tsx
Normal file
102
src/ui/settings/data.tsx
Normal 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
12
src/ui/settings/index.ts
Normal 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());
|
||||
}
|
|
@ -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());
|
||||
}
|
|
@ -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={<FormRow.Icon source={getAssetIDByName("ic_image")} />}
|
||||
trailing={FormRow.Arrow}
|
||||
onPress={() => navigation.push("VendettaAssetBrowser")}
|
||||
onPress={() => navigation.push("VendettaCustomPage", {
|
||||
title: "Asset Browser",
|
||||
render: AssetBrowser,
|
||||
})}
|
||||
/>
|
||||
<FormDivider />
|
||||
<FormRow
|
||||
|
|
39
src/ui/settings/patches/panels.tsx
Normal file
39
src/ui/settings/patches/panels.tsx
Normal 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());
|
||||
}
|
54
src/ui/settings/patches/you.tsx
Normal file
54
src/ui/settings/patches/you.tsx
Normal 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());
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue