refactor(plugins)

This commit is contained in:
amsyarasyiq 2024-06-25 02:28:04 +08:00
parent b3c1b748a0
commit 29caf1d09b
10 changed files with 73 additions and 67 deletions

View file

@ -1,6 +1,6 @@
import { Strings } from "@core/i18n";
import { ApplicationCommand, ApplicationCommandOptionType } from "@lib/api/commands/types";
import { BunnyPlugin, sourceToObject as pluginStorage } from "@lib/managers/plugins";
import { BunnyPlugin, sourceStore as pluginStorage } from "@lib/managers/plugins";
import { messageUtil } from "@metro/common";
export default () => <ApplicationCommand>{

View file

@ -3,7 +3,7 @@ import { requireAssetIndex } from "@lib/api/assets";
import { isThemeSupported } from "@lib/api/native/loader";
import { after } from "@lib/api/patcher";
import { useProxy } from "@lib/api/storage";
import { installPlugin, sourceToObject, removePlugin } from "@lib/managers/plugins";
import { installPlugin, removePlugin,sourceStore } from "@lib/managers/plugins";
import { installTheme, removeTheme, themes } from "@lib/managers/themes";
import { DISCORD_SERVER_ID, HTTP_REGEX_MULTI, PLUGINS_CHANNEL_ID, THEMES_CHANNEL_ID, VD_PROXY_PREFIX } from "@lib/utils/constants";
import { lazyDestructure } from "@lib/utils/lazy";
@ -21,7 +21,7 @@ const forumReactions = findByPropsProxy("MostCommonForumPostReaction");
const postMap = {
Plugin: {
storage: sourceToObject,
storage: sourceStore,
urlsFilter: (url: string) => url.startsWith(VD_PROXY_PREFIX),
installOrRemove: (url: string) => {
const isInstalled = postMap.Plugin.storage[url];
@ -63,7 +63,7 @@ function useExtractThreadContent(thread: any, _firstMessage = null, actionSheet
function useInstaller(thread: any, firstMessage = null, actionSheet = false): [true] | [false, PostType, boolean, boolean, () => Promise<void>] {
const [postType, url] = useExtractThreadContent(thread, firstMessage, actionSheet) ?? [];
useProxy(sourceToObject);
useProxy(sourceStore);
useProxy(themes);
const [isInstalling, setIsInstalling] = React.useState(false);

View file

@ -170,13 +170,13 @@ export const initVendettaObject = (): any => {
rawColors: color.rawColors
},
plugins: {
plugins: plugins.sourceToObject,
fetchPlugin: (source: string) => plugins.fetchPlugin(source),
plugins: plugins.sourceStore,
fetchPlugin: (source: string) => plugins.fetchAndStorePlugin(source),
installPlugin: (source: string, enabled = true) => plugins.installPlugin(source, enabled),
startPlugin: (id: string) => plugins.startPlugin(plugins.sourceToObject[id]!.pluginId),
stopPlugin: (id: string, disable = true) => plugins.stopPlugin(plugins.sourceToObject[id]!.pluginId, disable),
removePlugin: (id: string) => plugins.removePlugin(plugins.sourceToObject[id]!.pluginId),
getSettings: (id: string) => plugins.getSettingsComponent(plugins.sourceToObject[id]!.pluginId)
startPlugin: (id: string) => plugins.startPlugin(plugins.sourceStore[id]!.pluginId),
stopPlugin: (id: string, disable = true) => plugins.stopPlugin(plugins.sourceStore[id]!.pluginId, disable),
removePlugin: (id: string) => plugins.removePlugin(plugins.sourceStore[id]!.pluginId),
getSettings: (id: string) => plugins.getSettingsComponent(plugins.sourceStore[id]!.pluginId)
},
themes: {
themes: themes.themes,

View file

@ -15,7 +15,7 @@ import { View } from "react-native";
type SearchKeywords = Array<string | ((obj: any & {}) => string)>;
interface AddonPageProps<T> {
interface AddonPageProps<T extends object> {
title: string;
fetchFunction: (url: string) => Promise<void>;
items: Record<string, any>;
@ -30,7 +30,7 @@ interface AddonPageProps<T> {
// TODO: Move to somewhere else
const { FlashList } = findByProps("FlashList");
export default function AddonPage<T>({ card: CardComponent, ...props }: AddonPageProps<T>) {
export default function AddonPage<T extends object>({ card: CardComponent, ...props }: AddonPageProps<T>) {
useProxy(settings);
useProxy(props.items);
@ -67,6 +67,7 @@ export default function AddonPage<T>({ card: CardComponent, ...props }: AddonPag
data={data}
estimatedItemSize={136}
ListHeaderComponent={headerElement}
onLoad={console.log}
contentContainerStyle={{ paddingBottom: 90, paddingHorizontal: 5 }}
renderItem={({ item }: any) => (
<View style={{ paddingVertical: 6, paddingHorizontal: 8 }}>

View file

@ -7,7 +7,7 @@ import { lazyDestructure } from "@lib/utils/lazy";
import { findByProps } from "@metro";
import { NavigationNative, tokens } from "@metro/common";
import { Card, IconButton, Stack, TableSwitch, Text } from "@metro/common/components";
import { createContext, useContext } from "react";
import { createContext, memo, useContext } from "react";
import { Image, View } from "react-native";
import { usePluginCardStyles } from "./usePluginCardStyles";
@ -48,14 +48,11 @@ function Title() {
</Text>
);
if (!icon) return textElement;
return <View
return !icon ? textElement : <View
style={{
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
gap: 4
gap: 6
}}
>
<Image
@ -88,9 +85,35 @@ function Status() {
</View>;
}
export default function PluginCard({ item: plugin }: CardWrapper<BunnyPlugin>) {
const Actions = memo(() => {
const plugin = usePlugin();
const navigation = NavigationNative.useNavigation();
return <View style={{ flexDirection: "row", gap: 6 }}>
<IconButton
size="sm"
variant="secondary"
icon={requireAssetIndex("WrenchIcon")}
disabled={!getSettingsComponent(plugin.id)}
onPress={() => navigation.push("VendettaCustomPage", {
title: plugin.manifest.name,
render: getSettingsComponent(plugin.id),
})}
/>
<IconButton
size="sm"
variant="secondary"
icon={requireAssetIndex("CircleInformationIcon-primary")}
onPress={() => void showSheet(
"PluginInfoActionSheet",
import("./sheets/PluginInfoActionSheet"),
{ plugin, navigation }
)}
/>
</View>;
});
export default function PluginCard({ item: plugin }: CardWrapper<BunnyPlugin>) {
useProxy(plugin);
return (
@ -105,28 +128,7 @@ export default function PluginCard({ item: plugin }: CardWrapper<BunnyPlugin>) {
</View>
<View>
<Stack spacing={12} direction="horizontal">
<View style={{ flexDirection: "row", gap: 6 }}>
<IconButton
size="sm"
variant="secondary"
icon={requireAssetIndex("WrenchIcon")}
disabled={!getSettingsComponent(plugin.id)}
onPress={() => navigation.push("VendettaCustomPage", {
title: plugin.manifest.name,
render: getSettingsComponent(plugin.id),
})}
/>
<IconButton
size="sm"
variant="secondary"
icon={requireAssetIndex("CircleInformationIcon-primary")}
onPress={() => void showSheet(
"PluginInfoActionSheet",
import("./sheets/PluginInfoActionSheet"),
{ plugin, navigation }
)}
/>
</View>
<Actions />
<TableSwitch
value={plugin.enabled}
onValueChange={(v: boolean) => {

View file

@ -2,7 +2,7 @@ import { Strings } from "@core/i18n";
import AddonPage from "@core/ui/components/AddonPage";
import PluginCard from "@core/ui/settings/pages/Plugins/PluginCard";
import { useProxy } from "@lib/api/storage";
import { BunnyPlugin, idToSource, installPlugin, sourceToObject } from "@lib/managers/plugins";
import { BunnyPlugin, installPlugin, selectedSources, sourceStore } from "@lib/managers/plugins";
import { settings } from "@lib/settings";
import { Author } from "@lib/utils/types";
@ -17,8 +17,8 @@ export default function Plugins() {
"manifest.description",
p => p.manifest.authors?.map((a: Author) => a.name).join()
]}
items={idToSource}
resolveItem={id => sourceToObject[id]}
items={selectedSources}
resolveItem={source => sourceStore[source]}
fetchFunction={installPlugin}
safeModeMessage={Strings.SAFE_MODE_NOTICE_PLUGINS}
card={PluginCard}

View file

@ -1,7 +1,7 @@
import { formatString, Strings } from "@core/i18n";
import { requireAssetIndex } from "@lib/api/assets";
import { purgeStorage, useProxy } from "@lib/api/storage";
import { BunnyPlugin, fetchPlugin, getSettingsComponent, removePlugin, startPlugin, stopPlugin } from "@lib/managers/plugins";
import { BunnyPlugin, fetchAndStorePlugin, getSettingsComponent, removePlugin, startPlugin, stopPlugin } from "@lib/managers/plugins";
import { showConfirmationAlert } from "@lib/ui/alerts";
import { hideSheet } from "@lib/ui/sheets";
import { showToast } from "@lib/ui/toasts";
@ -50,7 +50,7 @@ export default function PluginInfoActionSheet({ plugin, navigation }: InfoProps)
if (plugin.enabled) stopPlugin(plugin.id, false);
try {
await fetchPlugin(plugin.id);
await fetchAndStorePlugin(plugin.id);
showToast(Strings.PLUGIN_REFETCH_SUCCESSFUL, requireAssetIndex("toast_image_saved"));
} catch {
showToast(Strings.PLUGIN_REFETCH_FAILED, requireAssetIndex("Small"));
@ -92,7 +92,7 @@ export default function PluginInfoActionSheet({ plugin, navigation }: InfoProps)
if (plugin.enabled) stopPlugin(plugin.id, false);
try {
await fetchPlugin(plugin.id);
await fetchAndStorePlugin(plugin.id);
showToast(Strings.PLUGIN_REFETCH_SUCCESSFUL, requireAssetIndex("toast_image_saved"));
} catch {
showToast(Strings.PLUGIN_REFETCH_FAILED, requireAssetIndex("Small"));

View file

@ -57,7 +57,9 @@ export default async () => {
window.bunny = lib;
// Once done, load plugins
initPlugins().then(u => lib.unload.push(u));
initPlugins()
.then(u => lib.unload.push(u))
.catch(() => alert("Failed to initialize Bunny plugins"));
// Update the fonts
updateFonts();

View file

@ -37,6 +37,7 @@ export interface BunnyPlugin {
* Message of plugin startup error. Gone after plugin boots successfully
* */
error?: string;
// TODO: Use fs to avoid unnecessary memory usage
js: string;
@ -46,16 +47,16 @@ export interface BunnyPlugin {
id: string;
}
const BUNNY_PROXY_PREFIX = "https://bn-plugins.github.io/vd-proxy";
export const BUNNY_PROXY_PREFIX = "https://bn-plugins.github.io/vd-proxy";
const VD_PROXY_PREFIX = "https://vd-plugins.github.io/proxy";
const OLD_BUNNY_PROXY_PREFIX = "https://bunny-mod.github.io/plugins-proxy";
const pluginsEnabled = () => !settings.safeMode?.enabled;
const _pluginInstances: Record<string, EvaledPlugin> = {};
export const idToSource = wrapSync(createStorage<Record<string, string | undefined>>(createMMKVBackend("SELECTED_PLUGINS")));
export const sourceToObject = proxyLazy(() => {
export const selectedSources = wrapSync(createStorage<Record<string, string | undefined>>(createMMKVBackend("SELECTED_PLUGIN_SOURCES")));
export const sourceStore = proxyLazy(() => {
type StorageInterface = Record<string, BunnyPlugin | undefined>;
const storagePromise = createStorage<StorageInterface>(createMMKVBackend("VENDETTA_PLUGINS"));
@ -70,21 +71,21 @@ export const sourceToObject = proxyLazy(() => {
});
export function getPluginById(id: string) {
if (!idToSource[id]) {
for (const plugin of Object.values(sourceToObject)) {
if (!selectedSources[id]) {
for (const plugin of Object.values(sourceStore)) {
if (plugin?.pluginId === id) {
idToSource[id] = plugin.source;
selectedSources[id] = plugin.source;
break;
}
}
}
return idToSource[id] ? sourceToObject[idToSource[id]!] : undefined;
return selectedSources[id] ? sourceStore[selectedSources[id]!] : undefined;
}
export async function fetchPlugin(source: string) {
export async function fetchAndStorePlugin(source: string) {
if (!source.endsWith("/")) source += "/";
const existingPlugin = sourceToObject[source];
const existingPlugin = sourceStore[source];
const fetch = (url: string) => safeFetch(
url
@ -112,7 +113,7 @@ export async function fetchPlugin(source: string) {
invariant(pluginJs || existingPlugin, `Failed to fetch JS from ${source}`);
return sourceToObject[source] = {
return sourceStore[source] = {
id: source,
source: source,
pluginId: pluginManifest.name,
@ -126,9 +127,9 @@ export async function fetchPlugin(source: string) {
export async function installPlugin(source: string, enabled = true) {
if (!source.endsWith("/")) source += "/";
invariant(!(source in sourceToObject), "Source was already installed");
invariant(!(source in sourceStore), "Source was already installed");
const plugin = await fetchPlugin(source);
const plugin = await fetchAndStorePlugin(source);
if (enabled) await startPlugin(plugin.pluginId);
}
@ -194,7 +195,7 @@ export async function removePlugin(id: string) {
const plugin = getPluginById(id);
invariant(plugin, "Removing non-existent plugin");
if (plugin.enabled) stopPlugin(id);
delete sourceToObject[plugin.source];
delete sourceStore[plugin.source];
await purgeStorage(plugin.source);
}
@ -202,29 +203,30 @@ export async function removePlugin(id: string) {
* @internal
*/
export async function initPlugins() {
await awaitStorage(sourceToObject, idToSource, settings);
await awaitStorage(sourceStore, selectedSources, settings);
// TODO: Optimize this
if (pluginsEnabled()) {
const allIds = [...new Set(Object.values(sourceToObject).map(s => s!.pluginId))];
const allIds = [...new Set(Object.values(sourceStore).map(s => s!.pluginId))];
// Loop over enabled plugins, update and start them
const enabledPlugins = allIds.filter(id => getPluginById(id)?.enabled);
await Promise.all(enabledPlugins.map(async pl => {
if (getPluginById(pl)!.update) {
try {
await fetchPlugin(pl);
await fetchAndStorePlugin(pl);
} catch (e) {
logger.error(e);
}
}
await startPlugin(pl);
}));
// Loop over disabled plugins that are allowed to update
allIds
.filter(id => !getPluginById(id)?.enabled && getPluginById(id)!.update)
.forEach(id => fetchPlugin(id));
.forEach(id => fetchAndStorePlugin(id));
}
return () => Object.keys(_pluginInstances).forEach(p => stopPlugin(p, false));

View file

@ -1,6 +1,5 @@
export const DISCORD_SERVER = "https://discord.gg/XjYgWXHb9Q";
export const GITHUB = "https://github.com/pyoncord";
export const VD_PROXY_PREFIX = "https://vd-plugins.github.io/proxy";
export const HTTP_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/;
export const HTTP_REGEX_MULTI = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&//=]*)/g;