Move types to their respective files

This commit is contained in:
amsyarasyiq 2024-02-23 15:11:35 +08:00
parent dea9ecb084
commit 00b5be7130
33 changed files with 418 additions and 617 deletions

522
src/def.d.ts vendored
View file

@ -1,522 +0,0 @@
import * as _spitroast from "spitroast";
import _React from "react";
import _RN from "react-native";
import _Clipboard from "@react-native-clipboard/clipboard";
import _moment from "moment";
import _chroma from "chroma-js";
import _lodash from "lodash";
type MetroModules = { [id: number]: any };
// Component types
interface SummaryProps {
label: string;
icon?: string;
noPadding?: boolean;
noAnimation?: boolean;
children: JSX.Element | JSX.Element[];
}
interface ErrorBoundaryProps {
children: JSX.Element | JSX.Element[];
}
interface CodeblockProps {
selectable?: boolean;
style?: _RN.TextStyle;
children?: string;
}
interface SearchProps {
onChangeText?: (v: string) => void;
placeholder?: string;
style?: _RN.TextStyle;
}
// Helper types for API functions
type PropIntellisense<P extends string | symbol> = Record<P, any> & Record<PropertyKey, any>;
type PropsFinder = <T extends string | symbol>(...props: T[]) => PropIntellisense<T>;
type PropsFinderAll = <T extends string | symbol>(...props: T[]) => PropIntellisense<T>[];
type LoggerFunction = (...messages: any[]) => void;
interface Logger {
log: LoggerFunction;
info: LoggerFunction;
warn: LoggerFunction;
error: LoggerFunction;
time: LoggerFunction;
trace: LoggerFunction;
verbose: LoggerFunction;
}
type SearchTree = Record<string, any>;
type SearchFilter = (tree: SearchTree) => boolean;
interface FindInTreeOptions {
walkable?: string[];
ignore?: string[];
maxDepth?: number;
}
interface Asset {
name: string;
id: number;
}
export enum ButtonColors {
BRAND = "brand",
RED = "red",
GREEN = "green",
PRIMARY = "primary",
TRANSPARENT = "transparent",
GREY = "grey",
LIGHTGREY = "lightgrey",
WHITE = "white",
LINK = "link"
}
interface ConfirmationAlertOptions {
title?: string;
content: string | JSX.Element | (string | JSX.Element)[];
confirmText?: string;
confirmColor?: ButtonColors;
onConfirm: () => void;
secondaryConfirmText?: string;
onConfirmSecondary?: () => void;
cancelText?: string;
onCancel?: () => void;
isDismissable?: boolean;
}
interface InputAlertProps {
title?: string;
confirmText?: string;
confirmColor?: ButtonColors;
onConfirm: (input: string) => (void | Promise<void>);
cancelText?: string;
placeholder?: string;
initialValue?: string;
secureTextEntry?: boolean;
}
interface Author {
name: string;
id?: string;
}
// See https://github.com/vendetta-mod/polymanifest
interface PluginManifest {
name: string;
description: string;
authors: Author[];
main: string;
hash: string;
// Vendor-specific field, contains our own data
vendetta?: {
icon?: string;
};
}
interface Plugin {
id: string;
manifest: PluginManifest;
enabled: boolean;
update: boolean;
js: string;
}
interface ThemeData {
name: string;
description?: string;
authors?: Author[];
spec: number;
semanticColors?: Record<string, (string | false)[]>;
rawColors?: Record<string, string>;
background?: {
url: string;
blur?: number;
/**
* The alpha value of the background.
* `CHAT_BACKGROUND` of semanticColors alpha value will be ignored when this is specified
*/
alpha?: number;
}
}
interface Theme {
id: string;
selected: boolean;
data: ThemeData;
}
interface Settings {
debuggerUrl: string;
developerSettings: boolean;
safeMode?: {
enabled: boolean;
currentThemeId?: string;
};
}
interface ApplicationCommand {
description: string;
name: string;
options: ApplicationCommandOption[];
execute: (args: any[], ctx: CommandContext) => CommandResult | void | Promise<CommandResult> | Promise<void>;
id?: string;
applicationId: string;
displayName: string;
displayDescription: string;
inputType: ApplicationCommandInputType;
type: ApplicationCommandType;
}
export enum ApplicationCommandInputType {
BUILT_IN,
BUILT_IN_TEXT,
BUILT_IN_INTEGRATION,
BOT,
PLACEHOLDER,
}
interface ApplicationCommandOption {
name: string;
description: string;
required?: boolean;
type: ApplicationCommandOptionType;
displayName: string;
displayDescription: string;
}
export enum ApplicationCommandOptionType {
SUB_COMMAND = 1,
SUB_COMMAND_GROUP,
STRING,
INTEGER,
BOOLEAN,
USER,
CHANNEL,
ROLE,
MENTIONABLE,
NUMBER,
ATTACHMENT,
}
export enum ApplicationCommandType {
CHAT = 1,
USER,
MESSAGE,
}
interface CommandContext {
channel: any;
guild: any;
}
interface CommandResult {
content: string;
tts?: boolean;
}
interface RNConstants extends _RN.PlatformConstants {
// Android
Version: number;
Release: string;
Serial: string;
Fingerprint: string;
Model: string;
Brand: string;
Manufacturer: string;
ServerHost?: string;
// iOS
forceTouchAvailable: boolean;
interfaceIdiom: string;
osVersion: string;
systemName: string;
}
/**
* A key-value storage based upon `SharedPreferences` on Android.
*
* These types are based on Android though everything should be the same between
* platforms.
*/
interface MMKVManager {
/**
* Get the value for the given `key`, or null
* @param key The key to fetch
*/
getItem: (key: string) => Promise<string | null>;
/**
* Deletes the value for the given `key`
* @param key The key to delete
*/
removeItem: (key: string) => void;
/**
* Sets the value of `key` to `value`
*/
setItem: (key: string, value: string) => void;
/**
* Goes through every item in storage and returns it, excluding the
* keys specified in `exclude`.
* @param exclude A list of items to exclude from result
*/
refresh: (exclude: string[]) => Promise<Record<string, string>>;
/**
* You will be murdered if you use this function.
* Clears ALL of Discord's settings.
*/
clear: () => void;
}
interface FileManager {
/**
* @param path **Full** path to file
*/
fileExists: (path: string) => Promise<boolean>;
/**
* Allowed URI schemes on Android: `file://`, `content://` ([See here](https://developer.android.com/reference/android/content/ContentResolver#accepts-the-following-uri-schemes:_3))
*/
getSize: (uri: string) => Promise<boolean>;
/**
* @param path **Full** path to file
* @param encoding Set to `base64` in order to encode response
*/
readFile(path: string, encoding: "base64" | "utf8"): Promise<string>;
saveFileToGallery?(uri: string, fileName: string, fileType: "PNG" | "JPEG"): Promise<string>;
/**
* Beware! This function has differing functionality on iOS and Android.
* @param storageDir Either `cache` or `documents`.
* @param path Path in `storageDir`, parents are recursively created.
* @param data The data to write to the file
* @param encoding Set to `base64` if `data` is base64 encoded.
* @returns Promise that resolves to path of the file once it got written
*/
writeFile(storageDir: "cache" | "documents", path: string, data: string, encoding: "base64" | "utf8"): Promise<string>;
removeFile(storageDir: "cache" | "documents", path: string): Promise<unknown>;
getConstants: () => {
/**
* The path the `documents` storage dir (see {@link writeFile}) represents.
*/
DocumentsDirPath: string;
CacheDirPath: string;
};
/**
* Will apparently cease to exist some time in the future so please use {@link getConstants} instead.
* @deprecated
*/
DocumentsDirPath: string;
}
type EmitterEvent = "SET" | "GET" | "DEL";
interface EmitterListenerData {
path: string[];
value?: any;
}
type EmitterListener = (
event: EmitterEvent,
data: EmitterListenerData | any
) => any;
type EmitterListeners = Record<string, Set<EmitterListener>>
interface Emitter {
listeners: EmitterListeners;
on: (event: EmitterEvent, listener: EmitterListener) => void;
off: (event: EmitterEvent, listener: EmitterListener) => void;
once: (event: EmitterEvent, listener: EmitterListener) => void;
emit: (event: EmitterEvent, data: EmitterListenerData) => void;
}
interface StorageBackend {
get: () => unknown | Promise<unknown>;
set: (data: unknown) => void | Promise<void>;
}
interface LoaderConfig {
customLoadUrl: {
enabled: boolean;
url: string;
};
loadReactDevTools: boolean;
}
interface LoaderIdentity {
name: string;
features: {
loaderConfig?: boolean;
devtools?: {
prop: string;
version: string;
},
themes?: {
prop: string;
}
}
}
interface DiscordStyleSheet {
[index: string]: any,
createStyles: <T extends _RN.StyleSheet.NamedStyles<T>>(sheet: T | (() => T)) => () => T;
createThemedStyleSheet: typeof import("react-native").StyleSheet.create;
}
interface VendettaObject {
patcher: {
after: typeof _spitroast.after;
before: typeof _spitroast.before;
instead: typeof _spitroast.instead;
};
metro: {
find: (filter: (m: any) => boolean) => any;
findAll: (filter: (m: any) => boolean) => any[];
findByProps: PropsFinder;
findByPropsAll: PropsFinderAll;
findByName: (name: string, defaultExp?: boolean) => any;
findByNameAll: (name: string, defaultExp?: boolean) => any[];
findByDisplayName: (displayName: string, defaultExp?: boolean) => any;
findByDisplayNameAll: (displayName: string, defaultExp?: boolean) => any[];
findByTypeName: (typeName: string, defaultExp?: boolean) => any;
findByTypeNameAll: (typeName: string, defaultExp?: boolean) => any[];
findByStoreName: (name: string) => any;
common: {
constants: PropIntellisense<"Fonts" | "Permissions">;
channels: PropIntellisense<"getVoiceChannelId">;
i18n: PropIntellisense<"Messages">;
url: PropIntellisense<"openURL">;
toasts: PropIntellisense<"open" | "close">;
stylesheet: DiscordStyleSheet;
clipboard: typeof _Clipboard;
assets: PropIntellisense<"registerAsset">;
invites: PropIntellisense<"acceptInviteAndTransitionToInviteChannel">;
commands: PropIntellisense<"getBuiltInCommands">;
navigation: PropIntellisense<"pushLazy">;
navigationStack: PropIntellisense<"createStackNavigator">;
NavigationNative: PropIntellisense<"NavigationContainer">;
// You may ask: "Why not just install Flux's types?"
// Answer: Discord have a (presumably proprietary) fork. It's wildly different.
Flux: PropIntellisense<"connectStores">;
FluxDispatcher: PropIntellisense<"_currentDispatchActionType">;
React: typeof _React;
ReactNative: typeof _RN;
moment: typeof _moment;
chroma: typeof _chroma;
lodash: typeof _lodash;
util: PropIntellisense<"inspect" | "isNullOrUndefined">;
};
};
constants: {
DISCORD_SERVER: string;
DISCORD_SERVER_ID: string;
PLUGINS_CHANNEL_ID: string;
THEMES_CHANNEL_ID: string;
GITHUB: string;
PROXY_PREFIX: string;
HTTP_REGEX: RegExp;
HTTP_REGEX_MULTI: RegExp;
};
utils: {
findInReactTree: (tree: SearchTree, filter: SearchFilter) => any;
findInTree: (tree: SearchTree, filter: SearchFilter, options: FindInTreeOptions) => any;
safeFetch: (input: RequestInfo | URL, options?: RequestInit, timeout?: number) => Promise<Response>;
unfreeze: (obj: object) => object;
without: <O extends object, K extends readonly (keyof O)[]>(object: O, ...keys: K) => Omit<O, typeof keys[number]>;
};
debug: {
connectToDebugger: (url: string) => void;
// TODO: Type output?
getDebugInfo: () => void;
}
ui: {
components: {
// Discord
Forms: PropIntellisense<"Form" | "FormSection">;
General: PropIntellisense<"Button" | "Text" | "View">;
Alert: _React.ComponentType;
Button: _React.ComponentType<any> & { Looks: any, Colors: ButtonColors, Sizes: any };
HelpMessage: _React.ComponentType;
SafeAreaView: typeof _RN.SafeAreaView;
// Vendetta
Summary: _React.ComponentType<SummaryProps>;
ErrorBoundary: _React.ComponentType<ErrorBoundaryProps>;
Codeblock: _React.ComponentType<CodeblockProps>;
Search: _React.ComponentType<SearchProps>;
}
toasts: {
showToast: (content: string, asset?: number) => void;
};
alerts: {
showConfirmationAlert: (options: ConfirmationAlertOptions) => void;
showCustomAlert: (component: _React.ComponentType, props: any) => void;
showInputAlert: (options: InputAlertProps) => void;
};
assets: {
all: Record<string, Asset>;
find: (filter: (a: any) => void) => Asset | null | undefined;
getAssetByName: (name: string) => Asset;
getAssetByID: (id: number) => Asset;
getAssetIDByName: (name: string) => number;
};
// TODO: Make a vain attempt to type these
semanticColors: Record<string, any>;
rawColors: Record<string, any>;
};
plugins: {
plugins: Record<string, Plugin>;
fetchPlugin: (id: string) => Promise<void>;
installPlugin: (id: string, enabled?: boolean) => Promise<void>;
startPlugin: (id: string) => Promise<void>;
stopPlugin: (id: string, disable?: boolean) => void;
removePlugin: (id: string) => void;
getSettings: (id: string) => JSX.Element;
};
themes: {
themes: Record<string, Theme>;
fetchTheme: (id: string, selected?: boolean) => Promise<void>;
installTheme: (id: string) => Promise<void>;
selectTheme: (id: string) => Promise<void>;
removeTheme: (id: string) => Promise<boolean>;
getCurrentTheme: () => Theme | null;
updateThemes: () => Promise<void>;
};
commands: {
registerCommand: (command: ApplicationCommand) => () => void;
};
storage: {
createProxy: <T>(target: T) => { proxy: T, emitter: Emitter };
useProxy: <T>(storage: T) => T;
createStorage: <T>(backend: StorageBackend) => Promise<Awaited<T>>;
wrapSync: <T extends Promise<any>>(store: T) => Awaited<T>;
awaitSyncWrapper: (store: any) => Promise<void>;
createMMKVBackend: (store: string) => StorageBackend;
createFileBackend: (file: string) => StorageBackend;
};
settings: Settings;
loader: {
identity?: LoaderIdentity;
config: LoaderConfig;
};
logger: Logger;
version: string;
unload: () => void;
}
interface VendettaPluginObject {
id: string;
manifest: PluginManifest;
storage: Record<string, any>;
}
declare global {
type React = typeof _React;
const __vendettaVersion: string;
interface Window {
[key: PropertyKey]: any;
modules: MetroModules;
vendetta: VendettaObject;
React: typeof _React;
}
}

