[SafeMode] Initial implementation (#61)
* [UI > Components] Add Discord's button * [SafeMode] Initial work, basic ErrorBoundary patch * [SafeMode] Custom error boundary (#57) * [SafeMode] Add mostly complete custom error boundary * [SafeMode] Use Button from @ui/components in error boundary * [SafeMode] Wrap the error boundary in our own error boundary * [SafeMode > ErrorBoundary] Code-style changes --------- Co-authored-by: Beef <beefers@riseup.net> * [TS] Add basic type for Discord's button * [UI] Move Codeblock to components, and use it * [UI > Settings] Allow disabling the ErrorBoundary in CustomPage * [UI > Settings] Move the ErrorBoundary triggers to Developer * [TS] Add Codeblock to types * [TS] Use ButtonColors in Button type * [SafeMode > ErrorBoundary] Rework * [UI] Add HelpMessage to components * [SafeMode] Proper implementation * [Global] SafeMode is optional (#59) * [UI > Developer] Restore the balance * [SafeMode > ErrorBoundary] Optimise for tablet UI * [SafeMode] Last-minute fixes --------- Co-authored-by: Jack <30497388+FieryFlames@users.noreply.github.com> Co-authored-by: Jack Matthews <jm5112356@gmail.com>
This commit is contained in:
parent
f85fc4b00c
commit
5344f0017a
16 changed files with 293 additions and 47 deletions
15
src/def.d.ts
vendored
15
src/def.d.ts
vendored
|
@ -18,7 +18,13 @@ interface SummaryProps {
|
|||
}
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: JSX.Element | JSX.Element[],
|
||||
children: JSX.Element | JSX.Element[];
|
||||
}
|
||||
|
||||
interface CodeblockProps {
|
||||
selectable?: boolean;
|
||||
style?: _RN.TextStyle;
|
||||
children?: string;
|
||||
}
|
||||
|
||||
// Helper types for API functions
|
||||
|
@ -125,6 +131,10 @@ interface Theme {
|
|||
interface Settings {
|
||||
debuggerUrl: string;
|
||||
developerSettings: boolean;
|
||||
safeMode?: {
|
||||
enabled: boolean;
|
||||
currentThemeId?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ApplicationCommand {
|
||||
|
@ -404,9 +414,12 @@ interface VendettaObject {
|
|||
General: PropIntellisense<"Button" | "Text" | "View">;
|
||||
Search: _React.ComponentType;
|
||||
Alert: _React.ComponentType;
|
||||
Button: _React.ComponentType<any> & { Looks: any, Colors: ButtonColors, Sizes: any };
|
||||
HelpMessage: _React.ComponentType;
|
||||
// Vendetta
|
||||
Summary: _React.ComponentType<SummaryProps>;
|
||||
ErrorBoundary: _React.ComponentType<ErrorBoundaryProps>;
|
||||
Codeblock: _React.ComponentType<CodeblockProps>;
|
||||
}
|
||||
toasts: {
|
||||
showToast: (content: string, asset: number) => void;
|
||||
|
|
|
@ -3,6 +3,7 @@ import { patchCommands } from "@lib/commands";
|
|||
import { initPlugins } from "@lib/plugins";
|
||||
import { patchAssets } from "@ui/assets";
|
||||
import initQuickInstall from "@ui/quickInstall";
|
||||
import initSafeMode from "@ui/safeMode";
|
||||
import initSettings from "@ui/settings";
|
||||
import initFixes from "@lib/fixes";
|
||||
import logger from "@lib/logger";
|
||||
|
@ -15,6 +16,7 @@ export default async () => {
|
|||
patchAssets(),
|
||||
patchCommands(),
|
||||
initFixes(),
|
||||
initSafeMode(),
|
||||
initSettings(),
|
||||
initQuickInstall(),
|
||||
]);
|
||||
|
|
|
@ -1,12 +1,27 @@
|
|||
import { RNConstants } from "@types";
|
||||
import { ReactNative as RN } from "@metro/common";
|
||||
import { after } from "@lib/patcher";
|
||||
import { ClientInfoManager, DeviceManager } from "@lib/native";
|
||||
import { ClientInfoManager, DeviceManager, BundleUpdaterManager } from "@lib/native";
|
||||
import { getCurrentTheme, selectTheme } from "@lib/themes";
|
||||
import { getAssetIDByName } from "@ui/assets";
|
||||
import { showToast } from "@ui/toasts";
|
||||
import settings from "@lib/settings";
|
||||
import logger from "@lib/logger";
|
||||
export let socket: WebSocket;
|
||||
|
||||
export async function toggleSafeMode() {
|
||||
settings.safeMode = { ...settings.safeMode, enabled: !settings.safeMode?.enabled }
|
||||
if (window.__vendetta_loader?.features.themes) {
|
||||
if (getCurrentTheme()?.id) settings.safeMode!.currentThemeId = getCurrentTheme()!.id;
|
||||
if (settings.safeMode?.enabled) {
|
||||
await selectTheme("default");
|
||||
} else if (settings.safeMode?.currentThemeId) {
|
||||
await selectTheme(settings.safeMode?.currentThemeId);
|
||||
}
|
||||
}
|
||||
setTimeout(BundleUpdaterManager.reload);
|
||||
}
|
||||
|
||||
export function connectToDebugger(url: string) {
|
||||
if (socket !== undefined && socket.readyState !== WebSocket.CLOSED) socket.close();
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Indexable, PluginManifest, Plugin } from "@types";
|
||||
import { awaitSyncWrapper, createMMKVBackend, createStorage, wrapSync } from "@lib/storage";
|
||||
import { MMKVManager } from "@lib/native";
|
||||
import settings from "@lib/settings";
|
||||
import logger, { logModule } from "@lib/logger";
|
||||
import safeFetch from "@utils/safeFetch";
|
||||
|
||||
|
@ -76,9 +77,11 @@ export async function startPlugin(id: string) {
|
|||
if (!plugin) throw new Error("Attempted to start non-existent plugin");
|
||||
|
||||
try {
|
||||
const pluginRet: EvaledPlugin = await evalPlugin(plugin);
|
||||
loadedPlugins[id] = pluginRet;
|
||||
pluginRet.onLoad?.();
|
||||
if (!settings.safeMode?.enabled) {
|
||||
const pluginRet: EvaledPlugin = await evalPlugin(plugin);
|
||||
loadedPlugins[id] = pluginRet;
|
||||
pluginRet.onLoad?.();
|
||||
}
|
||||
plugin.enabled = true;
|
||||
} catch(e) {
|
||||
logger.error(`Plugin ${plugin.id} errored whilst loading, and will be unloaded`, e);
|
||||
|
@ -99,15 +102,17 @@ export function stopPlugin(id: string, disable = true) {
|
|||
const plugin = plugins[id];
|
||||
const pluginRet = loadedPlugins[id];
|
||||
if (!plugin) throw new Error("Attempted to stop non-existent plugin");
|
||||
if (!pluginRet) throw new Error("Attempted to stop a non-started plugin");
|
||||
|
||||
try {
|
||||
pluginRet.onUnload?.();
|
||||
} catch(e) {
|
||||
logger.error(`Plugin ${plugin.id} errored whilst unloading`, e);
|
||||
if (!settings.safeMode?.enabled) {
|
||||
try {
|
||||
pluginRet?.onUnload?.();
|
||||
} catch(e) {
|
||||
logger.error(`Plugin ${plugin.id} errored whilst unloading`, e);
|
||||
}
|
||||
|
||||
delete loadedPlugins[id];
|
||||
}
|
||||
|
||||
delete loadedPlugins[id];
|
||||
disable && (plugin.enabled = false);
|
||||
}
|
||||
|
||||
|
@ -120,13 +125,16 @@ export function removePlugin(id: string) {
|
|||
}
|
||||
|
||||
export async function initPlugins() {
|
||||
await awaitSyncWrapper(settings);
|
||||
await awaitSyncWrapper(plugins);
|
||||
const allIds = Object.keys(plugins);
|
||||
|
||||
// Loop over any plugin that is enabled, update it if allowed, then start it.
|
||||
await Promise.allSettled(allIds.filter(pl => plugins[pl].enabled).map(async (pl) => (plugins[pl].update && await fetchPlugin(pl), await startPlugin(pl))));
|
||||
// Wait for the above to finish, then update all disabled plugins that are allowed to.
|
||||
allIds.filter(pl => !plugins[pl].enabled && plugins[pl].update).forEach(pl => fetchPlugin(pl));
|
||||
if (!settings.safeMode?.enabled) {
|
||||
// Loop over any plugin that is enabled, update it if allowed, then start it.
|
||||
await Promise.allSettled(allIds.filter(pl => plugins[pl].enabled).map(async (pl) => (plugins[pl].update && await fetchPlugin(pl), await startPlugin(pl))));
|
||||
// Wait for the above to finish, then update all disabled plugins that are allowed to.
|
||||
allIds.filter(pl => !plugins[pl].enabled && plugins[pl].update).forEach(pl => fetchPlugin(pl));
|
||||
};
|
||||
|
||||
return stopAllPlugins;
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ export default async (unloads: any[]): Promise<VendettaObject> => ({
|
|||
metro: { ...metro, common: { ...common } },
|
||||
constants,
|
||||
utils,
|
||||
debug: utils.without(debug, "versionHash", "patchLogHook"),
|
||||
debug: utils.without(debug, "versionHash", "patchLogHook", "toggleSafeMode"),
|
||||
ui: {
|
||||
components,
|
||||
toasts,
|
||||
|
|
30
src/ui/components/Codeblock.tsx
Normal file
30
src/ui/components/Codeblock.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { CodeblockProps } from "@types";
|
||||
import { ReactNative as RN, stylesheet, constants } from "@metro/common";
|
||||
import { semanticColors } from "@ui/color";
|
||||
|
||||
const styles = stylesheet.createThemedStyleSheet({
|
||||
codeBlock: {
|
||||
fontFamily: constants.Fonts.CODE_SEMIBOLD,
|
||||
fontSize: 12,
|
||||
textAlignVertical: "center",
|
||||
backgroundColor: semanticColors.BACKGROUND_SECONDARY,
|
||||
color: semanticColors.TEXT_NORMAL,
|
||||
borderWidth: 1,
|
||||
borderRadius: 4,
|
||||
borderColor: semanticColors.BACKGROUND_TERTIARY,
|
||||
padding: 10,
|
||||
},
|
||||
});
|
||||
|
||||
// iOS doesn't support the selectable property on RN.Text...
|
||||
const InputBasedCodeblock = ({ style, children }: CodeblockProps) => <RN.TextInput editable={false} multiline style={[styles.codeBlock, style && style]} value={children} />
|
||||
const TextBasedCodeblock = ({ selectable, style, children }: CodeblockProps) => <RN.Text selectable={selectable} style={[styles.codeBlock, style && style]}>{children}</RN.Text>
|
||||
|
||||
export default function Codeblock({ selectable, style, children }: CodeblockProps) {
|
||||
if (!selectable) return <TextBasedCodeblock style={style} children={children} />;
|
||||
|
||||
return RN.Platform.select({
|
||||
ios: <InputBasedCodeblock style={style} children={children} />,
|
||||
default: <TextBasedCodeblock style={style} children={children} selectable />,
|
||||
});
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import { ErrorBoundaryProps } from "@types";
|
||||
import { React, ReactNative as RN, stylesheet, constants } from "@metro/common";
|
||||
import { findByProps } from "@metro/filters";
|
||||
import { Forms } from "@ui/components";
|
||||
import { Forms, Button, Codeblock } from "@ui/components";
|
||||
import { semanticColors } from "@ui/color";
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
|
@ -9,8 +8,6 @@ interface ErrorBoundaryState {
|
|||
errText?: string;
|
||||
}
|
||||
|
||||
const Button = findByProps("Looks", "Colors", "Sizes") as any;
|
||||
|
||||
const styles = stylesheet.createThemedStyleSheet({
|
||||
view: {
|
||||
flex: 1,
|
||||
|
@ -22,15 +19,6 @@ const styles = stylesheet.createThemedStyleSheet({
|
|||
textAlign: "center",
|
||||
marginBottom: 5,
|
||||
},
|
||||
codeblock: {
|
||||
fontFamily: constants.Fonts.CODE_SEMIBOLD,
|
||||
includeFontPadding: false,
|
||||
fontSize: 12,
|
||||
backgroundColor: semanticColors.BACKGROUND_SECONDARY,
|
||||
padding: 5,
|
||||
borderRadius: 5,
|
||||
marginBottom: 5,
|
||||
}
|
||||
});
|
||||
|
||||
export default class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
|
@ -47,7 +35,7 @@ export default class ErrorBoundary extends React.Component<ErrorBoundaryProps, E
|
|||
return (
|
||||
<RN.ScrollView style={styles.view}>
|
||||
<Forms.FormText style={styles.title}>Uh oh.</Forms.FormText>
|
||||
<Forms.FormText style={styles.codeblock}>{this.state.errText}</Forms.FormText>
|
||||
<Codeblock selectable style={{ marginBottom: 5 }}>{this.state.errText}</Codeblock>
|
||||
<Button
|
||||
color={Button.Colors.RED}
|
||||
size={Button.Sizes.MEDIUM}
|
||||
|
|
|
@ -5,7 +5,10 @@ export const Forms = findByProps("Form", "FormSection");
|
|||
export const General = findByProps("Button", "Text", "View");
|
||||
export const Search = findByName("StaticSearchBarContainer");
|
||||
export const Alert = findByProps("alertDarkStyles", "alertLightStyles").default;
|
||||
export const Button = findByProps("Looks", "Colors", "Sizes") as React.ComponentType<any> & { Looks: any, Colors: any, Sizes: any };
|
||||
export const HelpMessage = findByName("HelpMessage");
|
||||
|
||||
// Vendetta
|
||||
export { default as Summary } from "@ui/components/Summary";
|
||||
export { default as ErrorBoundary } from "@ui/components/ErrorBoundary";
|
||||
export { default as Codeblock } from "@ui/components/Codeblock";
|
133
src/ui/safeMode.tsx
Normal file
133
src/ui/safeMode.tsx
Normal file
|
@ -0,0 +1,133 @@
|
|||
import { ButtonColors } from "@types";
|
||||
import { ReactNative as RN, stylesheet } from "@metro/common";
|
||||
import { findByName, findByProps, findByStoreName } from "@metro/filters";
|
||||
import { after } from "@lib/patcher";
|
||||
import { DeviceManager } from "@lib/native";
|
||||
import { toggleSafeMode } from "@lib/debug";
|
||||
import { semanticColors } from "@ui/color";
|
||||
import { Button, Codeblock, ErrorBoundary as _ErrorBoundary } from "@ui/components";
|
||||
import settings from "@lib/settings";
|
||||
|
||||
const ErrorBoundary = findByName("ErrorBoundary");
|
||||
|
||||
// React Native's included SafeAreaView only adds padding on iOS.
|
||||
const { SafeAreaView } = findByProps("useSafeAreaInsets");
|
||||
// Let's just pray they have this.
|
||||
const { BadgableTabBar } = findByProps("BadgableTabBar");
|
||||
|
||||
const ThemeStore = findByStoreName("ThemeStore");
|
||||
|
||||
const { TextStyleSheet } = findByProps("TextStyleSheet");
|
||||
const styles = stylesheet.createThemedStyleSheet({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: semanticColors.BACKGROUND_PRIMARY,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
header: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginVertical: 8,
|
||||
},
|
||||
headerTitle: {
|
||||
...TextStyleSheet["heading-md/semibold"],
|
||||
textAlign: "center",
|
||||
textTransform: "uppercase",
|
||||
color: semanticColors.HEADER_PRIMARY,
|
||||
},
|
||||
headerDescription: {
|
||||
...TextStyleSheet["text-sm/medium"],
|
||||
textAlign: "center",
|
||||
color: semanticColors.TEXT_MUTED,
|
||||
},
|
||||
footer: {
|
||||
flexDirection: DeviceManager.isTablet ? "row" : "column",
|
||||
justifyContent: "flex-end",
|
||||
marginVertical: 8,
|
||||
},
|
||||
});
|
||||
|
||||
interface Tab {
|
||||
id: string;
|
||||
title: string;
|
||||
trimWhitespace?: boolean;
|
||||
}
|
||||
|
||||
interface Button {
|
||||
text: string;
|
||||
// TODO: Proper types for the below
|
||||
color?: string;
|
||||
size?: string;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
const tabs: Tab[] = [
|
||||
{ id: "message",title: "Message" },
|
||||
{ id: "stack", title: "Stack Trace" },
|
||||
{ id: "componentStack", title: "Component", trimWhitespace: true },
|
||||
];
|
||||
|
||||
export default () => after("render", ErrorBoundary.prototype, function (this: any, _, ret) {
|
||||
if (!this.state.error) return;
|
||||
|
||||
// Not using setState here as we don't want to cause a re-render, we want this to be set in the initial render
|
||||
this.state.activeTab ??= "message";
|
||||
const tabData = tabs.find(t => t.id === this.state.activeTab);
|
||||
const errorText: string = this.state.error[this.state.activeTab];
|
||||
|
||||
// This is in the patch and not outside of it so that we can use `this`, e.g. for setting state
|
||||
const buttons: Button[] = [
|
||||
{ text: "Restart Discord", onPress: this.handleReload },
|
||||
...!settings.safeMode?.enabled ? [{ text: "Restart in Safe Mode", onPress: toggleSafeMode }] : [],
|
||||
{ text: "Retry Render", color: ButtonColors.RED, onPress: () => this.setState({ info: null, error: null }) },
|
||||
]
|
||||
|
||||
return (
|
||||
<_ErrorBoundary>
|
||||
<SafeAreaView style={styles.container}>
|
||||
<RN.View style={styles.header}>
|
||||
<RN.Image style={{ flex: 1, resizeMode: "contain", maxHeight: 96, paddingRight: 4 }} source={ThemeStore.theme === "light" ? ret.props.lightSource : ret.props.darkSource} />
|
||||
<RN.View style={{ flex: 2, paddingLeft: 4 }}>
|
||||
<RN.Text style={styles.headerTitle}>{ret.props.title}</RN.Text>
|
||||
<RN.Text style={styles.headerDescription}>{ret.props.body}</RN.Text>
|
||||
</RN.View>
|
||||
</RN.View>
|
||||
<RN.View style={{ flex: 6 }}>
|
||||
<RN.View style={{ paddingBottom: 8 }}>
|
||||
{/* Are errors caught by ErrorBoundary guaranteed to have the component stack? */}
|
||||
<BadgableTabBar
|
||||
tabs={tabs}
|
||||
activeTab={this.state.activeTab}
|
||||
onTabSelected={(tab: string) => { this.setState({ activeTab: tab }) }}
|
||||
/>
|
||||
</RN.View>
|
||||
<Codeblock
|
||||
selectable
|
||||
style={{ flex: 1, textAlignVertical: "top" }}
|
||||
>
|
||||
{/*
|
||||
TODO: I tried to get this working as intended using regex and failed.
|
||||
When trimWhitespace is true, each line should have it's whitespace removed but with it's spaces kept.
|
||||
*/}
|
||||
{tabData?.trimWhitespace ? errorText.split("\n").filter(i => i.length !== 0).map(i => i.trim()).join("\n") : errorText}
|
||||
</Codeblock>
|
||||
</RN.View>
|
||||
<RN.View style={styles.footer}>
|
||||
{buttons.map(button => {
|
||||
const buttonIndex = buttons.indexOf(button) !== 0 ? 8 : 0;
|
||||
|
||||
return <Button
|
||||
text={button.text}
|
||||
color={button.color ?? ButtonColors.BRAND}
|
||||
size={button.size ?? "small"}
|
||||
onPress={button.onPress}
|
||||
style={DeviceManager.isTablet ? { flex: `0.${buttons.length}`, marginLeft: buttonIndex } : { marginTop: buttonIndex }}
|
||||
/>
|
||||
})}
|
||||
</RN.View>
|
||||
</SafeAreaView>
|
||||
</_ErrorBoundary>
|
||||
)
|
||||
});
|
|
@ -48,7 +48,7 @@ interface CardProps {
|
|||
index?: number;
|
||||
headerLabel: string | React.ComponentType;
|
||||
headerIcon?: string;
|
||||
toggleType: "switch" | "radio";
|
||||
toggleType?: "switch" | "radio";
|
||||
toggleValue?: boolean;
|
||||
onToggleChange?: (v: boolean) => void;
|
||||
descriptionLabel?: string | React.ComponentType;
|
||||
|
@ -66,7 +66,7 @@ export default function Card(props: CardProps) {
|
|||
style={styles.header}
|
||||
label={props.headerLabel}
|
||||
leading={props.headerIcon && <FormRow.Icon source={getAssetIDByName(props.headerIcon)} />}
|
||||
trailing={props.toggleType === "switch" ?
|
||||
trailing={props.toggleType && (props.toggleType === "switch" ?
|
||||
(<FormSwitch
|
||||
style={RN.Platform.OS === "android" && { marginVertical: -15 }}
|
||||
value={props.toggleValue}
|
||||
|
@ -80,7 +80,7 @@ export default function Card(props: CardProps) {
|
|||
{/* TODO: Look into making this respect brand color */}
|
||||
<FormRadio selected={props.toggleValue} />
|
||||
</RN.Pressable>)
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<FormRow
|
||||
label={props.descriptionLabel}
|
||||
|
|
|
@ -2,9 +2,11 @@ import { ButtonColors, Theme } from "@types";
|
|||
import { clipboard } from "@metro/common";
|
||||
import { fetchTheme, removeTheme, selectTheme } from "@lib/themes";
|
||||
import { BundleUpdaterManager } from "@lib/native";
|
||||
import { useProxy } from "@lib/storage";
|
||||
import { getAssetIDByName } from "@ui/assets";
|
||||
import { showConfirmationAlert } from "@ui/alerts";
|
||||
import { showToast } from "@ui/toasts";
|
||||
import settings from "@lib/settings";
|
||||
import Card from "@ui/settings/components/Card";
|
||||
|
||||
interface ThemeCardProps {
|
||||
|
@ -18,6 +20,7 @@ async function selectAndReload(value: boolean, id: string) {
|
|||
}
|
||||
|
||||
export default function ThemeCard({ theme, index }: ThemeCardProps) {
|
||||
useProxy(settings);
|
||||
const [removed, setRemoved] = React.useState(false);
|
||||
|
||||
// This is needed because of React™
|
||||
|
@ -30,7 +33,7 @@ export default function ThemeCard({ theme, index }: ThemeCardProps) {
|
|||
index={index}
|
||||
headerLabel={`${theme.data.name} ${authors ? `by ${authors.map(i => i.name).join(", ")}` : ""}`}
|
||||
descriptionLabel={theme.data.description ?? "No description."}
|
||||
toggleType="radio"
|
||||
toggleType={!settings.safeMode?.enabled ? "radio" : undefined}
|
||||
toggleValue={theme.selected}
|
||||
onToggleChange={(v: boolean) => {
|
||||
selectAndReload(v, theme.id);
|
||||
|
|
|
@ -3,6 +3,7 @@ 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 "@utils/findInReactTree";
|
||||
import without from "@utils/without";
|
||||
import ErrorBoundary from "@ui/components/ErrorBoundary";
|
||||
|
@ -13,7 +14,7 @@ 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 { Forms } from "@ui/components";
|
||||
import settings from "@lib/settings";
|
||||
|
||||
const screensModule = findByName("getScreens", false);
|
||||
const settingsModule = findByName("UserSettingsOverviewWrapper", false);
|
||||
|
@ -36,7 +37,7 @@ export default function initSettings() {
|
|||
VendettaThemes: {
|
||||
title: "Themes",
|
||||
render: Themes,
|
||||
headerRight: () => <InstallButton alertTitle="Install Theme" installFunction={installTheme} />,
|
||||
headerRight: !settings.safeMode?.enabled && (() => <InstallButton alertTitle="Install Theme" installFunction={installTheme} />),
|
||||
},
|
||||
VendettaDeveloper: {
|
||||
title: "Developer",
|
||||
|
@ -48,11 +49,10 @@ export default function initSettings() {
|
|||
},
|
||||
VendettaCustomPage: {
|
||||
title: "Vendetta Page",
|
||||
render: ({ render: PageView, ...options }: { render: React.ComponentType } & Record<string, object>) => {
|
||||
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")), []);
|
||||
// TODO: Is wrapping this in ErrorBoundary a good idea?
|
||||
return <ErrorBoundary><PageView /></ErrorBoundary>;
|
||||
React.useEffect(() => options && navigation.setOptions(without(options, "render", "noErrorBoundary")), []);
|
||||
return noErrorBoundary ? <PageView /> : <ErrorBoundary><PageView /></ErrorBoundary>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { ReactNative as RN, NavigationNative } from "@metro/common";
|
||||
import { findByProps } from "@metro/filters";
|
||||
import { Forms } from "@ui/components";
|
||||
import { getAssetIDByName } from "@ui/assets";
|
||||
import { connectToDebugger } from "@lib/debug";
|
||||
|
@ -7,6 +8,8 @@ import settings, { loaderConfig } from "@lib/settings";
|
|||
import ErrorBoundary from "@ui/components/ErrorBoundary";
|
||||
|
||||
const { FormSection, FormRow, FormSwitchRow, FormInput, FormDivider } = Forms;
|
||||
const { hideActionSheet } = findByProps("openLazy", "hideActionSheet");
|
||||
const { showSimpleActionSheet } = findByProps("showSimpleActionSheet");
|
||||
|
||||
export default function Developer() {
|
||||
const navigation = NavigationNative.useNavigation();
|
||||
|
@ -79,6 +82,26 @@ export default function Developer() {
|
|||
trailing={FormRow.Arrow}
|
||||
onPress={() => navigation.push("VendettaAssetBrowser")}
|
||||
/>
|
||||
<FormDivider />
|
||||
<FormRow
|
||||
label="ErrorBoundary Tools"
|
||||
leading={<FormRow.Icon source={getAssetIDByName("ic_warning_24px")} />}
|
||||
trailing={FormRow.Arrow}
|
||||
onPress={() => showSimpleActionSheet({
|
||||
key: "ErrorBoundaryTools",
|
||||
header: {
|
||||
title: "Which ErrorBoundary do you want to trip?",
|
||||
icon: <FormRow.Icon style={{ marginRight: 8 }} source={getAssetIDByName("ic_warning_24px")} />,
|
||||
onClose: () => hideActionSheet(),
|
||||
},
|
||||
options: [
|
||||
// @ts-expect-error
|
||||
// Of course, to trigger an error, we need to do something incorrectly. The below will do!
|
||||
{ label: "Vendetta", onPress: () => navigation.push("VendettaCustomPage", { render: () => <undefined /> }) },
|
||||
{ label: "Discord", isDestructive: true, onPress: () => navigation.push("VendettaCustomPage", { noErrorBoundary: true }) },
|
||||
],
|
||||
})}
|
||||
/>
|
||||
</FormSection>
|
||||
</RN.ScrollView>
|
||||
</ErrorBoundary>
|
||||
|
|
|
@ -2,7 +2,7 @@ import { ReactNative as RN, url } from "@metro/common";
|
|||
import { getAssetIDByName } from "@ui/assets";
|
||||
import { Forms, Summary } from "@ui/components";
|
||||
import { DISCORD_SERVER, GITHUB } from "@lib/constants";
|
||||
import { getDebugInfo } from "@lib/debug";
|
||||
import { getDebugInfo, toggleSafeMode } from "@lib/debug";
|
||||
import { useProxy } from "@lib/storage";
|
||||
import { BundleUpdaterManager } from "@lib/native";
|
||||
import settings from "@lib/settings";
|
||||
|
@ -106,6 +106,13 @@ export default function General() {
|
|||
onPress={() => BundleUpdaterManager.reload()}
|
||||
/>
|
||||
<FormDivider />
|
||||
<FormRow
|
||||
label={settings.safeMode?.enabled ? "Return to Normal Mode" : "Reload in Safe Mode"}
|
||||
subLabel={`This will reload Discord ${settings.safeMode?.enabled ? "normally." : "without loading plugins."}`}
|
||||
leading={<FormRow.Icon source={getAssetIDByName("ic_privacy_24px")} />}
|
||||
onPress={toggleSafeMode}
|
||||
/>
|
||||
<FormDivider />
|
||||
<FormSwitchRow
|
||||
label="Developer Settings"
|
||||
leading={<FormRow.Icon source={getAssetIDByName("ic_progress_wrench_24px")} />}
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
import { ReactNative as RN } from "@metro/common";
|
||||
import { useProxy } from "@lib/storage";
|
||||
import { plugins } from "@lib/plugins";
|
||||
import { HelpMessage } from "@ui/components";
|
||||
import settings from "@lib/settings";
|
||||
import PluginCard from "@ui/settings/components/PluginCard";
|
||||
import ErrorBoundary from "@ui/components/ErrorBoundary";
|
||||
|
||||
export default function Plugins() {
|
||||
useProxy(settings)
|
||||
useProxy(plugins);
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<RN.View style={{ flex: 1 }}>
|
||||
{settings.safeMode?.enabled && <RN.View style={{ margin: 10 }}>
|
||||
<HelpMessage messageType={0}>You are in Safe Mode, so plugins cannot be loaded. Disable any misbehaving plugins, then return to Normal Mode from the General settings page. </HelpMessage>
|
||||
</RN.View>}
|
||||
<RN.FlatList
|
||||
data={Object.values(plugins)}
|
||||
renderItem={({ item, index }) => <PluginCard plugin={item} index={index} />}
|
||||
|
|
|
@ -1,15 +1,30 @@
|
|||
import { themes } from "@/lib/themes";
|
||||
import { useProxy } from "@lib/storage";
|
||||
import { ButtonColors } from "@types";
|
||||
import { ReactNative as RN } from "@metro/common";
|
||||
import ErrorBoundary from "@ui/components/ErrorBoundary";
|
||||
import { themes } from "@lib/themes";
|
||||
import { useProxy } from "@lib/storage";
|
||||
import { ErrorBoundary, Button, HelpMessage } from "@ui/components";
|
||||
import settings from "@lib/settings";
|
||||
import ThemeCard from "@ui/settings/components/ThemeCard";
|
||||
|
||||
export default function Themes() {
|
||||
useProxy(settings);
|
||||
useProxy(themes);
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<RN.View style={{ flex: 1 }}>
|
||||
{settings.safeMode?.enabled && <RN.View style={{ margin: 10 }}>
|
||||
<HelpMessage messageType={0}>You are in Safe Mode, meaning themes have been temporarily disabled.{settings.safeMode?.currentThemeId && " If a theme appears to be causing the issue, you can press below to disable it persistently."}</HelpMessage>
|
||||
{settings.safeMode?.currentThemeId && <Button
|
||||
text="Disable Theme"
|
||||
color={ButtonColors.BRAND}
|
||||
size="small"
|
||||
onPress={() => {
|
||||
delete settings.safeMode?.currentThemeId;
|
||||
}}
|
||||
style={{ marginTop: 8 }}
|
||||
/>}
|
||||
</RN.View>}
|
||||
<RN.FlatList
|
||||
data={Object.values(themes)}
|
||||
renderItem={({ item, index }) => <ThemeCard theme={item} index={index} />}
|
||||
|
|
Loading…
Reference in a new issue