feat(ui/plugins): Bunny plugin sheet impl

This commit is contained in:
pylixonly 2024-11-16 23:01:18 +08:00
parent cc879485ac
commit c168237fc9
11 changed files with 234 additions and 64 deletions

View file

@ -1,4 +1,5 @@
import { CardWrapper } from "@core/ui/components/AddonCard";
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";
@ -8,8 +9,6 @@ import chroma from "chroma-js";
import { createContext, useContext, useMemo } from "react";
import { Image, View } from "react-native";
import { UnifiedPluginModel } from "..";
const CardContext = createContext<{ plugin: UnifiedPluginModel, result: Fuzzysort.KeysResult<UnifiedPluginModel>; }>(null!);
const useCardContext = () => useContext(CardContext);
@ -50,6 +49,8 @@ function Title() {
function Authors() {
const { plugin, result } = useCardContext();
const styles = usePluginCardStyles();
if (!plugin.authors) return null;
// could be empty if the author(s) are irrelevant with the search!
@ -59,24 +60,23 @@ function Authors() {
</Text>
);
if (highlightedNode.length > 0) return (
<Text variant="text-md/semibold" color="text-muted">
by {highlightedNode}
</Text>
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>}
</View>
);
const children = ["by "];
for (const author of plugin.authors) {
children.push(typeof author === "string" ? author : author.name);
children.push(", ");
}
children.pop();
return <Text variant="text-md/semibold" color="text-muted">
{children}
</Text>;
}
function Description() {

View file

@ -17,24 +17,10 @@ import { Button, Card, FlashList, IconButton, Text } from "@metro/common/compone
import { ComponentProps } from "react";
import { View } from "react-native";
import { UnifiedPluginModel } from "./models";
import unifyBunnyPlugin from "./models/bunny";
import unifyVdPlugin from "./models/vendetta";
export interface UnifiedPluginModel {
id: string;
name: string;
description?: string;
authors?: Author[];
icon?: string;
isEnabled(): boolean;
usePluginState(): void;
isInstalled(): boolean;
toggle(start: boolean): void;
resolveSheetComponent(): Promise<{ default: React.ComponentType<any>; }>;
getPluginSettingsComponent(): React.ComponentType<any> | null | undefined;
}
const { openAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
const { AlertModal, AlertActions, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));

View file

@ -1,8 +1,9 @@
import { PyoncordIcon } from "@core/ui/settings";
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 "..";
import { UnifiedPluginModel } from ".";
export default function unifyBunnyPlugin(manifest: BunnyPluginManifest): UnifiedPluginModel {
return {
@ -10,6 +11,13 @@ export default function unifyBunnyPlugin(manifest: BunnyPluginManifest): Unified
name: manifest.display.name,
description: manifest.display.description,
authors: manifest.display.authors,
getBadges() {
return [
{ source: { uri: PyoncordIcon } },
// { source: findAssetId("CheckmarkLargeBoldIcon")! }
];
},
isEnabled: () => isPluginEnabled(manifest.id),
isInstalled: () => manifest.id in pluginSettings,
usePluginState() {

View file

@ -0,0 +1,23 @@
import { Author } from "@lib/addons/types";
import { ImageSourcePropType } from "react-native";
interface Badge {
source: ImageSourcePropType;
color?: string;
onPress?: () => void;
}
export interface UnifiedPluginModel {
id: string;
name: string;
description?: string;
authors?: Author[];
icon?: string;
getBadges(): Badge[];
isEnabled(): boolean;
usePluginState(): void;
isInstalled(): boolean;
toggle(start: boolean): void;
resolveSheetComponent(): Promise<{ default: React.ComponentType<any>; }>;
getPluginSettingsComponent(): React.ComponentType<any> | null | undefined;
}

View file

@ -1,7 +1,7 @@
import { VdPluginManager, VendettaPlugin } from "@core/vendetta/plugins";
import { useProxy } from "@core/vendetta/storage";
import { UnifiedPluginModel } from "..";
import { UnifiedPluginModel } from ".";
export default function unifyVdPlugin(vdPlugin: VendettaPlugin): UnifiedPluginModel {
return {
@ -11,6 +11,9 @@ export default function unifyVdPlugin(vdPlugin: VendettaPlugin): UnifiedPluginMo
authors: vdPlugin.manifest.authors,
icon: vdPlugin.manifest.vendetta?.icon,
getBadges() {
return [];
},
isEnabled: () => vdPlugin.enabled,
isInstalled: () => Boolean(vdPlugin && VdPluginManager.plugins[vdPlugin.id]),
usePluginState() {

View file

@ -1,45 +1,106 @@
import { startPlugin } from "@lib/addons/plugins";
import { findAssetId } from "@lib/api/assets";
import { hideSheet } from "@lib/ui/sheets";
import { ActionSheet, Button, Text } from "@metro/common/components";
import { ActionSheet, Card, ContextMenu, IconButton, Text } from "@metro/common/components";
import { ComponentProps } from "react";
import { ScrollView, View } from "react-native";
import { PluginInfoActionSheetProps } from "./common";
import TitleComponent from "./TitleComponent";
function PluginInfoIconButton(props: ComponentProps<typeof IconButton>) {
const { onPress } = props;
props.onPress &&= () => {
hideSheet("PluginInfoActionSheet");
onPress?.();
};
return <IconButton {...props} />;
}
export default function PluginInfoActionSheet({ plugin, navigation }: PluginInfoActionSheetProps) {
plugin.usePluginState();
return <ActionSheet>
<ScrollView contentContainerStyle={{ gap: 8, marginBottom: 12 }}>
<View style={{ flexDirection: "row", alignItems: "center", paddingVertical: 24 }}>
<View style={{ gap: 4 }}>
<Text variant="heading-xl/semibold">
{plugin.name}
</Text>
<Text variant="text-md/medium" color="text-muted">
{plugin.description}
</Text>
</View>
<View style={{ marginLeft: "auto" }}>
{plugin.getPluginSettingsComponent() && <Button
size="md"
text="Configure"
<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("CopyIcon"),
variant: "destructive",
action: () => {
}
},
{
label: "Uninstall",
iconSource: findAssetId("TrashIcon"),
variant: "destructive",
action: () => {
}
}
]}
>
{props => <IconButton
{...props}
icon={findAssetId("MoreHorizontalIcon")}
variant="secondary"
icon={findAssetId("WrenchIcon")}
onPress={() => {
hideSheet("PluginInfoActionSheet");
navigation.push("BUNNY_CUSTOM_PAGE", {
title: plugin.name,
render: plugin.getPluginSettingsComponent(),
});
}}
size="sm"
/>}
</View>
</ContextMenu>
</View>
<View style={{ flexDirection: "row", justifyContent: "center", alignContent: "center" }}>
{/* <Text variant="text-lg/medium">
Oops, you shouldn't see this!
</Text> */}
<View style={{ flexDirection: "row", justifyContent: "space-around", alignContent: "center" }}>
<PluginInfoIconButton
label="Configure"
variant="secondary"
disabled={!plugin.getPluginSettingsComponent()}
icon={findAssetId("WrenchIcon")}
onPress={() => {
navigation.push("BUNNY_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>;
}

View file

@ -0,0 +1,57 @@
import { UnifiedPluginModel } from "@core/ui/settings/pages/Plugins/models";
import { lazyDestructure } from "@lib/utils/lazy";
import { findByNameLazy, findByProps } from "@metro";
import { FluxUtils } from "@metro/common";
import { Avatar, AvatarPile, Text } from "@metro/common/components";
import { UserStore } from "@metro/common/stores";
import { View } from "react-native";
const showUserProfileActionSheet = findByNameLazy("showUserProfileActionSheet");
const { getUser: maybeFetchUser } = lazyDestructure(() => findByProps("getUser", "fetchProfile"));
export default function TitleComponent({ plugin }: { plugin: UnifiedPluginModel; }) {
const users: any[] = FluxUtils.useStateFromStoresArray([UserStore], () => {
plugin.authors?.forEach(a => a.id && maybeFetchUser(a.id));
return plugin.authors?.map(a => UserStore.getUser(a.id));
});
const { authors } = plugin;
const authorTextNode = [];
if (authors) {
for (const author of authors) {
authorTextNode.push(<Text
onPress={() => showUserProfileActionSheet({ userId: author.id })}
variant="text-md/medium"
>
{author.name}
</Text>);
authorTextNode.push(", ");
}
authorTextNode.pop();
}
return <View style={{ gap: 4 }}>
<View>
<Text variant="heading-xl/semibold">
{plugin.name}
</Text>
</View>
<View style={{ flexDirection: "row", flexShrink: 1 }}>
{authors?.length && <View style={{ flexDirection: "row", gap: 8, alignItems: "center", paddingVertical: 4, paddingHorizontal: 8, backgroundColor: "#00000016", borderRadius: 32 }}>
{users.length && <AvatarPile
size="xxsmall"
names={plugin.authors?.map(a => a.name)}
totalCount={plugin.authors?.length}
>
{users.map(a => <Avatar size="xxsmall" user={a} />)}
</AvatarPile>}
<Text variant="text-md/medium">
{authorTextNode}
</Text>
</View>}
</View>
</View>;
}

View file

@ -6,5 +6,17 @@ export const usePluginCardStyles = createStyles({
tintColor: tokens.colors.LOGO_PRIMARY,
height: 18,
width: 18,
},
badgeIcon: {
tintColor: tokens.colors.LOGO_PRIMARY,
height: 12,
width: 12,
},
badgesContainer: {
flexWrap: "wrap",
flexDirection: "row",
gap: 6,
borderRadius: 6,
padding: 4
}
});

View file

@ -267,6 +267,7 @@ export async function uninstallPlugin(id: string) {
pluginInstances.has(id) && stopPlugin(id);
delete pluginSettings[id];
await purgeStorage(`plugins/storage/${id}.json`);
await removeFile(`plugins/scripts/${id}.js`);
}

View file

@ -64,6 +64,7 @@ export const AlertActions = findProp("AlertActions");
export const AvatarPile = findSingular("AvatarPile");
// Misc.
export const ContextMenu = findProp("ContextMenu") as t.ContextMenu;
export const Stack = findProp("Stack") as t.Stack;
export const Avatar = findProp("default", "AvatarSizes", "getStatusSize");

View file

@ -164,6 +164,7 @@ type TextProps = React.ComponentProps<typeof RN.Text> & {
export type Text = React.FC<TextProps>;
interface IconButtonProps {
label?: string;
icon: ImageSourcePropType | ReactNode;
onPress: () => void;
disabled?: boolean;
@ -246,3 +247,20 @@ interface TableCheckboxRowProps extends TableRowBaseProps {
}
export type TableCheckboxRow = FC<TableCheckboxRowProps>;
interface ContextMenuItem {
label: string;
variant?: LiteralUnion<"destructive", string>;
iconSource?: number;
action: () => unknown;
}
interface ContextMenuProps {
triggerOnLongPress?: boolean;
items: ContextMenuItem[] | ContextMenuItem[][];
align?: "left" | "right" | "above" | "below" | "auto" | null;
title?: string;
children: React.FC<Record<"onPress" | "onLongPress" | "accessibilityActions" | "onAccessibilityAction", any>>;
}
export type ContextMenu = FC<ContextMenuProps>;