[Global] Major refactors, allow unloading!
This commit is contained in:
parent
0be8cdac05
commit
0ba9ee600c
10 changed files with 137 additions and 116 deletions
94
src/index.ts
94
src/index.ts
|
@ -1,79 +1,37 @@
|
|||
import patcher from "@lib/patcher";
|
||||
import logger from "@lib/logger";
|
||||
import copyText from "@utils/copyText";
|
||||
import findInReactTree from "@utils/findInReactTree";
|
||||
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 { patchLogHook } from "@lib/debug";
|
||||
import { patchCommands } from "@lib/commands";
|
||||
import { initPlugins } from "@lib/plugins";
|
||||
import { patchAssets } from "@ui/assets";
|
||||
import initSettings from "@ui/settings";
|
||||
import { fixTheme } from "@ui/fixTheme";
|
||||
import { connectToDebugger, patchLogHook, versionHash } from "@lib/debug";
|
||||
import { plugins, fetchPlugin, evalPlugin, stopPlugin, removePlugin, getSettings, initializePlugins } from "@lib/plugins";
|
||||
import settings from "@lib/settings";
|
||||
import { registerCommand } from "@lib/commands";
|
||||
import fixTheme from "@ui/fixTheme";
|
||||
import windowObject from "@lib/windowObject";
|
||||
import logger from "@lib/logger";
|
||||
|
||||
// This logs in the native logging implementation, e.g. logcat
|
||||
console.log("Hello from Vendetta!");
|
||||
|
||||
async function init() {
|
||||
let erroredOnLoad = false;
|
||||
|
||||
try {
|
||||
window.vendetta = {
|
||||
patcher: patcher,
|
||||
metro: { ...metro, common: { ...common } },
|
||||
constants: { ...constants },
|
||||
utils: {
|
||||
copyText: copyText,
|
||||
findInReactTree: findInReactTree,
|
||||
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,
|
||||
};
|
||||
// Load everything in parallel
|
||||
const unloads = await Promise.all([
|
||||
patchLogHook(),
|
||||
patchAssets(),
|
||||
patchCommands(),
|
||||
fixTheme(),
|
||||
initSettings(),
|
||||
]);
|
||||
|
||||
patchLogHook();
|
||||
patchAssets();
|
||||
fixTheme();
|
||||
initializePlugins();
|
||||
initSettings();
|
||||
} catch (e: Error | any) {
|
||||
erroredOnLoad = true;
|
||||
// Assign window object
|
||||
window.vendetta = await windowObject(unloads);
|
||||
|
||||
// Once done, load plugins
|
||||
unloads.push(await initPlugins());
|
||||
|
||||
// We good :)
|
||||
logger.log("Vendetta is ready!");
|
||||
} catch (e: any) {
|
||||
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";
|
||||
|
||||
const commandsModule = findByProps("getBuiltInCommands")
|
||||
|
||||
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 {
|
||||
// Get built in commands
|
||||
|
|
|
@ -34,14 +34,16 @@ export function connectToDebugger(url: string) {
|
|||
});
|
||||
}
|
||||
|
||||
export function patchLogHook() {
|
||||
after("nativeLoggingHook", globalThis, (args, ret) => {
|
||||
if (socket?.readyState === WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify({ message: args[0], level: args[1] }));
|
||||
}
|
||||
|
||||
export function patchLogHook() {
|
||||
const unpatch = after("nativeLoggingHook", globalThis, (args) => {
|
||||
if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ message: args[0], level: args[1] }));
|
||||
logger.log(args[0]);
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket && socket.close();
|
||||
unpatch();
|
||||
}
|
||||
}
|
||||
|
||||
export const versionHash = "__vendettaVersion";
|
||||
|
|
|
@ -26,7 +26,7 @@ for (const key in window.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 = [];
|
||||
|
||||
// Get the previous moment locale
|
||||
|
@ -53,7 +53,7 @@ export const filterModules = (modules: MetroModules, single = false) => (filter:
|
|||
found.push(module.default);
|
||||
}
|
||||
|
||||
if(filter(module)) {
|
||||
if (filter(module)) {
|
||||
if (single) return module;
|
||||
else found.push(module);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import { awaitSyncWrapper, createStorage, wrapSync } from "@lib/storage";
|
|||
import logger from "@lib/logger";
|
||||
import Subpage from "@ui/settings/components/Subpage";
|
||||
|
||||
// TODO: Properly implement hash-based updating
|
||||
|
||||
type EvaledPlugin = {
|
||||
onLoad?(): void;
|
||||
onUnload(): void;
|
||||
|
@ -20,7 +22,7 @@ export async function fetchPlugin(id: string, enabled = true) {
|
|||
let pluginManifest: PluginManifest;
|
||||
|
||||
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 {
|
||||
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
|
||||
try {
|
||||
// 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 {
|
||||
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) {
|
||||
// TODO: Refactor to not depend on own window object
|
||||
const vendettaForPlugins = {
|
||||
...window.vendetta,
|
||||
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 pluginRet = loadedPlugins[id];
|
||||
if (!plugin) throw new Error("Attempted to stop non-existent plugin");
|
||||
|
@ -102,7 +103,7 @@ export function stopPlugin(id: string) {
|
|||
}
|
||||
|
||||
delete loadedPlugins[id];
|
||||
plugin.enabled = false;
|
||||
disable && (plugin.enabled = false);
|
||||
}
|
||||
|
||||
export function removePlugin(id: string) {
|
||||
|
@ -111,15 +112,18 @@ export function removePlugin(id: string) {
|
|||
delete plugins[id];
|
||||
}
|
||||
|
||||
export async function initializePlugins() {
|
||||
export async function initPlugins() {
|
||||
await awaitSyncWrapper(plugins);
|
||||
|
||||
const allIds = Object.keys(plugins);
|
||||
await Promise.allSettled(allIds.map((pl) => fetchPlugin(pl, false)));
|
||||
for (const pl of allIds.filter((pl) => plugins[pl].enabled))
|
||||
startPlugin(pl);
|
||||
for (const pl of allIds.filter((pl) => plugins[pl].enabled)) startPlugin(pl);
|
||||
|
||||
return stopAllPlugins;
|
||||
}
|
||||
|
||||
const stopAllPlugins = () => Object.keys(plugins).forEach(p => stopPlugin(p, false));
|
||||
|
||||
export const getSettings = (id: string) => loadedPlugins[id]?.settings;
|
||||
|
||||
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,23 +1,23 @@
|
|||
import { Asset, Indexable, } from "@types";
|
||||
import { Asset, Indexable } from "@types";
|
||||
import { after } from "@lib/patcher";
|
||||
import { assets } from "@metro/common";
|
||||
|
||||
export const all: Indexable<Asset> = {};
|
||||
|
||||
export function patchAssets() {
|
||||
try {
|
||||
after("registerAsset", assets, (args: Asset[], id: number) => {
|
||||
const asset = args[0];
|
||||
all[asset.name] = { ...asset, id: id };
|
||||
});
|
||||
const unpatch = after("registerAsset", assets, (args: Asset[], id: number) => {
|
||||
const asset = args[0];
|
||||
all[asset.name] = { ...asset, id: id };
|
||||
});
|
||||
|
||||
for (let id = 1; ; id++) {
|
||||
const asset = assets.getAssetByID(id);
|
||||
if (!asset) break;
|
||||
if (all[asset.name]) continue;
|
||||
all[asset.name] = { ...asset, id: id };
|
||||
};
|
||||
} catch {};
|
||||
for (let id = 1; ; id++) {
|
||||
const asset = assets.getAssetByID(id);
|
||||
if (!asset) break;
|
||||
if (all[asset.name]) continue;
|
||||
all[asset.name] = { ...asset, id: id };
|
||||
};
|
||||
|
||||
return unpatch;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export function fixTheme() {
|
||||
export default function fixTheme() {
|
||||
try {
|
||||
if (ThemeStore) FluxDispatcher.subscribe("I18N_LOAD_START", override);
|
||||
} catch(e) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { NavigationNative } from "@metro/common";
|
||||
import { Forms } from "@ui/components";
|
||||
import { getAssetIDByName } from "@ui/assets";
|
||||
import { useProxy } from "@lib/storage";
|
||||
|
@ -5,11 +6,8 @@ import settings from "@lib/settings";
|
|||
|
||||
const { FormRow, FormSection, FormDivider } = Forms;
|
||||
|
||||
interface SettingsSectionProps {
|
||||
navigation: any;
|
||||
}
|
||||
|
||||
export default function SettingsSection({ navigation }: SettingsSectionProps) {
|
||||
export default function SettingsSection() {
|
||||
const navigation = NavigationNative.useNavigation();
|
||||
useProxy(settings);
|
||||
|
||||
return (
|
||||
|
|
|
@ -9,10 +9,11 @@ import Developer from "@ui/settings/pages/Developer";
|
|||
|
||||
const screensModule = findByDisplayName("getScreens", false);
|
||||
const settingsModule = findByDisplayName("UserSettingsOverviewWrapper", false);
|
||||
let prevPatches: Function[] = [];
|
||||
|
||||
export default function initSettings() {
|
||||
after("default", screensModule, (args, existingScreens) => {
|
||||
const patches = new Array<Function>;
|
||||
|
||||
patches.push(after("default", screensModule, (args, existingScreens) => {
|
||||
return {
|
||||
...existingScreens,
|
||||
VendettaSettings: {
|
||||
|
@ -28,24 +29,23 @@ export default function initSettings() {
|
|||
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");
|
||||
|
||||
// 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");
|
||||
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 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