Merge remote-tracking branch 'upstream/main'

This commit is contained in:
cocobo1 2025-07-21 19:21:48 +03:00
commit 8327c863c1
33 changed files with 1705 additions and 195 deletions

View file

@ -1,7 +1,7 @@
name: Bug report
description: Create a bug report to help us improve
title: "[bug]: <insert title here>"
labels: [bug]
name: 🐞 Bug report
description: Report a bug or an issue.
title: 'bug: '
labels: ['Bug report']
body:
- type: markdown
attributes:
@ -47,54 +47,28 @@ body:
required: true
- type: textarea
id: reproduce-steps
attributes:
label: Steps to reproduce the bug
description: What did you do for the bug to show up?
placeholder: |
Example:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
placeholder: |
Example:
"This should happen..."
validations:
required: true
- type: textarea
id: actual-behavior
attributes:
label: Actual behavior
placeholder: |
Example:
"This happened instead..."
validations:
required: true
- type: textarea
id: sreen-media
attributes:
label: Screenshots/Screen recordings
label: Bug description
description: |
A picture or video helps us understand the bug more.
You can upload them directly in the text box.
- Describe your bug in detail
- Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...)
- Add images and videos if possible
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs
description: |
Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
label: Stack trace
description: If this bug causes a JS crash, please paste the stack trace here.
render: shell
- type: textarea
attributes:
label: Component stack trace
description: If this bug causes a JS crash, please paste the component stack trace here.
render: shell
- type: textarea
attributes:
label: Native crash trace
description: If this bug causes a native crash, please paste the crash trace here. On Android, this can be accessed by doing `logcat | grep AndroidRuntime`.
render: shell
- type: input
@ -156,7 +130,6 @@ body:
required: true
- type: textarea
id: debug-info
attributes:
label: Debug information
description: |
@ -171,11 +144,18 @@ body:
Device: M2101K7BG (secret)
- type: textarea
id: additional-information
attributes:
label: Additional information
placeholder: |
Example:
"I have tried to do this..."
"This only happens in this situation..."
"This could be related to..."
label: Additional context
description: Add additional context here.
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your bug report will be closed if you don't follow the checklist below.
options:
- label: I have checked all open and closed bug reports and this is not a duplicate.
required: true
- label: I have chosen an appropriate title.
required: true
- label: All requested information has been provided properly.
required: true

View file

