port information menu look from ShiggyCord (was exclusive only for core
plugins)
This commit is contained in:
parent
beb52729f5
commit
93d5c354a1
5 changed files with 616 additions and 375 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue