feat: bunny plugins ui init
This commit is contained in:
parent
0341e9eb2f
commit
5fe3025339
13 changed files with 117 additions and 49 deletions
|
|
@ -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")
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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()];
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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] ?? "",
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue