feat: bunny plugins ui init

This commit is contained in:
amsyarasyiq 2024-07-30 20:42:10 +08:00
parent 0341e9eb2f
commit 5fe3025339
13 changed files with 117 additions and 49 deletions

View file

@ -7,7 +7,7 @@ interface CorePlugin {
// Called from @lib/plugins
export const getCorePlugins = (): Record<string, CorePlugin> => ({
"vd-quick-install": require("./vd-quick-install")
"bunny.quickinstall": require("./vd-quick-install")
});
/**

View file

@ -6,11 +6,11 @@ let patches = [] as (() => unknown)[];
export default defineCorePlugin({
manifest: {
id: "bunny.quick.install",
id: "bunny.quickinstall",
name: "QuickInstall",
version: "1.0.0",
description: "Quickly install Vendetta plugins and themes",
authors: [{ name: "pylixonly" }]
authors: ["pyoncord"]
},
start() {
patches = [patchForumPost(), patchUrl()];

View file

@ -17,7 +17,7 @@ type SearchKeywords = Array<string | ((obj: any & {}) => string)>;
interface AddonPageProps<T extends object> {
title: string;
fetchFunction: (url: string) => Promise<void>;
fetchFunction?: (url: string) => Promise<void>;
items: Record<string, any>;
resolveItem?: (value: any) => T | undefined;
safeModeMessage: string;
@ -32,7 +32,6 @@ const { FlashList } = findByProps("FlashList");
export default function AddonPage<T extends object>({ card: CardComponent, ...props }: AddonPageProps<T>) {
useProxy(settings);
useProxy(props.items);
const [search, setSearch] = React.useState("");
@ -74,7 +73,7 @@ export default function AddonPage<T extends object>({ card: CardComponent, ...pr
</View>
)}
/>
<FloatingActionButton
{(props.fetchFunction ?? props.onFABPress) && <FloatingActionButton
icon={findAssetId("PlusLargeIcon")}
onPress={props.onFABPress ?? (() => {
// from ./InstallButton.tsx
@ -82,13 +81,13 @@ export default function AddonPage<T extends object>({ card: CardComponent, ...pr
showInputAlert({
initialValue: content.match(HTTP_REGEX_MULTI)?.[0] ?? "",
placeholder: Strings.URL_PLACEHOLDER,
onConfirm: (input: string) => props.fetchFunction(input),
onConfirm: (input: string) => props.fetchFunction!(input),
confirmText: Strings.INSTALL,
cancelText: Strings.CANCEL,
})
);
})}
/>
/>}
</ErrorBoundary>
);
}

View file

@ -24,7 +24,7 @@ export default function InstallButton({ alertTitle, installFunction: fetchFuncti
return (
<TouchableOpacity onPress={() =>
clipboard.getString().then(content =>
clipboard.getString().then((content: string) =>
showInputAlert({
title: alertTitle,
initialValue: content.match(HTTP_REGEX_MULTI)?.[0] ?? "",

View file

@ -4,7 +4,11 @@ import PluginCard from "@core/ui/settings/pages/Plugins/PluginCard";
import { VdPluginManager, VendettaPlugin } from "@core/vendetta/plugins";
import { settings } from "@lib/api/settings";
import { useProxy } from "@lib/api/storage";
import { registeredPlugins } from "@lib/plugins";
import { BunnyPluginManifest } from "@lib/plugins/types";
import { Author } from "@lib/utils/types";
import { SegmentedControl, SegmentedControlPages, useSegmentedControlState } from "@metro/common/components";
import { useWindowDimensions, View } from "react-native";
export interface PluginManifest {
id: string;
@ -47,22 +51,80 @@ function resolveFromVdPlugin(vdPlugin: VendettaPlugin): PluginManifest {
};
}
function resolveFromBunnyPlugin(manifest: BunnyPluginManifest): PluginManifest {
return {
id: manifest.id,
name: manifest.name,
description: manifest.description,
authors: manifest.authors,
isEnabled() {
return false;
},
usePluginState() {
},
toggle(start: boolean) {
},
resolveSheetComponent() {
return import("./sheets/PluginInfoActionSheet");
},
getPluginSettingsComponent() {
return null;
},
};
}
function Page({ items, resolveItem, fetchFunction }: Record<string, any>) {
return <AddonPage<PluginManifest>
card={PluginCard}
title={Strings.PLUGINS}
searchKeywords={[
"name",
"description",
p => p.authors?.map((a: Author | string) => typeof a === "string" ? a : a.name).join()
]}
items={items}
resolveItem={resolveItem}
fetchFunction={fetchFunction}
safeModeMessage={Strings.SAFE_MODE_NOTICE_PLUGINS}
/>;
}
export default function Plugins() {
useProxy(settings);
return (
<AddonPage<PluginManifest>
title={Strings.PLUGINS}
searchKeywords={[
"name",
"description",
p => p.authors?.map((a: Author | string) => typeof a === "string" ? a : a.name).join()
]}
items={VdPluginManager.plugins}
resolveItem={resolveFromVdPlugin}
fetchFunction={async () => { /** TODO */ }}
safeModeMessage={Strings.SAFE_MODE_NOTICE_PLUGINS}
card={PluginCard}
/>
);
const { width: pageWidth } = useWindowDimensions();
const state = useSegmentedControlState({
pageWidth,
items: [
{
label: "Vendetta",
id: "vendetta-plugins",
page: (
<Page
items={VdPluginManager.plugins}
resolveItem={resolveFromVdPlugin}
fetchFunction={(url: string) => VdPluginManager.installPlugin(url)}
/>
)
},
{
label: "Bunny",
id: "bunny-plugins",
page: (
<Page
items={Object.fromEntries(registeredPlugins.entries())}
resolveItem={resolveFromBunnyPlugin}
/>
)
},
]
});
return <View style={{ margin: 8, gap: 8, alignItems: "center", justifyContent: "center", height: "100%" }}>
<SegmentedControl state={state} />
<SegmentedControlPages state={state} />
</View>;
}