@ -3,23 +3,17 @@ description: Suggest an idea for Kettu
title: "[feature]: <insert title here>"
labels: [enhancement]
body:
- type: checkboxes
id: checklist
- type: markdown
attributes:
label: Checklist
description: You should ensure the completion of the task before proceeding to check it off the checklist. Neglecting to do so may impede the efficiency of the issue resolution process. The developer has the right to delete the issue directly if you check the list blindly.
options:
- label: This is not a plugin request.
required: true
- label: I've checked that there is no other issue about this feature request.
required: true
- label: This issue contains only one feature request.
required: true
- label: The title of this issue accurately describes the feature request.
required: true
value: |
# Revenge feature request
Before creating a new feature request, please keep the following in mind:
- **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/revenge-mod/revenge-bundle/issues?q=label%3A%22Feature+request%22).
- **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/revenge-mod/revenge-bundle/blob/main/CONTRIBUTING.md).
- **Do not use the issue page for support**: If you need help or have questions, join us on [Discord](https://discord.gg/ddcQf3s2Uq).
- type: textarea
id: feature-description
attributes:
label: Feature description
description: What feature you want Kettu to have? Provide detailed description about what it should look like or where it should be added.
@ -27,17 +21,26 @@ body:
required: true
- type: textarea
id: why-is-the-feature-requested
attributes:
label: Why do you want this feature?
description: Describe the problem or limitation that motivates you to want this feature to be added.
label: Motivation
description: |
A strong motivation is necessary for a feature request to be considered.
- Why should this feature be implemented?
- What is the explicit use case?
- What are the benefits?
- What makes this feature important?
validations:
required: true
- type: textarea
id: additional-information
- type: checkboxes
id: acknowledgements
attributes:
label: Additional information
description: Add any other context or screenshots about the feature request here.
placeholder: |
Additional details and attachments.
label: Acknowledgements
description: Your feature request will be closed if you don't follow the checklist below.
options:
- label: I have checked all open and closed feature requests and this is not a duplicate
required: true
- label: I have chosen an appropriate title.
required: true
- label: All requested information has been provided properly.
required: true

View file

@ -13,6 +13,11 @@
},
"license": "BSD-3-Clause",
"devDependencies": {
"@saithodev/semantic-release-backmerge": "^4.0.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"semantic-release": "^24.1.3",
"@swc/core": "^1.7.0",
"@swc/helpers": "^0.5.12",
"@types/node": "^20.14.11",

1387
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -20,7 +20,8 @@ const metroDeps = await (async () => {
const args = yargs(process.argv.slice(2));
const {
"release-branch": releaseBranch,
"build-minify": buildMinify
"build-minify": buildMinify,
"dev": dev
} = args;
let context = null;

View file

@ -17,7 +17,7 @@ export default () => <ApplicationCommand>{
const info = getDebugInfo();
const content = [
"**Kettu Debug Info**",
`> Kettu: ${info.pupu.version} (${info.pupu.loader.name} ${info.pupu.loader.version})`,
`> Kettu: ${info.bunny.version} (${info.bunny.loader.name} ${info.bunny.loader.version})`,
`> Discord: ${info.discord.version} (${info.discord.build})`,
`> React: ${info.react.version} (RN ${info.react.nativeVersion})`,
`> Hermes: ${info.hermes.version} (bcv${info.hermes.bytecodeVersion})`,

View file

@ -13,9 +13,11 @@
"CLEAR": "Clear",
"CLEAR_BUNDLE": "Clear JS Bundle",
"CLEAR_BUNDLE_DESC": "Clear the cached bundle. This will force a re-download of the bundle next app launch.",
"CLEAR_BUNDLE": "Clear JS Bundle",
"CLEAR_BUNDLE_DESC": "Clear the cached bundle. This will force a re-download of the bundle next app launch.",
"CLEAR_DATA": "Clear data",
"CLEAR_DATA_FAILED": "Failed to clear data for {name}!",
"CLEAR_DATA_SUCCESSFUL": "Cleared data for {name}.",
"CLEAR_DATA_FAILED": "Failed to clear data for {name}",
"CLEAR_DATA_SUCCESSFUL": "Cleared data for {name}",
"CODENAME": "Codename",
"COMMAND_DEBUG_DESC": "Send Kettu debug info.",
"COMMAND_DEBUG_OPT_EPHEMERALLY": "Send debug info ephemerally.",
@ -26,7 +28,7 @@
"COMMAND_PLUGINS_OPT_EPHEMERALLY": "Send plugins list ephemerally.",
"COMPONENT": "Component",
"CONFIRMATION_LINK_IS_A_TYPE": "This link is a **{urlType, select, plugin {Plugin} theme {Theme} other {Add-on}}**, would you like to install it?",
"CONNECT_TO_DEBUG_WEBSOCKET": "Connect to debug websocket",
"CONNECT_TO_DEBUG_WEBSOCKET": "Connect to debug WebSocket",
"CONNECT_TO_REACT_DEVTOOLS": "Connect to React DevTools",
"CONTINUE": "Continue",
"COPIED_TO_CLIPBOARD": "Copied to clipboard",
@ -38,12 +40,12 @@
"DEVELOPER": "Developer",
"DEVELOPER_SETTINGS": "Developer Settings",
"DISABLE_THEME": "Disable Theme",
"DISABLE_UPDATES": "Disable updates",
"DISABLE_UPDATES": "Disable Updates",
"DISCORD_SERVER": "Discord Server",
"DONE": "Done",
"ENABLE_EVAL_COMMAND": "Enable /eval command",
"ENABLE_EVAL_COMMAND_DESC": "Evaluate JavaScript directly from command. Be cautious when using this command as it may pose a security risk. Make sure to know what you are doing.",
"ENABLE_UPDATES": "Enable updates",
"ENABLE_EVAL_COMMAND_DESC": "Evaluate JavaScript directly from a command. Be cautious when using this command as it may pose a security risk. Make sure to know what you are doing.",
"ENABLE_UPDATES": "Enable Updates",
"ERROR_BOUNDARY_TOOLS_LABEL": "ErrorBoundary Tools",
"EXTRACT": "Extract",
"FONT_NAME": "Font Name",
@ -57,7 +59,7 @@
"INSTALL_FONT": "Install a font",
"INSTALL_PLUGIN": "Install a plugin",
"INSTALL_REACT_DEVTOOLS": "Install React DevTools",
"INSTALL_THEME": "Install Theme",
"INSTALL_THEME": "Install a theme",
"LABEL_EXTRACT_FONTS_FROM_THEME": "Extract font from theme",
"LINKS": "Links",
"LOAD_FROM_CUSTOM_URL": "Load from custom URL",
@ -70,33 +72,35 @@
"MISCELLANEOUS": "Miscellaneous",
"MODAL_RELOAD_REQUIRED": "Reload app?",
"MODAL_RELOAD_REQUIRED_DESC": "A reload is required to see the changes. Do you want to reload now?",
"MODAL_RELOAD_REQUIRED": "Reload app?",
"MODAL_RELOAD_REQUIRED_DESC": "A reload is required to see the changes. Do you want to reload now?",
"MODAL_THEME_REFETCHED": "Theme refetched",
"MODAL_THEME_REFETCHED_DESC": "A reload is required to see the changes. Do you want to reload now?",
"MODAL_UNPROXIED_PLUGIN_DESC": "The plugin you are trying to install has not been proxied/verified by staffs. Are you sure you want to continue?",
"MODAL_UNPROXIED_PLUGIN_DESC": "The plugin you are trying to install has not been proxied/verified by staff. Are you sure you want to continue?",
"MODAL_UNPROXIED_PLUGIN_HEADER": "Unproxied Plugin",
"MODEL": "Model",
"OPEN_IN_BROWSER": "Open in Browser",
"OPERATING_SYSTEM": "Operating System",
"OVERFLOW_PLUGIN_SETTINGS": "Plugin settings",
"OVERFLOW_PLUGIN_SETTINGS": "Plugin Settings",
"PLATFORM": "Platform",
"PLUGIN_REFETCH_FAILED": "Failed to refetch plugin!",
"PLUGIN_REFETCH_SUCCESSFUL": "Successfully refetched plugin!",
"PLUGIN_REFETCH_FAILED": "Failed to refetch plugin",
"PLUGIN_REFETCH_SUCCESSFUL": "Successfully refetched plugin",
"PLUGINS": "Plugins",
"REFETCH": "Refetch",
"RELOAD": "Reload",
"RELOAD_DISCORD": "Reload Discord",
"RELOAD_IN_NORMAL_MODE": "Reload in Normal Mode",
"RELOAD_IN_NORMAL_MODE_DESC": "This will reload Discord normally",
"RELOAD_IN_NORMAL_MODE_DESC": "Safe mode currently enabled, tap to reload in normal mode",
"RELOAD_IN_SAFE_MODE": "Reload in Safe Mode",
"RELOAD_IN_SAFE_MODE_DESC": "This will reload Discord without loading addons",
"RELOAD_IN_SAFE_MODE_DESC": "Tap to reload Discord without loading addons",
"REMOVE": "Remove",
"RESTART_REQUIRED_TO_TAKE_EFFECT": "Restart is required to take effect",
"RETRY": "Retry",
"RETRY_RENDER": "Retry Render",
"SAFE_MODE": "Safe Mode",
"SAFE_MODE_NOTICE_FONTS": "You are in Safe Mode, meaning fonts have been temporarily disabled. {enabled, select, true {If a font appears to be causing the issue, you can press below to disable it persistently.} other {}}",
"SAFE_MODE_NOTICE_PLUGINS": "You are in Safe Mode, so plugins cannot be loaded. Disable any misbehaving plugins, then return to Normal Mode from the General settings page.",
"SAFE_MODE_NOTICE_THEMES": "You are in Safe Mode, meaning themes have been temporarily disabled. {enabled, select, true {If a theme appears to be causing the issue, you can press below to disable it persistently.} other {}}",
"SAFE_MODE_NOTICE_FONTS": "You are in safe mode, meaning fonts have been temporarily disabled. {enabled, select, true {If a font appears to be causing the issue, you can press below to disable it persistently.} other {}}",
"SAFE_MODE_NOTICE_PLUGINS": "You are in safe mode, so plugins cannot be loaded. Disable any misbehaving plugins, then return to Normal Mode from the General settings page.",
"SAFE_MODE_NOTICE_THEMES": "You are in safe mode, meaning themes have been temporarily disabled. {enabled, select, true {If a theme appears to be causing the issue, you can press below to disable it persistently.} other {}}",
"SEARCH": "Search",
"SEPARATOR": ", ",
"SETTINGS_ACTIVATE_DISCORD_EXPERIMENTS": "Activate Discord Experiments",
@ -104,16 +108,16 @@
"STACK_TRACE": "Stack Trace",
"SUCCESSFULLY_INSTALLED": "Successfully installed",
"THEME_EXTRACTOR_DESC": "This pack overrides the following: {fonts}",
"THEME_REFETCH_FAILED": "Failed to refetch theme!",
"THEME_REFETCH_SUCCESSFUL": "Successfully refetched theme.",
"THEME_REFETCH_FAILED": "Failed to refetch theme",
"THEME_REFETCH_SUCCESSFUL": "Successfully refetched theme",
"THEMES": "Themes",
"THEMES_RELOAD_FOR_CHANGES": "Reload the app to fully apply changes!",
"THEMES_RELOAD_FOR_CHANGES": "Reload the app to fully apply changes",
"TOASTS_INSTALLED_PLUGIN": "Installed plugin",
"TOASTS_PLUGIN_UPDATE": "{update, select, true {Enabled} other {Disabled}} updates for {name}.",
"UH_OH": "Uh oh.",
"TOASTS_PLUGIN_UPDATE": "{update, select, true {Enabled} other {Disabled}} updates for {name}",
"UH_OH": "Uh Oh",
"UNINSTALL": "Uninstall",
"UNINSTALL_TITLE": "Uninstall {title}",
"URL_PLACEHOLDER": "https://example.com",
"URL_PLACEHOLDER": "https://github.com/revenge-mod",
"VERSION": "Version",
"VERSIONS": "Versions"
}

View file

@ -6,13 +6,16 @@ import { findAssetId } from "@lib/api/assets";
import { isThemeSupported } from "@lib/api/native/loader";
import { after, instead } from "@lib/api/patcher";
import { VD_PROXY_PREFIX, VD_THEMES_CHANNEL_ID } from "@lib/utils/constants";
import { VD_PROXY_PREFIX, VD_THEMES_CHANNEL_ID } from "@lib/utils/constants";
import { lazyDestructure } from "@lib/utils/lazy";
import { channels } from "@metro/common";
import { channels } from "@metro/common";
import { byMutableProp } from "@metro/filters";
import { findExports } from "@metro/finders";
import { findByProps, findByPropsLazy } from "@metro/wrappers";
import { showToast } from "@ui/toasts";
import { Linking } from "react-native";
import { Linking } from "react-native";
const showSimpleActionSheet = findExports(byMutableProp("showSimpleActionSheet"));
const handleClick = findByPropsLazy("handleClick");
@ -75,7 +78,7 @@ export default () => {
confirmText: Strings.INSTALL,
cancelText: Strings.CANCEL,
secondaryConfirmText: Strings.OPEN_IN_BROWSER,
onConfirmSecondary: () => Linking.openURL(url),
onConfirmSecondary: () => Linking.openURL(url),
});
})
);

