Merge branch 'dev' into github
This commit is contained in:
commit
cf713c19e2
14 changed files with 475 additions and 50 deletions
|
|
@ -9,9 +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
|
||||
- [KettuTweak](https://github.com/C0C0B01/KettuTweak)
|
||||
- [BTLoader](https://github.com/CloudySn0w/BTLoader)
|
||||
|
||||
- **Jailbroken** - [KettuTweak](https://github.com/C0C0B01/KettuTweak)
|
||||
- **Jailed** - [BTLoader](https://github.com/CloudySn0w/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).
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,30 +1,70 @@
|
|||
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 } 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 (
|
||||
<ErrorBoundary>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Search
|
||||
style={{ margin: 10 }}
|
||||
onChangeText={(v: string) => setSearch(v)}
|
||||
/>
|
||||
<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 style={{ flexDirection: 'row', alignItems: 'center', margin: 10 }}>
|
||||
<View style={{ flex: 1, marginRight: 10 }}>
|
||||
<Search
|
||||
onChangeText={(v: string) => setSearch(v)}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
padding: 12,
|
||||
backgroundColor: showNonImages ? '#0f1013' : '#303139',
|
||||
borderRadius: 8,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minWidth: 44,
|
||||
minHeight: 44,
|
||||
}}
|
||||
onPress={() => setShowNonImages(!showNonImages)}
|
||||
>
|
||||
<Image
|
||||
style={{ width: 20, height: 20 }}
|
||||
source={findAssetId("ic_image")!}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<ScrollView>
|
||||
<View style={{ flex: 1, borderRadius: 16, paddingHorizontal: 12, overflow: 'hidden', backgroundColor: 'transparent' }}>
|
||||
{showNonImages && (
|
||||
<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={filteredData}
|
||||
renderItem={({ item }: any) => <AssetDisplay asset={item} showNonImages={showNonImages} />}
|
||||
contentContainerStyle={{ overflow: 'hidden', backgroundColor: 'transparent', borderRadius: 16 }}
|
||||
scrollEnabled={false}
|
||||
keyExtractor={a => a.id.toString()}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<ErrorBoundary>
|
||||
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: 38 }}>
|
||||
<Stack style={{ paddingVertical: 24, paddingHorizontal: 12 }} spacing={24}>
|
||||
<TextInput
|
||||
label={Strings.DEBUGGER_URL}
|
||||
placeholder="127.0.0.1:9090"
|
||||
size="md"
|
||||
leadingIcon={() => <LegacyFormText style={styles.leadingText}>ws://</LegacyFormText>}
|
||||
defaultValue={settings.debuggerUrl}
|
||||
onChange={(v: string) => settings.debuggerUrl = v}
|
||||
/>
|
||||
<TableRowGroup title={Strings.DEBUG}>
|
||||
<TableRow
|
||||
label={Strings.CONNECT_TO_DEBUG_WEBSOCKET}
|
||||
icon={<TableRow.Icon source={findAssetId("copy")} />}
|
||||
onPress={() => connectToDebugger(settings.debuggerUrl)}
|
||||
/>
|
||||
{isReactDevToolsPreloaded() && <>
|
||||
<TableRow
|
||||
label={Strings.CONNECT_TO_REACT_DEVTOOLS}
|
||||
icon={<TableRow.Icon source={findAssetId("ic_badge_staff")} />}
|
||||
onPress={() => window[getReactDevToolsProp() || "__vendetta_rdc"]?.connectToDevTools({
|
||||
host: settings.debuggerUrl.split(":")?.[0],
|
||||
resolveRNStyle: StyleSheet.flatten,
|
||||
})}
|
||||
/>
|
||||
</>}
|
||||
|
||||
<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={{
|
||||
marginTop: 4,
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<TableRow
|
||||
label={Strings.CONNECT_TO_DEBUG_WEBSOCKET}
|
||||
icon={<TableRow.Icon source={findAssetId("copy")} />}
|
||||
onPress={() => connectToDebugger(settings.debuggerUrl)}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
</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'
|
||||
}}>
|
||||
<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"));
|
||||
}
|
||||
}}
|
||||
|
||||
/>
|
||||
</Stack>
|
||||
</TableRowGroup>
|
||||
</>}
|
||||
{isLoaderConfigSupported() && <>
|
||||
<TableRowGroup title="Loader config">
|
||||
<TableSwitchRow
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ import * as lib from "./lib";
|
|||
export default async () => {
|
||||
// Load everything in parallel
|
||||
|
||||
updatePlugins();
|
||||
|
||||
await Promise.all([
|
||||
initThemes(),
|
||||
injectFluxInterceptor(),
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {};
|
||||
|
||||
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<string, string> = {
|
||||
"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<string, string>) {
|
||||
if (!rawColors) return;
|
||||
|
||||
|
|
@ -129,6 +216,11 @@ export function applyAndroidAlphaKeys(rawColors?: Record<string, string>) {
|
|||
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export interface VendettaThemeManifest {
|
|||
|
||||
/** @internal */
|
||||
export interface InternalColorDefinition {
|
||||
spec: 2 | 3;
|
||||
spec: 1 | 2 | 3;
|
||||
reference: "darker" | "light";
|
||||
semantic: Record<string, {
|
||||
value: string;
|
||||
|
|
|
|||
|
|
@ -53,19 +53,26 @@ function processData(data: VendettaThemeManifest) {
|
|||
if (Platform.OS === "android") applyAndroidAlphaKeys(rawColors);
|
||||
}
|
||||
|
||||
// this field is required by the spec but vd seems to ignore this
|
||||
// so are most vd themes
|
||||
data.spec ??= 2;
|
||||
if (data.spec === undefined) {
|
||||
if (!('theme_color_map' in data)) {
|
||||
data.spec = 2;
|
||||
}
|
||||
//If not then Enmity theme and no spec
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function validateTheme(themeJSON: any): boolean {
|
||||
if (typeof themeJSON !== "object" || themeJSON === null) return false;
|
||||
if (themeJSON.spec !== 2 && themeJSON.spec !== 3) return false;
|
||||
|
||||
if (themeJSON.spec === 3 && !themeJSON.main) return false;
|
||||
|
||||
return true;
|
||||
if (themeJSON.spec === 2) return true;
|
||||
|
||||
if (themeJSON.theme_color_map) return true;
|
||||
|
||||
return themeJSON.spec === 2 || themeJSON.spec === 3;
|
||||
}
|
||||
|
||||
export async function fetchTheme(url: string, selected = false) {
|
||||
|
|
|
|||
16
src/lib/api/native/modules/bubblemodule.ts
Normal file
16
src/lib/api/native/modules/bubblemodule.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { processColor } from "react-native";
|
||||
import { getModule } from "./loader";
|
||||
import type { LoaderModule } from "./loader";
|
||||
|
||||
interface BubbleModule extends LoaderModule {
|
||||
hookBubbles(): Promise<void>;
|
||||
unhookBubbles(): Promise<void>;
|
||||
configure(avatarRadius?: number, bubbleRadius?: number, bubbleColor?: string): Promise<void>;
|
||||
}
|
||||
|
||||
export default getModule<BubbleModule>({
|
||||
name: "BubbleModule",
|
||||
argumentProcessors: {
|
||||
configure: args => [args[0], args[1], Number(processColor(args[2]))],
|
||||
},
|
||||
});
|
||||
|
|
@ -33,3 +33,6 @@ export const NativeThemeModule = getNativeModule(
|
|||
export const BundleUpdaterManager = getNativeModule(
|
||||
"BundleUpdaterManager"
|
||||
)!;
|
||||
export const ImageLoader = getNativeModule(
|
||||
"ImageLoader"
|
||||
)!;
|
||||
82
src/lib/api/native/modules/loader.ts
Normal file
82
src/lib/api/native/modules/loader.ts
Normal file
|
|
@ -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<unknown>;
|
||||
}
|
||||
|
||||
type ModuleProps<T extends LoaderModule> = {
|
||||
name: string;
|
||||
argumentProcessors?: Partial<{ [key in keyof T]: (args: any[]) => any[] }>;
|
||||
};
|
||||
|
||||
export function getModule<T extends LoaderModule>({ name, argumentProcessors: processArgs }: ModuleProps<T>) {
|
||||
const module = loaderPayload.loader.modules[name];
|
||||
if (!module) {
|
||||
throw new Error(`Module ${name} is not registered`);
|
||||
}
|
||||
|
||||
const baseObject: Record<string, any> = {
|
||||
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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import { getLoaderConfigPath } from "@lib/api/native/loader";
|
|||
|
||||
export interface Settings {
|
||||
debuggerUrl: string;
|
||||
devToolsUrl: string;
|
||||
developerSettings: boolean;
|
||||
enableDiscordDeveloperSettings: boolean;
|
||||
safeMode?: {
|
||||
|
|
|
|||
|
|
@ -238,3 +238,100 @@ export function* getCachedPolyfillModules(name: string) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface WaitForOptions {
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export type ModuleFilter<T = any> = {
|
||||
(exports: any): T | undefined;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
export function waitFor<T = any>(
|
||||
filter: ModuleFilter<T>,
|
||||
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<T = any>(
|
||||
filter: ModuleFilter<T>,
|
||||
options: WaitForOptions = {}
|
||||
): Promise<T> {
|
||||
return new Promise((resolve) => {
|
||||
waitFor(filter, (exports) => resolve(exports), options);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue