real
This commit is contained in:
parent
0e48cbcb9e
commit
3bdf403264
15 changed files with 400 additions and 221 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
};
|
||||
|
|
@ -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>
|
||||
</>}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue