feat(ui/plugins): Bunny plugin sheet impl
This commit is contained in:
parent
cc879485ac
commit
c168237fc9
11 changed files with 234 additions and 64 deletions
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
23
src/core/ui/settings/pages/Plugins/models/index.ts
Normal file
23
src/core/ui/settings/pages/Plugins/models/index.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
57
src/core/ui/settings/pages/Plugins/sheets/TitleComponent.tsx
Normal file
57
src/core/ui/settings/pages/Plugins/sheets/TitleComponent.tsx
Normal 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>;
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue