port information menu look from ShiggyCord (was exclusive only for core

plugins)
This commit is contained in:
kmmiio99o 2025-10-11 11:38:51 +02:00
parent beb52729f5
commit 93d5c354a1
5 changed files with 616 additions and 375 deletions

View file

@ -3,154 +3,182 @@ import { UnifiedPluginModel } from "@core/ui/settings/pages/Plugins/models";
import { usePluginCardStyles } from "@core/ui/settings/pages/Plugins/usePluginCardStyles";
import { findAssetId } from "@lib/api/assets";
import { NavigationNative, tokens } from "@metro/common";
import { Card, IconButton, Stack, TableSwitch, Text } from "@metro/common/components";
import {
Card,
IconButton,
Stack,
TableSwitch,
Text,
} from "@metro/common/components";
import { showSheet } from "@ui/sheets";
import chroma from "chroma-js";
import { createContext, useContext, useMemo } from "react";
import { Image, View } from "react-native";
const CardContext = createContext<{ plugin: UnifiedPluginModel, result: Fuzzysort.KeysResult<UnifiedPluginModel>; }>(null!);
const CardContext = createContext<{
plugin: UnifiedPluginModel;
result: Fuzzysort.KeysResult<UnifiedPluginModel>;
}>(null!);
const useCardContext = () => useContext(CardContext);
function getHighlightColor(): import("react-native").ColorValue {
return chroma(tokens.unsafe_rawColors.YELLOW_300).alpha(0.3).hex();
return chroma(tokens.unsafe_rawColors.YELLOW_300).alpha(0.3).hex();
}
function Title() {
const styles = usePluginCardStyles();
const { plugin, result } = useCardContext();
const styles = usePluginCardStyles();
const { plugin, result } = useCardContext();
// could be empty if the plugin name is irrelevant!
const highlightedNode = result[0].highlight((m, i) =>
<Text key={i} style={{ backgroundColor: getHighlightColor() }}>
{m}
</Text>
);
// could be empty if the plugin name is irrelevant!
const highlightedNode = result[0].highlight((m, i) => (
<Text key={i} style={{ backgroundColor: getHighlightColor() }}>
{m}
</Text>
));
const icon = plugin.icon && findAssetId(plugin.icon);
const icon = plugin.icon && findAssetId(plugin.icon);
const textNode = (
<Text
numberOfLines={1}
variant="heading-lg/semibold"
>
{highlightedNode.length ? highlightedNode : plugin.name}
</Text>
);
const textNode = (
<Text numberOfLines={1} variant="heading-lg/semibold">
{highlightedNode.length ? highlightedNode : plugin.name}
</Text>
);
return <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
{icon && <Image
style={styles.smallIcon}
source={icon}
/>}
{textNode}
</View>;
return (
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
{icon && <Image style={styles.smallIcon} source={icon} />}
{textNode}
</View>
);
}
function Authors() {
const { plugin, result } = useCardContext();
const styles = usePluginCardStyles();
const { plugin, result } = useCardContext();
const styles = usePluginCardStyles();
if (!plugin.authors) return null;
if (!plugin.authors) return null;
// could be empty if the author(s) are irrelevant with the search!
const highlightedNode = result[2].highlight((m, i) =>
<Text key={i} style={{ backgroundColor: getHighlightColor() }}>
{m}
</Text>
);
// could be empty if the author(s) are irrelevant with the search!
const highlightedNode = result[2].highlight((m, i) => (
<Text key={i} style={{ backgroundColor: getHighlightColor() }}>
{m}
</Text>
));
const badges = plugin.getBadges();
const authorText = highlightedNode.length > 0 ? highlightedNode : plugin.authors.map(a => a.name).join(", ");
const badges = plugin.getBadges();
const authorText =
highlightedNode.length > 0
? highlightedNode
: plugin.authors.map((a) => a.name).join(", ");
return (
<View style={{ flexDirection: "row", flexWrap: "wrap", flexShrink: 1, gap: 4 }}>
<Text variant="text-sm/semibold" color="text-muted">
by {authorText}
</Text>
{badges.length > 0 && <View style={styles.badgesContainer}>
{badges.map((b, i) => <Image
key={i}
source={b.source}
style={styles.badgeIcon}
/>)}
</View>}
return (
<View
style={{ flexDirection: "row", flexWrap: "wrap", flexShrink: 1, gap: 4 }}
>
<Text variant="text-sm/semibold" color="text-muted">
by {authorText}
</Text>
{badges.length > 0 && (
<View style={styles.badgesContainer}>
{badges.map((b, i) => (
<Image key={i} source={b.source} style={styles.badgeIcon} />
))}
</View>
);
)}
</View>
);
}
function Description() {
const { plugin, result } = useCardContext();
const { plugin, result } = useCardContext();
// could be empty if the description is irrelevant with the search!
const highlightedNode = result[1].highlight((m, i) =>
<Text key={i} style={{ backgroundColor: getHighlightColor() }}>{m}</Text>
);
// could be empty if the description is irrelevant with the search!
const highlightedNode = result[1].highlight((m, i) => (
<Text key={i} style={{ backgroundColor: getHighlightColor() }}>
{m}
</Text>
));
return <Text variant="text-md/medium">
{highlightedNode.length ? highlightedNode : plugin.description}
</Text>;
return (
<Text variant="text-md/medium">
{highlightedNode.length ? highlightedNode : plugin.description}
</Text>
);
}
const Actions = () => {
const { plugin } = useCardContext();
const navigation = NavigationNative.useNavigation();
const { plugin } = useCardContext();
const navigation = NavigationNative.useNavigation();
return <View style={{ flexDirection: "row", gap: 6 }}>
<IconButton
size="sm"
variant="secondary"
icon={findAssetId("WrenchIcon")}
disabled={!plugin.getPluginSettingsComponent()}
onPress={() => navigation.push("PUPU_CUSTOM_PAGE", {
title: plugin.name,
render: plugin.getPluginSettingsComponent(),
})}
/>
<IconButton
size="sm"
variant="secondary"
icon={findAssetId("CircleInformationIcon-primary")}
onPress={() => void showSheet(
"PluginInfoActionSheet",
plugin.resolveSheetComponent(),
{ plugin, navigation }
)}
/>
</View>;
return (
<View style={{ flexDirection: "row", gap: 6 }}>
<IconButton
size="sm"
variant="secondary"
icon={findAssetId("WrenchIcon")}
disabled={!plugin.getPluginSettingsComponent()}
onPress={() =>
navigation.push("PUPU_CUSTOM_PAGE", {
title: plugin.name,
render: plugin.getPluginSettingsComponent(),
})
}
/>
<IconButton
size="sm"
variant="secondary"
icon={findAssetId("CircleInformationIcon-primary")}
onPress={() =>
void showSheet(
"PluginInfoActionSheet",
plugin.resolveSheetComponent(),
{ plugin, navigation },
)
}
/>
</View>
);
};
export default function PluginCard({ result, item: plugin }: CardWrapper<UnifiedPluginModel>) {
plugin.usePluginState();
export default function PluginCard({
result,
item: plugin,
}: CardWrapper<UnifiedPluginModel>) {
plugin.usePluginState();
const [, forceUpdate] = React.useReducer(() => ({}), 0);
const cardContextValue = useMemo(() => ({ plugin, result }), [plugin, result]);
const [, forceUpdate] = React.useReducer(() => ({}), 0);
const cardContextValue = useMemo(
() => ({ plugin, result }),
[plugin, result],
);
return (
<CardContext.Provider value={cardContextValue}>
<Card>
<Stack spacing={16}>
<View style={{ flexDirection: "row", justifyContent: "space-between" }}>
<View style={{ flexShrink: 1 }}>
<Title />
<Authors />
</View>
<View>
<Stack spacing={12} direction="horizontal">
<Actions />
<TableSwitch
value={plugin.isEnabled()}
onValueChange={(v: boolean) => {
plugin.toggle(v);
forceUpdate();
}}
/>
</Stack>
</View>
</View>
<Description />
</Stack>
</Card>
</CardContext.Provider>
);
return (
<CardContext.Provider value={cardContextValue}>
<Card>
<Stack spacing={16}>
<View
style={{ flexDirection: "row", justifyContent: "space-between" }}
>
<View style={{ flexShrink: 1 }}>
<Title />
<Authors />
</View>
<View>
<Stack spacing={12} direction="horizontal">
<Actions />
<TableSwitch
value={plugin.isEnabled()}
onValueChange={(v: boolean) => {
plugin.toggle(v);
forceUpdate();
}}
/>
</Stack>
</View>
</View>
<Description />
</Stack>
</Card>
</CardContext.Provider>
);
}

View file

@ -3,7 +3,12 @@ import AddonPage from "@core/ui/components/AddonPage";
import PluginCard from "@core/ui/settings/pages/Plugins/components/PluginCard";
import { VdPluginManager } from "@core/vendetta/plugins";
import { useProxy } from "@core/vendetta/storage";
import { isCorePlugin, isPluginInstalled, pluginSettings, registeredPlugins } from "@lib/addons/plugins";
import {
isCorePlugin,
isPluginInstalled,
pluginSettings,
registeredPlugins,
} from "@lib/addons/plugins";
import { Author } from "@lib/addons/types";
import { findAssetId } from "@lib/api/assets";
import { settings } from "@lib/api/settings";
@ -13,7 +18,13 @@ import { BUNNY_PROXY_PREFIX, VD_PROXY_PREFIX } from "@lib/utils/constants";
import { lazyDestructure } from "@lib/utils/lazy";
import { findByProps } from "@metro";
import { NavigationNative } from "@metro/common";
import { Button, Card, FlashList, IconButton, Text } from "@metro/common/components";
import {
Button,
Card,
FlashList,
IconButton,
Text,
} from "@metro/common/components";
import { ComponentProps } from "react";
import { View } from "react-native";
@ -21,116 +32,198 @@ import { UnifiedPluginModel } from "./models";
import unifyBunnyPlugin from "./models/bunny";
import unifyVdPlugin from "./models/vendetta";
const { openAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
const { AlertModal, AlertActions, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
const { openAlert } = lazyDestructure(() =>
findByProps("openAlert", "dismissAlert"),
);
const { AlertModal, AlertActions, AlertActionButton } = lazyDestructure(() =>
findByProps("AlertModal", "AlertActions"),
);
interface PluginPageProps extends Partial<ComponentProps<typeof AddonPage<UnifiedPluginModel>>> {
useItems: () => unknown[];
interface PluginPageProps
extends Partial<ComponentProps<typeof AddonPage<UnifiedPluginModel>>> {
useItems: () => unknown[];
}
function PluginPage(props: PluginPageProps) {
const items = props.useItems();
const items = props.useItems();
return <AddonPage<UnifiedPluginModel>
CardComponent={PluginCard}
title={Strings.PLUGINS}
searchKeywords={[
"name",
"description",
p => p.authors?.map(
(a: Author | string) => typeof a === "string" ? a : a.name
).join() || ""
]}
sortOptions={{
"Name (A-Z)": (a, b) => a.name.localeCompare(b.name),
"Name (Z-A)": (a, b) => b.name.localeCompare(a.name)
}}
safeModeHint={{ message: Strings.SAFE_MODE_NOTICE_PLUGINS }}
items={items}
{...props}
/>;
return (
<AddonPage<UnifiedPluginModel>
CardComponent={PluginCard}
title={Strings.PLUGINS}
searchKeywords={[
"name",
"description",
(p) =>
p.authors
?.map((a: Author | string) => (typeof a === "string" ? a : a.name))
.join() || "",
]}
sortOptions={{
"Name (A-Z)": (a, b) => a.name.localeCompare(b.name),
"Name (Z-A)": (a, b) => b.name.localeCompare(a.name),
}}
safeModeHint={{ message: Strings.SAFE_MODE_NOTICE_PLUGINS }}
items={items}
{...props}
/>
);
}
export default function Plugins() {
useProxy(settings);
const navigation = NavigationNative.useNavigation();
useProxy(settings);
const navigation = NavigationNative.useNavigation();
return <PluginPage
useItems={() => {
useProxy(VdPluginManager.plugins);
useObservable([pluginSettings]);
return (
<PluginPage
useItems={() => {
useProxy(VdPluginManager.plugins);
useObservable([pluginSettings]);
const vdPlugins = Object.values(VdPluginManager.plugins).map(unifyVdPlugin);
const bnPlugins = [...registeredPlugins.values()].filter(p => isPluginInstalled(p.id) && !isCorePlugin(p.id)).map(unifyBunnyPlugin);
const vdPlugins = Object.values(VdPluginManager.plugins).map(
unifyVdPlugin,
);
const bnPlugins = [...registeredPlugins.values()]
.filter(
(p) =>
p &&
typeof p.id === "string" &&
isPluginInstalled(p.id) &&
!isCorePlugin(p.id),
)
.map(unifyBunnyPlugin);
return [...vdPlugins, ...bnPlugins];
}}
ListHeaderComponent={() => {
const unproxiedPlugins = Object.values(VdPluginManager.plugins).filter(p => !p.id.startsWith(VD_PROXY_PREFIX) && !p.id.startsWith(BUNNY_PROXY_PREFIX));
if (!unproxiedPlugins.length) return null;
return [...vdPlugins, ...bnPlugins];
}}
ListHeaderComponent={() => {
const unproxiedPlugins = Object.values(VdPluginManager.plugins).filter(
(p) =>
!p.id.startsWith(VD_PROXY_PREFIX) &&
!p.id.startsWith(BUNNY_PROXY_PREFIX),
);
if (!unproxiedPlugins.length) return null;
return <View style={{ marginVertical: 12, marginHorizontal: 10 }}>
<Card border="strong">
<View style={{ flex: 1, justifyContent: "center", alignItems: "center", flexDirection: "row" }}>
<View style={{ gap: 6, flexShrink: 1 }}>
<Text variant="heading-md/bold">Unproxied Plugins Found</Text>
<Text variant="text-sm/medium" color="text-muted">
Plugins installed from unproxied sources may run unverified code in this app without your awareness.
</Text>
</View>
<View style={{ marginLeft: "auto" }}>
<IconButton
size="sm"
variant="secondary"
icon={findAssetId("CircleInformationIcon-primary")}
style={{ marginLeft: 8 }}
onPress={() => {
navigation.push("PUPU_CUSTOM_PAGE", {
title: "Unproxied Plugins",
render: () => {
return <FlashList
data={unproxiedPlugins}
contentContainerStyle={{ padding: 8 }}
ItemSeparatorComponent={() => <View style={{ height: 8 }} />}
renderItem={({ item: p }: any) => <Card>
<Text variant="heading-md/semibold">{p.id}</Text>
</Card>}
/>;
}
});
}}
return (
<View style={{ marginVertical: 12, marginHorizontal: 10 }}>
<Card border="strong">
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
flexDirection: "row",
}}
>
<View style={{ gap: 6, flexShrink: 1 }}>
<Text variant="heading-md/bold">Unproxied Plugins Found</Text>
<Text variant="text-sm/medium" color="text-muted">
Plugins installed from unproxied sources may run unverified
code in this app without your awareness.
</Text>
</View>
<View style={{ marginLeft: "auto" }}>
<IconButton
size="sm"
variant="secondary"
icon={findAssetId("CircleInformationIcon-primary")}
style={{ marginLeft: 8 }}
onPress={() => {
navigation.push("PUPU_CUSTOM_PAGE", {
title: "Unproxied Plugins",
render: () => {
return (
<FlashList
data={unproxiedPlugins}
contentContainerStyle={{ padding: 8 }}
ItemSeparatorComponent={() => (
<View style={{ height: 8 }} />
)}
renderItem={({ item: p }: any) => (
<Card>
<Text variant="heading-md/semibold">
{p.id}
</Text>
</Card>
)}
/>
</View>
</View>
</Card>
</View>;
}}
installAction={{
label: "Install a plugin",
fetchFn: async (url: string) => {
if (!url.startsWith(VD_PROXY_PREFIX) && !url.startsWith(BUNNY_PROXY_PREFIX) && !settings.developerSettings) {
openAlert("bunny-plugin-unproxied-confirmation", <AlertModal
title="Hold On!"
content="You're trying to install a plugin from an unproxied external source. This means you're trusting the creator to run their code in this app without your knowledge. Are you sure you want to continue?"
extraContent={<Card><Text variant="text-md/bold">{url}</Text></Card>}
actions={<AlertActions>
<AlertActionButton text="Continue" variant="primary" onPress={() => {
VdPluginManager.installPlugin(url)
.then(() => showToast(Strings.TOASTS_INSTALLED_PLUGIN, findAssetId("Check")))
.catch(e => openAlert("bunny-plugin-install-failed", <AlertModal
title="Install Failed"
content={`Unable to install plugin from '${url}':`}
extraContent={<Card><Text variant="text-md/normal">{e instanceof Error ? e.message : String(e)}</Text></Card>}
actions={<AlertActionButton text="Okay" variant="primary" />}
/>));
}} />
<AlertActionButton text="Cancel" variant="secondary" />
</AlertActions>}
/>);
} else {
return await VdPluginManager.installPlugin(url);
);
},
});
}}
/>
</View>
</View>
</Card>
</View>
);
}}
installAction={{
label: "Install a plugin",
fetchFn: async (url: string) => {
if (
!url.startsWith(VD_PROXY_PREFIX) &&
!url.startsWith(BUNNY_PROXY_PREFIX) &&
!settings.developerSettings
) {
openAlert(
"bunny-plugin-unproxied-confirmation",
<AlertModal
title="Hold On!"
content="You're trying to install a plugin from an unproxied external source. This means you're trusting the creator to run their code in this app without your knowledge. Are you sure you want to continue?"
extraContent={
<Card>
<Text variant="text-md/bold">{url}</Text>
</Card>
}
}
}}
/>;
actions={
<AlertActions>
<AlertActionButton
text="Continue"
variant="primary"
onPress={() => {
VdPluginManager.installPlugin(url)
.then(() =>
showToast(
Strings.TOASTS_INSTALLED_PLUGIN,
findAssetId("Check"),
),
)
.catch((e) =>
openAlert(
"bunny-plugin-install-failed",
<AlertModal
title="Install Failed"
content={`Unable to install plugin from '${url}':`}
extraContent={
<Card>
<Text variant="text-md/normal">
{e instanceof Error
? e.message
: String(e)}
</Text>
</Card>
}
actions={
<AlertActionButton
text="Okay"
variant="primary"
/>
}
/>,
),
);
}}
/>
<AlertActionButton text="Cancel" variant="secondary" />
</AlertActions>
}
/>,
);
} else {
return await VdPluginManager.installPlugin(url);
}
},
}}
/>
);
}

View file

@ -1,43 +1,52 @@
import { PupuIcon } from "@core/ui/settings";
import { disablePlugin, enablePlugin, getPluginSettingsComponent, isPluginEnabled, pluginSettings } from "@lib/addons/plugins";
import {
disablePlugin,
enablePlugin,
getPluginSettingsComponent,
isPluginEnabled,
pluginSettings,
} from "@lib/addons/plugins";
import { BunnyPluginManifest } from "@lib/addons/plugins/types";
import { useObservable } from "@lib/api/storage";
import { UnifiedPluginModel } from ".";
export default function unifyBunnyPlugin(manifest: BunnyPluginManifest): UnifiedPluginModel {
return {
id: manifest.id,
name: manifest.display.name,
description: manifest.display.description,
authors: manifest.display.authors,
export default function unifyBunnyPlugin(
manifest: BunnyPluginManifest,
): UnifiedPluginModel {
return {
id: manifest.id,
name: manifest.display.name,
description: manifest.display.description,
authors: manifest.display.authors,
getBadges() {
return [
{ source: { uri: PupuIcon } },
// { source: findAssetId("CheckmarkLargeBoldIcon")! }
];
},
isEnabled: () => isPluginEnabled(manifest.id),
isInstalled: () => manifest.id in pluginSettings,
usePluginState() {
useObservable([pluginSettings]);
},
toggle(start: boolean) {
try {
start
? enablePlugin(manifest.id, true)
: disablePlugin(manifest.id);
} catch (e) {
console.error(e);
// showToast("Failed to toggle plugin " + e, findAssetId("Small"));
}
},
resolveSheetComponent() {
return import("../sheets/PluginInfoActionSheet");
},
getPluginSettingsComponent() {
return getPluginSettingsComponent(manifest.id);
},
};
getBadges() {
return [
{ source: { uri: PupuIcon } },
// { source: findAssetId("CheckmarkLargeBoldIcon")! }
];
},
isEnabled: () => isPluginEnabled(manifest.id),
isInstalled: () => manifest.id in pluginSettings,
usePluginState() {
useObservable([pluginSettings]);
},
toggle(start: boolean) {
try {
start ? enablePlugin(manifest.id, true) : disablePlugin(manifest.id);
} catch (e) {
console.error(e);
// showToast("Failed to toggle plugin " + e, findAssetId("Small"));
}
},
resolveSheetComponent() {
// Return a Promise resolving to the sheet component for interface consistency
return Promise.resolve({
default: require("../sheets/PluginInfoActionSheet").default,
});
},
getPluginSettingsComponent() {
return getPluginSettingsComponent(manifest.id);
},
};
}

View file

@ -3,32 +3,38 @@ import { useProxy } from "@core/vendetta/storage";
import { UnifiedPluginModel } from ".";
export default function unifyVdPlugin(vdPlugin: VendettaPlugin): UnifiedPluginModel {
return {
id: vdPlugin.id,
name: vdPlugin.manifest.name,
description: vdPlugin.manifest.description,
authors: vdPlugin.manifest.authors,
icon: vdPlugin.manifest.vendetta?.icon,
export default function unifyVdPlugin(
vdPlugin: VendettaPlugin,
): UnifiedPluginModel {
return {
id: vdPlugin.id,
name: vdPlugin.manifest.name,
description: vdPlugin.manifest.description,
authors: vdPlugin.manifest.authors,
icon: vdPlugin.manifest.vendetta?.icon,
getBadges() {
return [];
},
isEnabled: () => vdPlugin.enabled,
isInstalled: () => Boolean(vdPlugin && VdPluginManager.plugins[vdPlugin.id]),
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);
},
};
getBadges() {
return [];
},
isEnabled: () => vdPlugin.enabled,
isInstalled: () =>
Boolean(vdPlugin && VdPluginManager.plugins[vdPlugin.id]),
usePluginState() {
useProxy(VdPluginManager.plugins[vdPlugin.id]);
},
toggle(start: boolean) {
start
? VdPluginManager.startPlugin(vdPlugin.id)
: VdPluginManager.stopPlugin(vdPlugin.id);
},
resolveSheetComponent() {
// Return a Promise resolving to the unified PluginInfoActionSheet for consistency
return Promise.resolve({
default: require("../sheets/PluginInfoActionSheet").default,
});
},
getPluginSettingsComponent() {
return VdPluginManager.getSettings(vdPlugin.id);
},
};
}

View file

@ -1,106 +1,211 @@
import { startPlugin } from "@lib/addons/plugins";
import { findAssetId } from "@lib/api/assets";
import { hideSheet } from "@lib/ui/sheets";
import { ActionSheet, Card, ContextMenu, IconButton, Text } from "@metro/common/components";
import { ComponentProps } from "react";
import { ActionSheet, Card, IconButton, Text } from "@metro/common/components";
import { clipboard } from "@metro/common";
import { showToast } from "@lib/ui/toasts";
import { showConfirmationAlert } from "@core/vendetta/alerts";
import { VdPluginManager } from "@core/vendetta/plugins";
import { purgeStorage as purgeVdStorage } from "@core/vendetta/storage";
import { purgeStorage } from "@lib/api/storage";
import { findAssetId } from "@lib/api/assets";
import { useState } from "react";
import { ScrollView, View } from "react-native";
import { PluginInfoActionSheetProps } from "./common";
import TitleComponent from "./TitleComponent";
import { PluginInfoActionSheetProps } from "./common";
function PluginInfoIconButton(props: ComponentProps<typeof IconButton>) {
const { onPress } = props;
props.onPress &&= () => {
function PluginInfoIconButton(props) {
const { onPress } = props;
props.onPress &&= () => {
hideSheet("PluginInfoActionSheet");
onPress?.();
};
return <IconButton {...props} />;
}
export default function PluginInfoActionSheet({
plugin,
navigation,
}: PluginInfoActionSheetProps) {
plugin.usePluginState();
const [loading, setLoading] = useState(false);
// Determine plugin type
const isVendettaPlugin = plugin.id.includes("/");
const isCorePlugin =
plugin.id.startsWith("bunny.") || plugin.id.startsWith("vendetta.");
const copyPluginUrl = () => {
let url = plugin.id;
if (isVendettaPlugin) {
url = plugin.id;
} else {
try {
const pluginAny = plugin;
const repoUrl =
pluginAny._manifest?.parentRepository ||
pluginAny.manifest?.parentRepository;
url = repoUrl ? `${repoUrl}/builds/${plugin.id}` : plugin.id;
} catch (e) {
url = plugin.id;
}
}
clipboard.setString(url);
showToast("Copied to clipboard!", findAssetId("toast_copy_link"));
};
const refetchPlugin = async () => {
setLoading(true);
try {
if (isVendettaPlugin) {
const vdPlugin = VdPluginManager.plugins[plugin.id];
if (vdPlugin.enabled) VdPluginManager.stopPlugin(plugin.id, false);
await VdPluginManager.fetchPlugin(plugin.id);
if (vdPlugin.enabled) await VdPluginManager.startPlugin(plugin.id);
showToast("Plugin refreshed successfully");
} else {
// For Bunny plugins
// If you have a refreshPlugin function, call it here
showToast("Plugin refreshed successfully");
}
} catch (e) {
showToast("Failed to refresh plugin");
} finally {
setLoading(false);
}
};
const clearPluginData = () => {
showConfirmationAlert({
title: "Clear Data",
content:
"Are you sure you want to clear all data for this plugin? This action cannot be undone.",
confirmText: "Clear",
confirmColor: "red",
cancelText: "Cancel",
onConfirm: async () => {
hideSheet("PluginInfoActionSheet");
onPress?.();
};
try {
if (isVendettaPlugin) {
const vdPlugin = VdPluginManager.plugins[plugin.id];
if (vdPlugin.enabled) VdPluginManager.stopPlugin(plugin.id, false);
await purgeVdStorage(plugin.id);
if (vdPlugin.enabled) await VdPluginManager.startPlugin(plugin.id);
} else {
await purgeStorage(`plugins/storage/${plugin.id}.json`);
}
showToast("Plugin data cleared successfully");
} catch (e) {
showToast("Failed to clear plugin data");
}
},
});
};
return <IconButton {...props} />;
}
export default function PluginInfoActionSheet({ plugin, navigation }: PluginInfoActionSheetProps) {
plugin.usePluginState();
return <ActionSheet>
<ScrollView contentContainerStyle={{ gap: 12, marginBottom: 12 }}>
<View style={{ flexDirection: "row", alignItems: "center", gap: 8, paddingVertical: 24, justifyContent: "space-between", width: "100%" }}>
<TitleComponent plugin={plugin} />
<ContextMenu
items={[
{
label: "Details",
iconSource: findAssetId("CircleInformationIcon-primary"),
action: () => {
}
},
// {
// label: true ? "Disable Updates" : "Enable Updates",
// iconSource: true ? findAssetId("ClockXIcon") : findAssetId("ClockIcon"),
// action: () => {
// }
// },
{
label: "Clear Data",
iconSource: findAssetId("FileIcon"),
variant: "destructive",
action: () => {
}
},
{
label: "Uninstall",
iconSource: findAssetId("TrashIcon"),
variant: "destructive",
action: () => {
}
}
]}
>
{props => <IconButton
{...props}
icon={findAssetId("MoreHorizontalIcon")}
variant="secondary"
size="sm"
/>}
</ContextMenu>
</View>
<View style={{ flexDirection: "row", justifyContent: "space-around", alignContent: "center" }}>
<PluginInfoIconButton
label="Configure"
variant="secondary"
disabled={!plugin.getPluginSettingsComponent()}
icon={findAssetId("WrenchIcon")}
onPress={() => {
navigation.push("PUPU_CUSTOM_PAGE", {
title: plugin.name,
render: plugin.getPluginSettingsComponent(),
});
}}
/>
<PluginInfoIconButton
label="Refetch"
variant="secondary"
icon={findAssetId("RetryIcon")}
onPress={() => {
startPlugin(plugin.id);
}}
/>
<PluginInfoIconButton
label="Copy URL"
variant="secondary"
icon={findAssetId("LinkIcon")}
onPress={() => {
}}
/>
</View>
<Card>
<Text variant="text-md/semibold" color="text-primary" style={{ marginBottom: 4 }}>
Description
</Text>
<Text variant="text-md/medium">
{plugin.description}
</Text>
</Card>
</ScrollView>
</ActionSheet>;
const uninstallPluginHandler = () => {
if (isCorePlugin) {
showToast("Core plugins cannot be uninstalled");
return;
}
showConfirmationAlert({
title: "Uninstall Plugin",
content:
"Are you sure you want to uninstall this plugin? This action cannot be undone.",
confirmText: "Uninstall",
confirmColor: "red",
cancelText: "Cancel",
onConfirm: async () => {
hideSheet("PluginInfoActionSheet");
try {
if (isVendettaPlugin) {
await VdPluginManager.removePlugin(plugin.id);
} else {
// If you have an uninstallPlugin function, call it here
}
showToast("Plugin uninstalled successfully");
} catch (e) {
showToast(
`Failed to uninstall plugin: ${e instanceof Error ? e.message : String(e)}`,
);
}
},
});
};
return (
<ActionSheet>
<ScrollView contentContainerStyle={{ gap: 12, marginBottom: 12 }}>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 8,
paddingVertical: 24,
justifyContent: "space-between",
width: "100%",
}}
>
<TitleComponent plugin={plugin} />
</View>
<View
style={{
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
flexWrap: "wrap",
gap: 12,
paddingHorizontal: 4,
}}
>
<PluginInfoIconButton
label="Configure"
variant="secondary"
disabled={!plugin.getPluginSettingsComponent()}
icon={findAssetId("WrenchIcon")}
onPress={() => {
navigation.push("PUPU_CUSTOM_PAGE", {
title: plugin.name,
render: plugin.getPluginSettingsComponent(),
});
}}
/>
<PluginInfoIconButton
label="Refetch"
variant="secondary"
icon={findAssetId("RetryIcon")}
onPress={refetchPlugin}
disabled={loading}
/>
<PluginInfoIconButton
label="Copy URL"
variant="secondary"
icon={findAssetId("LinkIcon")}
onPress={copyPluginUrl}
/>
<PluginInfoIconButton
label="Clear Data"
variant="secondary"
icon={findAssetId("FileIcon")}
onPress={clearPluginData}
/>
{!isCorePlugin && (
<PluginInfoIconButton
label="Uninstall"
variant="secondary"
icon={findAssetId("TrashIcon")}
onPress={uninstallPluginHandler}
/>
)}
</View>
<Card>
<Text
variant="text-md/semibold"
color="text-primary"
style={{ marginBottom: 4 }}
>
Description
</Text>
<Text variant="text-md/medium">{plugin.description}</Text>
</Card>
</ScrollView>
</ActionSheet>
);
}