This commit is contained in:
amsyarasyiq 2023-12-21 03:30:21 +08:00
parent 0e48cbcb9e
commit 3bdf403264
15 changed files with 400 additions and 221 deletions

View file

@ -9,6 +9,7 @@ import initSettings from "@ui/settings";
import initFixes from "@lib/fixes";
import logger from "@lib/logger";
import windowObject from "@lib/windowObject";
import settings from "./lib/settings";
export default async () => {
// Load everything in parallel

View file

@ -42,7 +42,7 @@ export default function InputAlert({ title, confirmText, confirmColor, onConfirm
secureTextEntry={secureTextEntry}
autoFocus={true}
showBorder={true}
style={{ paddingVertical: 5, alignSelf: "stretch", paddingHorizontal: 0 }}
style={{ alignSelf: "stretch" }}
/>
</Alert>
);

View file

@ -1,17 +1,61 @@
import { SearchProps } from "@types";
import { stylesheet } from "@metro/common";
import { findByName } from "@metro/filters";
import { ReactNative as RN } from "@metro/common";
import { findByProps } from "@metro/filters";
import ErrorBoundary from "./ErrorBoundary";
import { getAssetIDByName } from "../assets";
const Search = findByName("StaticSearchBarContainer");
const { FormIcon } = findByProps("FormIcon");
const { TextInput } = findByProps("TableRow");
const styles = stylesheet.createThemedStyleSheet({
search: {
margin: 0,
padding: 0,
borderBottomWidth: 0,
background: "none",
backgroundColor: "none",
const props = {
"placeholder": "Search",
"returnKeyType": "search",
"size": "md",
"onFocus": "ƒ () {}",
"value": "",
"grow": true,
"autoCorrect": false,
"autoCapitalize": "none",
"accessibilityRole": "search",
"leadingIcon": "ƒ () {}",
"isClearable": true,
"onChangeText": "ƒ () {}",
"editable": true,
"onBlur": "ƒ () {}",
"style": [
"{padding: 12, paddingTop: 12}",
"{color: \"#e4e5e8\", fontFamily: \"ggsans-Medium, Noto…}",
"{paddingEnd: undefined, paddingStart: 36.3636360168…}",
"{padding: 12, paddingTop: 12}",
"{color: \"#e4e5e8\", fontFamily: \"ggsans-Medium, Noto…}",
"{paddingEnd: undefined, paddingStart: 36.3636360168…}"
],
"placeholderTextColor": "#828391"
};
function SearchIcon() {
return <RN.Image style={{ transform: [{ scale: 0.8 }]}} source={getAssetIDByName("search")} />
}
export default ({ onChangeText, placeholder, style }: SearchProps) => {
const [query, setQuery] = React.useState("");
const onChange = (value: string) => {
setQuery(value);
onChangeText?.(value);
}
});
export default ({ onChangeText, placeholder, style }: SearchProps) => <Search style={[styles.search, style]} placeholder={placeholder} onChangeText={onChangeText} />
return <ErrorBoundary>
<RN.View style={style}>
<TextInput isClearable grow
leadingIcon={SearchIcon}
placeholder={placeholder ?? "Search"}
onChange={onChange}
returnKeyType={"search"}
size="md"
autoCapitalize="none"
autoCorrect={false}
value={query}
/>
</RN.View>
</ErrorBoundary>
};

View file

@ -1,17 +1,19 @@
import { SummaryProps } from "@types";
import { ReactNative as RN } from "@metro/common";
import { getAssetIDByName } from "@ui/assets";
import { Forms } from "@ui/components";
import { findByProps } from "@/lib/metro/filters";
const { FormRow } = findByProps("FormRow");
const { TableRow } = findByProps("TableRow");
export default function Summary({ label, icon, noPadding = false, noAnimation = false, children }: SummaryProps) {
const { FormRow, FormDivider } = Forms;
const [hidden, setHidden] = React.useState(true);
return (
<>
<FormRow
<TableRow
label={label}
leading={icon && <FormRow.Icon source={getAssetIDByName(icon)} />}
icon={icon && <TableRow.Icon source={getAssetIDByName(icon)} />}
trailing={<FormRow.Arrow style={{ transform: [{ rotate: `${hidden ? 180 : 90}deg` }] }} />}
onPress={() => {
setHidden(!hidden);
@ -19,7 +21,6 @@ export default function Summary({ label, icon, noPadding = false, noAnimation =
}}
/>
{!hidden && <>
<FormDivider />
<RN.View style={!noPadding && { paddingHorizontal: 15 }}>{children}</RN.View>
</>}
</>

View file

@ -8,7 +8,7 @@ export const Alert = findByDisplayName("FluxContainer(Alert)");
export const Button = findByProps("Looks", "Colors", "Sizes") as React.ComponentType<any> & { Looks: any, Colors: any, Sizes: any };
export const HelpMessage = findByName("HelpMessage");
// React Native's included SafeAreaView only adds padding on iOS.
export const SafeAreaView = findByProps("useSafeAreaInsets").SafeAreaView as typeof RN.SafeAreaView;
export const SafeAreaView = findByProps("useSafeAreaInsets").SafeAreaView as any;
// Vendetta
export { default as Summary } from "@ui/components/Summary";

View file

@ -1,58 +1,129 @@
import { findByName, findByProps } from "@metro/filters";
import { DISCORD_SERVER_ID, PLUGINS_CHANNEL_ID, THEMES_CHANNEL_ID, HTTP_REGEX_MULTI, PROXY_PREFIX } from "@lib/constants";
import { after } from "@lib/patcher";
import { installPlugin } from "@lib/plugins";
import { installTheme } from "@lib/themes";
import { installPlugin, plugins, removePlugin } from "@lib/plugins";
import { installTheme, removeTheme, themes } from "@lib/themes";
import { findInReactTree } from "@lib/utils";
import { getAssetIDByName } from "@ui/assets";
import { showToast } from "@ui/toasts";
import { Forms } from "@ui/components";
import { useProxy } from "@/lib/storage";
type PostType = "Plugin" | "Theme";
const ForumPostLongPressActionSheet = findByName("ForumPostLongPressActionSheet", false);
const { FormRow, FormIcon } = Forms;
const { ActionSheetRow } = findByProps("ActionSheetRow");
const { Button } = findByProps("TableRow");
const { useFirstForumPostMessage } = findByProps("useFirstForumPostMessage");
const { hideActionSheet } = findByProps("openLazy", "hideActionSheet");
const forumReactions = findByProps("MostCommonForumPostReaction");
export default () => after("default", ForumPostLongPressActionSheet, ([{ thread }], res) => {
const postMap = {
Plugin: {
storage: plugins,
urlsFilter: (url: string) => url.startsWith(PROXY_PREFIX),
installOrRemove: (url: string) => {
const isInstalled = postMap.Plugin.storage[url];
return isInstalled ? removePlugin(url) : installPlugin(url);
}
},
Theme: {
storage: themes,
urlsFilter: (url: string) => url.endsWith(".json"),
installOrRemove: (url: string) => {
const isInstalled = postMap.Theme.storage[url];
return isInstalled ? removeTheme(url) : installTheme(url);
},
}
}
function useExtractThreadContent(thread: any, _firstMessage = null, actionSheet = false): ([PostType, string]) | void {
if (thread.guild_id !== DISCORD_SERVER_ID) return;
// Determine what type of addon this is.
let postType: "Plugin" | "Theme";
let postType: PostType;
if (thread.parent_id === PLUGINS_CHANNEL_ID) {
postType = "Plugin";
} else if (thread.parent_id === THEMES_CHANNEL_ID && window.__vendetta_loader?.features.themes) {
postType = "Theme";
} else return;
const { firstMessage } = useFirstForumPostMessage(thread);
const { firstMessage } = actionSheet ? useFirstForumPostMessage(thread) : { firstMessage: _firstMessage };
let urls = firstMessage?.content?.match(HTTP_REGEX_MULTI);
if (!urls) return;
let urls = firstMessage?.content?.match(HTTP_REGEX_MULTI)?.filter(postMap[postType].urlsFilter);
if (!urls || !urls[0]) return;
if (postType === "Plugin") {
urls = urls.filter((url: string) => url.startsWith(PROXY_PREFIX));
} else {
urls = urls.filter((url: string) => url.endsWith(".json"));
// Sync with lib/plugins
if (postType === "Plugin" && !urls[0].endsWith("/")) urls[0] += "/";
return [postType, urls[0]];
}
function useInstaller(thread: any, firstMessage = null, actionSheet = false): [true] | [false, PostType, boolean, boolean, () => Promise<void>] {
const [postType, url] = useExtractThreadContent(thread, firstMessage, actionSheet) ?? [];
useProxy(plugins);
useProxy(themes);
const [isInstalling, setIsInstalling] = React.useState(false);
if (!postType || !url) return [true];
const isInstalled = Boolean(postMap[postType].storage[url]);
const installOrRemove = async () => {
setIsInstalling(true);
try {
await postMap[postType].installOrRemove(url);
} catch (e: any) {
showToast(e.message, getAssetIDByName("Small"));
} finally {
setIsInstalling(false);
}
};
const url = urls[0];
if (!url) return;
return [false, postType, isInstalled, isInstalling, installOrRemove]
}
const actionSheetPatch = () => after("default", ForumPostLongPressActionSheet, ([{ thread }], res) => {
const [shouldReturn, postType, installed, loading, installOrRemove] = useInstaller(thread);
if (shouldReturn) return;
const actions = findInReactTree(res, (t) => t?.[0]?.key);
const ActionsSection = actions[0].type;
actions.unshift(<ActionsSection key="install">
<FormRow
leading={<FormIcon style={{ opacity: 1 }} source={getAssetIDByName("ic_download_24px")} />}
label={`Install ${postType}`}
onPress={() =>
(postType === "Plugin" ? installPlugin : installTheme)(url).then(() => {
showToast(`Successfully installed ${thread.name}`, getAssetIDByName("Check"));
}).catch((e: Error) => {
showToast(e.message, getAssetIDByName("Small"));
}).finally(() => hideActionSheet())
}
<ActionSheetRow
icon={<ActionSheetRow.Icon source={getAssetIDByName(installed ? "ic_message_delete" : "DownloadIcon")} />}
label={`${installed ? "Uninstall" : "Install"} ${postType}`}
disabled={loading}
onPress={installOrRemove}
/>
</ActionsSection>);
});
const installButtonPatch = () => after("MostCommonForumPostReaction", forumReactions, ([{ thread, firstMessage }], res) => {
const [shouldReturn, _, installed, loading, installOrRemove] = useInstaller(thread, firstMessage, true);
if (shouldReturn) return;
return <>
{res}
<Button
size="sm"
loading={loading}
disabled={loading}
variant={installed ? "danger" : "primary"}
text={installed ? "Uninstall" : "Install"}
onPress={installOrRemove}
icon={getAssetIDByName(installed ? "ic_message_delete" : "DownloadIcon")}
style={{ marginLeft: 8 }}
/>
</>
});
export default () => {
const patches = [
actionSheetPatch(),
installButtonPatch()
]
return () => patches.map(p => p());
}

View file

@ -81,13 +81,13 @@ export default () => after("render", ErrorBoundary.prototype, function (this: an
...!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 }}>
<ret.props.Illustration style={{ transform: [{ scale: 0.6 }], marginLeft: -40, marginRight: -80 }} />
<RN.View style={{ flex: 2, paddingLeft: 24 }}>
<RN.Text style={styles.headerTitle}>{ret.props.title}</RN.Text>
<RN.Text style={styles.headerDescription}>{ret.props.body}</RN.Text>
</RN.View>

View file

@ -1,19 +1,32 @@
import { ReactNative as RN } from "@metro/common";
import { NavigationNative, ReactNative as RN, clipboard } from "@metro/common";
import { useProxy } from "@lib/storage";
import { HelpMessage, ErrorBoundary, Search } from "@ui/components";
import { CardWrapper } from "@ui/settings/components/Card";
import settings from "@lib/settings";
import { findByProps } from "@/lib/metro/filters";
import { showToast } from "@/ui/toasts";
import { HTTP_REGEX_MULTI } from "@/lib/constants";
import { showInputAlert } from "@/ui/alerts";
import { getAssetIDByName } from "@/ui/assets";
interface AddonPageProps<T> {
title: string;
fetchFunction: (url: string) => Promise<void>;
items: Record<string, T & { id: string }>;
safeModeMessage: string;
safeModeExtras?: JSX.Element | JSX.Element[];
card: React.ComponentType<CardWrapper<T>>;
}
export default function AddonPage<T>({ items, safeModeMessage, safeModeExtras, card: CardComponent }: AddonPageProps<T>) {
const reanimated = findByProps("useSharedValue");
const { FloatingActionButton } = findByProps("FloatingActionButton");
export default function AddonPage<T>({ title, fetchFunction, items, safeModeMessage, safeModeExtras, card: CardComponent }: AddonPageProps<T>) {
useProxy(settings)
useProxy(items);
const collapseText = reanimated.useSharedValue(0);
const yOffset = React.useRef<number>(0);
const [search, setSearch] = React.useState("");
return (
@ -26,16 +39,40 @@ export default function AddonPage<T>({ items, safeModeMessage, safeModeExtras, c
{safeModeExtras}
</RN.View>}
<Search
style={{ marginBottom: 10 }}
style={{ marginBottom: 15 }}
onChangeText={(v: string) => setSearch(v.toLowerCase())}
placeholder="Search"
/>
</>}
onScroll={e => {
if (e.nativeEvent.contentOffset.y <= 0) return;
collapseText.value = Number(e.nativeEvent.contentOffset.y > yOffset.current);
yOffset.current = e.nativeEvent.contentOffset.y;
}}
onMomentumScrollEnd={() => collapseText.value = 0}
style={{ paddingHorizontal: 10, paddingTop: 10 }}
contentContainerStyle={{ paddingBottom: 20 }}
contentContainerStyle={{ paddingBottom: 70 }}
data={Object.values(items).filter(i => i.id?.toLowerCase().includes(search))}
renderItem={({ item, index }) => <CardComponent item={item} index={index} />}
/>
<FloatingActionButton
text={`Install ${title}`}
icon={getAssetIDByName("DownloadIcon")}
state={{ collapseText }}
onPress={() => {
// from ./InstallButton.tsx
clipboard.getString().then((content) =>
showInputAlert({
title: `Install ${title}`,
initialValue: content.match(HTTP_REGEX_MULTI)?.[0] ?? "",
placeholder: "https://example.com/",
onConfirm: (input: string) => fetchFunction(input),
confirmText: "Install",
cancelText: "Cancel",
})
)
}}
/>
</ErrorBoundary>
)
}

View file

@ -4,7 +4,8 @@ import { getAssetIDByName } from "@ui/assets";
import { semanticColors } from "@ui/color";
import { Forms } from "@ui/components";
const { FormRow, FormSwitch, FormRadio } = Forms;
const { TableRow, TableRowIcon, TableSwitchRow, TableCheckboxRow, TableRowGroup } = findByProps("TableRow");
const { FormSwitch, FormRadio } = Forms;
const { hideActionSheet } = findByProps("openLazy", "hideActionSheet");
const { showSimpleActionSheet } = findByProps("showSimpleActionSheet");
@ -12,19 +13,18 @@ const { showSimpleActionSheet } = findByProps("showSimpleActionSheet");
const styles = stylesheet.createThemedStyleSheet({
card: {
backgroundColor: semanticColors?.BACKGROUND_SECONDARY,
borderRadius: 5,
borderRadius: 15,
overflow: "hidden"
},
header: {
padding: 0,
backgroundColor: semanticColors?.BACKGROUND_TERTIARY,
borderTopLeftRadius: 5,
borderTopRightRadius: 5,
},
actions: {
flexDirection: "row-reverse",
alignItems: "center",
},
icon: {
actionIcon: {
width: 22,
height: 22,
marginLeft: 5,
@ -63,29 +63,29 @@ interface CardProps {
export default function Card(props: CardProps) {
let pressableState = props.toggleValue ?? false;
return (
const headerProps = {
style: styles.header,
label: props.headerLabel,
icon: props.headerIcon && <TableRowIcon source={getAssetIDByName(props.headerIcon)} />
};
return (
<RN.View style={[styles.card, { marginTop: props.index !== 0 ? 10 : 0 }]}>
<FormRow
style={styles.header}
label={props.headerLabel}
leading={props.headerIcon && <FormRow.Icon source={getAssetIDByName(props.headerIcon)} />}
trailing={props.toggleType && (props.toggleType === "switch" ?
(<FormSwitch
style={RN.Platform.OS === "android" && { marginVertical: -15 }}
value={props.toggleValue}
onValueChange={props.onToggleChange}
/>)
:
(<RN.Pressable onPress={() => {
<TableRowGroup>
{props.toggleType && (props.toggleType === "switch" ?
<TableSwitchRow
{...headerProps}
value={props.toggleValue}
onValueChange={props.onToggleChange}
/> : <TableCheckboxRow
{...headerProps}
onPress={() => {
pressableState = !pressableState;
props.onToggleChange?.(pressableState)
}}>
{/* TODO: Look into making this respect brand color */}
<FormRadio selected={props.toggleValue} />
</RN.Pressable>)
)}
/>
<FormRow
}}
checked={props.toggleValue}
/>)}
<TableRow
label={props.descriptionLabel}
trailing={
<RN.View style={styles.actions}>
@ -94,24 +94,25 @@ export default function Card(props: CardProps) {
key: "CardOverflow",
header: {
title: props.overflowTitle,
icon: props.headerIcon && <FormRow.Icon style={{ marginRight: 8 }} source={getAssetIDByName(props.headerIcon)} />,
icon: props.headerIcon && <TableRowIcon style={{ marginRight: 8 }} source={getAssetIDByName(props.headerIcon)} />,
onClose: () => hideActionSheet(),
},
options: props.overflowActions?.map(i => ({ ...i, icon: getAssetIDByName(i.icon) })),
})}
>
<RN.Image style={styles.icon} source={getAssetIDByName("ic_more_24px")} />
<RN.Image style={styles.actionIcon} source={getAssetIDByName("ic_more_24px")} />
</RN.TouchableOpacity>}
{props.actions?.map(({ icon, onPress }) => (
<RN.TouchableOpacity
onPress={onPress}
>
<RN.Image style={styles.icon} source={getAssetIDByName(icon)} />
<RN.Image style={styles.actionIcon} source={getAssetIDByName(icon)} />
</RN.TouchableOpacity>
))}
</RN.View>
}
/>
</TableRowGroup>
</RN.View>
)
}

View file

@ -2,20 +2,24 @@ import { clipboard } from "@metro/common";
import { getAssetIDByName } from "@ui/assets";
import { showToast } from "@ui/toasts";
import { Forms } from "@ui/components";
import { findByProps } from "@/lib/metro/filters";
interface VersionProps {
label: string;
version: string;
icon: string;
padding: boolean;
}
const { FormRow, FormText } = Forms;
const { TableRow } = findByProps("TableRow");
const { FormText } = Forms;
export default function Version({ label, version, icon }: VersionProps) {
export default function Version({ label, version, icon, padding }: VersionProps) {
return (
<FormRow
<TableRow
style={padding && { paddingHorizontal: 15, paddingVertical: -15 }}
label={label}
leading={<FormRow.Icon source={getAssetIDByName(icon)} />}
icon={<TableRow.Icon source={getAssetIDByName(icon)} />}
trailing={<FormText>{version}</FormText>}
onPress={() => {
clipboard.setString(`${label} - ${version}`);

View file

@ -14,6 +14,9 @@ import Plugins from "@ui/settings/pages/Plugins";
import Themes from "@ui/settings/pages/Themes";
import Developer from "@ui/settings/pages/Developer";
import { PROXY_PREFIX } from "@/lib/constants";
import { findByProps } from "@/lib/metro/filters";
const { useSafeAreaInsets } = findByProps("useSafeAreaInsets");
interface Screen {
[index: string]: any;
@ -25,7 +28,13 @@ interface Screen {
render: React.ComponentType<any>;
}
const styles = stylesheet.createThemedStyleSheet({ container: { flex: 1, backgroundColor: semanticColors.BACKGROUND_MOBILE_PRIMARY } });
const styles = stylesheet.createThemedStyleSheet({
container: {
flex: 1,
backgroundColor: semanticColors.BACKGROUND_MOBILE_PRIMARY
}
});
const formatKey = (key: string, youKeys: boolean) => youKeys ? lodash.snakeCase(key).toUpperCase() : key;
// 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?
@ -119,8 +128,9 @@ export const getYouData = () => {
relationships: keyMap(screens, null),
rendererConfigs: keyMap(screens, (s) => {
const WrappedComponent = React.memo(({ navigation, route }: any) => {
const { bottom: paddingBottom } = useSafeAreaInsets();
navigation.addListener("focus", () => navigation.setOptions(s.options));
return <RN.View style={styles.container}><s.render {...route.params} /></RN.View>
return <RN.View style={[{ paddingBottom }, styles.container]}><s.render {...route.params} /></RN.View>
});
return {

View file

@ -1,15 +1,26 @@
import { ReactNative as RN, NavigationNative } from "@metro/common";
import { ReactNative as RN, NavigationNative, stylesheet } from "@metro/common";
import { findByProps } from "@metro/filters";
import { connectToDebugger } from "@lib/debug";
import { useProxy } from "@lib/storage";
import { getAssetIDByName } from "@ui/assets";
import { Forms, ErrorBoundary } from "@ui/components";
import { ErrorBoundary, Forms } from "@ui/components";
import settings, { loaderConfig } from "@lib/settings";
import AssetBrowser from "@ui/settings/pages/AssetBrowser";
import { semanticColors } from "@ui/color";
const { FormSection, FormRow, FormSwitchRow, FormInput, FormDivider } = Forms;
const { Stack, TableRow, TableSwitchRow, TableRowGroup, TextInput } = findByProps("TableRow");
const { FormText } = Forms;
const { hideActionSheet } = findByProps("openLazy", "hideActionSheet");
const { showSimpleActionSheet } = findByProps("showSimpleActionSheet");
const { TextStyleSheet } = findByProps("TextStyleSheet");
const styles = stylesheet.createThemedStyleSheet({
leadingText: {
...TextStyleSheet["heading-md/semibold"],
color: semanticColors.TEXT_MUTED,
marginRight: -4
},
});
export default function Developer() {
const navigation = NavigationNative.useNavigation();
@ -20,92 +31,93 @@ export default function Developer() {
return (
<ErrorBoundary>
<RN.ScrollView style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: 38 }}>
<FormSection title="Debug" titleStyleType="no_border">
<FormInput
value={settings.debuggerUrl}
onChange={(v: string) => settings.debuggerUrl = v}
<Stack style={{ paddingVertical: 24, paddingHorizontal: 12 }} spacing={24}>
<TextInput
label="Debugger URL"
placeholder="127.0.0.1:9090"
title="DEBUGGER URL"
size="md"
leadingIcon={() => <FormText style={styles.leadingText}>ws://</FormText>}
defaultValue={settings.debuggerUrl}
onChange={(v: string) => settings.debuggerUrl = v}
/>
<FormDivider />
<FormRow
label="Connect to debug websocket"
leading={<FormRow.Icon source={getAssetIDByName("copy")} />}
onPress={() => connectToDebugger(settings.debuggerUrl)}
/>
{window.__vendetta_rdc && <>
<FormDivider />
<FormRow
label="Connect to React DevTools"
leading={<FormRow.Icon source={getAssetIDByName("ic_badge_staff")} />}
onPress={() => window.__vendetta_rdc?.connectToDevTools({
host: settings.debuggerUrl.split(":")?.[0],
resolveRNStyle: RN.StyleSheet.flatten,
<TableRowGroup title="Debug">
<TableRow
label="Connect to debug websocket"
icon={<TableRow.Icon source={getAssetIDByName("copy")} />}
onPress={() => connectToDebugger(settings.debuggerUrl)}
/>
{window.__vendetta_rdc && <>
<TableRow
label="Connect to React DevTools"
icon={<TableRow.Icon source={getAssetIDByName("ic_badge_staff")} />}
onPress={() => window.__vendetta_rdc?.connectToDevTools({
host: settings.debuggerUrl.split(":")?.[0],
resolveRNStyle: RN.StyleSheet.flatten,
})}
/>
</>}
</TableRowGroup>
{window.__vendetta_loader?.features.loaderConfig && <>
<TableRowGroup title="Loader config">
<TableSwitchRow
label="Load from custom url"
subLabel={"Load Vendetta from a custom endpoint."}
icon={<TableRow.Icon source={getAssetIDByName("copy")} />}
value={loaderConfig.customLoadUrl.enabled}
onValueChange={(v: boolean) => {
loaderConfig.customLoadUrl.enabled = v;
}}
/>
{loaderConfig.customLoadUrl.enabled && <TableRow label={<TextInput
defaultValue={loaderConfig.customLoadUrl.url}
size="md"
onChange={(v: string) => loaderConfig.customLoadUrl.url = v}
placeholder="http://localhost:4040/vendetta.js"
label="Vendetta URL"
/>} />}
{window.__vendetta_loader.features.devtools && <TableSwitchRow
label="Load React DevTools"
subLabel={`Version: ${window.__vendetta_loader.features.devtools.version}`}
icon={<TableRow.Icon source={getAssetIDByName("ic_badge_staff")} />}
value={loaderConfig.loadReactDevTools}
onValueChange={(v: boolean) => {
loaderConfig.loadReactDevTools = v;
}}
/>}
</TableRowGroup>
</>}
<TableRowGroup title="Other">
<TableRow
arrow
label="Asset Browser"
icon={<TableRow.Icon source={getAssetIDByName("ic_image")} />}
trailing={TableRow.Arrow}
onPress={() => navigation.push("VendettaCustomPage", {
title: "Asset Browser",
render: AssetBrowser,
})}
/>
</>}
</FormSection>
{window.__vendetta_loader?.features.loaderConfig && <FormSection title="Loader config">
<FormSwitchRow
label="Load from custom url"
subLabel={"Load Vendetta from a custom endpoint."}
leading={<FormRow.Icon source={getAssetIDByName("copy")} />}
value={loaderConfig.customLoadUrl.enabled}
onValueChange={(v: boolean) => {
loaderConfig.customLoadUrl.enabled = v;
}}
/>
<FormDivider />
{loaderConfig.customLoadUrl.enabled && <>
<FormInput
value={loaderConfig.customLoadUrl.url}
onChange={(v: string) => loaderConfig.customLoadUrl.url = v}
placeholder="http://localhost:4040/vendetta.js"
title="VENDETTA URL"
<TableRow
arrow
label="ErrorBoundary Tools"
icon={<TableRow.Icon source={getAssetIDByName("ic_warning_24px")} />}
onPress={() => showSimpleActionSheet({
key: "ErrorBoundaryTools",
header: {
title: "Which ErrorBoundary do you want to trip?",
icon: <TableRow.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 }) },
],
})}
/>
<FormDivider />
</>}
{window.__vendetta_loader.features.devtools && <FormSwitchRow
label="Load React DevTools"
subLabel={`Version: ${window.__vendetta_loader.features.devtools.version}`}
leading={<FormRow.Icon source={getAssetIDByName("ic_badge_staff")} />}
value={loaderConfig.loadReactDevTools}
onValueChange={(v: boolean) => {
loaderConfig.loadReactDevTools = v;
}}
/>}
</FormSection>}
<FormSection title="Other">
<FormRow
label="Asset Browser"
leading={<FormRow.Icon source={getAssetIDByName("ic_image")} />}
trailing={FormRow.Arrow}
onPress={() => navigation.push("VendettaCustomPage", {
title: "Asset Browser",
render: AssetBrowser,
})}
/>
<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>
</TableRowGroup>
</Stack>
</RN.ScrollView>
</ErrorBoundary>
)

View file

@ -1,15 +1,23 @@
import { ReactNative as RN, url } from "@metro/common";
import { ReactNative as RN, stylesheet, url } from "@metro/common";
import { DISCORD_SERVER, GITHUB } from "@lib/constants";
import { getDebugInfo, toggleSafeMode } from "@lib/debug";
import { useProxy } from "@lib/storage";
import { BundleUpdaterManager } from "@lib/native";
import { getAssetIDByName } from "@ui/assets";
import { Forms, Summary, ErrorBoundary } from "@ui/components";
import { Summary } from "@ui/components";
import settings from "@lib/settings";
import Version from "@ui/settings/components/Version";
import { findByProps } from "@lib/metro/filters";
import { semanticColors } from "@ui/color";
const { FormRow, FormSwitchRow, FormSection, FormDivider } = Forms;
const debugInfo = getDebugInfo();
const { Stack, TableRow, TableSwitchRow, TableRowGroup } = findByProps("TableRow");
const debugInfo = getDebugInfo() as any;
const styles = stylesheet.createThemedStyleSheet({
container: {
flex: 1,
backgroundColor: semanticColors.BACKGROUND_MOBILE_SECONDARY,
}
})
export default function General() {
useProxy(settings);
@ -56,7 +64,7 @@ export default function General() {
...(debugInfo.os.sdk ? [{
label: "SDK",
version: debugInfo.os.sdk,
icon: "ic_profile_badge_verified_developer_color"
icon: "pencil"
}] : []),
{
label: "Manufacturer",
@ -81,66 +89,52 @@ export default function General() {
];
return (
<ErrorBoundary>
<RN.ScrollView style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: 38 }}>
<FormSection title="Links" titleStyleType="no_border">
<FormRow
<RN.ScrollView style={styles.container}>
<Stack style={{ paddingVertical: 24, paddingHorizontal: 12 }} spacing={24}>
<TableRowGroup title="Links">
<TableRow
label="Discord Server"
leading={<FormRow.Icon source={getAssetIDByName("Discord")} />}
trailing={FormRow.Arrow}
icon={<TableRow.Icon source={getAssetIDByName("Discord")} />}
trailing={TableRow.Arrow}
onPress={() => url.openDeeplink(DISCORD_SERVER)}
/>
<FormDivider />
<FormRow
<TableRow
label="GitHub"
leading={<FormRow.Icon source={getAssetIDByName("img_account_sync_github_white")} />}
trailing={FormRow.Arrow}
icon={<TableRow.Icon source={getAssetIDByName("img_account_sync_github_white")} />}
trailing={TableRow.Arrow}
onPress={() => url.openURL(GITHUB)}
/>
</FormSection>
<FormSection title="Actions">
<FormRow
</TableRowGroup>
<TableRowGroup title="Actions">
<TableRow
label="Reload Discord"
leading={<FormRow.Icon source={getAssetIDByName("ic_message_retry")} />}
onPress={() => BundleUpdaterManager.reload()}
icon={<TableRow.Icon source={getAssetIDByName("ic_message_retry")} />}
onPress={() => RN.NativeModules.BundleUpdaterManager.reload()}
/>
<FormDivider />
<FormRow
<TableRow
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")} />}
subLabel={`This will reload Discord ${settings.safeMode?.enabled ? "normally." : "without loading addons."}`}
icon={<TableRow.Icon source={getAssetIDByName("ic_privacy_24px")} />}
onPress={toggleSafeMode}
/>
<FormDivider />
<FormSwitchRow
<TableSwitchRow
label="Developer Settings"
leading={<FormRow.Icon source={getAssetIDByName("ic_progress_wrench_24px")} />}
icon={<TableRow.Icon source={getAssetIDByName("ic_progress_wrench_24px")} />}
value={settings.developerSettings}
onValueChange={(v: boolean) => {
settings.developerSettings = v;
}}
/>
</FormSection>
<FormSection title="Info">
<Summary label="Versions" icon="ic_information_filled_24px">
{versions.map((v, i) => (
<>
<Version label={v.label} version={v.version} icon={v.icon} />
{i !== versions.length - 1 && <FormDivider />}
</>
))}
</TableRowGroup>
<TableRowGroup title="Info">
<Summary noPadding label="Versions" icon="ic_information_filled_24px">
{versions.map(v => <Version padding label={v.label} version={v.version} icon={v.icon} />)}
</Summary>
<FormDivider />
<Summary label="Platform" icon="ic_mobile_device">
{platformInfo.map((p, i) => (
<>
<Version label={p.label} version={p.version} icon={p.icon} />
{i !== platformInfo.length - 1 && <FormDivider />}
</>
))}
<Summary noPadding label="Platform" icon="ic_mobile_device">
{platformInfo.map(p => <Version padding label={p.label} version={p.version} icon={p.icon} />)}
</Summary>
</FormSection>
</RN.ScrollView>
</ErrorBoundary>
</TableRowGroup>
</Stack>
</RN.ScrollView>
)
}

View file

@ -1,6 +1,6 @@
import { Plugin } from "@types";
import { useProxy } from "@lib/storage";
import { plugins } from "@lib/plugins";
import { installPlugin, plugins } from "@lib/plugins";
import settings from "@lib/settings";
import AddonPage from "@ui/settings/components/AddonPage";
import PluginCard from "@ui/settings/components/PluginCard";
@ -10,6 +10,8 @@ export default function Plugins() {
return (
<AddonPage<Plugin>
title="Plugin"
fetchFunction={installPlugin}
items={plugins}
safeModeMessage="You are in Safe Mode, so plugins cannot be loaded. Disable any misbehaving plugins, then return to Normal Mode from the General settings page."
card={PluginCard}

View file

@ -1,6 +1,6 @@
import { Theme, ButtonColors } from "@types";
import { useProxy } from "@lib/storage";
import { themes } from "@lib/themes";
import { installTheme, themes } from "@lib/themes";
import { Button } from "@ui/components";
import settings from "@lib/settings";
import AddonPage from "@ui/settings/components/AddonPage";
@ -11,6 +11,8 @@ export default function Themes() {
return (
<AddonPage<Theme>
title={"Themes"}
fetchFunction={installTheme}
items={themes}
safeModeMessage={`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." : ""}`}
safeModeExtras={settings.safeMode?.currentThemeId ? <Button