Merge remote-tracking branch 'upstream/dev' into dev

This commit is contained in:
kmmiio99o 2025-12-07 15:49:23 +01:00
commit 2a8496613b
3 changed files with 277 additions and 411 deletions

View file

@ -4,337 +4,244 @@ import AssetBrowser from "@core/ui/settings/pages/Developer/AssetBrowser";
import { useProxy } from "@core/vendetta/storage";
import { findAssetId } from "@lib/api/assets";
import { connectToDebugger } from "@lib/api/debug";
import {
getReactDevToolsProp,
getReactDevToolsVersion,
isLoaderConfigSupported,
isReactDevToolsPreloaded,
isVendettaLoader,
} from "@lib/api/native/loader";
import { getReactDevToolsProp, getReactDevToolsVersion, isLoaderConfigSupported, isReactDevToolsPreloaded, isVendettaLoader } from "@lib/api/native/loader";
import { loaderConfig, settings } from "@lib/api/settings";
import { lazyDestructure } from "@lib/utils/lazy";
import { NavigationNative } from "@metro/common";
import {
Button,
LegacyFormText,
Stack,
TableRow,
TableRowGroup,
TableSwitchRow,
TextInput,
} from "@metro/common/components";
import { Button, LegacyFormText, Stack, TableRow, TableRowGroup, TableSwitchRow, TextInput } from "@metro/common/components";
import { findByProps } from "@metro/wrappers";
import { semanticColors } from "@ui/color";
import { ErrorBoundary } from "@ui/components";
import ErrorBoundaryScreen from "@core/ui/reporter/components/ErrorBoundaryScreen";
import { createStyles, TextStyleSheet } from "@ui/styles";
import { NativeModules } from "react-native";
import { ScrollView, StyleSheet } from "react-native";
import { showToast } from "@ui/toasts";
const { hideActionSheet } = lazyDestructure(() =>
findByProps("openLazy", "hideActionSheet"),
);
const { showSimpleActionSheet } = lazyDestructure(() =>
findByProps("showSimpleActionSheet"),
);
const { openAlert } = lazyDestructure(() =>
findByProps("openAlert", "dismissAlert"),
);
const { AlertModal, AlertActionButton } = lazyDestructure(() =>
findByProps("AlertModal", "AlertActions"),
);
const { hideActionSheet } = lazyDestructure(() => findByProps("openLazy", "hideActionSheet"));
const { showSimpleActionSheet } = lazyDestructure(() => findByProps("showSimpleActionSheet"));
const { openAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
const { AlertModal, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
const RDT_EMBED_LINK = "https://cdn.bwlok.dev/rdt/devTools.js";
const useStyles = createStyles({
leadingText: {
...TextStyleSheet["heading-md/semibold"],
color: semanticColors.TEXT_MUTED,
marginRight: -4,
},
inputGroup: {
marginTop: 4,
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
overflow: "hidden",
},
leadingText: {
...TextStyleSheet["heading-md/semibold"],
color: semanticColors.TEXT_MUTED,
marginRight: -4
},
});
export default function Developer() {
const [rdtFileExists, fs] = useFileExists("preloads/reactDevtools.js");
const [rdtFileExists, fs] = useFileExists("preloads/reactDevtools.js");
const styles = useStyles();
const navigation = NavigationNative.useNavigation();
const styles = useStyles();
const navigation = NavigationNative.useNavigation();
useProxy(settings);
useProxy(loaderConfig);
useProxy(settings);
useProxy(loaderConfig);
return (
<ErrorBoundary>
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={{ paddingBottom: 38 }}
>
<Stack
style={{ paddingVertical: 24, paddingHorizontal: 12 }}
spacing={24}
>
<TableRowGroup title={Strings.DEBUGGER_URL}>
<TextInput
placeholder="127.0.0.1:9090"
size="md"
leadingIcon={() => (
<LegacyFormText style={styles.leadingText}>
ws://
</LegacyFormText>
)}
defaultValue={settings.debuggerUrl}
onChange={(v: string) => (settings.debuggerUrl = v)}
/>
<Stack style={styles.inputGroup}>
<TableRow
label={Strings.CONNECT_TO_DEBUG_WEBSOCKET}
subLabel="Connect to Chrome DevTools for debugging"
icon={<TableRow.Icon source={findAssetId("WrenchIcon")} />}
onPress={() => connectToDebugger(settings.debuggerUrl)}
/>
</Stack>
</TableRowGroup>
return (
<ErrorBoundary>
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: 38 }}>
<Stack style={{ paddingVertical: 24, paddingHorizontal: 12 }} spacing={24}>
{isReactDevToolsPreloaded() && (
<TableRowGroup title="React Development">
<TextInput
placeholder="127.0.0.1:8097"
size="md"
leadingIcon={() => (
<LegacyFormText style={styles.leadingText}>
ws://
</LegacyFormText>
)}
defaultValue={settings.devToolsUrl}
onChange={(v: string) => (settings.devToolsUrl = v)}
/>
<Stack style={styles.inputGroup}>
<TableRow
label={Strings.CONNECT_TO_REACT_DEVTOOLS}
subLabel="Connect React DevTools for component debugging"
icon={
<TableRow.Icon source={findAssetId("ic_badge_staff")} />
}
onPress={async () => {
if (!settings.devToolsUrl?.trim()) {
showToast("Invalid devTools URL!", findAssetId("Small"));
return;
}
try {
const devTools =
window[getReactDevToolsProp() || "__vendetta_rdc"];
if (!devTools?.connectToDevTools) {
showToast(
"React DevTools not available",
findAssetId("Small"),
);
return;
}
await devTools.connectToDevTools({
host: settings.devToolsUrl.split(":")?.[0],
resolveRNStyle: StyleSheet.flatten,
});
} catch (error) {
showToast(
"Failed to connect to React DevTools",
findAssetId("Small"),
);
}
}}
/>
</Stack>
{isLoaderConfigSupported() && isVendettaLoader() && (
<TableSwitchRow
label={Strings.LOAD_REACT_DEVTOOLS}
subLabel={`${Strings.VERSION}: ${getReactDevToolsVersion()}`}
icon={
<TableRow.Icon source={findAssetId("ic_badge_staff")} />
}
value={loaderConfig.loadReactDevTools}
onValueChange={(v: boolean) => {
loaderConfig.loadReactDevTools = v;
}}
/>
)}
</TableRowGroup>
)}
{isLoaderConfigSupported() && (
<TableRowGroup title="Loader Configuration">
<TableSwitchRow
label={Strings.LOAD_FROM_CUSTOM_URL}
subLabel={Strings.LOAD_FROM_CUSTOM_URL_DEC}
icon={<TableRow.Icon source={findAssetId("LinkIcon")} />}
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/shiggycord.js"
label={Strings.SHIGGYCORD_URL}
/>
}
/>
)}
</TableRowGroup>
)}
<TableRowGroup title="Inspection & Testing">
<TableRow
arrow
label={Strings.ASSET_BROWSER}
subLabel="Browse and inspect Discord's assets"
icon={<TableRow.Icon source={findAssetId("ic_image")} />}
trailing={TableRow.Arrow}
onPress={() =>
navigation.push("SHIGGYCORD_CUSTOM_PAGE", {
title: Strings.ASSET_BROWSER,
render: AssetBrowser,
})
}
/>
<TableRow
label={Strings.INSTALL_REACT_DEVTOOLS}
subLabel={Strings.RESTART_REQUIRED_TO_TAKE_EFFECT}
icon={<TableRow.Icon source={findAssetId("DownloadIcon")} />}
trailing={
<Button
size="sm"
loading={rdtFileExists === CheckState.LOADING}
disabled={rdtFileExists === CheckState.LOADING}
variant={
rdtFileExists === CheckState.TRUE ? "secondary" : "primary"
}
text={
rdtFileExists === CheckState.TRUE
? Strings.UNINSTALL
: Strings.INSTALL
}
onPress={async () => {
if (rdtFileExists === CheckState.FALSE) {
fs.downloadFile(
RDT_EMBED_LINK,
"preloads/reactDevtools.js",
);
} else if (rdtFileExists === CheckState.TRUE) {
fs.removeFile("preloads/reactDevtools.js");
}
}}
icon={findAssetId(
rdtFileExists === CheckState.TRUE
? "ic_message_delete"
: "DownloadIcon",
)}
style={{ marginLeft: 8 }}
/>
}
/>
<TableRow
arrow
label={Strings.ERROR_BOUNDARY_TOOLS_LABEL}
subLabel="Test error boundaries and crash handling"
icon={<TableRow.Icon source={findAssetId("ic_warning_24px")} />}
onPress={() =>
showSimpleActionSheet({
key: "ErrorBoundaryTools",
header: {
title: "Which ErrorBoundary do you want to trip?",
icon: (
<TableRow.Icon
style={{ marginRight: 8 }}
source={findAssetId("ic_warning_24px")}
<TableRowGroup title={Strings.DEBUGGER_URL}>
<TextInput
placeholder="127.0.0.1:9090"
size="md"
leadingIcon={() => <LegacyFormText style={styles.leadingText}>ws://</LegacyFormText>}
defaultValue={settings.debuggerUrl}
onChange={(v: string) => settings.debuggerUrl = v}
/>
),
},
options: [
{
label: Strings.SHIGGYCORD,
onPress: () =>
navigation.push("SHIGGYCORD_CUSTOM_PAGE", {
render: () => (
<ErrorBoundaryScreen
error={new Error("ShiggyCord test crash")}
rerender={() => {}}
/>
),
}),
},
{
label: "Discord",
isDestructive: true,
onPress: () =>
navigation.push("SHIGGYCORD_CUSTOM_PAGE", {
noErrorBoundary: true,
}),
},
],
})
}
/>
<TableSwitchRow
label={Strings.ENABLE_EVAL_COMMAND}
subLabel={Strings.ENABLE_EVAL_COMMAND_DESC}
icon={<TableRow.Icon source={findAssetId("PencilIcon")} />}
value={!!settings.enableEvalCommand}
onValueChange={(v: boolean) => {
settings.enableEvalCommand = v;
}}
/>
</TableRowGroup>
<TableRowGroup title="Dangerous Actions">
<TableRow
label={Strings.CLEAR_BUNDLE}
subLabel="Clear cached bundle and force reload"
icon={<TableRow.Icon source={findAssetId("TrashIcon")} />}
onPress={() => {
openAlert(
"shiggycord-clear-bundle-reload-confirmation",
<AlertModal
title={Strings.MODAL_RELOAD_REQUIRED}
content={Strings.MODAL_RELOAD_REQUIRED_DESC}
actions={
<Stack>
<AlertActionButton
text={Strings.RELOAD}
variant="destructive"
onPress={() =>
NativeModules.BundleUpdaterManager.reload()
}
/>
<AlertActionButton
text={Strings.CANCEL}
variant="secondary"
<Stack style={{
marginTop: 4,
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
overflow: 'hidden'
}}>
<TableSwitchRow
label={Strings.AUTO_DEBUGGER}
icon={<TableRow.Icon source={findAssetId("copy")} />}
value={settings.autoDebugger}
onValueChange={(v: boolean) => {
settings.autoDebugger = v;
}}
/>
</Stack>
}
/>,
);
}}
/>
</TableRowGroup>
</Stack>
</ScrollView>
</ErrorBoundary>
);
}
<TableRow
label={Strings.CONNECT_TO_DEBUG_WEBSOCKET}
icon={<TableRow.Icon source={findAssetId("copy")} />}
onPress={() => connectToDebugger(settings.debuggerUrl)}
/>
</TableRowGroup>
{isReactDevToolsPreloaded() && <>
<TableRowGroup title={Strings.DEVTOOLS_URL}>
<TextInput
placeholder="127.0.0.1:8097"
size="md"
leadingIcon={() => <LegacyFormText style={styles.leadingText}>ws://</LegacyFormText>}
defaultValue={settings.devToolsUrl}
onChange={(v: string) => settings.devToolsUrl = v}
/>
<Stack style={{
marginTop: 4,
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
overflow: 'hidden'
}}>
<TableSwitchRow
label={Strings.AUTO_DEVTOOLS}
icon={<TableRow.Icon source={findAssetId("ic_badge_staff")} />}
value={settings.autoDevTools}
onValueChange={(v: boolean) => {
settings.autoDevTools = v;
}}
/>
</Stack>
<TableRow
label={Strings.CONNECT_TO_REACT_DEVTOOLS}
icon={<TableRow.Icon source={findAssetId("ic_badge_staff")} />}
onPress={async () => {
if (!settings.devToolsUrl?.trim()) {
showToast("Invalid devTools URL!", findAssetId("Small"));
return;
}
try {
const devTools = window[getReactDevToolsProp() || "__vendetta_rdc"];
if (!devTools?.connectToDevTools) {
showToast("Invalid devTools URL!", findAssetId("Small"));
return;
}
await devTools.connectToDevTools({
host: settings.devToolsUrl.split(":")?.[0],
resolveRNStyle: StyleSheet.flatten,
});
} catch (error) {
showToast("Invalid devTools URL!", findAssetId("Small"));
}
}}
/>
</TableRowGroup>
</>}
{isLoaderConfigSupported() && <>
<TableRowGroup title="Loader config">
<TableSwitchRow
label={Strings.LOAD_FROM_CUSTOM_URL}
subLabel={Strings.LOAD_FROM_CUSTOM_URL_DEC}
icon={<TableRow.Icon source={findAssetId("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/kettu.js"
label={Strings.PUPU_URL}
/>} />}
{isReactDevToolsPreloaded() && isVendettaLoader() && <TableSwitchRow
label={Strings.LOAD_REACT_DEVTOOLS}
subLabel={`${Strings.VERSION}: ${getReactDevToolsVersion()}`}
icon={<TableRow.Icon source={findAssetId("ic_badge_staff")} />}
value={loaderConfig.loadReactDevTools}
onValueChange={(v: boolean) => {
loaderConfig.loadReactDevTools = v;
}}
/>}
</TableRowGroup>
</>}
<TableRowGroup title="Other">
<TableRow
// Thanks for this Revenge team!
label={Strings.CLEAR_BUNDLE}
icon={<TableRow.Icon source={findAssetId("TrashIcon")} />}
onPress={() => {
openAlert("pupu-clear-bundle-reload-confirmation", <AlertModal
title={Strings.MODAL_RELOAD_REQUIRED}
content={Strings.MODAL_RELOAD_REQUIRED_DESC}
actions={
<Stack>
<AlertActionButton text={Strings.RELOAD} variant="destructive" onPress={() => NativeModules.BundleUpdaterManager.reload()} />
<AlertActionButton text={Strings.CANCEL} variant="secondary" />
</Stack>
}
/>);
}}
/>
<TableRow
arrow
label={Strings.ASSET_BROWSER}
icon={<TableRow.Icon source={findAssetId("ic_image")} />}
trailing={TableRow.Arrow}
onPress={() => navigation.push("PUPU_CUSTOM_PAGE", {
title: Strings.ASSET_BROWSER,
render: AssetBrowser,
})}
/>
<TableRow
arrow
label={Strings.ERROR_BOUNDARY_TOOLS_LABEL}
icon={<TableRow.Icon source={findAssetId("ic_warning_24px")} />}
onPress={() => showSimpleActionSheet({
key: "ErrorBoundaryTools",
header: {
title: "Which ErrorBoundary do you want to trip?",
icon: <TableRow.Icon style={{ marginRight: 8 }} source={findAssetId("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: Strings.PUPU, onPress: () => navigation.push("PUPU_CUSTOM_PAGE", { render: () => <undefined /> }) },
{ label: "Discord", isDestructive: true, onPress: () => navigation.push("PUPU_CUSTOM_PAGE", { noErrorBoundary: true }) },
],
})}
/>
<TableRow
label={Strings.INSTALL_REACT_DEVTOOLS}
subLabel={Strings.RESTART_REQUIRED_TO_TAKE_EFFECT}
icon={<TableRow.Icon source={findAssetId("DownloadIcon")} />}
trailing={
<Button
size="sm"
loading={rdtFileExists === CheckState.LOADING}
disabled={rdtFileExists === CheckState.LOADING}
variant={rdtFileExists === CheckState.TRUE ? "secondary" : "primary"}
text={rdtFileExists === CheckState.TRUE ? Strings.UNINSTALL : Strings.INSTALL}
onPress={async () => {
if (rdtFileExists === CheckState.FALSE) {
fs.downloadFile(RDT_EMBED_LINK, "preloads/reactDevtools.js");
} else if (rdtFileExists === CheckState.TRUE) {
fs.removeFile("preloads/reactDevtools.js");
}
}}
icon={findAssetId(rdtFileExists === CheckState.TRUE ? "ic_message_delete" : "DownloadIcon")}
style={{ marginLeft: 8 }}
/>
}
/>
<TableSwitchRow
label={Strings.ENABLE_EVAL_COMMAND}
subLabel={Strings.ENABLE_EVAL_COMMAND_DESC}
icon={<TableRow.Icon source={findAssetId("PencilIcon")} />}
value={!!settings.enableEvalCommand}
onValueChange={(v: boolean) => {
settings.enableEvalCommand = v;
}}
/>
</TableRowGroup>
</Stack>
</ScrollView>
</ErrorBoundary>
);
}

View file

@ -1,6 +1,6 @@
import { getThemeFromLoader, selectTheme, themes } from "@lib/addons/themes";
import { findAssetId } from "@lib/api/assets";
import { getLoaderName, getLoaderVersion, isThemeSupported } from "@lib/api/native/loader";
import { getLoaderName, getLoaderVersion, isThemeSupported, getReactDevToolsProp } from "@lib/api/native/loader";
import { BundleUpdaterManager, NativeClientInfoModule, NativeDeviceModule } from "@lib/api/native/modules";
import { after } from "@lib/api/patcher";
import { settings } from "@lib/api/settings";
@ -8,6 +8,7 @@ import { logger } from "@lib/utils/logger";
import { showToast } from "@ui/toasts";
import { version } from "bunny-build-info";
import { Platform, type PlatformConstants } from "react-native";
import { StyleSheet } from "react-native";
export interface RNConstants extends PlatformConstants {
// Android
@ -164,3 +165,37 @@ export function getDebugInfo() {
)!
};
}
/**
* @internal
*/
export function initDebugger() {
if (settings.autoDebugger) {
try {
connectToDebugger(settings.debuggerUrl);
} catch (e) {
logger.error("Failed to connect to Debugger during startup:", e);
}
}
if (settings.autoDevTools) {
try {
(async () => {
const devTools = window[getReactDevToolsProp() || "__vendetta_rdc"];
if (!devTools?.connectToDevTools) {
showToast("Invalid devTools URL!", findAssetId("Small"));
return;
}
await devTools.connectToDevTools({
host: settings.devToolsUrl.split(":")?.[0],
resolveRNStyle: StyleSheet.flatten,
});
})();
} catch (e) {
logger.error("Failed to connect to ReactDevTools during startup:", e);
}
}
}

View file

@ -1,111 +1,35 @@
import {
createFileBackend,
createMMKVBackend,
createStorage,
wrapSync,
} from "@core/vendetta/storage";
import { createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@core/vendetta/storage";
import { getLoaderConfigPath } from "@lib/api/native/loader";
export interface Settings {
debuggerUrl: string;
developerSettings: boolean;
enableDiscordDeveloperSettings: boolean;
safeMode?: {
enabled: boolean;
currentThemeId?: string;
};
enableEvalCommand?: boolean;
debuggerUrl: string;
devToolsUrl: string;
autoDebugger: boolean;
autoDevTools: boolean;
developerSettings: boolean;
enableDiscordDeveloperSettings: boolean;
safeMode?: {
enabled: boolean;
currentThemeId?: string;
};
enableEvalCommand?: boolean;
}
export interface LoaderConfig {
customLoadUrl: {
enabled: boolean;
url: string;
};
loadReactDevTools: boolean;
customLoadUrl: {
enabled: boolean;
url: string;
};
loadReactDevTools: boolean;
}
/**
* New: Updater settings persisted in MMKV.
* Fields:
* - autoUpdateEnabled: whether updates are applied automatically
* - checkOnStart: whether to check for updates on app start
* - updateChannel: optional update channel (stable/beta/alpha)
* - lastChecked: ISO timestamp of last update check
*
* Plugin-related additions:
* - fetchPluginsOnStart: when true, plugins (external repositories) are fetched at startup;
* when false, plugin repository fetching is deferred and controlled from the Updater UI.
* - repoAutoFetchOverrides: per-repository overrides for auto-fetch behavior
* (keyed by repository URL, boolean value overrides fetchPluginsOnStart).
*/
export interface UpdaterSettings {
// Whether updates (bundle-level auto-updates) should be applied automatically
autoUpdateEnabled: boolean;
// Whether to perform an update check on app start
checkOnStart: boolean;
// Optional channel preference for updates
updateChannel?: "stable" | "beta" | "alpha";
// ISO timestamp of the last time the updater checked (plugins + bundle)
lastChecked?: string | null;
export const settings = wrapSync(createStorage<Settings>(createMMKVBackend("VENDETTA_SETTINGS")));
// -------------------------
// Plugin-related settings
// -------------------------
// Whether external plugin repositories should be fetched at startup
fetchPluginsOnStart?: boolean;
// Per-repo override for auto-fetch behavior (repoUrl => boolean)
repoAutoFetchOverrides?: Record<string, boolean>;
// -------------------------
// Bundle update settings
// -------------------------
// How often (in hours) to check the project's releases for bundle updates.
// Default should be treated as 3 if unset.
bundleCheckIntervalHours?: number;
// Releases URL (used for checking the bundle). Defaults to:
// https://github.com/kmmiio99o/ShiggyCord/releases/
bundleReleasesUrl?: string;
// Whether to ignore prerelease releases when checking bundle updates.
// Defaults to true.
bundleIgnorePrereleases?: boolean;
// ISO timestamp of the last bundle check
lastBundleChecked?: string | null;
// Last known bundle version discovered by the updater (if any)
lastBundleVersion?: string | null;
// Latest discovered bundle tag (extracted from custom URL content or GitHub).
// This represents the most recent tag/identifier the updater detected.
bundleLatestTag?: string | null;
// The URL used to determine the latest bundle (download URL or release page).
bundleLatestUrl?: string | null;
// Whether a newer bundle than the installed one is available.
bundleAvailable?: boolean;
}
export const settings = wrapSync(
createStorage<Settings>(createMMKVBackend("VENDETTA_SETTINGS")),
);
export const loaderConfig = wrapSync(
createStorage<LoaderConfig>(
export const loaderConfig = wrapSync(createStorage<LoaderConfig>(
createFileBackend(getLoaderConfigPath(), {
customLoadUrl: {
enabled: false,
url: "http://localhost:4040/shiggycord.js",
},
loadReactDevTools: false,
}),
),
);
/**
* Updater settings storage.
* Stored under MMKV key: VENDETTA_UPDATER_SETTINGS
* Defaults should be handled by UI code when reading values if necessary.
*/
export const updaterSettings = wrapSync(
createStorage<UpdaterSettings>(
createMMKVBackend("VENDETTA_UPDATER_SETTINGS"),
),
);
customLoadUrl: {
enabled: false,
url: "http://localhost:4040/kettu.js"
}
})
));