From de063fe26056a8d92b72742ed880d1995b959710 Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Wed, 22 Oct 2025 22:20:58 +0700 Subject: [PATCH] feat(plugins/settings): add sheet for additional plugin options --- .../start/settings/components/PluginCard.tsx | 246 ++++-------------- .../PluginClearDataConfirmationAlert.tsx | 37 +++ .../components/PluginHasDependenciesAlert.tsx | 60 +++++ .../components/PluginHasDependentsAlert.tsx | 60 +++++ .../components/PluginOptionsActionSheet.tsx | 152 +++++++++++ src/plugins/start/settings/utils/actions.tsx | 38 +++ src/plugins/start/settings/utils/alerts.tsx | 73 ++++++ src/plugins/start/settings/utils/sheets.tsx | 20 ++ 8 files changed, 495 insertions(+), 191 deletions(-) create mode 100644 src/plugins/start/settings/components/PluginClearDataConfirmationAlert.tsx create mode 100644 src/plugins/start/settings/components/PluginHasDependenciesAlert.tsx create mode 100644 src/plugins/start/settings/components/PluginHasDependentsAlert.tsx create mode 100644 src/plugins/start/settings/components/PluginOptionsActionSheet.tsx create mode 100644 src/plugins/start/settings/utils/actions.tsx create mode 100644 src/plugins/start/settings/utils/alerts.tsx create mode 100644 src/plugins/start/settings/utils/sheets.tsx diff --git a/src/plugins/start/settings/components/PluginCard.tsx b/src/plugins/start/settings/components/PluginCard.tsx index 9c70243..e51bd2d 100644 --- a/src/plugins/start/settings/components/PluginCard.tsx +++ b/src/plugins/start/settings/components/PluginCard.tsx @@ -1,27 +1,15 @@ -import { useNavigation } from '@react-navigation/native' import { getAssetIdByName } from '@revenge-mod/assets' import { styles } from '@revenge-mod/components/_' import FormSwitch from '@revenge-mod/components/FormSwitch' -import { AlertActionCreators } from '@revenge-mod/discord/actions' import { Tokens } from '@revenge-mod/discord/common' import { Design } from '@revenge-mod/discord/design' -import { - enablePlugin, - getPluginDependencies, - isPluginEnabled, - isPluginEssential, - runPluginLate, -} from '@revenge-mod/plugins/_' +import { isPluginEssential } from '@revenge-mod/plugins/_' import { memo } from 'react' -import { - Image, - PixelRatio, - Pressable, - useWindowDimensions, - View, -} from 'react-native' -import { PluginFlashList } from './PluginList' -import PluginStatesProvider, { usePluginEnabled } from './PluginStateProvider' +import { Image, Pressable } from 'react-native' +import { handleDisablePlugin, handleEnablePlugin } from '../utils/actions' +import { openPluginSettings } from '../utils/alerts' +import { showPluginOptionsActionSheet } from '../utils/sheets' +import { usePluginEnabled } from './PluginStateProvider' import { enableTooltipTarget, essentialTooltipTarget, @@ -35,6 +23,7 @@ const { Card, Text, Stack, IconButton, createStyles } = Design const PuzzlePieceIcon = getAssetIdByName('PuzzlePieceIcon', 'png')! const SettingsIcon = getAssetIdByName('SettingsIcon', 'png')! +const MoreVerticalIcon = getAssetIdByName('MoreVerticalIcon', 'png')! export const PLUGIN_CARD_ESTIMATED_SIZE = 116 @@ -66,6 +55,37 @@ export const PluginCard = memo(function PluginCard({ rowEnd && styles_.rowEnd, ]} > + + + ) +}) + +export const PluginInfo = memo(function PluginInfo({ + name, + description, + author, + icon, + actions, + aligned, +}: { + name: string + description: string + author: string + icon?: string + actions?: React.ReactNode + aligned?: boolean +}) { + const styles_ = usePluginCardStyles() + + return ( + {actions} - + - + ) }) @@ -116,16 +139,13 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({ rowEnd?: boolean columnEnd?: boolean }) { - const navigation = useNavigation() const enabled = usePluginEnabled(plugin) const { manifest: { name, description, author, icon }, } = plugin - const dependencies = getPluginDependencies(plugin) const essential = isPluginEssential(meta) - const { dependents } = meta const settingsRef = useClickOutsideTooltip(enableTooltipTarget, () => { setEnablePluginTooltipVisible?.(false) @@ -145,6 +165,14 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({ columnEnd={columnEnd} actions={ <> + { + showPluginOptionsActionSheet(plugin) + }} + /> {plugin.SettingsComponent && ( { @@ -165,7 +193,7 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({ variant="secondary" icon={SettingsIcon} onPress={() => { - navigation.navigate(plugin.manifest.id) + openPluginSettings(plugin) }} disabled={!enabled} /> @@ -186,20 +214,12 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({ ref={switchRef} > { - if (enabled) - return handleEnablePlugin( - plugin, - dependencies, - ) - else - return handleDisablePlugin( - plugin, - dependents, - ) + if (enabled) handleEnablePlugin(plugin) + else handleDisablePlugin(plugin) }} value={enabled} /> @@ -210,162 +230,6 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({ ) }) -async function handleEnablePlugin( - plugin: AnyPlugin, - dependencies: AnyPlugin[], -) { - const disabledDeps = dependencies.filter(dep => !isPluginEnabled(dep)) - - async function action() { - await enablePlugin(plugin) - await runPluginLate(plugin) - } - - if (disabledDeps.length) - AlertActionCreators.openAlert( - 'plugin-has-dependencies', - - - , - ) - else await action() -} - -async function handleDisablePlugin(plugin: AnyPlugin, dependents: AnyPlugin[]) { - const action = plugin.disable - - const enabledDeps = dependents.filter(isPluginEnabled) - - if (enabledDeps.length) - AlertActionCreators.openAlert( - 'plugin-has-dependents', - - - , - ) - else await action() -} - -function PluginHasDependenciesAlert({ - plugin, - dependencies, - action, -}: { - plugin: AnyPlugin - dependencies: AnyPlugin[] - action: () => Promise -}) { - const { height: windowHeight } = useWindowDimensions() - const maxHeight = PixelRatio.get() * windowHeight * 0.35 - 64 - - return ( - - Plugin{' '} - - {plugin.manifest.name} - {' '} - depends on other plugins to function. - {'\n'} - Enabling it will also enable the following plugins: - - } - extraContent={ - - - - } - actions={ - <> - - - - } - /> - ) -} - -function PluginHasDependentsAlert({ - plugin, - dependents, - action, -}: { - plugin: AnyPlugin - dependents: AnyPlugin[] - action: () => Promise -}) { - const { height: windowHeight } = useWindowDimensions() - const maxHeight = PixelRatio.get() * windowHeight * 0.35 - 64 - - return ( - - Other plugins depend on{' '} - - {plugin.manifest.name} - {' '} - to function. - {'\n'} - Disabling it will also disable the following plugins: - - } - extraContent={ - - - - } - actions={ - <> - - - - } - /> - ) -} - const usePluginCardStyles = createStyles({ icon: { width: 20, diff --git a/src/plugins/start/settings/components/PluginClearDataConfirmationAlert.tsx b/src/plugins/start/settings/components/PluginClearDataConfirmationAlert.tsx new file mode 100644 index 0000000..fe3479f --- /dev/null +++ b/src/plugins/start/settings/components/PluginClearDataConfirmationAlert.tsx @@ -0,0 +1,37 @@ +import { Design } from '@revenge-mod/discord/design' +import type { AnyPlugin } from '@revenge-mod/plugins/_' + +const { AlertModal, AlertActionButton, Text } = Design + +export default function PluginClearDataConfirmationAlert({ + plugin, + action, +}: { + plugin: AnyPlugin + action: () => Promise +}) { + return ( + + All data, including files and settings for{' '} + + {plugin.manifest.name} + {' '} + will be deleted + + } + actions={ + <> + + + + } + /> + ) +} diff --git a/src/plugins/start/settings/components/PluginHasDependenciesAlert.tsx b/src/plugins/start/settings/components/PluginHasDependenciesAlert.tsx new file mode 100644 index 0000000..b008f18 --- /dev/null +++ b/src/plugins/start/settings/components/PluginHasDependenciesAlert.tsx @@ -0,0 +1,60 @@ +import { Design } from '@revenge-mod/discord/design' +import { PixelRatio, useWindowDimensions, View } from 'react-native' +import { PLUGIN_CARD_ESTIMATED_SIZE } from './PluginCard' +import { PluginFlashList } from './PluginList' +import type { AnyPlugin } from '@revenge-mod/plugins/_' + +const { AlertModal, AlertActionButton, Text } = Design + +export default function PluginHasDependenciesAlert({ + plugin, + dependencies, + action, +}: { + plugin: AnyPlugin + dependencies: AnyPlugin[] + action: () => Promise +}) { + const { height: windowHeight } = useWindowDimensions() + const maxHeight = PixelRatio.get() * windowHeight * 0.35 - 64 + + return ( + + Plugin{' '} + + {plugin.manifest.name} + {' '} + depends on other plugins to function. + {'\n'} + Enabling it will also enable the following plugins: + + } + extraContent={ + + + + } + actions={ + <> + + + + } + /> + ) +} diff --git a/src/plugins/start/settings/components/PluginHasDependentsAlert.tsx b/src/plugins/start/settings/components/PluginHasDependentsAlert.tsx new file mode 100644 index 0000000..066066b --- /dev/null +++ b/src/plugins/start/settings/components/PluginHasDependentsAlert.tsx @@ -0,0 +1,60 @@ +import { Design } from '@revenge-mod/discord/design' +import { PixelRatio, useWindowDimensions, View } from 'react-native' +import { PLUGIN_CARD_ESTIMATED_SIZE } from './PluginCard' +import { PluginFlashList } from './PluginList' +import type { AnyPlugin } from '@revenge-mod/plugins/_' + +const { AlertModal, AlertActionButton, Text } = Design + +export default function PluginHasDependentsAlert({ + plugin, + dependents, + action, +}: { + plugin: AnyPlugin + dependents: AnyPlugin[] + action: () => Promise +}) { + const { height: windowHeight } = useWindowDimensions() + const maxHeight = PixelRatio.get() * windowHeight * 0.35 - 64 + + return ( + + Other plugins depend on{' '} + + {plugin.manifest.name} + {' '} + to function. + {'\n'} + Disabling it will also disable the following plugins: + + } + extraContent={ + + + + } + actions={ + <> + + + + } + /> + ) +} diff --git a/src/plugins/start/settings/components/PluginOptionsActionSheet.tsx b/src/plugins/start/settings/components/PluginOptionsActionSheet.tsx new file mode 100644 index 0000000..2dedf65 --- /dev/null +++ b/src/plugins/start/settings/components/PluginOptionsActionSheet.tsx @@ -0,0 +1,152 @@ +import { getAssetIdByName } from '@revenge-mod/assets' +import { FormSwitch } from '@revenge-mod/components' +import { + ActionSheetActionCreators, + ToastActionCreators, +} from '@revenge-mod/discord/actions' +import { Design } from '@revenge-mod/discord/design' +import { Clipboard } from '@revenge-mod/externals/react-native-clipboard' +import { + getInternalPluginMeta, + isPluginEssential, +} from '@revenge-mod/plugins/_' +import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord' +import { getErrorStack } from '@revenge-mod/utils/error' +import { handleDisablePlugin, handleEnablePlugin } from '../utils/actions' +import { + openPluginSettings, + showPluginClearDataConfirmation, +} from '../utils/alerts' +import { PluginInfo } from './PluginCard' +import { usePluginEnabled } from './PluginStateProvider' +import type { AnyPlugin } from '@revenge-mod/plugins/_' + +export interface PluginOptionsActionSheetProps { + plugin: AnyPlugin + sheetKey: string +} + +const { ActionSheet, IconButton, TableRowGroup, TableRow, Stack } = Design + +const FileWarningIcon = getAssetIdByName('FileWarningIcon', 'png')! +const SettingsIcon = getAssetIdByName('SettingsIcon', 'png')! + +export default function PluginOptionsActionSheet({ + plugin, + sheetKey, +}: PluginOptionsActionSheetProps) { + const enabled = usePluginEnabled(plugin) + const meta = getInternalPluginMeta(plugin) + const { dependencies, dependents } = meta! + const essential = isPluginEssential(meta) + const { + manifest: { id, name, author, description, icon }, + errors, + flags, + } = plugin + + const closeActionSheet = () => { + ActionSheetActionCreators.hideActionSheet(sheetKey) + } + + return ( + + + { + if (v) handleEnablePlugin(plugin) + else handleDisablePlugin(plugin) + }} + /> + ) + } + /> + + { + showPluginClearDataConfirmation( + plugin, + closeActionSheet, + ) + }} + /> + {plugin.SettingsComponent && ( + { + openPluginSettings(plugin) + closeActionSheet() + }} + /> + )} + + + + {errors.length && ( + { + Clipboard.setString( + errors.map(getErrorStack).join('\n'), + ) + showCopiedToClipboardToast() + }} + /> + )} + + + { + Clipboard.setString(id) + showCopiedToClipboardToast() + }} + /> + {dependencies?.length && ( + + )} + {dependents?.length && ( + + )} + + + + ) +} + +const CopyIcon = lookupGeneratedIconComponent('CopyIcon')! + +function showCopiedToClipboardToast() { + ToastActionCreators.open({ + key: 'REVENGE_PLUGIN_SETTINGS_COPIED', + content: 'Copied to clipboard', + IconComponent: CopyIcon, + }) +} diff --git a/src/plugins/start/settings/utils/actions.tsx b/src/plugins/start/settings/utils/actions.tsx new file mode 100644 index 0000000..1ae26ea --- /dev/null +++ b/src/plugins/start/settings/utils/actions.tsx @@ -0,0 +1,38 @@ +import { + disablePlugin, + enablePlugin, + getInternalPluginMeta, + getPluginDependencies, + isPluginEnabled, + runPluginLate, +} from '@revenge-mod/plugins/_' +import { + showPluginHasDependenciesAlert, + showPluginHasDependentsAlert, +} from './alerts' +import type { AnyPlugin } from '@revenge-mod/plugins/_' + +export async function handleEnablePlugin(plugin: AnyPlugin) { + const dependencies = getPluginDependencies(plugin)! + const disabledDeps = dependencies?.filter(dep => !isPluginEnabled(dep)) + + async function action() { + await enablePlugin(plugin) + await runPluginLate(plugin) + } + + if (disabledDeps?.length) + showPluginHasDependenciesAlert(plugin, disabledDeps, action) + else await action() +} + +export async function handleDisablePlugin(plugin: AnyPlugin) { + const { dependents } = getInternalPluginMeta(plugin)! + const action = () => disablePlugin(plugin) + + const enabledDeps = dependents.filter(isPluginEnabled) + + if (enabledDeps.length) + showPluginHasDependentsAlert(plugin, enabledDeps, action) + else await action() +} diff --git a/src/plugins/start/settings/utils/alerts.tsx b/src/plugins/start/settings/utils/alerts.tsx new file mode 100644 index 0000000..29810d7 --- /dev/null +++ b/src/plugins/start/settings/utils/alerts.tsx @@ -0,0 +1,73 @@ +import { AlertActionCreators } from '@revenge-mod/discord/actions' +import { RootNavigationRef } from '@revenge-mod/discord/modules/main_tabs_v2' +import PluginClearDataConfirmationAlert from '../components/PluginClearDataConfirmationAlert' +import PluginHasDependenciesAlert from '../components/PluginHasDependenciesAlert' +import PluginHasDependentsAlert from '../components/PluginHasDependentsAlert' +import PluginStatesProvider from '../components/PluginStateProvider' +import type { AnyPlugin } from '@revenge-mod/plugins/_' +import type { InitPluginApi } from '@revenge-mod/plugins/types' +import type { AnyObject } from '@revenge-mod/utils/types' + +export function showPluginClearDataConfirmation( + plugin: AnyPlugin, + callback: () => void, +) { + const KEY = 'plugin-clear-data-confirmation' + + async function action() { + const { storage } = plugin.api as InitPluginApi<{ storage: AnyObject }> + if (await storage.exists()) await storage.delete() + callback() + } + + AlertActionCreators.openAlert( + KEY, + , + ) +} + +export function openPluginSettings(plugin: AnyPlugin) { + if (!plugin.SettingsComponent) return + + const navigation = RootNavigationRef.getRootNavigationRef() + if (navigation.isReady()) navigation.navigate(plugin.manifest.id) + else + navigation.addListener('ready', function self() { + navigation.navigate(plugin.manifest.id) + navigation.removeListener('ready', self) + }) +} + +export function showPluginHasDependenciesAlert( + plugin: AnyPlugin, + dependencies: AnyPlugin[], + action: () => Promise, +) { + AlertActionCreators.openAlert( + 'plugin-has-dependencies', + + + , + ) +} + +export function showPluginHasDependentsAlert( + plugin: AnyPlugin, + dependents: AnyPlugin[], + action: () => Promise, +) { + AlertActionCreators.openAlert( + 'plugin-has-dependents', + + + , + ) +} diff --git a/src/plugins/start/settings/utils/sheets.tsx b/src/plugins/start/settings/utils/sheets.tsx new file mode 100644 index 0000000..fcda7f5 --- /dev/null +++ b/src/plugins/start/settings/utils/sheets.tsx @@ -0,0 +1,20 @@ +import { ActionSheetActionCreators } from '@revenge-mod/discord/actions' +import PluginStatesProvider from '../components/PluginStateProvider' +import type { AnyPlugin } from '@revenge-mod/plugins/_' +import type { PluginOptionsActionSheetProps } from '../components/PluginOptionsActionSheet' + +export function showPluginOptionsActionSheet(plugin: AnyPlugin) { + const KEY = 'plugin-options-action-sheet' + + ActionSheetActionCreators.openLazy( + import('../components/PluginOptionsActionSheet').then(m => ({ + default: (props: PluginOptionsActionSheetProps) => ( + + + + ), + })), + KEY, + { plugin, sheetKey: KEY }, + ) +}