View file

@ -4,7 +4,7 @@ import { settings } from "@lib/api/settings";
import { dismissAlert, openAlert } from "@lib/ui/alerts";
import { showSheet } from "@lib/ui/sheets";
import isValidHttpUrl from "@lib/utils/isValidHttpUrl";
import { lazyDestructure } from "@lib/utils/lazy";
import { lazyDestructure, proxyLazy } from "@lib/utils/lazy";
import { findByProps } from "@metro";
import { clipboard, NavigationNative } from "@metro/common";
import { AlertActionButton, AlertModal, Button, FlashList, FloatingActionButton, HelpMessage, IconButton, Stack, Text, TextInput, useSafeAreaInsets } from "@metro/common/components";

View file

@ -17,12 +17,10 @@ const useStyles = createStyles({
backgroundColor: tokens.colors.BG_BASE_SECONDARY,
paddingHorizontal: 16,
height: "100%",
padding: 8,
gap: 12
}
});
export default function ErrorBoundaryScreen(props: {
error: Error;
rerender: () => void;
@ -31,12 +29,11 @@ export default function ErrorBoundaryScreen(props: {
const debugInfo = getDebugInfo();
return <ErrorBoundary>
{/* for iOS */}
<SafeAreaProvider>
<SafeAreaView style={styles.container}>
<View style={{ gap: 4 }}>
<Text variant="display-lg">Uh oh.</Text>
<Text variant="text-md/normal">A crash occurred while rendering a component. This could be caused by a plugin, Bunny or Discord itself.</Text>
<Text variant="text-md/normal">A crash occurred while rendering a component. This could be caused by a plugin, Revenge, or Discord itself.</Text>
<Text variant="text-sm/normal" color="text-muted">{debugInfo.os.name}; {debugInfo.discord.build} ({debugInfo.discord.version}); {debugInfo.bunny.version}</Text>
</View>
<ScrollView fadingEdgeLength={64} contentContainerStyle={{ gap: 12 }}>

View file

@ -10,6 +10,7 @@ import { version } from "bunny-build-info";
export { PupuIcon };
export default function initSettings() {
registerSection({
name: "Kettu",
items: [

View file

@ -1,6 +1,6 @@
import AssetDisplay from "@core/ui/settings/pages/Developer/AssetDisplay";
import { iterateAssets } from "@lib/api/assets";
import { LegacyFormDivider } from "@metro/common/components";
import { Text } from "@metro/common/components";
import { ErrorBoundary, Search } from "@ui/components";
import { useMemo } from "react";
import { FlatList, View } from "react-native";
@ -16,12 +16,15 @@ export default function AssetBrowser() {
style={{ margin: 10 }}
onChangeText={(v: string) => setSearch(v)}
/>
<FlatList
data={all.filter(a => a.name.includes(search) || a.id.toString() === search)}
renderItem={({ item }) => <AssetDisplay asset={item} />}
ItemSeparatorComponent={LegacyFormDivider}
keyExtractor={item => item.name}
/>
<View style={{ flex: 1, borderRadius: 16, paddingHorizontal: 12, overflow: 'hidden', backgroundColor: 'transparent' }}>
<Text variant='text-sm/medium' color='text-danger' style={{ marginBottom: 16 }}>Some assets types cannot be displayed and will be marked in red.</Text>
<FlatList
data={all.filter(a => a.name.includes(search) || a.id.toString() === search)}
renderItem={({ item }: any) => <AssetDisplay asset={item} />}
contentContainerStyle={{ overflow: 'hidden', backgroundColor: 'transparent', borderRadius: 16 }}
keyExtractor={a => a.name}
/>
</View>
</View>
</ErrorBoundary>
);

View file

@ -1,20 +1,65 @@
import { Asset } from "@lib/api/assets";
import { Asset, findAssetId } from "@lib/api/assets";
import { lazyDestructure } from "@lib/utils/lazy";
import { findByProps } from "@metro";
import { clipboard } from "@metro/common";
import { LegacyFormRow } from "@metro/common/components";
import { Stack, TableRow, Text } from "@metro/common/components";
import { showToast } from "@ui/toasts";
import { Image } from "react-native";
const { openAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
const { AlertModal, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
const displayable = new Set(['png', 'jpg', 'svg']);
const iconMap = {
jsona: 'ic_file_text',
lottie: 'ic_image',
webm: 'CirclePlayIcon-primary',
ttf: 'ic_add_text',
default: 'UnknownGameIcon'
};
interface AssetDisplayProps { asset: Asset; }
export default function AssetDisplay({ asset }: AssetDisplayProps) {
return (
<LegacyFormRow
label={`${asset.name} - ${asset.id}`}
trailing={<Image source={asset.id} style={{ width: 32, height: 32 }} />}
onPress={() => {
clipboard.setString(asset.name);
showToast.showCopyToClipboard();
}}
<TableRow
variant={displayable.has(asset.type) ? 'default' : 'danger'}
label={asset.name}
subLabel={`Index: ${asset.id} Type: ${asset.type}`}
icon={
displayable.has(asset.type)
? <Image source={asset.id} style={{ width: 32, height: 32 }} />
: <TableRow.Icon
variant='danger'
source={findAssetId(asset.type in iconMap ? iconMap[asset.type as keyof typeof iconMap] : iconMap.default)}
/>
}
onPress={() =>
openAlert("revenge-asset-display-details", <AlertModal
title={asset.name}
content={`Index: ${asset.id}\nModule ID: ${asset.moduleId}\nType: ${asset.type}`}
extraContent={
displayable.has(asset.type)
? <Image resizeMode="contain" source={asset.id} style={{ flex: 1, width: 'auto', height: 192 }} />
: (<Text variant='text-sm/medium' color="text-danger" style={{ width: '100%', textAlign: 'center' }}>
Asset type {asset.type.toUpperCase()} is not supported for preview.
</Text>)
}
actions={
<Stack>
<AlertActionButton text="Copy asset name" variant="primary" onPress={() => copyToClipboard(asset.name)} />
<AlertActionButton text="Copy asset index" variant="secondary" onPress={() => copyToClipboard(asset.id.toString())} />
</Stack>
}
/>)
}
/>
);
}
const copyToClipboard = (text: string) => {
clipboard.setString(text);
showToast.showCopyToClipboard();
};

View file

@ -13,15 +13,15 @@ import { findByProps } from "@metro/wrappers";
import { semanticColors } from "@ui/color";
import { ErrorBoundary } from "@ui/components";
import { createStyles, TextStyleSheet } from "@ui/styles";
import { ScrollView, StyleSheet } from "react-native";
import { NativeModules } from "react-native";
import { ScrollView, StyleSheet } from "react-native";
const { hideActionSheet } = lazyDestructure(() => findByProps("openLazy", "hideActionSheet"));
const { showSimpleActionSheet } = lazyDestructure(() => findByProps("showSimpleActionSheet"));
const { AlertModal, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
const { openAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
const { AlertModal, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
const RDT_EMBED_LINK = "https://raw.githubusercontent.com/amsyarasyiq/rdt-embedder/main/dist.js";
const RDT_EMBED_LINK = "https://github.com/revenge-mod/react-devtools-core/releases/latest/download/rdtc.js";
const useStyles = createStyles({
leadingText: {
@ -173,7 +173,7 @@ export default function Developer() {
label={Strings.ENABLE_EVAL_COMMAND}
subLabel={Strings.ENABLE_EVAL_COMMAND_DESC}
icon={<TableRow.Icon source={findAssetId("PencilIcon")} />}
value={settings.enableEvalCommand}
value={!!settings.enableEvalCommand}
onValueChange={(v: boolean) => {
settings.enableEvalCommand = v;
}}

View file

@ -90,7 +90,7 @@ export default function FontCard({ item: font }: CardWrapper<FontDefinition>) {
size="sm"
variant="secondary"
disabled={selected}
icon={findAssetId("PencilIcon")}
icon={findAssetId("WrenchIcon")}
/>
<Button
size="sm"

View file

@ -1,17 +1,37 @@
import { formatString, Strings } from "@core/i18n";
import { createProxy, useProxy } from "@core/vendetta/storage";
import { FontDefinition, fonts, removeFont, saveFont, validateFont } from "@lib/addons/fonts";
import { FontDefinition, fonts, removeFont, saveFont, updateFont, validateFont } from "@lib/addons/fonts";
import { getCurrentTheme } from "@lib/addons/themes";
import { findAssetId } from "@lib/api/assets";
import { safeFetch } from "@lib/utils";
import { lazyDestructure } from "@lib/utils/lazy";
import { NavigationNative } from "@metro/common";
import { ActionSheet, BottomSheetTitleHeader, Button, IconButton, Stack, TableRow, TableRowGroup, Text, TextInput } from "@metro/common/components";
import { findByPropsLazy } from "@metro/wrappers";
import { findByProps, findByPropsLazy } from "@metro/wrappers";
import { ErrorBoundary } from "@ui/components";
import { useMemo, useRef, useState } from "react";
import { ScrollView, View } from "react-native";
const actionSheet = findByPropsLazy("hideActionSheet");
const { openAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
const { AlertModal, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
function promptDetachConfirmationForThen(fontName: string | undefined, cb: () => void) {
if (fontName && fonts[fontName].source) openAlert("revenge-fonts-detach-source-confirmation", <AlertModal
title="Detach font pack URL?"
content="You need to detach the font pack URL from this font pack before you can manually edit its font entries. Do you want to detach the font pack URL?"
actions={
<Stack>
<AlertActionButton text="Detach" variant="destructive" onPress={() => {
delete fonts[fontName!].source;
cb();
}} />
<AlertActionButton text={Strings.CANCEL} variant="secondary" />
</Stack>
}
/>);
else cb();
};
function guessFontName(urls: string[]) {
const fileNames = urls.map(url => {
@ -121,7 +141,6 @@ function JsonFontImporter({ fonts, setName, setSource }: {
.then(() => actionSheet.hideActionSheet())
.catch(e => setError(String(e)))
.finally(() => setSaving(false));
}}
/>
</View>;
@ -130,6 +149,7 @@ function JsonFontImporter({ fonts, setName, setSource }: {
function EntryEditorActionSheet(props: {
fontEntries: Record<string, string>;
name: string;
onChange: () => void;
}) {
const [familyName, setFamilyName] = useState<string>(props.name);
const [fontUrl, setFontUrl] = useState<string>(props.fontEntries[props.name]);
@ -157,6 +177,8 @@ function EntryEditorActionSheet(props: {
onPress={() => {
delete props.fontEntries[props.name];
props.fontEntries[familyName] = fontUrl;
props.onChange();
actionSheet.hideActionSheet();
}}
/>
</View>;
@ -182,7 +204,7 @@ function promptActionSheet(
);
}
function NewEntryRow({ fontEntry }: { fontEntry: Record<string, string>; }) {
function NewEntryRow({ fontName, fontEntry }: { fontName: string | undefined, fontEntry: Record<string, string>; }) {
const nameRef = useRef<string>();
const urlRef = useRef<string>();
@ -215,7 +237,7 @@ function NewEntryRow({ fontEntry }: { fontEntry: Record<string, string>; }) {
<IconButton
size="md"
variant="primary"
onPress={() => {
onPress={() => promptDetachConfirmationForThen(fontName, () => {
if (!nameSet && nameRef.current) {
setNameSet(true);
} else if (nameSet && nameRef.current && urlRef.current) {
@ -233,7 +255,7 @@ function NewEntryRow({ fontEntry }: { fontEntry: Record<string, string>; }) {
setError(String(e));
}
}
}}
})}
icon={findAssetId(nameSet ? "PlusSmallIcon" : "ArrowLargeRightIcon")}
/>
</View>;
@ -243,8 +265,9 @@ export default function FontEditor(props: {
name?: string;
}) {
const [name, setName] = useState<string | undefined>(props.name);
const [source, setSource] = useState<string>();
const [source, setSource] = useState<string | undefined>(props.name && fonts[props.name].source);
const [importing, setIsImporting] = useState<boolean>(false);
const [errors, setErrors] = useState<Array<Error | undefined> | undefined>();
const memoEntry = useMemo(() => {
return createProxy(props.name ? { ...fonts[props.name].main } : {}).proxy;
@ -254,6 +277,8 @@ export default function FontEditor(props: {
const navigation = NavigationNative.useNavigation();
const [, forceUpdate] = React.useReducer(() => ({}), 0);
return <ScrollView style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: 38 }}>
<Stack style={{ paddingVertical: 24, paddingHorizontal: 12 }} spacing={12}>
{!props.name
@ -277,15 +302,14 @@ export default function FontEditor(props: {
label={"Refetch fonts from source"}
icon={<TableRow.Icon source={findAssetId("RetryIcon")} />}
onPress={async () => {
const ftCopy = { ...fonts[props.name!] };
await removeFont(props.name!);
await saveFont(ftCopy);
await updateFont(fonts[props.name!]);
navigation.goBack();
}}
/>
<TableRow
variant="danger"
label={"Delete font pack"}
icon={<TableRow.Icon source={findAssetId("TrashIcon")} />}
icon={<TableRow.Icon variant="danger" source={findAssetId("TrashIcon")} />}
onPress={() => removeFont(props.name!).then(() => navigation.goBack())}
/>
</TableRowGroup>}
@ -296,32 +320,50 @@ export default function FontEditor(props: {
placeholder={"Whitney"}
onChange={setName}
/>
{props.name && fonts[props.name].source && <TextInput
size="lg"
value={source}
label="Font Pack URL"
onChange={setSource}
/>}
<TableRowGroup title="Font Entries">
{Object.entries(fontEntries).map(([name, url]) => {
{Object.entries(fontEntries).map(([name, url], index) => {
const error = errors?.[index];
return <TableRow
label={name}
subLabel={url}
trailing={<Stack spacing={2} direction="horizontal">
subLabel={error ? <Text variant='text-xs/medium' color='text-danger'>{error.message}</Text> : url}
trailing={<Stack spacing={8} direction="horizontal">
<IconButton
size="sm"
variant="secondary"
icon={findAssetId("PencilIcon")}
onPress={() => promptActionSheet(EntryEditorActionSheet, fontEntries, {
name,
fontEntries,
})}
onPress={() => promptDetachConfirmationForThen(props.name, () =>
promptActionSheet(EntryEditorActionSheet, fontEntries, {
name,
fontEntries,
onChange: () => {
setErrors(undefined);
forceUpdate();
}
})
)}
/>
<IconButton
size="sm"
variant="secondary"
icon={findAssetId("TrashIcon")}
onPress={() => delete fontEntries[name]}
onPress={() => promptDetachConfirmationForThen(props.name, () => {
delete fontEntries[name];
setErrors(undefined);
})}
/>
</Stack>}
/>;
})}
<TableRow label={<NewEntryRow fontEntry={fontEntries} />} />
<TableRow label={<NewEntryRow fontName={props.name} fontEntry={fontEntries} />} />
</TableRowGroup>
{errors && <Text variant='text-sm/medium' color='text-danger'>Some font entries cannot be imported. Please modify the entries and try again.</Text>}
<View style={{ flexDirection: "row", justifyContent: "flex-end", bottom: 0, left: 0 }}>
<Button
size="lg"
@ -332,6 +374,7 @@ export default function FontEditor(props: {
onPress={async () => {
if (!name) return;
setErrors(undefined);
setIsImporting(true);
if (!props.name) {
@ -339,18 +382,21 @@ export default function FontEditor(props: {
spec: 1,
name: name,
main: fontEntries,
__source: source
source
})
.then(() => navigation.goBack())
.catch(e => setErrors(e))
.finally(() => setIsImporting(false));
} else {
Object.assign(fonts[props.name], {
name: name,
main: fontEntries,
__edited: true
});
setIsImporting(false);
navigation.goBack();
updateFont(fonts[props.name])
.then(() => navigation.goBack())
.catch(e => setErrors(e))
.finally(() => setIsImporting(false));
}
}}
icon={findAssetId(props.name ? "toast_image_saved" : "DownloadIcon")}

View file

@ -1,6 +1,6 @@
import { findAssetId } from "@lib/api/assets";
import { clipboard } from "@metro/common";
import { LegacyFormText, TableRow } from "@metro/common/components";
import { TableRow, TableRowTrailingText } from "@metro/common/components";
import { showToast } from "@ui/toasts";
import { ImageURISource } from "react-native";
@ -14,8 +14,8 @@ export default function Version({ label, version, icon }: VersionProps) {
return (
<TableRow
label={label}
trailing={<TableRowTrailingText text={version} />}
icon={<TableRow.Icon source={typeof icon === "string" ? findAssetId(icon) : icon} />}
trailing={<LegacyFormText>{version}</LegacyFormText>}
onPress={() => {
clipboard.setString(`${label} - ${version}`);
showToast.showCopyToClipboard();

View file

@ -10,8 +10,10 @@ import { settings } from "@lib/api/settings";
import { openAlert } from "@lib/ui/alerts";
import { DISCORD_SERVER, GITHUB } from "@lib/utils/constants";
import { NavigationNative } from "@metro/common";
import { NavigationNative } from "@metro/common";
import { AlertActionButton, AlertActions, AlertModal, Stack, TableRow, TableRowGroup, TableSwitchRow } from "@metro/common/components";
import { Linking, ScrollView } from "react-native";
import { Linking, ScrollView } from "react-native";
export default function General() {
useProxy(settings);

View file

@ -1,4 +1,4 @@
import { UnifiedPluginModel } from "..";
import { UnifiedPluginModel } from "../models";
export interface PluginInfoActionSheetProps {
plugin: UnifiedPluginModel;

View file

@ -34,7 +34,7 @@ export async function createVdPluginObject(plugin: VendettaPlugin) {
// Wrapping this with wrapSync is NOT an option.
storage: await createStorage<Record<string, any>>(storage.createMMKVBackend(plugin.id)),
},
logger: new LoggerClass(`Bunny » ${plugin.manifest.name}`),
logger: new LoggerClass(`Revenge » ${plugin.manifest.name}`),
};
}

View file

@ -98,7 +98,7 @@ export const VdPluginManager = {
// Wrapping this with wrapSync is NOT an option.
storage: await createStorage<Record<string, any>>(createMMKVBackend(plugin.id)),
},
logger: new LoggerClass(`Bunny » ${plugin.manifest.name}`),
logger: new LoggerClass(`Revenge » ${plugin.manifest.name}`),
};
const pluginString = `vendetta=>{return ${plugin.js}}\n//# sourceURL=${plugin.id}`;

View file

@ -66,11 +66,7 @@ if (typeof globalThis.__r !== "undefined") {
_requireFunc = function patchedRequire(a: number) {
// Initializing index.ts(x)
if (a === 0) {
// https://github.com/facebook/metro/commit/1361405ffe6f1bdef54295addfef0f30523aaab2
if (window.modules instanceof Map) {
window.modules = Object.fromEntries([...window.modules]);
}
if (window.modules instanceof Map) window.modules = Object.fromEntries(window.modules);
onceIndexRequired(v);
_requireFunc = v;
} else return v(a);

View file

@ -4,13 +4,12 @@ import { safeFetch } from "@lib/utils";
type FontMap = Record<string, string>;
export interface FontDefinition {
export type FontDefinition = {
spec: 1;
name: string;
description?: string;
main: FontMap;
__source?: string;
__edited?: boolean;
source?: string
}
type FontStorage = Record<string, FontDefinition> & { __selected?: string; };
@ -40,12 +39,9 @@ export function validateFont(font: FontDefinition) {
export async function saveFont(data: string | FontDefinition, selected = false) {
let fontDefJson: FontDefinition;
if (typeof data === "object" && data.__source) data = data.__source;
if (typeof data === "string") {
try {
fontDefJson = await (await safeFetch(data)).json();
fontDefJson.__source = data;
} catch (e) {
throw new Error(`Failed to fetch fonts at ${data}`, { cause: e });
}
@ -55,16 +51,14 @@ export async function saveFont(data: string | FontDefinition, selected = false)
validateFont(fontDefJson);
try {
await Promise.all(Object.entries(fontDefJson.main).map(async ([font, url]) => {
let ext = url.split(".").pop();
if (ext !== "ttf" && ext !== "otf") ext = "ttf";
const path = `downloads/fonts/${fontDefJson.name}/${font}.${ext}`;
if (!await fileExists(path)) await downloadFile(url, path);
}));
} catch (e) {
throw new Error("Failed to download font assets", { cause: e });
}
const errors = await Promise.allSettled(Object.entries(fontDefJson.main).map(async ([font, url]) => {
let ext = url.split(".").pop();
if (ext !== "ttf" && ext !== "otf") ext = "ttf";
const path = `downloads/fonts/${fontDefJson.name}/${font}.${ext}`;
if (!await fileExists(path)) await downloadFile(url, path);
})).then(it => it.map(it => it.status === 'fulfilled' ? undefined : it.reason));
if (errors.some(it => it)) throw errors
fonts[fontDefJson.name] = fontDefJson;
@ -72,14 +66,21 @@ export async function saveFont(data: string | FontDefinition, selected = false)
return fontDefJson;
}
export async function installFont(url: string, selected = false) {
if (
typeof url !== "string"
|| Object.values(fonts).some(f => typeof f === "object" && f.__source === url)
) {
throw new Error("Invalid source or font was already installed");
export async function updateFont(fontDef: FontDefinition) {
let fontDefCopy = { ...fontDef }
if (fontDefCopy.source) fontDefCopy = {
...await fetch(fontDefCopy.source).then(it => it.json()),
// Can't change these properties
name: fontDef.name,
source: fontDef.source
}
const selected = fonts.__selected === fontDef.name
await removeFont(fontDef.name)
await saveFont(fontDefCopy, selected)
}
export async function installFont(url: string, selected = false) {
const font = await saveFont(url);
if (selected) await selectFont(font.name);
}

View file

@ -8,6 +8,7 @@ export interface Asset {
id: number;
name: string;
moduleId: number;
type: string;
}
// Cache common usage

View file

@ -18,6 +18,15 @@ export async function removeFile(path: string, { prefix = "pyoncord/" } = {}) {
return void await NativeFileModule.removeFile("documents", `${prefix}${path}`);
}
/**
* Remove file from given path, currently no check for any failure
* @param path Path to the file
*/
export async function removeCacheFile(path: string, prefix = "pyoncord/") {
if (typeof NativeFileModule.removeFile !== "function") throw new Error("'fs.removeFile' is not supported");
return void await NativeFileModule.removeFile("cache", `${prefix}${path}`);
}
/**
* Check if the file or directory given by the path exists
* @param path Path to the file
@ -57,7 +66,7 @@ export async function readFile(path: string, { prefix = "pyoncord/" } = {}): Pro
export async function downloadFile(url: string, path: string, { prefix = "pyoncord/" } = {}) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to download file from ${url}: ${response.statusText}`);
throw new Error(`Failed to download file from ${url}: ${response.status}`);
}
const arrayBuffer = await response.arrayBuffer();

View file

@ -1,4 +1,5 @@
import { VdThemeInfo } from "@lib/addons/themes";
import { removeCacheFile } from "./fs";
// @ts-ignore
const pyonLoaderIdentity = globalThis.__PYON_LOADER__;
@ -222,3 +223,8 @@ export function isFontSupported() {
return false;
}
export async function clearBundle() {
// TODO: This should be not be hardcoded, maybe put in loader.json?
return void await removeCacheFile("bundle.js");
}

View file

@ -23,6 +23,12 @@ export function deleteJsxCreate(Component: string, callback: Callback) {
*/
export function patchJsx() {
const callback = ([Component]: unknown[], ret: JSX.Element) => {
// Band-aid fix for iOS invalid element type crashes
if (typeof ret.type === 'undefined') {
ret.type = 'RCTView'
return ret
}
// The check could be more complex, but this is fine for now to avoid overhead
if (typeof Component === "function" && callbacks.has(Component.name)) {
const cbs = callbacks.get(Component.name)!;

View file

@ -12,7 +12,7 @@ export interface SearchProps {
}
function SearchIcon() {
return <Image style={{ width: 16, height: 16 }} source={findAssetId("search")} />;
return <Image style={{ width: 16, height: 16 }} source={findAssetId("search")!} />;
}
export default ({ onChangeText, placeholder, style, isRound }: SearchProps) => {

View file

@ -2,7 +2,6 @@
import { lazy } from "react";
import type { ImageURISource } from "react-native";
import { patchPanelUI } from "./patches/panel";
import { patchTabsUI } from "./patches/tabs";
export interface RowConfig {
@ -11,6 +10,7 @@ export interface RowConfig {
onPress?: () => any;
render?: Parameters<typeof lazy>[0];
icon?: ImageURISource | number;
IconComponent?: React.ReactNode,
usePredicate?: () => boolean,
useTrailing?: () => string | JSX.Element,
rawTabsConfig?: Record<string, any>;
@ -31,7 +31,6 @@ export function registerSection(section: { name: string; items: RowConfig[]; })
export function patchSettings() {
const unpatches = new Array<() => boolean>;
patchPanelUI(unpatches);
patchTabsUI(unpatches);
return () => unpatches.forEach(u => u());

View file

@ -5,6 +5,8 @@ import { findByNameLazy, findByPropsLazy } from "@metro/wrappers";
import { registeredSections } from "@ui/settings";
import { CustomPageRenderer, wrapOnPress } from "./shared";
import { Strings } from "@core/i18n";
import { TableRowIcon } from "@metro/common/components";
const settingConstants = findByPropsLazy("SETTING_RENDERER_CONFIG");
const SettingsOverviewScreen = findByNameLazy("SettingsOverviewScreen", false);

View file

@ -3,6 +3,7 @@ import { findByFilePathLazy, findByProps, findByPropsLazy } from "@metro/wrapper
import { Linking } from "react-native";
import type { Dispatcher } from "./types/flux";
import { Linking } from "react-native";
export * as components from "./components";
@ -10,6 +11,21 @@ export * as components from "./components";
export const constants = findByPropsLazy("Fonts", "Permissions");
export const channels = findByPropsLazy("getVoiceChannelId");
export const i18n = findByPropsLazy("Messages");
// Polyfill LinkingUtils
const openURL = (url: string) => Linking.openURL(url);
export const url = nativeModuleProxy.NativeLinkingModule || nativeModuleProxy.DCDLinkingManager ? {
openURL,
openDeeplink: openURL,
handleSupportedURL: openURL,
isDiscordConnectOauth2Deeplink: () => {
console.warn("url.isDiscordConnectOauth2Deeplink is not implemented and will always return false");
return false;
},
showLongPressUrlActionSheet: () => console.warn("url.showLongPressUrlActionSheet is not implemented"),
handleMessageLinking: findByFilePathLazy("modules/links/native/handleContentLinking.tsx", true),
} : findByPropsLazy("openURL", "openDeeplink");
export const clipboard = findByPropsLazy("setString", "getString", "hasString");
export const assets = findByPropsLazy("registerAsset");
export const invites = findByPropsLazy("acceptInviteAndTransitionToInviteChannel");

View file

@ -142,7 +142,6 @@ interface FABProps {
style?: Style;
onPress: () => void;
positionBottom?: number;
}
export type FloatingActionButton = React.FC<FABProps>;

View file

@ -6,9 +6,7 @@ import { ModuleFlags, ModulesMapInternal } from "./enums";
const { before, instead } = require("spitroast");
export const metroModules: Metro.ModuleList = window.modules;
// Since modules registry is now a Map, the id is now strictly a number
// Making the codebase to adapt to this change would be more ideal, though
const metroRequire = (id: number | string) => window.__r(+id);
const metroRequire = (id: string | number) => window.__r(+id);
// eslint-disable-next-line func-call-spacing
const moduleSubscriptions = new Map<number, Set<() => void>>();