From 2293e288d1041f5ca910d3c455eddb93162086d6 Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Thu, 23 Oct 2025 17:50:32 +0700 Subject: [PATCH] feat(plugins/settings): improve additional plugin options sheet - Disable Settings icon button with tooltip hint if plugin is not enabled - Added Status - Moved Flags to Advanced - Added Internal Flags Other changes (to make the above work): - TooltipProvider now properly uses React Contexts - Added usePluginStatus (PluginStateProvider now subscribes to plugin statuses) --- .../start/settings/components/PluginCard.tsx | 49 +++--- .../start/settings/components/PluginList.tsx | 4 +- .../components/PluginOptionsActionSheet.tsx | 144 +++++++++++++----- .../components/PluginStateProvider.tsx | 37 ++++- .../settings/components/TooltipProvider.tsx | 102 ++++++++----- 5 files changed, 228 insertions(+), 108 deletions(-) diff --git a/src/plugins/start/settings/components/PluginCard.tsx b/src/plugins/start/settings/components/PluginCard.tsx index e51bd2d..1e62ec0 100644 --- a/src/plugins/start/settings/components/PluginCard.tsx +++ b/src/plugins/start/settings/components/PluginCard.tsx @@ -11,11 +11,9 @@ import { openPluginSettings } from '../utils/alerts' import { showPluginOptionsActionSheet } from '../utils/sheets' import { usePluginEnabled } from './PluginStateProvider' import { - enableTooltipTarget, - essentialTooltipTarget, - setEnablePluginTooltipVisible, - setEssentialPluginTooltipVisible, useClickOutsideTooltip, + useEnablePluginTooltip, + useEssentialPluginTooltip, } from './TooltipProvider' import type { AnyPlugin, InternalPluginMeta } from '@revenge-mod/plugins/_' @@ -147,13 +145,15 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({ const essential = isPluginEssential(meta) - const settingsRef = useClickOutsideTooltip(enableTooltipTarget, () => { - setEnablePluginTooltipVisible?.(false) - }) + const enableTooltip = useEnablePluginTooltip() + const essentialTooltip = useEssentialPluginTooltip() - const switchRef = useClickOutsideTooltip(essentialTooltipTarget, () => { - setEssentialPluginTooltipVisible?.(false) - }) + const settingsRef = useClickOutsideTooltip(useEnablePluginTooltip, () => {}) + + const switchRef = useClickOutsideTooltip( + useEssentialPluginTooltip, + () => {}, + ) return ( {plugin.SettingsComponent && ( { - if (enabled) return - - e.stopPropagation() - - requestAnimationFrame(() => { - enableTooltipTarget.current = - settingsRef.current - setEnablePluginTooltipVisible!(true) - }) + onPress={() => { + if (!enabled) + requestAnimationFrame(() => { + enableTooltip.targetRef.current = + settingsRef.current + enableTooltip.setVisible(true) + }) }} > )} { - if (essential) { - e.stopPropagation() - + onPress={() => { + if (essential) requestAnimationFrame(() => { - essentialTooltipTarget.current = + essentialTooltip.targetRef.current = switchRef.current - setEssentialPluginTooltipVisible!(true) + essentialTooltip.setVisible(true) }) - } }} ref={switchRef} > { diff --git a/src/plugins/start/settings/components/PluginList.tsx b/src/plugins/start/settings/components/PluginList.tsx index cb36c54..81bc45e 100644 --- a/src/plugins/start/settings/components/PluginList.tsx +++ b/src/plugins/start/settings/components/PluginList.tsx @@ -7,7 +7,7 @@ import { PLUGIN_CARD_ESTIMATED_SIZE, PluginCard, } from './PluginCard' -import { resetTooltips } from './TooltipProvider' +import { useResetTooltips } from './TooltipProvider' import type { AnyPlugin, InternalPluginMeta } from '@revenge-mod/plugins/_' const { Text } = Design @@ -15,6 +15,7 @@ const { Text } = Design export function PluginFlashList({ plugins }: { plugins: AnyPlugin[] }) { const { width, height } = useWindowDimensions() const actualWidth = width - styles.pagePadding.paddingHorizontal * 2 + const resetTooltips = useResetTooltips() return ( { - ActionSheetActionCreators.hideActionSheet(sheetKey) - } - return ( @@ -69,41 +75,25 @@ export default function PluginOptionsActionSheet({ ) } /> - - { - showPluginClearDataConfirmation( - plugin, - closeActionSheet, - ) - }} - /> - {plugin.SettingsComponent && ( - { - openPluginSettings(plugin) - closeActionSheet() + + + { + ActionSheetActionCreators.hideActionSheet( + sheetKey, + ) }} /> - )} - + + } - label="Flags (TODO)" - subLabel={`${flags}`} + icon={ + + } + label="Status (TODO)" + subLabel={bitFieldToString(PluginStatus, status)} /> {errors.length && ( } subLabel={`${errors.length} errors. Tap to copy.`} @@ -135,6 +125,21 @@ export default function PluginOptionsActionSheet({ showCopiedToClipboardToast() }} /> + } + label="Flags" + subLabel={bitFieldToString(PluginFlags, flags)} + /> + {meta.iflags && ( + } + label="Internal Flags" + subLabel={bitFieldToString( + InternalPluginFlags, + meta.iflags, + )} + /> + )} {dependencies?.length && ( } @@ -155,6 +160,71 @@ export default function PluginOptionsActionSheet({ ) } +function bitFieldToString(map: Record, bitField: number) { + return ( + Object.entries(map) + .filter(([, value]) => bitField & value) + .map(([key]) => key) + .join(', ') || '-' + ) +} + +function PluginActions({ + plugin, + closeSheet, +}: { + plugin: AnyPlugin + closeSheet: () => void +}) { + const enabled = usePluginEnabled(plugin) + const enableTooltip = useEnablePluginTooltip() + const settingsRef = useClickOutsideTooltip(useEnablePluginTooltip, () => {}) + + return ( + + { + showPluginClearDataConfirmation(plugin, closeSheet) + }} + /> + {plugin.SettingsComponent && ( + { + if (enabled) return + + requestAnimationFrame(() => { + enableTooltip.targetRef.current = + settingsRef.current + enableTooltip.setVisible(true) + }) + }} + > + { + openPluginSettings(plugin) + closeSheet() + }} + /> + + )} + + ) +} + const CopyIcon = lookupGeneratedIconComponent('CopyIcon')! function showCopiedToClipboardToast() { diff --git a/src/plugins/start/settings/components/PluginStateProvider.tsx b/src/plugins/start/settings/components/PluginStateProvider.tsx index e24d024..f302dd5 100644 --- a/src/plugins/start/settings/components/PluginStateProvider.tsx +++ b/src/plugins/start/settings/components/PluginStateProvider.tsx @@ -9,6 +9,7 @@ export interface PluginEnabledStates { const PluginStatesContext = createContext<{ enabled: PluginEnabledStates + status: { [pluginId: string]: number } } | null>(null) export default function PluginStatesProvider({ @@ -24,7 +25,22 @@ export default function PluginStatesProvider({ return states }) + const [status, setStatus] = useState<{ [pluginId: string]: number }>(() => { + const statuses: { [pluginId: string]: number } = {} + for (const plugin of pList.values()) { + statuses[plugin.manifest.id] = plugin.status + } + return statuses + }) + useEffect(() => { + const handleStatusChange = (plugin: AnyPlugin) => { + setStatus(prev => ({ + ...prev, + [plugin.manifest.id]: plugin.status, + })) + } + const handleEnabled = (plugin: AnyPlugin) => { setEnabled(prev => ({ ...prev, @@ -39,6 +55,11 @@ export default function PluginStatesProvider({ })) } + pEmitter.on('preInited', handleStatusChange) + pEmitter.on('inited', handleStatusChange) + pEmitter.on('started', handleStatusChange) + pEmitter.on('stopped', handleStatusChange) + pEmitter.on('enabled', handleEnabled) pEmitter.on('disabled', handleDisabled) @@ -50,9 +71,10 @@ export default function PluginStatesProvider({ const contextValue = useMemo( () => ({ - enabled: enabled, + enabled, + status, }), - [enabled], + [enabled, status], ) return ( @@ -72,3 +94,14 @@ export function usePluginEnabled(plugin: AnyPlugin) { return context.enabled[plugin.manifest.id] } + +export function usePluginStatus(plugin: AnyPlugin) { + const context = useContext(PluginStatesContext) + + if (!context) { + console.warn('usePluginStatus not used within a PluginStateProvider') + return plugin.status + } + + return context.status[plugin.manifest.id] +} diff --git a/src/plugins/start/settings/components/TooltipProvider.tsx b/src/plugins/start/settings/components/TooltipProvider.tsx index 500c6fd..bd25c5d 100644 --- a/src/plugins/start/settings/components/TooltipProvider.tsx +++ b/src/plugins/start/settings/components/TooltipProvider.tsx @@ -1,48 +1,42 @@ -/* - Only one tooltip can be visible at a time, so we use a global state to control the visibility of the tooltip. - Why? Because we don't want to run useTooltip on every single plugin card. This is ugly, but it works. -*/ - import { Design } from '@revenge-mod/discord/design' -import { useEffect, useState } from 'react' +import { createContext, useContext, useRef, useState } from 'react' import { useClickOutside } from 'react-native-click-outside' import type { RefObject } from 'react' import type { View } from 'react-native' const { useTooltip } = Design -export let setEnablePluginTooltipVisible: ((v: boolean) => void) | undefined -export const enableTooltipTarget: RefObject = { - current: null, +interface TooltipContextValue { + targetRef: RefObject + setVisible: (visible: boolean) => void } +const EnablePluginTooltipContext = createContext( + null, +) +const EssentialPluginTooltipContext = createContext( + null, +) + export function EnablePluginTooltipProvider({ children, }: { children: React.ReactNode }) { const [visible, setVisible] = useState(false) + const targetRef = useRef(null) - useEffect(() => { - setEnablePluginTooltipVisible = setVisible - return () => { - enableTooltipTarget.current = null - setEnablePluginTooltipVisible = undefined - } - }, []) - - useTooltip(enableTooltipTarget, { + useTooltip(targetRef, { label: 'Plugin must be enabled first', position: 'top', visible, }) - return children -} - -export let setEssentialPluginTooltipVisible: ((v: boolean) => void) | undefined -export const essentialTooltipTarget: RefObject = { - current: null, + return ( + + {children} + + ) } export function EssentialPluginTooltipProvider({ @@ -51,36 +45,64 @@ export function EssentialPluginTooltipProvider({ children: React.ReactNode }) { const [visible, setVisible] = useState(false) + const targetRef = useRef(null) - useEffect(() => { - setEssentialPluginTooltipVisible = setVisible - return () => { - essentialTooltipTarget.current = null - setEssentialPluginTooltipVisible = undefined - } - }, []) - - useTooltip(essentialTooltipTarget, { + useTooltip(targetRef, { label: 'Plugin needed for Revenge to function properly', position: 'top', visible, }) - return children + return ( + + {children} + + ) } -export function resetTooltips() { - setEnablePluginTooltipVisible?.(false) - setEssentialPluginTooltipVisible?.(false) +export function useEnablePluginTooltip() { + const context = useContext(EnablePluginTooltipContext) + if (!context) { + throw new Error( + 'useEnablePluginTooltip must be used within EnablePluginTooltipProvider', + ) + } + return context +} + +export function useEssentialPluginTooltip() { + const context = useContext(EssentialPluginTooltipContext) + if (!context) { + throw new Error( + 'useEssentialPluginTooltip must be used within EssentialPluginTooltipProvider', + ) + } + return context +} + +export function useResetTooltips() { + const enableTooltip = useContext(EnablePluginTooltipContext) + const essentialTooltip = useContext(EssentialPluginTooltipContext) + + return () => { + enableTooltip?.setVisible(false) + essentialTooltip?.setVisible(false) + } } export function useClickOutsideTooltip( - target: RefObject, + contextHook: () => TooltipContextValue, onClickOutside: () => void, ) { + const { targetRef, setVisible } = contextHook() + const ref = useClickOutside(() => { - resetTooltips() - if (target.current === ref.current) onClickOutside() + setVisible(false) + if (targetRef.current === ref.current) { + onClickOutside() + } }) return ref