feat(ui): "generalize" plugin components
This commit is contained in:
parent
605685e86e
commit
e5c87994db
6 changed files with 217 additions and 176 deletions
|
|
@ -1,27 +1,25 @@
|
|||
import { CardWrapper } from "@core/ui/components/AddonCard";
|
||||
import { VdPluginManager, VendettaPlugin } from "@core/vendetta/plugins";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { findByProps } from "@metro";
|
||||
import { NavigationNative } from "@metro/common";
|
||||
import { Card, IconButton, Stack, TableSwitch, Text } from "@metro/common/components";
|
||||
import { showSheet } from "@ui/sheets";
|
||||
import { createContext, memo, useContext } from "react";
|
||||
import { Image, View } from "react-native";
|
||||
|
||||
import { PluginManifest } from ".";
|
||||
import { usePluginCardStyles } from "./usePluginCardStyles";
|
||||
|
||||
const { useToken } = lazyDestructure(() => findByProps("useToken"));
|
||||
const PluginContext = createContext<VendettaPlugin>(null!);
|
||||
const PluginContext = createContext<PluginManifest>(null!);
|
||||
const usePlugin = () => useContext(PluginContext);
|
||||
|
||||
function Authors() {
|
||||
const plugin = usePlugin();
|
||||
if (!plugin.authors) return null;
|
||||
|
||||
const children = ["by "];
|
||||
|
||||
for (const author of plugin.manifest.authors) {
|
||||
children.push(author.name);
|
||||
for (const author of plugin.authors) {
|
||||
children.push(typeof author === "string" ? author : author.name);
|
||||
children.push(", ");
|
||||
}
|
||||
|
||||
|
|
@ -36,15 +34,14 @@ function Title() {
|
|||
const styles = usePluginCardStyles();
|
||||
const plugin = usePlugin();
|
||||
|
||||
const iconName = plugin.manifest.vendetta?.icon;
|
||||
const icon = iconName && findAssetId(iconName);
|
||||
const icon = plugin.icon && findAssetId(plugin.icon);
|
||||
|
||||
const textElement = (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
variant="heading-lg/semibold"
|
||||
>
|
||||
{plugin.manifest.name}
|
||||
{plugin.name}
|
||||
</Text>
|
||||
);
|
||||
|
||||
|
|
@ -94,10 +91,10 @@ const Actions = memo(() => {
|
|||
size="sm"
|
||||
variant="secondary"
|
||||
icon={findAssetId("WrenchIcon")}
|
||||
disabled={!VdPluginManager.getSettings(plugin.id)}
|
||||
disabled={!plugin.getPluginSettingsComponent()}
|
||||
onPress={() => navigation.push("VendettaCustomPage", {
|
||||
title: plugin.manifest.name,
|
||||
render: VdPluginManager.getSettings(plugin.id),
|
||||
title: plugin.name,
|
||||
render: plugin.getPluginSettingsComponent(),
|
||||
})}
|
||||
/>
|
||||
<IconButton
|
||||
|
|
@ -106,15 +103,15 @@ const Actions = memo(() => {
|
|||
icon={findAssetId("CircleInformationIcon-primary")}
|
||||
onPress={() => void showSheet(
|
||||
"PluginInfoActionSheet",
|
||||
import("./sheets/PluginInfoActionSheet"),
|
||||
plugin.resolveSheetComponent(),
|
||||
{ plugin, navigation }
|
||||
)}
|
||||
/>
|
||||
</View>;
|
||||
});
|
||||
|
||||
export default function PluginCard({ item: plugin }: CardWrapper<VendettaPlugin>) {
|
||||
useProxy(plugin);
|
||||
export default function PluginCard({ item: plugin }: CardWrapper<PluginManifest>) {
|
||||
plugin.usePluginState();
|
||||
|
||||
return (
|
||||
<PluginContext.Provider value={plugin}>
|
||||
|
|
@ -130,17 +127,16 @@ export default function PluginCard({ item: plugin }: CardWrapper<VendettaPlugin>
|
|||
<Stack spacing={12} direction="horizontal">
|
||||
<Actions />
|
||||
<TableSwitch
|
||||
value={plugin.enabled}
|
||||
value={plugin.isEnabled()}
|
||||
onValueChange={(v: boolean) => {
|
||||
if (v) VdPluginManager.startPlugin(plugin.id);
|
||||
else VdPluginManager.stopPlugin(plugin.id);
|
||||
plugin.toggle(v);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</View>
|
||||
</View>
|
||||
<Text variant="text-md/medium">
|
||||
{plugin.manifest.description}
|
||||
{plugin.description}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -6,19 +6,61 @@ import { settings } from "@lib/api/settings";
|
|||
import { useProxy } from "@lib/api/storage";
|
||||
import { Author } from "@lib/utils/types";
|
||||
|
||||
export interface PluginManifest {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
authors?: Array<Author | string>;
|
||||
icon?: string;
|
||||
|
||||
isEnabled(): boolean;
|
||||
usePluginState(): void;
|
||||
toggle(start: boolean): void;
|
||||
resolveSheetComponent(): Promise<{ default: React.ComponentType<any>; }>;
|
||||
getPluginSettingsComponent(): React.ComponentType<any> | null;
|
||||
}
|
||||
|
||||
function resolveFromVdPlugin(vdPlugin: VendettaPlugin): PluginManifest {
|
||||
return {
|
||||
id: vdPlugin.id,
|
||||
name: vdPlugin.manifest.name,
|
||||
description: vdPlugin.manifest.description,
|
||||
authors: vdPlugin.manifest.authors,
|
||||
icon: vdPlugin.manifest.vendetta?.icon,
|
||||
isEnabled() {
|
||||
return vdPlugin.enabled;
|
||||
},
|
||||
usePluginState() {
|
||||
useProxy(VdPluginManager.plugins[vdPlugin.id]);
|
||||
},
|
||||
toggle(start: boolean) {
|
||||
start
|
||||
? VdPluginManager.startPlugin(vdPlugin.id)
|
||||
: VdPluginManager.stopPlugin(vdPlugin.id);
|
||||
},
|
||||
resolveSheetComponent() {
|
||||
return import("./sheets/VdPluginInfoActionSheet");
|
||||
},
|
||||
getPluginSettingsComponent() {
|
||||
return VdPluginManager.getSettings(vdPlugin.id);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function Plugins() {
|
||||
useProxy(settings);
|
||||
|
||||
return (
|
||||
<AddonPage<VendettaPlugin>
|
||||
<AddonPage<PluginManifest>
|
||||
title={Strings.PLUGINS}
|
||||
searchKeywords={[
|
||||
"manifest.name",
|
||||
"manifest.description",
|
||||
p => p.manifest.authors?.map((a: Author) => a.name).join()
|
||||
"name",
|
||||
"description",
|
||||
p => p.authors?.map((a: Author | string) => typeof a === "string" ? a : a.name).join()
|
||||
]}
|
||||
items={VdPluginManager.plugins}
|
||||
fetchFunction={VdPluginManager.installPlugin.bind(VdPluginManager)}
|
||||
resolveItem={resolveFromVdPlugin}
|
||||
fetchFunction={async () => { /** TODO */ }}
|
||||
safeModeMessage={Strings.SAFE_MODE_NOTICE_PLUGINS}
|
||||
card={PluginCard}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,141 +1,3 @@
|
|||
import { formatString, Strings } from "@core/i18n";
|
||||
import { VdPluginManager, VendettaPlugin } from "@core/vendetta/plugins";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { purgeStorage, useProxy } from "@lib/api/storage";
|
||||
import { ButtonColors } from "@lib/utils/types";
|
||||
import { clipboard } from "@metro/common";
|
||||
import { ActionSheet, ActionSheetRow, Button, TableRow, Text } from "@metro/common/components";
|
||||
import { showConfirmationAlert } from "@ui/alerts";
|
||||
import { hideSheet } from "@ui/sheets";
|
||||
import { showToast } from "@ui/toasts";
|
||||
import { ScrollView, View } from "react-native";
|
||||
|
||||
interface InfoProps {
|
||||
plugin: VendettaPlugin;
|
||||
navigation: any;
|
||||
}
|
||||
|
||||
export default function PluginInfoActionSheet({ plugin, navigation }: InfoProps) {
|
||||
useProxy(plugin);
|
||||
|
||||
const Settings = VdPluginManager.getSettings(plugin.id);
|
||||
|
||||
return <ActionSheet>
|
||||
<ScrollView>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", paddingVertical: 24 }}>
|
||||
<Text variant="heading-xl/semibold">
|
||||
{plugin.manifest.name}
|
||||
</Text>
|
||||
<View style={{ marginLeft: "auto" }}>
|
||||
{Settings && <Button
|
||||
size="md"
|
||||
text="Configure"
|
||||
variant="secondary"
|
||||
icon={findAssetId("WrenchIcon")}
|
||||
onPress={() => {
|
||||
hideSheet("PluginInfoActionSheet");
|
||||
navigation.push("VendettaCustomPage", {
|
||||
title: plugin.manifest.name,
|
||||
render: Settings,
|
||||
});
|
||||
}}
|
||||
/>}
|
||||
</View>
|
||||
</View>
|
||||
<ActionSheetRow.Group>
|
||||
<ActionSheetRow
|
||||
label={Strings.REFETCH}
|
||||
icon={<TableRow.Icon source={findAssetId("RetryIcon")} />}
|
||||
onPress={async () => {
|
||||
if (plugin.enabled) VdPluginManager.stopPlugin(plugin.id, false);
|
||||
|
||||
try {
|
||||
await VdPluginManager.fetchPlugin(plugin.id);
|
||||
showToast(Strings.PLUGIN_REFETCH_SUCCESSFUL, findAssetId("toast_image_saved"));
|
||||
} catch {
|
||||
showToast(Strings.PLUGIN_REFETCH_FAILED, findAssetId("Small"));
|
||||
}
|
||||
|
||||
if (plugin.enabled) await VdPluginManager.startPlugin(plugin.id);
|
||||
hideSheet("PluginInfoActionSheet");
|
||||
}}
|
||||
/>
|
||||
<ActionSheetRow
|
||||
label={Strings.COPY_URL}
|
||||
icon={<TableRow.Icon source={findAssetId("copy")} />}
|
||||
onPress={() => {
|
||||
clipboard.setString(plugin.id);
|
||||
showToast.showCopyToClipboard();
|
||||
}}
|
||||
/>
|
||||
<ActionSheetRow
|
||||
label={plugin.update ? Strings.DISABLE_UPDATES : Strings.ENABLE_UPDATES}
|
||||
icon={<TableRow.Icon source={findAssetId("ic_download_24px")} />}
|
||||
onPress={() => {
|
||||
plugin.update = !plugin.update;
|
||||
showToast(formatString("TOASTS_PLUGIN_UPDATE", {
|
||||
update: plugin.update,
|
||||
name: plugin.manifest.name
|
||||
}), findAssetId("toast_image_saved"));
|
||||
}}
|
||||
/>
|
||||
<ActionSheetRow
|
||||
label={Strings.CLEAR_DATA}
|
||||
icon={<TableRow.Icon source={findAssetId("ic_duplicate")} />}
|
||||
onPress={() => showConfirmationAlert({
|
||||
title: Strings.HOLD_UP,
|
||||
content: formatString("ARE_YOU_SURE_TO_CLEAR_DATA", { name: plugin.manifest.name }),
|
||||
confirmText: Strings.CLEAR,
|
||||
cancelText: Strings.CANCEL,
|
||||
confirmColor: ButtonColors.RED,
|
||||
onConfirm: async () => {
|
||||
if (plugin.enabled) VdPluginManager.stopPlugin(plugin.id, false);
|
||||
|
||||
try {
|
||||
await VdPluginManager.fetchPlugin(plugin.id);
|
||||
showToast(Strings.PLUGIN_REFETCH_SUCCESSFUL, findAssetId("toast_image_saved"));
|
||||
} catch {
|
||||
showToast(Strings.PLUGIN_REFETCH_FAILED, findAssetId("Small"));
|
||||
}
|
||||
|
||||
let message: any[];
|
||||
try {
|
||||
purgeStorage(plugin.id);
|
||||
message = ["CLEAR_DATA_SUCCESSFUL", "trash"];
|
||||
} catch {
|
||||
message = ["CLEAR_DATA_FAILED", "Small"];
|
||||
}
|
||||
|
||||
showToast(
|
||||
formatString(message[0], { name: plugin.manifest.name }),
|
||||
findAssetId(message[1])
|
||||
);
|
||||
|
||||
if (plugin.enabled) await VdPluginManager.startPlugin(plugin.id);
|
||||
hideSheet("PluginInfoActionSheet");
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<ActionSheetRow
|
||||
label={Strings.DELETE}
|
||||
icon={<TableRow.Icon source={findAssetId("ic_message_delete")} />}
|
||||
onPress={() => showConfirmationAlert({
|
||||
title: Strings.HOLD_UP,
|
||||
content: formatString("ARE_YOU_SURE_TO_DELETE_PLUGIN", { name: plugin.manifest.name }),
|
||||
confirmText: Strings.DELETE,
|
||||
cancelText: Strings.CANCEL,
|
||||
confirmColor: ButtonColors.RED,
|
||||
onConfirm: () => {
|
||||
try {
|
||||
VdPluginManager.removePlugin(plugin.id);
|
||||
} catch (e) {
|
||||
showToast(String(e), findAssetId("Small"));
|
||||
}
|
||||
hideSheet("PluginInfoActionSheet");
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</ActionSheetRow.Group>
|
||||
</ScrollView>
|
||||
</ActionSheet>;
|
||||
export default function PluginInfoActionSheet() {
|
||||
return <></>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,144 @@
|
|||
import { formatString, Strings } from "@core/i18n";
|
||||
import { VdPluginManager } from "@core/vendetta/plugins";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { purgeStorage } from "@lib/api/storage";
|
||||
import { ButtonColors } from "@lib/utils/types";
|
||||
import { clipboard } from "@metro/common";
|
||||
import { ActionSheet, ActionSheetRow, Button, TableRow, Text } from "@metro/common/components";
|
||||
import { showConfirmationAlert } from "@ui/alerts";
|
||||
import { hideSheet } from "@ui/sheets";
|
||||
import { showToast } from "@ui/toasts";
|
||||
import { ScrollView, View } from "react-native";
|
||||
|
||||
import { PluginManifest } from "..";
|
||||
|
||||
interface InfoProps {
|
||||
plugin: PluginManifest;
|
||||
navigation: any;
|
||||
}
|
||||
|
||||
export default function PluginInfoActionSheet({ plugin, navigation }: InfoProps) {
|
||||
plugin.usePluginState();
|
||||
|
||||
const vdPlugin = VdPluginManager.plugins[plugin.id];
|
||||
const SettingsComponent = plugin.getPluginSettingsComponent();
|
||||
|
||||
return <ActionSheet>
|
||||
<ScrollView>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", paddingVertical: 24 }}>
|
||||
<Text variant="heading-xl/semibold">
|
||||
{plugin.name}
|
||||
</Text>
|
||||
<View style={{ marginLeft: "auto" }}>
|
||||
{SettingsComponent && <Button
|
||||
size="md"
|
||||
text="Configure"
|
||||
variant="secondary"
|
||||
icon={findAssetId("WrenchIcon")}
|
||||
onPress={() => {
|
||||
hideSheet("PluginInfoActionSheet");
|
||||
navigation.push("VendettaCustomPage", {
|
||||
title: plugin.name,
|
||||
render: SettingsComponent,
|
||||
});
|
||||
}}
|
||||
/>}
|
||||
</View>
|
||||
</View>
|
||||
<ActionSheetRow.Group>
|
||||
<ActionSheetRow
|
||||
label={Strings.REFETCH}
|
||||
icon={<TableRow.Icon source={findAssetId("RetryIcon")} />}
|
||||
onPress={async () => {
|
||||
if (vdPlugin.enabled) VdPluginManager.stopPlugin(plugin.id, false);
|
||||
|
||||
try {
|
||||
await VdPluginManager.fetchPlugin(plugin.id);
|
||||
showToast(Strings.PLUGIN_REFETCH_SUCCESSFUL, findAssetId("toast_image_saved"));
|
||||
} catch {
|
||||
showToast(Strings.PLUGIN_REFETCH_FAILED, findAssetId("Small"));
|
||||
}
|
||||
|
||||
if (vdPlugin.enabled) await VdPluginManager.startPlugin(plugin.id);
|
||||
hideSheet("PluginInfoActionSheet");
|
||||
}}
|
||||
/>
|
||||
<ActionSheetRow
|
||||
label={Strings.COPY_URL}
|
||||
icon={<TableRow.Icon source={findAssetId("copy")} />}
|
||||
onPress={() => {
|
||||
clipboard.setString(plugin.id);
|
||||
showToast.showCopyToClipboard();
|
||||
}}
|
||||
/>
|
||||
<ActionSheetRow
|
||||
label={vdPlugin.update ? Strings.DISABLE_UPDATES : Strings.ENABLE_UPDATES}
|
||||
icon={<TableRow.Icon source={findAssetId("ic_download_24px")} />}
|
||||
onPress={() => {
|
||||
vdPlugin.update = !vdPlugin.update;
|
||||
showToast(formatString("TOASTS_PLUGIN_UPDATE", {
|
||||
update: vdPlugin.update,
|
||||
name: plugin.name
|
||||
}), findAssetId("toast_image_saved"));
|
||||
}}
|
||||
/>
|
||||
<ActionSheetRow
|
||||
label={Strings.CLEAR_DATA}
|
||||
icon={<TableRow.Icon source={findAssetId("ic_duplicate")} />}
|
||||
onPress={() => showConfirmationAlert({
|
||||
title: Strings.HOLD_UP,
|
||||
content: formatString("ARE_YOU_SURE_TO_CLEAR_DATA", { name: plugin.name }),
|
||||
confirmText: Strings.CLEAR,
|
||||
cancelText: Strings.CANCEL,
|
||||
confirmColor: ButtonColors.RED,
|
||||
onConfirm: async () => {
|
||||
if (vdPlugin.enabled) VdPluginManager.stopPlugin(plugin.id, false);
|
||||
|
||||
try {
|
||||
await VdPluginManager.fetchPlugin(plugin.id);
|
||||
showToast(Strings.PLUGIN_REFETCH_SUCCESSFUL, findAssetId("toast_image_saved"));
|
||||
} catch {
|
||||
showToast(Strings.PLUGIN_REFETCH_FAILED, findAssetId("Small"));
|
||||
}
|
||||
|
||||
let message: any[];
|
||||
try {
|
||||
purgeStorage(plugin.id);
|
||||
message = ["CLEAR_DATA_SUCCESSFUL", "trash"];
|
||||
} catch {
|
||||
message = ["CLEAR_DATA_FAILED", "Small"];
|
||||
}
|
||||
|
||||
showToast(
|
||||
formatString(message[0], { name: plugin.name }),
|
||||
findAssetId(message[1])
|
||||
);
|
||||
|
||||
if (vdPlugin.enabled) await VdPluginManager.startPlugin(plugin.id);
|
||||
hideSheet("PluginInfoActionSheet");
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<ActionSheetRow
|
||||
label={Strings.DELETE}
|
||||
icon={<TableRow.Icon source={findAssetId("ic_message_delete")} />}
|
||||
onPress={() => showConfirmationAlert({
|
||||
title: Strings.HOLD_UP,
|
||||
content: formatString("ARE_YOU_SURE_TO_DELETE_PLUGIN", { name: plugin.name }),
|
||||
confirmText: Strings.DELETE,
|
||||
cancelText: Strings.CANCEL,
|
||||
confirmColor: ButtonColors.RED,
|
||||
onConfirm: () => {
|
||||
try {
|
||||
VdPluginManager.removePlugin(plugin.id);
|
||||
} catch (e) {
|
||||
showToast(String(e), findAssetId("Small"));
|
||||
}
|
||||
hideSheet("PluginInfoActionSheet");
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</ActionSheetRow.Group>
|
||||
</ScrollView>
|
||||
</ActionSheet>;
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import { Author } from "@lib/utils/types";
|
|||
type EvaledPlugin = {
|
||||
onLoad?(): void;
|
||||
onUnload(): void;
|
||||
settings: JSX.Element;
|
||||
settings: React.ComponentType<unknown>;
|
||||
};
|
||||
|
||||
// See https://github.com/vendetta-mod/polymanifest
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// New iteration of storage API, mostly yoinked from unreleased pyoncord (and sunrise?)
|
||||
import { fileExists, readFile, writeFile } from "@lib/api/native/fs";
|
||||
import { Emitter } from "@lib/utils/Emitter";
|
||||
import invariant from "@lib/utils/invariant";
|
||||
|
||||
interface StorageBackend<T = unknown> {
|
||||
get: () => Promise<T>;
|
||||
|
|
@ -21,7 +20,7 @@ function createFileBackend<T>(filePath: string): StorageBackend<T> {
|
|||
try {
|
||||
return JSON.parse(await readFile(filePath));
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to parse storage '${filePath}: ${e}'`);
|
||||
throw new Error(`Failed to parse storage from '${filePath}'`, { cause: e });
|
||||
}
|
||||
},
|
||||
set: async data => {
|
||||
|
|
@ -158,15 +157,16 @@ export async function createStorageAsync<T>(path: string, dflt = {} as T): Promi
|
|||
|
||||
export const createStorage = <T>(path: string, dflt = {} as T): T & { [key: symbol]: any; } => {
|
||||
const promise = new Promise(r => resolvePromise = r);
|
||||
let awaited: any, error: any, resolvePromise: (val?: unknown) => void;
|
||||
let awaited: any, resolved: boolean, error: any, resolvePromise: (val?: unknown) => void;
|
||||
|
||||
createStorageAndCallback(path, dflt, proxy => {
|
||||
awaited = proxy;
|
||||
resolved = true;
|
||||
resolvePromise();
|
||||
});
|
||||
|
||||
const check = () => {
|
||||
if (awaited) return true;
|
||||
if (resolved) return true;
|
||||
throw new Error("Attempted to access storage without initializing");
|
||||
};
|
||||
|
||||
|
|
@ -190,11 +190,8 @@ export async function preloadStorageIfExists(path: string) {
|
|||
if (_loadedPath[path]) return;
|
||||
if (!await fileExists(path)) return;
|
||||
|
||||
try {
|
||||
const data = await createFileBackend<any>(path).get();
|
||||
invariant(data !== undefined);
|
||||
_loadedPath[path] = data;
|
||||
} catch {}
|
||||
const data = await createFileBackend<any>(path).get();
|
||||
_loadedPath[path] = data;
|
||||
}
|
||||
|
||||
export function awaitStorage(...proxies: any[]) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue