[Global] Major refactors, allow unloading!
This commit is contained in:
parent
0be8cdac05
commit
0ba9ee600c
10 changed files with 137 additions and 116 deletions
92
src/index.ts
92
src/index.ts
|
@ -1,79 +1,37 @@
|
||||||
import patcher from "@lib/patcher";
|
import { patchLogHook } from "@lib/debug";
|
||||||
import logger from "@lib/logger";
|
import { patchCommands } from "@lib/commands";
|
||||||
import copyText from "@utils/copyText";
|
import { initPlugins } from "@lib/plugins";
|
||||||
import findInReactTree from "@utils/findInReactTree";
|
import { patchAssets } from "@ui/assets";
|
||||||
import findInTree from "@utils/findInTree";
|
|
||||||
import * as constants from "@lib/constants";
|
|
||||||
import * as metro from "@metro/filters";
|
|
||||||
import * as common from "@metro/common";
|
|
||||||
import * as components from "@ui/components";
|
|
||||||
import * as toasts from "@ui/toasts";
|
|
||||||
import * as storage from "@lib/storage";
|
|
||||||
import { patchAssets, all, find, getAssetByID, getAssetByName, getAssetIDByName } from "@ui/assets";
|
|
||||||
import initSettings from "@ui/settings";
|
import initSettings from "@ui/settings";
|
||||||
import { fixTheme } from "@ui/fixTheme";
|
import fixTheme from "@ui/fixTheme";
|
||||||
import { connectToDebugger, patchLogHook, versionHash } from "@lib/debug";
|
import windowObject from "@lib/windowObject";
|
||||||
import { plugins, fetchPlugin, evalPlugin, stopPlugin, removePlugin, getSettings, initializePlugins } from "@lib/plugins";
|
import logger from "@lib/logger";
|
||||||
import settings from "@lib/settings";
|
|
||||||
import { registerCommand } from "@lib/commands";
|
|
||||||
|
|
||||||
|
// This logs in the native logging implementation, e.g. logcat
|
||||||
console.log("Hello from Vendetta!");
|
console.log("Hello from Vendetta!");
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
let erroredOnLoad = false;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
window.vendetta = {
|
// Load everything in parallel
|
||||||
patcher: patcher,
|
const unloads = await Promise.all([
|
||||||
metro: { ...metro, common: { ...common } },
|
patchLogHook(),
|
||||||
constants: { ...constants },
|
patchAssets(),
|
||||||
utils: {
|
patchCommands(),
|
||||||
copyText: copyText,
|
fixTheme(),
|
||||||
findInReactTree: findInReactTree,
|
initSettings(),
|
||||||
findInTree: findInTree,
|
]);
|
||||||
},
|
|
||||||
debug: {
|
|
||||||
connectToDebugger: connectToDebugger,
|
|
||||||
},
|
|
||||||
ui: {
|
|
||||||
components: { ...components },
|
|
||||||
toasts: { ...toasts },
|
|
||||||
assets: {
|
|
||||||
all: all,
|
|
||||||
find: find,
|
|
||||||
getAssetByID: getAssetByID,
|
|
||||||
getAssetByName: getAssetByName,
|
|
||||||
getAssetIDByName: getAssetIDByName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
plugins: plugins,
|
|
||||||
fetchPlugin: fetchPlugin,
|
|
||||||
evalPlugin: evalPlugin,
|
|
||||||
stopPlugin: stopPlugin,
|
|
||||||
removePlugin: removePlugin,
|
|
||||||
getSettings: getSettings,
|
|
||||||
},
|
|
||||||
commands: {
|
|
||||||
registerCommand: registerCommand,
|
|
||||||
},
|
|
||||||
storage: { ...storage },
|
|
||||||
settings: settings,
|
|
||||||
logger: logger,
|
|
||||||
version: versionHash,
|
|
||||||
};
|
|
||||||
|
|
||||||
patchLogHook();
|
// Assign window object
|
||||||
patchAssets();
|
window.vendetta = await windowObject(unloads);
|
||||||
fixTheme();
|
|
||||||
initializePlugins();
|
// Once done, load plugins
|
||||||
initSettings();
|
unloads.push(await initPlugins());
|
||||||
} catch (e: Error | any) {
|
|
||||||
erroredOnLoad = true;
|
// We good :)
|
||||||
|
logger.log("Vendetta is ready!");
|
||||||
|
} catch (e: any) {
|
||||||
alert(`Vendetta failed to initialize... ${e.stack || e.toString()}`);
|
alert(`Vendetta failed to initialize... ${e.stack || e.toString()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!erroredOnLoad) logger.log("Vendetta is ready!");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
init();
|
init();
|
|
@ -3,10 +3,15 @@ import { findByProps } from "@metro/filters";
|
||||||
import { after } from "@lib/patcher";
|
import { after } from "@lib/patcher";
|
||||||
|
|
||||||
const commandsModule = findByProps("getBuiltInCommands")
|
const commandsModule = findByProps("getBuiltInCommands")
|
||||||
|
|
||||||
let commands: ApplicationCommand[] = [];
|
let commands: ApplicationCommand[] = [];
|
||||||
|
|
||||||
after("getBuiltInCommands", commandsModule, (args, res) => res.concat(commands));
|
export function patchCommands() {
|
||||||
|
const unpatch = after("getBuiltInCommands", commandsModule, (args, res) => res.concat(commands));
|
||||||
|
return () => {
|
||||||
|
commands = [];
|
||||||
|
unpatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function registerCommand(command: ApplicationCommand): () => void {
|
export function registerCommand(command: ApplicationCommand): () => void {
|
||||||
// Get built in commands
|
// Get built in commands
|
||||||
|
|
|
@ -35,13 +35,15 @@ export function connectToDebugger(url: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function patchLogHook() {
|
export function patchLogHook() {
|
||||||
after("nativeLoggingHook", globalThis, (args, ret) => {
|
const unpatch = after("nativeLoggingHook", globalThis, (args) => {
|
||||||
if (socket?.readyState === WebSocket.OPEN) {
|
if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ message: args[0], level: args[1] }));
|
||||||
socket.send(JSON.stringify({ message: args[0], level: args[1] }));
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.log(args[0]);
|
logger.log(args[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket && socket.close();
|
||||||
|
unpatch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const versionHash = "__vendettaVersion";
|
export const versionHash = "__vendettaVersion";
|
||||||
|
|
|
@ -26,7 +26,7 @@ for (const key in window.modules) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to filter through modules
|
// Function to filter through modules
|
||||||
export const filterModules = (modules: MetroModules, single = false) => (filter: (m: any) => boolean) => {
|
const filterModules = (modules: MetroModules, single = false) => (filter: (m: any) => boolean) => {
|
||||||
const found = [];
|
const found = [];
|
||||||
|
|
||||||
// Get the previous moment locale
|
// Get the previous moment locale
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { awaitSyncWrapper, createStorage, wrapSync } from "@lib/storage";
|
||||||
import logger from "@lib/logger";
|
import logger from "@lib/logger";
|
||||||
import Subpage from "@ui/settings/components/Subpage";
|
import Subpage from "@ui/settings/components/Subpage";
|
||||||
|
|
||||||
|
// TODO: Properly implement hash-based updating
|
||||||
|
|
||||||
type EvaledPlugin = {
|
type EvaledPlugin = {
|
||||||
onLoad?(): void;
|
onLoad?(): void;
|
||||||
onUnload(): void;
|
onUnload(): void;
|
||||||
|
@ -20,7 +22,7 @@ export async function fetchPlugin(id: string, enabled = true) {
|
||||||
let pluginManifest: PluginManifest;
|
let pluginManifest: PluginManifest;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
pluginManifest = await (await fetch(new URL("manifest.json", id), { cache: "no-store" })).json();
|
pluginManifest = await (await fetch(id + "manifest.json", { cache: "no-store" })).json();
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error(`Failed to fetch manifest for ${id}`);
|
throw new Error(`Failed to fetch manifest for ${id}`);
|
||||||
}
|
}
|
||||||
|
@ -30,7 +32,7 @@ export async function fetchPlugin(id: string, enabled = true) {
|
||||||
// TODO: Remove duplicate error if possible
|
// TODO: Remove duplicate error if possible
|
||||||
try {
|
try {
|
||||||
// by polymanifest spec, plugins should always specify their main file, but just in case
|
// by polymanifest spec, plugins should always specify their main file, but just in case
|
||||||
pluginJs = await (await fetch(new URL(pluginManifest.main || "index.js", id), { cache: "no-store" })).text();
|
pluginJs = await (await fetch(id + (pluginManifest.main || "index.js"), { cache: "no-store" })).text();
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error(`Failed to fetch JS for ${id}`);
|
throw new Error(`Failed to fetch JS for ${id}`);
|
||||||
}
|
}
|
||||||
|
@ -49,7 +51,6 @@ export async function fetchPlugin(id: string, enabled = true) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function evalPlugin(plugin: Plugin) {
|
export async function evalPlugin(plugin: Plugin) {
|
||||||
// TODO: Refactor to not depend on own window object
|
|
||||||
const vendettaForPlugins = {
|
const vendettaForPlugins = {
|
||||||
...window.vendetta,
|
...window.vendetta,
|
||||||
plugin: {
|
plugin: {
|
||||||
|
@ -89,7 +90,7 @@ export async function startPlugin(id: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stopPlugin(id: string) {
|
export function stopPlugin(id: string, disable = true) {
|
||||||
const plugin = plugins[id];
|
const plugin = plugins[id];
|
||||||
const pluginRet = loadedPlugins[id];
|
const pluginRet = loadedPlugins[id];
|
||||||
if (!plugin) throw new Error("Attempted to stop non-existent plugin");
|
if (!plugin) throw new Error("Attempted to stop non-existent plugin");
|
||||||
|
@ -102,7 +103,7 @@ export function stopPlugin(id: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
delete loadedPlugins[id];
|
delete loadedPlugins[id];
|
||||||
plugin.enabled = false;
|
disable && (plugin.enabled = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removePlugin(id: string) {
|
export function removePlugin(id: string) {
|
||||||
|
@ -111,15 +112,18 @@ export function removePlugin(id: string) {
|
||||||
delete plugins[id];
|
delete plugins[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initializePlugins() {
|
export async function initPlugins() {
|
||||||
await awaitSyncWrapper(plugins);
|
await awaitSyncWrapper(plugins);
|
||||||
|
|
||||||
const allIds = Object.keys(plugins);
|
const allIds = Object.keys(plugins);
|
||||||
await Promise.allSettled(allIds.map((pl) => fetchPlugin(pl, false)));
|
await Promise.allSettled(allIds.map((pl) => fetchPlugin(pl, false)));
|
||||||
for (const pl of allIds.filter((pl) => plugins[pl].enabled))
|
for (const pl of allIds.filter((pl) => plugins[pl].enabled)) startPlugin(pl);
|
||||||
startPlugin(pl);
|
|
||||||
|
return stopAllPlugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stopAllPlugins = () => Object.keys(plugins).forEach(p => stopPlugin(p, false));
|
||||||
|
|
||||||
export const getSettings = (id: string) => loadedPlugins[id]?.settings;
|
export const getSettings = (id: string) => loadedPlugins[id]?.settings;
|
||||||
|
|
||||||
export function showSettings(plugin: Plugin) {
|
export function showSettings(plugin: Plugin) {
|
||||||
|
|
54
src/lib/windowObject.ts
Normal file
54
src/lib/windowObject.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { VendettaObject } from "@types";
|
||||||
|
import patcher from "@lib/patcher";
|
||||||
|
import logger from "@lib/logger";
|
||||||
|
import settings from "@lib/settings";
|
||||||
|
import copyText from "@utils/copyText";
|
||||||
|
import findInReactTree from "@utils/findInReactTree";
|
||||||
|
import findInTree from "@utils/findInTree";
|
||||||
|
import * as constants from "@lib/constants";
|
||||||
|
import * as debug from "@lib/debug";
|
||||||
|
import * as plugins from "@lib/plugins";
|
||||||
|
import * as commands from "@lib/commands";
|
||||||
|
import * as storage from "@lib/storage";
|
||||||
|
import * as metro from "@metro/filters";
|
||||||
|
import * as common from "@metro/common";
|
||||||
|
import * as components from "@ui/components";
|
||||||
|
import * as toasts from "@ui/toasts";
|
||||||
|
import * as assets from "@ui/assets";
|
||||||
|
|
||||||
|
function without<T extends Record<string, any>>(object: T, ...keys: string[]) {
|
||||||
|
const cloned = { ...object };
|
||||||
|
keys.forEach((k) => delete cloned[k]);
|
||||||
|
return cloned;
|
||||||
|
}
|
||||||
|
|
||||||
|
// I wish Hermes let me do async arrow functions
|
||||||
|
export default async function windowObject(unloads: any[]): Promise<VendettaObject> {
|
||||||
|
return {
|
||||||
|
patcher: without(patcher, "unpatchAll"),
|
||||||
|
metro: { ...metro, common: { ...common } },
|
||||||
|
constants: { ...constants },
|
||||||
|
utils: {
|
||||||
|
copyText: copyText,
|
||||||
|
findInReactTree: findInReactTree,
|
||||||
|
findInTree: findInTree,
|
||||||
|
},
|
||||||
|
debug: without(debug, "versionHash", "patchLogHook"),
|
||||||
|
ui: {
|
||||||
|
components,
|
||||||
|
toasts,
|
||||||
|
assets,
|
||||||
|
},
|
||||||
|
plugins: without(plugins, "initPlugins"),
|
||||||
|
commands: without(commands, "patchCommands"),
|
||||||
|
storage,
|
||||||
|
settings,
|
||||||
|
logger,
|
||||||
|
version: debug.versionHash,
|
||||||
|
unload: () => {
|
||||||
|
unloads.filter(i => typeof i === "function").forEach(p => p());
|
||||||
|
// @ts-expect-error explode
|
||||||
|
delete window.vendetta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,11 @@
|
||||||
import { Asset, Indexable, } from "@types";
|
import { Asset, Indexable } from "@types";
|
||||||
import { after } from "@lib/patcher";
|
import { after } from "@lib/patcher";
|
||||||
import { assets } from "@metro/common";
|
import { assets } from "@metro/common";
|
||||||
|
|
||||||
export const all: Indexable<Asset> = {};
|
export const all: Indexable<Asset> = {};
|
||||||
|
|
||||||
export function patchAssets() {
|
export function patchAssets() {
|
||||||
try {
|
const unpatch = after("registerAsset", assets, (args: Asset[], id: number) => {
|
||||||
after("registerAsset", assets, (args: Asset[], id: number) => {
|
|
||||||
const asset = args[0];
|
const asset = args[0];
|
||||||
all[asset.name] = { ...asset, id: id };
|
all[asset.name] = { ...asset, id: id };
|
||||||
});
|
});
|
||||||
|
@ -17,7 +16,8 @@ export function patchAssets() {
|
||||||
if (all[asset.name]) continue;
|
if (all[asset.name]) continue;
|
||||||
all[asset.name] = { ...asset, id: id };
|
all[asset.name] = { ...asset, id: id };
|
||||||
};
|
};
|
||||||
} catch {};
|
|
||||||
|
return unpatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const find = (filter: (a: any) => void): Asset | null | undefined => Object.values(all).find(filter);
|
export const find = (filter: (a: any) => void): Asset | null | undefined => Object.values(all).find(filter);
|
||||||
|
|
|
@ -18,7 +18,7 @@ function override() {
|
||||||
FluxDispatcher.unsubscribe("I18N_LOAD_START", override);
|
FluxDispatcher.unsubscribe("I18N_LOAD_START", override);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fixTheme() {
|
export default function fixTheme() {
|
||||||
try {
|
try {
|
||||||
if (ThemeStore) FluxDispatcher.subscribe("I18N_LOAD_START", override);
|
if (ThemeStore) FluxDispatcher.subscribe("I18N_LOAD_START", override);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { NavigationNative } from "@metro/common";
|
||||||
import { Forms } from "@ui/components";
|
import { Forms } from "@ui/components";
|
||||||
import { getAssetIDByName } from "@ui/assets";
|
import { getAssetIDByName } from "@ui/assets";
|
||||||
import { useProxy } from "@lib/storage";
|
import { useProxy } from "@lib/storage";
|
||||||
|
@ -5,11 +6,8 @@ import settings from "@lib/settings";
|
||||||
|
|
||||||
const { FormRow, FormSection, FormDivider } = Forms;
|
const { FormRow, FormSection, FormDivider } = Forms;
|
||||||
|
|
||||||
interface SettingsSectionProps {
|
export default function SettingsSection() {
|
||||||
navigation: any;
|
const navigation = NavigationNative.useNavigation();
|
||||||
}
|
|
||||||
|
|
||||||
export default function SettingsSection({ navigation }: SettingsSectionProps) {
|
|
||||||
useProxy(settings);
|
useProxy(settings);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -9,10 +9,11 @@ import Developer from "@ui/settings/pages/Developer";
|
||||||
|
|
||||||
const screensModule = findByDisplayName("getScreens", false);
|
const screensModule = findByDisplayName("getScreens", false);
|
||||||
const settingsModule = findByDisplayName("UserSettingsOverviewWrapper", false);
|
const settingsModule = findByDisplayName("UserSettingsOverviewWrapper", false);
|
||||||
let prevPatches: Function[] = [];
|
|
||||||
|
|
||||||
export default function initSettings() {
|
export default function initSettings() {
|
||||||
after("default", screensModule, (args, existingScreens) => {
|
const patches = new Array<Function>;
|
||||||
|
|
||||||
|
patches.push(after("default", screensModule, (args, existingScreens) => {
|
||||||
return {
|
return {
|
||||||
...existingScreens,
|
...existingScreens,
|
||||||
VendettaSettings: {
|
VendettaSettings: {
|
||||||
|
@ -28,24 +29,23 @@ export default function initSettings() {
|
||||||
render: Developer
|
render: Developer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
after("default", settingsModule, (args, ret) => {
|
|
||||||
for (let p of prevPatches) p();
|
|
||||||
prevPatches = [];
|
|
||||||
|
|
||||||
|
after("default", settingsModule, (_, ret) => {
|
||||||
const Overview = findInReactTree(ret.props.children, i => i.type && i.type.name === "UserSettingsOverview");
|
const Overview = findInReactTree(ret.props.children, i => i.type && i.type.name === "UserSettingsOverview");
|
||||||
|
|
||||||
// Upload logs button gone
|
// Upload logs button gone
|
||||||
prevPatches.push(after("renderSupportAndAcknowledgements", Overview.type.prototype, (args, { props: { children } }) => {
|
patches.push(after("renderSupportAndAcknowledgements", Overview.type.prototype, (_, { props: { children } }) => {
|
||||||
const index = children.findIndex((c: any) => c?.type?.name === "UploadLogsButton");
|
const index = children.findIndex((c: any) => c?.type?.name === "UploadLogsButton");
|
||||||
if (index !== -1) children.splice(index, 1);
|
if (index !== -1) children.splice(index, 1);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
prevPatches.push(after("render", Overview.type.prototype, (args, { props: { children } }) => {
|
patches.push(after("render", Overview.type.prototype, (_, { props: { children } }) => {
|
||||||
const titles = [i18n.Messages["BILLING_SETTINGS"], i18n.Messages["PREMIUM_SETTINGS"]];
|
const titles = [i18n.Messages["BILLING_SETTINGS"], i18n.Messages["PREMIUM_SETTINGS"]];
|
||||||
const index = children.findIndex((c: any) => titles.includes(c.props.title));
|
const index = children.findIndex((c: any) => titles.includes(c.props.title));
|
||||||
children.splice(index === -1 ? 4 : index, 0, <SettingsSection navigation={Overview.props.navigation} />);
|
children.splice(index === -1 ? 4 : index, 0, <SettingsSection />);
|
||||||
}));
|
}));
|
||||||
});
|
}, true);
|
||||||
|
|
||||||
|
return () => patches.forEach(p => p());
|
||||||
}
|
}
|
Loading…
Reference in a new issue