diff --git a/src/plugins/init/settings/components/FilterAndSortActionSheet.tsx b/src/plugins/init/settings/components/FilterAndSortActionSheet.tsx index 42d4e8f..b9f75ad 100644 --- a/src/plugins/init/settings/components/FilterAndSortActionSheet.tsx +++ b/src/plugins/init/settings/components/FilterAndSortActionSheet.tsx @@ -2,14 +2,14 @@ import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon' import { Design } from '@revenge-mod/discord/design' import { useState } from 'react' import type { AssetId } from '@revenge-mod/assets/types' -import type { AnyPlugin } from '@revenge-mod/plugins/_' +import type { AnyPlugin, InternalPluginMeta } from '@revenge-mod/plugins/_' export interface FilterAndSortActionSheetProps { filters: Record< string, { icon: AssetId - filter: (p: AnyPlugin, iflags: number) => boolean + filter: (p: AnyPlugin, meta: InternalPluginMeta) => boolean } > filter: string[] diff --git a/src/plugins/init/settings/components/PluginCard.tsx b/src/plugins/init/settings/components/PluginCard.tsx index 21f01f9..71b4390 100644 --- a/src/plugins/init/settings/components/PluginCard.tsx +++ b/src/plugins/init/settings/components/PluginCard.tsx @@ -3,7 +3,7 @@ import { styles } from '@revenge-mod/components/_' import FormSwitch from '@revenge-mod/components/FormSwitch' import { Tokens } from '@revenge-mod/discord/common' import { Design } from '@revenge-mod/discord/design' -import { RootNavigationRef } from '@revenge-mod/discord/modules/main_tabs_v2' +import { ReactNavigationNative } from '@revenge-mod/externals/react-navigation' import { enablePlugin, InternalPluginFlags, @@ -11,8 +11,10 @@ import { preInitPlugin, startPlugin, } from '@revenge-mod/plugins/_' +import { memo } from 'react' import { Image, Pressable } from 'react-native' import { useClickOutside } from 'react-native-click-outside' +import { usePluginEnabled } from './PluginStateProvider' import { enableTooltipTarget, essentialTooltipTarget, @@ -20,7 +22,7 @@ import { setEnablePluginTooltipVisible, setEssentialPluginTooltipVisible, } from './TooltipProvider' -import type { AnyPlugin } from '@revenge-mod/plugins/_' +import type { AnyPlugin, InternalPluginMeta } from '@revenge-mod/plugins/_' import type { View } from 'react-native' const { Card, Text, Stack, IconButton, createStyles } = Design @@ -28,22 +30,23 @@ const { Card, Text, Stack, IconButton, createStyles } = Design const PuzzlePieceIcon = getAssetIdByName('PuzzlePieceIcon', 'png')! const SettingsIcon = getAssetIdByName('SettingsIcon', 'png')! -export function InstalledPluginCard({ +export const InstalledPluginCard = memo(function InstalledPluginCard({ plugin, - iflags, - enabled, + meta, rightGap, }: { plugin: AnyPlugin - iflags: number - enabled: boolean + meta: InternalPluginMeta rightGap?: boolean }) { + const navigation = ReactNavigationNative.useNavigation() + const enabled = usePluginEnabled(plugin) + const { manifest: { name, description, author, icon }, } = plugin - const essential = Boolean(iflags & InternalPluginFlags.Essential) + const essential = Boolean(meta.iflags & InternalPluginFlags.Essential) const styles_ = usePluginCardStyles() const settingsRef = useClickOutside(() => { @@ -94,9 +97,7 @@ export function InstalledPluginCard({ variant="secondary" icon={SettingsIcon} onPress={() => { - RootNavigationRef.getRootNavigationRef().navigate( - plugin.manifest.id, - ) + navigation.navigate(plugin.manifest.id) }} disabled={!enabled} /> @@ -146,7 +147,7 @@ export function InstalledPluginCard({ ) -} +}) const usePluginCardStyles = createStyles({ icon: { diff --git a/src/plugins/init/settings/components/PluginStateProvider.tsx b/src/plugins/init/settings/components/PluginStateProvider.tsx new file mode 100644 index 0000000..414b76e --- /dev/null +++ b/src/plugins/init/settings/components/PluginStateProvider.tsx @@ -0,0 +1,77 @@ +import { pEmitter, pList } from '@revenge-mod/plugins/_' +import { PluginFlags } from '@revenge-mod/plugins/constants' +import { createContext, useContext, useEffect, useMemo, useState } from 'react' +import type { AnyPlugin } from '@revenge-mod/plugins/_' +import type { ReactNode } from 'react' + +export interface PluginEnabledStates { + [pluginId: string]: boolean +} + +const PluginStatesContext = createContext<{ + enabled: PluginEnabledStates +} | null>(null) + +export default function PluginStatesProvider({ + children, +}: { + children: ReactNode +}) { + const [enabled, setEnabled] = useState(() => { + const states: PluginEnabledStates = {} + for (const plugin of pList.values()) { + states[plugin.manifest.id] = Boolean( + plugin.flags & PluginFlags.Enabled, + ) + } + return states + }) + + useEffect(() => { + const handleEnabled = (plugin: AnyPlugin) => { + setEnabled(prev => ({ + ...prev, + [plugin.manifest.id]: true, + })) + } + + const handleDisabled = (plugin: AnyPlugin) => { + setEnabled(prev => ({ + ...prev, + [plugin.manifest.id]: false, + })) + } + + pEmitter.on('enabled', handleEnabled) + pEmitter.on('disabled', handleDisabled) + + return () => { + pEmitter.off('enabled', handleEnabled) + pEmitter.off('disabled', handleDisabled) + } + }, []) + + const contextValue = useMemo( + () => ({ + enabled: enabled, + }), + [enabled], + ) + + return ( + + {children} + + ) +} + +export function usePluginEnabled(plugin: AnyPlugin) { + const context = useContext(PluginStatesContext) + + if (!context) { + console.warn('usePluginEnabled not used within a PluginStateProvider') + return Boolean(plugin.flags & PluginFlags.Enabled) + } + + return context.enabled[plugin.manifest.id] +} diff --git a/src/plugins/init/settings/definitions/RevengePluginsSetting.tsx b/src/plugins/init/settings/definitions/RevengePluginsSetting.tsx index 0892634..965ed13 100644 --- a/src/plugins/init/settings/definitions/RevengePluginsSetting.tsx +++ b/src/plugins/init/settings/definitions/RevengePluginsSetting.tsx @@ -15,10 +15,7 @@ const RevengePluginsSetting: SettingsItem = { type: 'route', IconComponent: () => , title: () => 'Plugins', - useTrailing: () => { - useUpdateOnPluginStatesChange() - return `${enabledCount} enabled` - }, + useTrailing: () => `${useEnabledPluginCount()} enabled`, screen: { route: RouteNames[Setting.RevengePlugins], getComponent: () => @@ -39,7 +36,7 @@ pEmitter.on('enabled', () => { enabledCount++ }) -export function useUpdateOnPluginStatesChange() { +function useEnabledPluginCount() { const reRender = useReRender() useEffect(() => { @@ -51,6 +48,8 @@ export function useUpdateOnPluginStatesChange() { pEmitter.off('enabled', reRender) } }, [reRender]) + + return enabledCount } const { AlertActionButton, AlertModal, Text } = Design diff --git a/src/plugins/init/settings/screens/RevengePluginsSettingScreen.tsx b/src/plugins/init/settings/screens/RevengePluginsSettingScreen.tsx index 3404dea..a730ee4 100644 --- a/src/plugins/init/settings/screens/RevengePluginsSettingScreen.tsx +++ b/src/plugins/init/settings/screens/RevengePluginsSettingScreen.tsx @@ -14,15 +14,15 @@ import { useWindowDimensions, View } from 'react-native' import { ClickOutsideProvider } from 'react-native-click-outside' import RevengeIcon from '~assets/RevengeIcon' import { InstalledPluginCard } from '../components/PluginCard' +import PluginStatesProvider from '../components/PluginStateProvider' import { EnablePluginTooltipProvider, EssentialPluginTooltipProvider, resetTooltips, } from '../components/TooltipProvider' -import { useUpdateOnPluginStatesChange } from '../definitions/RevengePluginsSetting' import type { RouteProp } from '@react-navigation/core' import type { ReactNavigationParamList } from '@revenge-mod/externals/react-navigation' -import type { AnyPlugin } from '@revenge-mod/plugins/_' +import type { AnyPlugin, InternalPluginMeta } from '@revenge-mod/plugins/_' import type { FilterAndSortActionSheetProps } from '../components/FilterAndSortActionSheet' import type { RouteNames, Setting } from '../constants' @@ -34,13 +34,15 @@ export default function RevengePluginsSettingScreen() { return ( - - - - - - - + + + + + + + + + ) @@ -63,7 +65,8 @@ const Filters: FilterAndSortActionSheetProps['filters'] = { }, Essential: { icon: getAssetIdByName('StarIcon')!, - filter: (_, iflags) => Boolean(iflags & InternalPluginFlags.Essential), + filter: (_, meta) => + Boolean(meta.iflags & InternalPluginFlags.Essential), }, } satisfies FilterAndSortActionSheetProps['filters'] const DefaultFilters: FilterAndSortActionSheetProps['filter'] = [] @@ -82,8 +85,6 @@ const Sorts = { } satisfies FilterAndSortActionSheetProps['sorts'] function Screen() { - useUpdateOnPluginStatesChange() - const navigation = ReactNavigationNative.useNavigation() const route = ReactNavigationNative.useRoute< @@ -108,18 +109,18 @@ function Screen() { const allPlugins = useMemo( () => [...pList.values()].map( - plugin => [plugin, pMetadata.get(plugin)!.iflags] as const, + plugin => [plugin, pMetadata.get(plugin)!] as const, ), [], ) const plugins = allPlugins - .filter(([plugin, iflags]) => { + .filter(([plugin, meta]) => { if (filter.length === 0) return true if (matchAll) - return filter.every(f => Filters[f].filter(plugin, iflags)) + return filter.every(f => Filters[f].filter(plugin, meta)) - return filter.some(f => Filters[f].filter(plugin, iflags)) + return filter.some(f => Filters[f].filter(plugin, meta)) }) .filter(([plugin]) => { const { name, description, author } = plugin.manifest @@ -132,8 +133,7 @@ function Screen() { }) .sort(([a], [b]) => { const result = Sorts[sort as keyof typeof Sorts][1](a, b) - if (reverse) return -result - return result + return reverse ? -result : result }) return ( @@ -190,7 +190,7 @@ function Screen() { function PluginMasonryFlashList({ plugins, }: { - plugins: (readonly [AnyPlugin, number])[] + plugins: (readonly [AnyPlugin, InternalPluginMeta])[] }) { const { width, height } = useWindowDimensions() const numColumns = Math.floor((width - 16) / 448) @@ -205,12 +205,10 @@ function PluginMasonryFlashList({ estimatedItemSize={116} numColumns={numColumns} ListEmptyComponent={NoPlugins} - renderItem={({ item: [plugin, iflags], columnIndex }) => ( + renderItem={({ item: [plugin, meta], columnIndex }) => ( )}