From 187dfc74ffaff295c31f6933b89f6b9deda4a19b Mon Sep 17 00:00:00 2001 From: Bwlok Date: Mon, 3 Nov 2025 22:02:48 +0100 Subject: [PATCH 1/8] fix(dev): Fix React Devtools & split input section --- src/core/i18n/default.json | 1 + .../ui/settings/pages/Developer/index.tsx | 95 ++++++++++++++----- src/lib/api/settings.ts | 1 + 3 files changed, 72 insertions(+), 25 deletions(-) diff --git a/src/core/i18n/default.json b/src/core/i18n/default.json index d97d968..3ab3350 100644 --- a/src/core/i18n/default.json +++ b/src/core/i18n/default.json @@ -35,6 +35,7 @@ "COPY_URL": "Copy URL", "DEBUG": "Debug", "DEBUGGER_URL": "Debugger URL", + "DEVTOOLS_URL": "DevTools URL", "DELETE": "Delete", "DESC_EXTRACT_FONTS_FROM_THEME": "Looks out for \"fonts\" field in your currently applied theme and install it.", "DEVELOPER": "Developer", diff --git a/src/core/ui/settings/pages/Developer/index.tsx b/src/core/ui/settings/pages/Developer/index.tsx index 6f66a81..788f0b1 100644 --- a/src/core/ui/settings/pages/Developer/index.tsx +++ b/src/core/ui/settings/pages/Developer/index.tsx @@ -15,13 +15,14 @@ import { ErrorBoundary } from "@ui/components"; 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 RDT_EMBED_LINK = ""; +const RDT_EMBED_LINK = "https://cdn.bwlok.dev/rdt/devTools.js"; const useStyles = createStyles({ leadingText: { @@ -44,31 +45,75 @@ export default function Developer() { - ws://} - defaultValue={settings.debuggerUrl} - onChange={(v: string) => settings.debuggerUrl = v} - /> - - } - onPress={() => connectToDebugger(settings.debuggerUrl)} - /> - {isReactDevToolsPreloaded() && <> - } - onPress={() => window[getReactDevToolsProp() || "__vendetta_rdc"]?.connectToDevTools({ - host: settings.debuggerUrl.split(":")?.[0], - resolveRNStyle: StyleSheet.flatten, - })} - /> - } + + + ws://} + defaultValue={settings.debuggerUrl} + onChange={(v: string) => settings.debuggerUrl = v} + /> + + } + onPress={() => connectToDebugger(settings.debuggerUrl)} + /> + + + {isReactDevToolsPreloaded() && <> + + + ws://} + defaultValue={settings.devToolsUrl} + onChange={(v: string) => settings.devToolsUrl = v} + /> + + } + 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")); + } + }} + + /> + + + } {isLoaderConfigSupported() && <> Date: Tue, 4 Nov 2025 08:14:44 +0100 Subject: [PATCH 2/8] redma: update KettuTweak infomation --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 76ef8e4..0854838 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,14 @@ A mod for Discord on Android - **Root** with Xposed - [KettuXposed](https://github.com/C0C0B01/KettuXposed/releases/latest) - **Non-root** - [KettuManager](https://github.com/C0C0B01/KettuManager/releases/latest) +If you do not know what root is, go with the manager + ### iOS -- [BTLoader](https://github.com/CloudySn0w/BTLoader) + +- ** Jailbroken** - [KettuTweak](https://github.com/C0C0B01/KettuTweak) +- **Jailed** - [BTLoader](https://github.com/CloudySn0w/BTLoader) + +if you dont know the difference, go with BTLoader ## Building 1. Install a Kettu loader with loader config support (any mentioned in the [Installing](#installing) section). From 396394fc8ccca6674794f91184f4c6775a6633f5 Mon Sep 17 00:00:00 2001 From: cocobo1 Date: Tue, 4 Nov 2025 08:39:14 +0100 Subject: [PATCH 3/8] readme: update markdown --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0854838..e910881 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ If you do not know what root is, go with the manager ### iOS -- ** Jailbroken** - [KettuTweak](https://github.com/C0C0B01/KettuTweak) +- **Jailbroken** - [KettuTweak](https://github.com/C0C0B01/KettuTweak) - **Jailed** - [BTLoader](https://github.com/CloudySn0w/BTLoader) if you dont know the difference, go with BTLoader From 0983f7ea25826a032717607cd0d6f8487bf39ea4 Mon Sep 17 00:00:00 2001 From: cocobo1 Date: Tue, 4 Nov 2025 18:53:02 +0200 Subject: [PATCH 4/8] readme: make language clearer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e910881..116de2d 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ If you do not know what root is, go with the manager - **Jailbroken** - [KettuTweak](https://github.com/C0C0B01/KettuTweak) - **Jailed** - [BTLoader](https://github.com/CloudySn0w/BTLoader) -if you dont know the difference, go with BTLoader +if you dont know what jailbreak is, go with BTLoader ## Building 1. Install a Kettu loader with loader config support (any mentioned in the [Installing](#installing) section). From ddcb1d66a80658cf2868e407bfc470ffe3d38bdc Mon Sep 17 00:00:00 2001 From: Bwlok Date: Thu, 6 Nov 2025 23:48:54 +0100 Subject: [PATCH 5/8] feat(Themes): add compatibility with Enmity themes --- src/lib/addons/themes/colors/parser.ts | 95 +++++++++++++++++++++++++- src/lib/addons/themes/colors/types.ts | 2 +- src/lib/addons/themes/index.ts | 17 +++-- 3 files changed, 106 insertions(+), 8 deletions(-) diff --git a/src/lib/addons/themes/colors/parser.ts b/src/lib/addons/themes/colors/parser.ts index e7e5c40..e2d7ced 100644 --- a/src/lib/addons/themes/colors/parser.ts +++ b/src/lib/addons/themes/colors/parser.ts @@ -90,7 +90,6 @@ export function parseColorManifest(manifest: ColorManifest): InternalColorDefini manifest.rawColors = draft; } - return { spec: 2, reference: resolveType(), @@ -100,9 +99,97 @@ export function parseColorManifest(manifest: ColorManifest): InternalColorDefini }; } + if (manifest.spec === 1 || (manifest.theme_color_map && !manifest.spec)) { + + const semanticDefinitions: InternalColorDefinition["semantic"] = {}; + const rawDefinitions: Record = {}; + + const themeIndex = resolveType() === "light" ? 1 : 0; + + if (manifest.theme_color_map) { + for (const [key, colorArray] of Object.entries(manifest.theme_color_map)) { + if (!Array.isArray(colorArray) || colorArray.length < 2) { + console.warn(`[Parser] Invalid color array for ${key}:`, colorArray); + continue; + } + + const colorValue = colorArray[themeIndex]; + + if (!colorValue) { + console.warn(`[Parser] Missing color value for ${key} at index ${themeIndex}`); + continue; + } + + const normalized = normalizeToHex(colorValue); + if (normalized) { + semanticDefinitions[key] = { + value: normalized, + opacity: 1 + }; + } + } + } + + const rawColors = manifest.colours || manifest.colors; + if (rawColors) { + for (const [key, colorValue] of Object.entries(rawColors)) { + if (typeof colorValue !== "string") continue; + + const normalized = normalizeToHex(colorValue); + if (normalized) { + const discordKey = convertEnmityToDiscordRawKey(key); + rawDefinitions[discordKey] = normalized; + + if (discordKey !== key) { + rawDefinitions[key] = normalized; + } + } + } + } + + if (manifest.unsafe_colors) { + for (const [key, colorValue] of Object.entries(manifest.unsafe_colors)) { + if (typeof colorValue !== "string") continue; + + const normalized = normalizeToHex(colorValue); + if (normalized) { + rawDefinitions[key] = normalized; + } + } + } + + if (Platform.OS === "android") applyAndroidAlphaKeys(rawDefinitions); + + return { + spec: 2, + reference: resolveType(), + semantic: semanticDefinitions, + raw: rawDefinitions, + background: manifest.background + }; + } throw new Error("Invalid theme spec"); } + function convertEnmityToDiscordRawKey(enmityKey: string): string { + const conversions: Record = { + "PRIMARY_DARK": "PRIMARY_100", + "PRIMARY_DARK_100": "PRIMARY_100", + "PRIMARY_DARK_200": "PRIMARY_200", + "PRIMARY_DARK_300": "PRIMARY_300", + "PRIMARY_DARK_360": "PRIMARY_360", + "PRIMARY_DARK_400": "PRIMARY_400", + "PRIMARY_DARK_500": "PRIMARY_500", + "PRIMARY_DARK_600": "PRIMARY_600", + "PRIMARY_DARK_630": "PRIMARY_630", + "PRIMARY_DARK_700": "PRIMARY_700", + "PRIMARY_DARK_800": "PRIMARY_800", + "PRIMARY_DARK_900": "PRIMARY_900", + }; + + return conversions[enmityKey] || enmityKey; + } + export function applyAndroidAlphaKeys(rawColors?: Record) { if (!rawColors) return; @@ -129,6 +216,11 @@ export function applyAndroidAlphaKeys(rawColors?: Record) { export function normalizeToHex(colorString: string | undefined): string | undefined { if (colorString === undefined) return undefined; + + if (colorString.toLowerCase() === "transparent") { + return "#00000000"; + } + if (chroma.valid(colorString)) return chroma(colorString).hex(); const color = Number(processColor(colorString)); @@ -140,4 +232,3 @@ export function normalizeToHex(colorString: string | undefined): string | undefi color >> 24 & 0xff // alpha ).hex(); } - diff --git a/src/lib/addons/themes/colors/types.ts b/src/lib/addons/themes/colors/types.ts index 99b6e39..203f3c0 100644 --- a/src/lib/addons/themes/colors/types.ts +++ b/src/lib/addons/themes/colors/types.ts @@ -39,7 +39,7 @@ export interface VendettaThemeManifest { /** @internal */ export interface InternalColorDefinition { - spec: 2 | 3; + spec: 1 | 2 | 3; reference: "darker" | "light"; semantic: Record Date: Fri, 7 Nov 2025 06:55:56 +0100 Subject: [PATCH 6/8] Improve AssetBrowser displaying --- .../settings/pages/Developer/AssetBrowser.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/core/ui/settings/pages/Developer/AssetBrowser.tsx b/src/core/ui/settings/pages/Developer/AssetBrowser.tsx index b1bdcbb..bc91c95 100644 --- a/src/core/ui/settings/pages/Developer/AssetBrowser.tsx +++ b/src/core/ui/settings/pages/Developer/AssetBrowser.tsx @@ -3,7 +3,7 @@ import { iterateAssets } from "@lib/api/assets"; import { Text } from "@metro/common/components"; import { ErrorBoundary, Search } from "@ui/components"; import { useMemo } from "react"; -import { FlatList, View } from "react-native"; +import { FlatList, View, ScrollView } from "react-native"; export default function AssetBrowser() { const [search, setSearch] = React.useState(""); @@ -16,15 +16,18 @@ export default function AssetBrowser() { style={{ margin: 10 }} onChangeText={(v: string) => setSearch(v)} /> - - Some assets types cannot be displayed and will be marked in red. - a.name.includes(search) || a.id.toString() === search)} - renderItem={({ item }: any) => } - contentContainerStyle={{ overflow: 'hidden', backgroundColor: 'transparent', borderRadius: 16 }} - keyExtractor={a => a.name} - /> - + + + Some assets types cannot be displayed and will be marked in red. + a.name.includes(search) || a.id.toString() === search)} + renderItem={({ item }: any) => } + contentContainerStyle={{ overflow: 'hidden', backgroundColor: 'transparent', borderRadius: 16 }} + scrollEnabled={false} + keyExtractor={a => a.id.toString()} + /> + + ); From f80888f8afa15d0797805becdf4a652fcdbd02b1 Mon Sep 17 00:00:00 2001 From: Bwlok Date: Fri, 7 Nov 2025 06:57:46 +0100 Subject: [PATCH 7/8] feat(Dev): Add option to filter NonImage Assets --- .../settings/pages/Developer/AssetBrowser.tsx | 55 ++++++++++++++++--- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/src/core/ui/settings/pages/Developer/AssetBrowser.tsx b/src/core/ui/settings/pages/Developer/AssetBrowser.tsx index bc91c95..785cb78 100644 --- a/src/core/ui/settings/pages/Developer/AssetBrowser.tsx +++ b/src/core/ui/settings/pages/Developer/AssetBrowser.tsx @@ -1,27 +1,64 @@ import AssetDisplay from "@core/ui/settings/pages/Developer/AssetDisplay"; -import { iterateAssets } from "@lib/api/assets"; +import { iterateAssets, findAssetId } from "@lib/api/assets"; import { Text } from "@metro/common/components"; import { ErrorBoundary, Search } from "@ui/components"; import { useMemo } from "react"; -import { FlatList, View, ScrollView } from "react-native"; +import { FlatList, View, ScrollView, TouchableOpacity, Image } from "react-native"; + +const displayable = new Set(['png', 'jpg', 'svg']); export default function AssetBrowser() { const [search, setSearch] = React.useState(""); + const [showNonImages, setShowNonImages] = React.useState(false); + const all = useMemo(() => Array.from(iterateAssets()), []); + const filteredData = useMemo(() => { + let result = all.filter(a => a.name.includes(search) || a.id.toString() === search); + + if (!showNonImages) { + result = result.filter(a => displayable.has(a.type)); + } + + return result; + }, [all, search, showNonImages]); + + return ( - setSearch(v)} - /> + + + setSearch(v)} + /> + + setShowNonImages(!showNonImages)} + > + + + - Some assets types cannot be displayed and will be marked in red. + {showNonImages && ( + Some assets types cannot be displayed and will be marked in red. + )} a.name.includes(search) || a.id.toString() === search)} - renderItem={({ item }: any) => } + data={filteredData} + renderItem={({ item }: any) => } contentContainerStyle={{ overflow: 'hidden', backgroundColor: 'transparent', borderRadius: 16 }} scrollEnabled={false} keyExtractor={a => a.id.toString()} From fef67515352543494230f886f0ad3db60a45891f Mon Sep 17 00:00:00 2001 From: cocobo1 Date: Fri, 7 Nov 2025 22:12:36 +0200 Subject: [PATCH 8/8] modules: rudimentry bubblechat support --- src/index.ts | 2 - src/lib/api/native/modules/bubblemodule.ts | 16 ++++ src/lib/api/native/modules/index.ts | 3 + src/lib/api/native/modules/loader.ts | 82 ++++++++++++++++++ src/lib/api/native/modules/types.ts | 39 +++++++++ src/metro/internals/modules.ts | 97 ++++++++++++++++++++++ 6 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 src/lib/api/native/modules/bubblemodule.ts create mode 100644 src/lib/api/native/modules/loader.ts diff --git a/src/index.ts b/src/index.ts index 341b32f..ec767b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,8 +19,6 @@ import * as lib from "./lib"; export default async () => { // Load everything in parallel - updatePlugins(); - await Promise.all([ initThemes(), injectFluxInterceptor(), diff --git a/src/lib/api/native/modules/bubblemodule.ts b/src/lib/api/native/modules/bubblemodule.ts new file mode 100644 index 0000000..8b84133 --- /dev/null +++ b/src/lib/api/native/modules/bubblemodule.ts @@ -0,0 +1,16 @@ +import { processColor } from "react-native"; +import { getModule } from "./loader"; +import type { LoaderModule } from "./loader"; + +interface BubbleModule extends LoaderModule { + hookBubbles(): Promise; + unhookBubbles(): Promise; + configure(avatarRadius?: number, bubbleRadius?: number, bubbleColor?: string): Promise; +} + +export default getModule({ + name: "BubbleModule", + argumentProcessors: { + configure: args => [args[0], args[1], Number(processColor(args[2]))], + }, +}); \ No newline at end of file diff --git a/src/lib/api/native/modules/index.ts b/src/lib/api/native/modules/index.ts index d6919cc..2c63415 100644 --- a/src/lib/api/native/modules/index.ts +++ b/src/lib/api/native/modules/index.ts @@ -33,3 +33,6 @@ export const NativeThemeModule = getNativeModule( export const BundleUpdaterManager = getNativeModule( "BundleUpdaterManager" )!; +export const ImageLoader = getNativeModule( + "ImageLoader" +)!; \ No newline at end of file diff --git a/src/lib/api/native/modules/loader.ts b/src/lib/api/native/modules/loader.ts new file mode 100644 index 0000000..7a6fd65 --- /dev/null +++ b/src/lib/api/native/modules/loader.ts @@ -0,0 +1,82 @@ +import { ImageLoader } from "./"; +import type { LoaderPayload } from "./types"; + +type InteropReturn = Promise<{ ret: string } | { err: string } | { cancelled: true; reason: string }>; + +export const loaderPayload = window.__PYON_LOADER__ as LoaderPayload; + +export function isModuleRegistered(module: string) { + return loaderPayload.loader.modules[module] !== undefined; +} + +export function isFunctionRegistered(module: string, function_: string) { + return loaderPayload.loader.modules[module]?.functions[function_] !== undefined; +} + +export async function callFunction(module: string, functionName: string, args: unknown[]) { + if (!isFunctionRegistered(module, functionName)) { + throw new Error(`Function ${module}.${functionName} is not registered`); + } + + const promise: InteropReturn = ImageLoader.queryCache([ + "__pyon_bridge", + JSON.stringify({ m: module, f: functionName, a: args }), + ]); + + const result = await promise; + + if ("ret" in result) { + return JSON.parse(result.ret); + } + + if ("cancelled" in result) { + throw new Error(`Function ${module}.${functionName} was cancelled: ${result.reason}`); + } + + if ("err" in result) { + throw new Error(result.err); + } +} + +export interface LoaderModule { + hasFunction(functionName: string): boolean; + callFunction(functionName: string, args: unknown[]): Promise; +} + +type ModuleProps = { + name: string; + argumentProcessors?: Partial<{ [key in keyof T]: (args: any[]) => any[] }>; +}; + +export function getModule({ name, argumentProcessors: processArgs }: ModuleProps) { + const module = loaderPayload.loader.modules[name]; + if (!module) { + throw new Error(`Module ${name} is not registered`); + } + + const baseObject: Record = { + hasFunction: (functionName: string) => isFunctionRegistered(name, functionName), + callFunction: (functionName: string, args: unknown[]) => callFunction(name, functionName, args), + }; + + for (const [functionName] of Object.entries(module.functions)) { + baseObject[functionName] = (...args: unknown[]) => + callFunction( + name, + functionName, + // @ts-expect-error + processArgs && functionName in processArgs ? processArgs[functionName](args) : args, + ); + } + + return baseObject as T; +} + +/** + * Determines if safe mode is currently enabled. The result remains consistent over time. + * This will not change until the app is restarted. For draft changes, use the InitConfigStore. + * @returns True if safe mode is enabled for the current instance, otherwise false. + */ +export function isSafeModeEnabled() { + return Boolean(loaderPayload.loader.initConfig.safeMode); +} \ No newline at end of file diff --git a/src/lib/api/native/modules/types.ts b/src/lib/api/native/modules/types.ts index fdb3a32..cf54956 100644 --- a/src/lib/api/native/modules/types.ts +++ b/src/lib/api/native/modules/types.ts @@ -134,3 +134,42 @@ export namespace RNModules { } } } + +export interface InitConfig { + baseUrl: string; + forceUpdate: boolean; + skipUpdate: boolean; + bundlePath: string | null; + safeMode: boolean; +} + +interface LoaderConstants { + WINTRY_DIR: string; + DEFAULT_BASE_URL: string; +} + +interface LoaderModule { + functions: { + [functionName: string]: number; + }; + constants: { + [constantName: string]: any; + }; +} + +interface LoaderModules { + [moduleName: string]: LoaderModule; +} + +export interface LoaderPayload { + bundle: { + revision: string; + }; + loader: { + name: string; + version: string; + initConfig: InitConfig; + constants: LoaderConstants; + modules: LoaderModules; + }; +} \ No newline at end of file diff --git a/src/metro/internals/modules.ts b/src/metro/internals/modules.ts index 964b116..81aa485 100644 --- a/src/metro/internals/modules.ts +++ b/src/metro/internals/modules.ts @@ -238,3 +238,100 @@ export function* getCachedPolyfillModules(name: string) { } } } + +export interface WaitForOptions { + count?: number; +} + +export type ModuleFilter = { + (exports: any): T | undefined; + key?: string; +} + +export function waitFor( + filter: ModuleFilter, + callback: (exports: T, id: Metro.ModuleID) => void, + options: WaitForOptions = {} +): () => void { + const { count = 1 } = options; + let currentCount = 0; + const unsubscribers: Array<() => void> = []; + let isActive = true; + + const cleanup = () => { + if (!isActive) return; + isActive = false; + unsubscribers.forEach(unsub => unsub()); + unsubscribers.length = 0; + }; + + function checkModule(id: Metro.ModuleID): boolean { + if (!isActive) return true; + + const exports = requireModule(id); + if (isBadExports(exports)) return false; + + const result = filter(exports); + if (!result) return false; + + callback(result, id); + + if (++currentCount >= count) { + cleanup(); + return true; + } + + return false; + } + + if (filter.key) { + const cache = getMetroCache().findIndex[filter.key]; + if (cache) { + for (const id in cache) { + if (id[0] === "_") continue; + const numId = Number(id); + + if (metroModules[numId]?.isInitialized) { + if (checkModule(numId)) return cleanup; + } else { + const unsub = subscribeModule(numId, () => { + checkModule(numId); + }); + unsubscribers.push(unsub); + } + } + } + } + + for (const id in metroModules) { + if (!isActive) break; + const numId = Number(id); + + if (metroModules[numId]?.isInitialized && !metroModules[numId]?.hasError) { + if (checkModule(numId)) return cleanup; + } + } + + if (isActive) { + for (const id in metroModules) { + const numId = Number(id); + if (!metroModules[numId]?.isInitialized) { + const unsub = subscribeModule(numId, () => { + checkModule(numId); + }); + unsubscribers.push(unsub); + } + } + } + + return cleanup; +} + +export function waitForModule( + filter: ModuleFilter, + options: WaitForOptions = {} +): Promise { + return new Promise((resolve) => { + waitFor(filter, (exports) => resolve(exports), options); + }); +}