View file

@ -58,6 +58,7 @@ export function createProxy(target: any = {}): { proxy: any; emitter: Emitter; }
export function useProxy<T>(storage: T): T {
const emitter = (storage as any)[emitterSymbol] as Emitter;
if (!emitter) throw new Error("storage[emitterSymbol] is undefined");
const [, forceUpdate] = React.useReducer(n => ~n, 0);

View file

@ -26,7 +26,7 @@ export interface BunnyPluginManifest {
readonly name: string;
readonly description: string;
readonly version: string;
readonly authors: Author[];
readonly authors: (Author | string)[];
}
export interface BunnyPluginManifestInternal extends BunnyPluginManifest {

View file

@ -1,4 +1,5 @@
import { Strings } from "@core/i18n";
import { Nullish } from "@lib/utils/types";
import { React } from "@metro/common";
import { Button, LegacyFormText } from "@metro/common/components";
import { Codeblock } from "@ui/components";
@ -13,7 +14,7 @@ type ErrorBoundaryState = {
};
export interface ErrorBoundaryProps {
children: JSX.Element | JSX.Element[];
children: JSX.Element | Nullish | (JSX.Element | Nullish)[];
}
const styles = createThemedStyleSheet({

View file

@ -1,9 +1,4 @@
// Discord
// export * as discord from "./discord";
// Vendetta
export { default as Codeblock } from "@ui/components/Codeblock";
export { default as ContextMenu } from "@ui/components/ContextMenu";
export { default as ErrorBoundary } from "@ui/components/ErrorBoundary";
export { default as Search } from "@ui/components/Search";
export { default as Summary } from "@ui/components/Summary";

View file

@ -59,6 +59,8 @@ export const TextInput = findSingular("TextInput") as t.TextInput;
// SegmentedControl
export const SegmentedControl = findProp("SegmentedControl") as t.SegmentedControl;
export const SegmentedControlPages = findProp("SegmentedControlPages") as t.SegmentedControlPages;
export const useSegmentedControlState = findSingular("useSegmentedControlState") as (...args: unknown[]) => t.SegmentedControlState;
export const CompatSegmentedControl = findProp("CompatSegmentedControl") as t.CompatSegmentedControl;
export const FloatingActionButton = findProp("FloatingActionButton") as t.FloatingActionButton;

View file

@ -1,8 +1,9 @@
import { Nullish } from "@lib/utils/types";
import { DiscordTextStyles } from "@ui/types";
import { ReactNode } from "react";
import { MutableRefObject, ReactNode, RefObject } from "react";
import type * as RN from "react-native";
import { ImageSourcePropType, PressableProps } from "react-native";
import { SharedValue } from "react-native-reanimated";
import { LiteralUnion } from "type-fest";
// Abandon all hope, ye who enter here
@ -32,30 +33,37 @@ export type Button = React.ForwardRefExoticComponent<ButtonProps>;
interface SegmentedControlItem {
id: string;
label: string;
page: JSX.Element | null;
page?: JSX.Element | null;
}
export interface SegmentedControlState {
activeIndex: SharedValue<number>;
pagerRef: RefObject<unknown>;
scrollTarget: SharedValue<number>;
scrollOverflow: SharedValue<number>;
scrollOffset: SharedValue<number>;
items: SegmentedControlItem[];
itemDimensions: SharedValue<unknown[]>;
pageWidth: number;
pressedIndex: SharedValue<number>;
onPageChangeRef: MutableRefObject<unknown>; // { current: undefined }
setActiveIndex: (index: number) => void;
setItemDimensions: (index: number, dimensions: unknown[]) => void;
useReducedMotion: boolean;
}
interface SegmentedControlProps {
state: any;
// state: {
// activeIndex: SharedValue<number>;
// pagerRef: RefObject<View>;
// scrollTarget: SharedValue<number>;
// scrollOverflow: SharedValue<number>;
// scrollOffset: SharedValue<number>;
// items: SegmentedControlItem[];
// itemDimensions: SharedValue<unknown[]>;
// pageWidth: number;
// pressedIndex: SharedValue<number>;
// onPageChangeRef: MutableRefObject<unknown>; // { current: undefined }
// setActiveIndex: (index: number) => void;
// setItemDimensions: (index: number, dimensions: unknown[]) => void;
// useReducedMotion: boolean;
// };
state: SegmentedControlState;
}
export type SegmentedControl = React.FC<SegmentedControlProps>;
interface SegmentedControlPagesProps {
state: SegmentedControlState;
}
export type SegmentedControlPages = React.FC<SegmentedControlPagesProps>;
interface CompatSegmentedControlProps {
values: string[];
selectedSegmentIndex?: number;