15
src/global.d.ts vendored Normal file
View file

@ -0,0 +1,15 @@
import { MetroModules } from "./lib/metro/filters";
declare global {
type React = typeof import("react");
const __vendettaVersion: string;
interface Window {
[key: string]: any;
modules: MetroModules;
vendetta: any;
React: typeof import("react");
}
}
export {};

View file

@ -1,9 +1,68 @@
import { ApplicationCommand, ApplicationCommandType } from "@types";
import { commands as commandsModule } from "@metro/common";
import { after } from "@lib/patcher";
let commands: ApplicationCommand[] = [];
export interface ApplicationCommand {
description: string;
name: string;
options: ApplicationCommandOption[];
execute: (args: any[], ctx: CommandContext) => CommandResult | void | Promise<CommandResult> | Promise<void>;
id?: string;
applicationId: string;
displayName: string;
displayDescription: string;
inputType: ApplicationCommandInputType;
type: ApplicationCommandType;
}
export enum ApplicationCommandInputType {
BUILT_IN,
BUILT_IN_TEXT,
BUILT_IN_INTEGRATION,
BOT,
PLACEHOLDER,
}
export interface ApplicationCommandOption {
name: string;
description: string;
required?: boolean;
type: ApplicationCommandOptionType;
displayName: string;
displayDescription: string;
}
export enum ApplicationCommandOptionType {
SUB_COMMAND = 1,
SUB_COMMAND_GROUP,
STRING,
INTEGER,
BOOLEAN,
USER,
CHANNEL,
ROLE,
MENTIONABLE,
NUMBER,
ATTACHMENT,
}
export enum ApplicationCommandType {
CHAT = 1,
USER,
MESSAGE,
}
export interface CommandContext {
channel: any;
guild: any;
}
export interface CommandResult {
content: string;
tts?: boolean;
}
export function patchCommands() {
const unpatch = after("getBuiltInCommands", commandsModule, ([type], res: ApplicationCommand[]) => {
if (type === ApplicationCommandType.CHAT) return res.concat(commands);

View file

@ -1,4 +1,3 @@
import { RNConstants } from "@types";
import { ReactNative as RN } from "@metro/common";
import { after } from "@lib/patcher";
import { getThemeFromLoader, selectTheme } from "@lib/themes";
@ -8,8 +7,27 @@ import { showToast } from "@ui/toasts";
import settings from "@lib/settings";
import logger from "@lib/logger";
import { getLoaderName, isThemeSupported } from "./loader";
import type { PlatformConstants } from "react-native";
export let socket: WebSocket;
export interface RNConstants extends PlatformConstants {
// Android
Version: number;
Release: string;
Serial: string;
Fingerprint: string;
Model: string;
Brand: string;
Manufacturer: string;
ServerHost?: string;
// iOS
forceTouchAvailable: boolean;
interfaceIdiom: string;
osVersion: string;
systemName: string;
}
export async function toggleSafeMode() {
settings.safeMode = { ...settings.safeMode, enabled: !settings.safeMode?.enabled }
if (isThemeSupported()) {

View file

@ -1,11 +1,32 @@
import { Emitter, EmitterEvent, EmitterListener, EmitterListenerData, EmitterListeners } from "@types";
export enum Events {
GET = "GET",
SET = "SET",
DEL = "DEL",
};
export type EmitterEvent = "SET" | "GET" | "DEL";
export interface EmitterListenerData {
path: string[];
value?: any;
}
export type EmitterListener = (
event: EmitterEvent,
data: EmitterListenerData | any
) => any;
export type EmitterListeners = Record<string, Set<EmitterListener>>
export interface Emitter {
listeners: EmitterListeners;
on: (event: EmitterEvent, listener: EmitterListener) => void;
off: (event: EmitterEvent, listener: EmitterListener) => void;
once: (event: EmitterEvent, listener: EmitterListener) => void;
emit: (event: EmitterEvent, data: EmitterListenerData) => void;
}
export default function createEmitter(): Emitter {
return {
listeners: Object.values(Events).reduce<EmitterListeners>((acc, val: string) => ((acc[val] = new Set<EmitterListener>()), acc), {}) as EmitterListeners,

View file

@ -1,10 +1,24 @@
import type { LoaderConfig, LoaderIdentity, Theme } from "@types";
import { Theme } from "./themes";
// @ts-ignore
const pyonLoaderIdentity = globalThis.__PYON_LOADER__;
// @ts-ignore
const vendettaLoaderIdentity = globalThis.__vendetta_loader;
export interface VendettaLoaderIdentity {
name: string;
features: {
loaderConfig?: boolean;
devtools?: {
prop: string;
version: string;
},
themes?: {
prop: string;
}
}
}
export function isVendettaLoader() {
return vendettaLoaderIdentity != null;
}
@ -48,10 +62,10 @@ function polyfillVendettaLoaderIdentity() {
configurable: true
});
return loader as LoaderIdentity;
return loader as VendettaLoaderIdentity;
}
export function getVendettaLoaderIdentity(): LoaderIdentity | null {
export function getVendettaLoaderIdentity(): VendettaLoaderIdentity | null {
// @ts-ignore
if (globalThis.__vendetta_loader) return globalThis.__vendetta_loader;
return polyfillVendettaLoaderIdentity();

View file

@ -1,6 +1,16 @@
import { Logger } from "@types";
import { findByProps } from "@metro/filters";
export type LoggerFunction = (...messages: any[]) => void;
export interface Logger {
log: LoggerFunction;
info: LoggerFunction;
warn: LoggerFunction;
error: LoggerFunction;
time: LoggerFunction;
trace: LoggerFunction;
verbose: LoggerFunction;
}
export const logModule = findByProps("setLogFn").default;
const logger: Logger = new logModule("Vendetta");

View file

@ -1,8 +1,13 @@
import { find, findByProps, findByStoreName } from "@metro/filters";
import { DiscordStyleSheet } from "@types";
import { ReactNative as RN } from "@lib/preinit";
import type { StyleSheet } from "react-native";
export interface DiscordStyleSheet {
[index: string]: any,
createStyles: <T extends import("react-native").StyleSheet.NamedStyles<T>>(sheet: T | (() => T)) => () => T;
createThemedStyleSheet: typeof import("react-native").StyleSheet.create;
}
const ThemeStore = findByStoreName("ThemeStore");
const colorModule = findByProps("colors", "unsafe_rawColors");
const colorResolver = colorModule.internal ?? colorModule.meta;

View file

@ -1,6 +1,10 @@
import { MetroModules, PropsFinder, PropsFinderAll } from "@types";
import { instead } from "@lib/patcher";
export type MetroModules = { [id: number]: any };
export type PropIntellisense<P extends string | symbol> = Record<P, any> & Record<PropertyKey, any>;
export type PropsFinder = <T extends string | symbol>(...props: T[]) => PropIntellisense<T>;
export type PropsFinderAll = <T extends string | symbol>(...props: T[]) => PropIntellisense<T>[];
// Metro require
declare const __r: (moduleId: number) => any;

View file

@ -1,9 +1,81 @@
import { MMKVManager as _MMKVManager, FileManager as _FileManager } from "@types";
const nmp = window.nativeModuleProxy;
export const MMKVManager = nmp.MMKVManager as _MMKVManager;
/**
* A key-value storage based upon `SharedPreferences` on Android.
*
* These types are based on Android though everything should be the same between
* platforms.
*/
export interface MMKVManager {
/**
* Get the value for the given `key`, or null
* @param key The key to fetch
*/
getItem: (key: string) => Promise<string | null>;
/**
* Deletes the value for the given `key`
* @param key The key to delete
*/
removeItem: (key: string) => void;
/**
* Sets the value of `key` to `value`
*/
setItem: (key: string, value: string) => void;
/**
* Goes through every item in storage and returns it, excluding the
* keys specified in `exclude`.
* @param exclude A list of items to exclude from result
*/
refresh: (exclude: string[]) => Promise<Record<string, string>>;
/**
* You will be murdered if you use this function.
* Clears ALL of Discord's settings.
*/
clear: () => void;
}
export interface FileManager {
/**
* @param path **Full** path to file
*/
fileExists: (path: string) => Promise<boolean>;
/**
* Allowed URI schemes on Android: `file://`, `content://` ([See here](https://developer.android.com/reference/android/content/ContentResolver#accepts-the-following-uri-schemes:_3))
*/
getSize: (uri: string) => Promise<boolean>;
/**
* @param path **Full** path to file
* @param encoding Set to `base64` in order to encode response
*/
readFile(path: string, encoding: "base64" | "utf8"): Promise<string>;
saveFileToGallery?(uri: string, fileName: string, fileType: "PNG" | "JPEG"): Promise<string>;
/**
* Beware! This function has differing functionality on iOS and Android.
* @param storageDir Either `cache` or `documents`.
* @param path Path in `storageDir`, parents are recursively created.
* @param data The data to write to the file
* @param encoding Set to `base64` if `data` is base64 encoded.
* @returns Promise that resolves to path of the file once it got written
*/
writeFile(storageDir: "cache" | "documents", path: string, data: string, encoding: "base64" | "utf8"): Promise<string>;
removeFile(storageDir: "cache" | "documents", path: string): Promise<unknown>;
getConstants: () => {
/**
* The path the `documents` storage dir (see {@link writeFile}) represents.
*/
DocumentsDirPath: string;
CacheDirPath: string;
};
/**
* Will apparently cease to exist some time in the future so please use {@link getConstants} instead.
* @deprecated
*/
DocumentsDirPath: string;
}
export const MMKVManager = nmp.MMKVManager as MMKVManager;
//! 173.10 renamed this to RTNFileManager.
export const FileManager = (nmp.DCDFileManager ?? nmp.RTNFileManager) as _FileManager;
export const FileManager = (nmp.DCDFileManager ?? nmp.RTNFileManager) as FileManager;
//! 173.13 renamed this to RTNClientInfoManager.
export const ClientInfoManager = nmp.InfoDictionaryManager ?? nmp.RTNClientInfoManager;
//! 173.14 renamed this to RTNDeviceManager.

View file

@ -1,9 +1,9 @@
import { PluginManifest, Plugin } from "@types";
import { safeFetch } from "@lib/utils";
import { awaitSyncWrapper, createMMKVBackend, createStorage, purgeStorage, wrapSync } from "@lib/storage";
import { allSettled } from "@lib/polyfills";
import logger, { logModule } from "@lib/logger";
import settings from "@lib/settings";
import { Author } from "./types";
type EvaledPlugin = {
onLoad?(): void;
@ -11,6 +11,27 @@ type EvaledPlugin = {
settings: JSX.Element;
};
// See https://github.com/vendetta-mod/polymanifest
export interface PluginManifest {
name: string;
description: string;
authors: Author[];
main: string;
hash: string;
// Vendor-specific field, contains our own data
vendetta?: {
icon?: string;
};
}
export interface Plugin {
id: string;
manifest: PluginManifest;
enabled: boolean;
update: boolean;
js: string;
}
export const plugins = wrapSync(createStorage<Record<string, Plugin>>(createMMKVBackend("VENDETTA_PLUGINS")));
const loadedPlugins: Record<string, EvaledPlugin> = {};

View file

@ -1,6 +1,22 @@
import { LoaderConfig, Settings } from "@types";
import { createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@lib/storage";
import { getLoaderConfigPath } from "./loader";
export interface Settings {
debuggerUrl: string;
developerSettings: boolean;
safeMode?: {
enabled: boolean;
currentThemeId?: string;
};
}
export interface LoaderConfig {
customLoadUrl: {
enabled: boolean;
url: string;
};
loadReactDevTools: boolean;
}
export default wrapSync(createStorage<Settings>(createMMKVBackend("VENDETTA_SETTINGS")));
export const loaderConfig = wrapSync(createStorage<LoaderConfig>(createFileBackend(getLoaderConfigPath())));

View file

@ -1,7 +1,11 @@
import { StorageBackend } from "@types";
import { MMKVManager, FileManager } from "@lib/native";
import { ReactNative as RN } from "@metro/common";
export interface StorageBackend {
get: () => unknown | Promise<unknown>;
set: (data: unknown) => void | Promise<void>;
}
const ILLEGAL_CHARS_REGEX = /[<>:"\/\\|?*]/g;
const filePathFixer = (file: string): string => RN.Platform.select({

View file

@ -1,5 +1,5 @@
import { Emitter, StorageBackend } from "@types";
import createEmitter from "@lib/emitter";
import createEmitter, { Emitter } from "@lib/emitter";
import { StorageBackend } from "@lib/storage/backends";
const emitterSymbol = Symbol.for("vendetta.storage.emitter");
const syncAwaitSymbol = Symbol.for("vendetta.storage.accessor");

View file

@ -1,4 +1,3 @@
import { Theme, ThemeData } from "@types";
import { ReactNative as RN, chroma } from "@metro/common";
import { findInReactTree, safeFetch } from "@lib/utils";
import { find, findByName, findByProps, findByStoreName } from "@metro/filters";
@ -7,6 +6,31 @@ import { createFileBackend, createMMKVBackend, createStorage, wrapSync, awaitSyn
import logger from "@lib/logger";
import { ThemeManager } from "./native";
import { getStoredTheme, getThemeFilePath } from "./loader";
import { Author } from "./types";
export interface ThemeData {
name: string;
description?: string;
authors?: Author[];
spec: number;
semanticColors?: Record<string, (string | false)[]>;
rawColors?: Record<string, string>;
background?: {
url: string;
blur?: number;
/**
* The alpha value of the background.
* `CHAT_BACKGROUND` of semanticColors alpha value will be ignored when this is specified
*/
alpha?: number;
}
}
export interface Theme {
id: string;
selected: boolean;
data: ThemeData;
}
//! As of 173.10, early-finding this does not work.
// Somehow, this is late enough, though?

16
src/lib/types.ts Normal file
View file

@ -0,0 +1,16 @@
export interface Author {
name: string;
id?: string;
}
export enum ButtonColors {
BRAND = "brand",
RED = "red",
GREEN = "green",
PRIMARY = "primary",
TRANSPARENT = "transparent",
GREY = "grey",
LIGHTGREY = "lightgrey",
WHITE = "white",
LINK = "link"
}

View file

@ -1,5 +1,5 @@
import { SearchFilter } from "@types";
import { findInTree } from "@lib/utils";
import { SearchFilter } from "./findInTree";
export default (tree: { [key: string]: any }, filter: SearchFilter): any => findInTree(tree, filter, {
walkable: ["props", "children", "child", "sibling"],

View file

@ -1,7 +1,13 @@
// This has been completely reimplemented at this point, but the disclaimer at the end of disclaimers still counts.
// https://github.com/Cordwood/Cordwood/blob/91c0b971bbf05e112927df75415df99fa105e1e7/src/lib/utils/findInTree.ts
import { FindInTreeOptions, SearchTree, SearchFilter } from "@types";
export type SearchTree = Record<string, any>;
export type SearchFilter = (tree: SearchTree) => boolean;
export interface FindInTreeOptions {
walkable?: string[];
ignore?: string[];
maxDepth?: number;
}
function treeSearch(tree: SearchTree, filter: SearchFilter, opts: Required<FindInTreeOptions>, depth: number): any {
if (depth > opts.maxDepth) return;

View file

@ -1,4 +1,3 @@
import { VendettaObject } from "@types";
import patcher from "@lib/patcher";
import { getVendettaLoaderIdentity } from "@lib/loader";
import settings, { loaderConfig } from "@lib/settings";
@ -16,23 +15,23 @@ import * as assets from "@ui/assets";
import * as color from "@ui/color";
import * as utils from "@lib/utils";
export const createVendettaObject = (unloads: any[]): VendettaObject => ({
export const createVendettaObject = (unloads: any[]): any => ({
patcher: {
before: patcher.before,
after: patcher.after,
instead: patcher.instead
},
metro: {
find: (filter) => metro.find(filter),
findAll: (filter) => metro.findAll(filter),
findByProps: (filter) => metro.findByProps(filter),
findByPropsAll: (filter) => metro.findByPropsAll(filter),
findByName: (name, defaultExp?) => metro.findByName(name, defaultExp),
findByNameAll: (name, defaultExp?) => metro.findByNameAll(name, defaultExp),
findByDisplayName: (displayName, defaultExp?) => metro.findByDisplayName(displayName, defaultExp),
findByDisplayNameAll: (displayName, defaultExp?) => metro.findByDisplayNameAll(displayName, defaultExp),
findByTypeName: (typeName, defaultExp?) => metro.findByTypeName(typeName, defaultExp),
findByTypeNameAll: (typeName, defaultExp?) => metro.findByTypeNameAll(typeName, defaultExp),
find: (filter: (m: any) => boolean) => metro.find(filter),
findAll: (filter: (m: any) => boolean) => metro.findAll(filter),
findByProps: (filter: any) => metro.findByProps(filter),
findByPropsAll: (filter: any) => metro.findByPropsAll(filter),
findByName: (name: string, defaultExp?: boolean | undefined) => metro.findByName(name, defaultExp),
findByNameAll: (name: string, defaultExp?: boolean | undefined) => metro.findByNameAll(name, defaultExp),
findByDisplayName: (displayName: string, defaultExp?: boolean | undefined) => metro.findByDisplayName(displayName, defaultExp),
findByDisplayNameAll: (displayName: string, defaultExp?: boolean | undefined) => metro.findByDisplayNameAll(displayName, defaultExp),
findByTypeName: (typeName: string, defaultExp?: boolean | undefined) => metro.findByTypeName(typeName, defaultExp),
findByTypeNameAll: (typeName: string, defaultExp?: boolean | undefined) => metro.findByTypeNameAll(typeName, defaultExp),
findByStoreName: (name: string) => metro.findByStoreName(name),
common: {
constants: common.constants,
@ -69,11 +68,11 @@ export const createVendettaObject = (unloads: any[]): VendettaObject => ({
THEMES_CHANNEL_ID: "1091880434939482202",
},
utils: {
findInReactTree: (tree, filter) => utils.findInReactTree(tree, filter),
findInTree: (tree, filter, options) => utils.findInTree(tree, filter, options),
safeFetch: (input, options?, timeout?) => utils.safeFetch(input, options, timeout),
findInReactTree: (tree: { [key: string]: any; }, filter: any) => utils.findInReactTree(tree, filter),
findInTree: (tree: any, filter: any, options: any) => utils.findInTree(tree, filter, options),
safeFetch: (input: RequestInfo | URL, options?: RequestInit | undefined, timeout?: number | undefined) => utils.safeFetch(input, options, timeout),
unfreeze: (obj: object) => utils.unfreeze(obj),
without: (object, ...keys) => utils.without(object, ...keys)
without: (object: any, ...keys: any) => utils.without(object, ...keys)
},
debug: {
connectToDebugger: (url: string) => debug.connectToDebugger(url),
@ -96,9 +95,9 @@ export const createVendettaObject = (unloads: any[]): VendettaObject => ({
showToast: (content: string, asset?: number) => toasts.showToast(content, asset)
},
alerts: {
showConfirmationAlert: (options) => alerts.showConfirmationAlert(options),
showCustomAlert: (component, props: any) => alerts.showCustomAlert(component, props),
showInputAlert: (options) => alerts.showInputAlert(options)
showConfirmationAlert: (options: any) => alerts.showConfirmationAlert(options),
showCustomAlert: (component: React.ComponentType<any>, props: any) => alerts.showCustomAlert(component, props),
showInputAlert: (options: any) => alerts.showInputAlert(options)
},
assets: {
all: assets.all,
@ -132,7 +131,7 @@ export const createVendettaObject = (unloads: any[]): VendettaObject => ({
registerCommand: commands.registerCommand
},
storage: {
createProxy: (target) => storage.createProxy(target),
createProxy: (target: any) => storage.createProxy(target),
useProxy: <T>(_storage: T) => storage.useProxy(_storage),
createStorage: (backend: any) => storage.createStorage(backend),
wrapSync: (store: any) => storage.wrapSync(store),
@ -146,18 +145,17 @@ export const createVendettaObject = (unloads: any[]): VendettaObject => ({
config: loaderConfig,
},
logger: {
log: (...message) => console.log(...message),
info: (...message) => console.info(...message),
warn: (...message) => console.warn(...message),
error: (...message) => console.error(...message),
time: (...message) => console.time(...message),
trace: (...message) => console.trace(...message),
verbose: (...message) => console.log(...message)
log: (...message: any) => console.log(...message),
info: (...message: any) => console.info(...message),
warn: (...message: any) => console.warn(...message),
error: (...message: any) => console.error(...message),
time: (...message: any) => console.time(...message),
trace: (...message: any) => console.trace(...message),
verbose: (...message: any) => console.log(...message)
},
version: debug.versionHash,
unload: () => {
unloads.filter(i => typeof i === "function").forEach(p => p());
// @ts-expect-error explode
delete window.vendetta;
},
});

View file

@ -1,6 +1,6 @@
import { ConfirmationAlertOptions, InputAlertProps } from "@types";
import { findByProps } from "@metro/filters";
import InputAlert from "@ui/components/InputAlert";
import InputAlert, { InputAlertProps } from "@ui/components/InputAlert";
import { ButtonColors } from "@/lib/types";
const Alerts = findByProps("openLazy", "close");
@ -9,6 +9,19 @@ interface InternalConfirmationAlertOptions extends Omit<ConfirmationAlertOptions
body?: ConfirmationAlertOptions["content"];
};
export interface ConfirmationAlertOptions {
title?: string;
content: string | JSX.Element | (string | JSX.Element)[];
confirmText?: string;
confirmColor?: ButtonColors;
onConfirm: () => void;
secondaryConfirmText?: string;
onConfirmSecondary?: () => void;
cancelText?: string;
onCancel?: () => void;
isDismissable?: boolean;
}
export function showConfirmationAlert(options: ConfirmationAlertOptions) {
const internalOptions = options as InternalConfirmationAlertOptions;

View file

@ -1,9 +1,13 @@
import { Asset } from "@types";
import { assets } from "@metro/common";
import { after } from "@lib/patcher";
export const all: Record<string, Asset> = {};
export interface Asset {
name: string;
id: number;
}
export function patchAssets() {
const unpatch = after("registerAsset", assets, (args: Asset[], id: number) => {
const asset = args[0];

View file

@ -1,7 +1,12 @@
import { CodeblockProps } from "@types";
import { ReactNative as RN, stylesheet, constants } from "@metro/common";
import { semanticColors } from "@ui/color";
export interface CodeblockProps {
selectable?: boolean;
style?: import("react-native").TextStyle;
children?: string;
}
const styles = stylesheet.createThemedStyleSheet({
codeBlock: {
fontFamily: constants.Fonts.CODE_SEMIBOLD,

View file

@ -1,4 +1,3 @@
import { ErrorBoundaryProps } from "@types";
import { React, ReactNative as RN, stylesheet } from "@metro/common";
import { Forms, Button, Codeblock } from "@ui/components";
@ -7,6 +6,10 @@ interface ErrorBoundaryState {
errText?: string;
}
export interface ErrorBoundaryProps {
children: JSX.Element | JSX.Element[];
}
const styles = stylesheet.createThemedStyleSheet({
view: {
flex: 1,

View file

@ -1,10 +1,21 @@
import { InputAlertProps } from "@types";
import { ButtonColors } from "@/lib/types";
import { findByProps } from "@metro/filters";
import { Forms, Alert } from "@ui/components";
const { FormInput } = Forms;
const Alerts = findByProps("openLazy", "close");
export interface InputAlertProps {
title?: string;
confirmText?: string;
confirmColor?: ButtonColors;
onConfirm: (input: string) => (void | Promise<void>);
cancelText?: string;
placeholder?: string;
initialValue?: string;
secureTextEntry?: boolean;
}
export default function InputAlert({ title, confirmText, confirmColor, onConfirm, cancelText, placeholder, initialValue = "", secureTextEntry }: InputAlertProps) {
const [value, setValue] = React.useState(initialValue);
const [error, setError] = React.useState("");

View file

@ -1,41 +1,21 @@
import { SearchProps } from "@types";
import { ReactNative as RN } from "@metro/common";
import { findByProps } from "@metro/filters";
import ErrorBoundary from "@ui/components/ErrorBoundary";
import { getAssetIDByName } from "@ui/assets";
const { FormIcon } = findByProps("FormIcon");
const { TextInput } = findByProps("TableRow");
const props = {
"placeholder": "Search",
"returnKeyType": "search",
"size": "md",
"onFocus": "ƒ () {}",
"value": "",
"grow": true,
"autoCorrect": false,
"autoCapitalize": "none",
"accessibilityRole": "search",
"leadingIcon": "ƒ () {}",
"isClearable": true,
"onChangeText": "ƒ () {}",
"editable": true,
"onBlur": "ƒ () {}",
"style": [
"{padding: 12, paddingTop: 12}",
"{color: \"#e4e5e8\", fontFamily: \"ggsans-Medium, Noto…}",
"{paddingEnd: undefined, paddingStart: 36.3636360168…}",
"{padding: 12, paddingTop: 12}",
"{color: \"#e4e5e8\", fontFamily: \"ggsans-Medium, Noto…}",
"{paddingEnd: undefined, paddingStart: 36.3636360168…}"
],
"placeholderTextColor": "#828391"
};
export interface SearchProps {
onChangeText?: (v: string) => void;
placeholder?: string;
style?: import("react-native").TextStyle;
}
function SearchIcon() {
return <RN.Image style={{ transform: [{ scale: 0.8 }]}} source={getAssetIDByName("search")} />
}
export default ({ onChangeText, placeholder, style }: SearchProps) => {
const [query, setQuery] = React.useState("");

View file

@ -1,10 +1,16 @@
import { SummaryProps } from "@types";
import { ReactNative as RN } from "@metro/common";
import { getAssetIDByName } from "@ui/assets";
import { findByProps } from "@/lib/metro/filters";
const { FormRow } = findByProps("FormRow");
const { TableRow } = findByProps("TableRow");
export interface SummaryProps {
label: string;
icon?: string;
noPadding?: boolean;
noAnimation?: boolean;
children: JSX.Element | JSX.Element[];
}
export default function Summary({ label, icon, noPadding = false, noAnimation = false, children }: SummaryProps) {
const [hidden, setHidden] = React.useState(true);

View file

@ -1,4 +1,3 @@
import { ButtonColors } from "@types";
import { ReactNative as RN, stylesheet } from "@metro/common";
import { findByName, findByProps, findByStoreName } from "@metro/filters";
import { after } from "@lib/patcher";
@ -7,6 +6,7 @@ import { DeviceManager } from "@lib/native";
import { semanticColors } from "@ui/color";
import { Button, Codeblock, ErrorBoundary as _ErrorBoundary, SafeAreaView } from "@ui/components";
import settings from "@lib/settings";
import { ButtonColors } from "@/lib/types";
const ErrorBoundary = findByName("ErrorBoundary");

View file

@ -1,7 +1,7 @@
import { Asset } from "@types";
import { ReactNative as RN, clipboard } from "@metro/common";
import { showToast } from "@ui/toasts";
import { Forms } from "@ui/components";
import { Asset } from "@/ui/assets";
interface AssetDisplayProps { asset: Asset }

View file

@ -1,12 +1,12 @@
import { ButtonColors, Plugin } from "@types";
import { NavigationNative, clipboard } from "@metro/common";
import { removePlugin, startPlugin, stopPlugin, getSettings, fetchPlugin } from "@lib/plugins";
import { removePlugin, startPlugin, stopPlugin, getSettings, fetchPlugin, Plugin } from "@lib/plugins";
import { MMKVManager } from "@lib/native";
import { getAssetIDByName } from "@ui/assets";
import { showToast } from "@ui/toasts";
import { showConfirmationAlert } from "@ui/alerts";
import Card, { CardWrapper } from "@ui/settings/components/Card";
import { useProxy } from "@/lib/storage";
import { ButtonColors } from "@/lib/types";
async function stopThenStart(plugin: Plugin, callback: Function) {
if (plugin.enabled) stopPlugin(plugin.id, false);

View file

@ -1,6 +1,5 @@
import { ButtonColors, Theme } from "@types";
import { clipboard } from "@metro/common";
import { applyTheme, fetchTheme, removeTheme, selectTheme, themes } from "@lib/themes";
import { Theme, applyTheme, fetchTheme, removeTheme, selectTheme, themes } from "@lib/themes";
import { useProxy } from "@lib/storage";
import { BundleUpdaterManager } from "@lib/native";
import { getAssetIDByName } from "@ui/assets";
@ -8,6 +7,7 @@ import { showConfirmationAlert } from "@ui/alerts";
import { showToast } from "@ui/toasts";
import settings from "@lib/settings";
import Card, { CardWrapper } from "@ui/settings/components/Card";
import { ButtonColors } from "@/lib/types";
async function selectAndApply(value: boolean, id: string) {
try {

View file

@ -1,6 +1,5 @@
import { Plugin } from "@types";
import { useProxy } from "@lib/storage";
import { installPlugin, plugins } from "@lib/plugins";
import { Plugin, installPlugin, plugins } from "@lib/plugins";
import settings from "@lib/settings";
import AddonPage from "@ui/settings/components/AddonPage";
import PluginCard from "@ui/settings/components/PluginCard";

View file

@ -1,10 +1,10 @@
import { Theme, ButtonColors } from "@types";
import { useProxy } from "@lib/storage";
import { installTheme, themes } from "@lib/themes";
import { Theme, installTheme, themes } from "@lib/themes";
import { Button } from "@ui/components";
import settings from "@lib/settings";
import AddonPage from "@ui/settings/components/AddonPage";
import ThemeCard from "@ui/settings/components/ThemeCard";
import { ButtonColors } from "@/lib/types";
export default function Themes() {
useProxy(settings);

View file

@ -13,8 +13,7 @@
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"paths": {
"@/*": ["src/*"],
"@types": ["src/def.d.ts"],
"@*": ["src*"],
"@lib/*": ["src/lib/*"],
"@metro/*": ["src/lib/metro/*"],
"@ui/*": ["src/ui/*"]