revenge-bundle-next/src/plugins/start/settings/screens/RevengePluginsSettingScreen.tsx
PalmDevs 8d4581f9c1
fix: register start-only plugins during start stage
Previously they were registered on init which may have caused some minor performance issues.
2025-07-14 23:13:52 +07:00

227 lines
8.2 KiB
TypeScript

import { getAssetIdByName } from '@revenge-mod/assets'
import { styles } from '@revenge-mod/components/_'
import Page from '@revenge-mod/components/Page'
import SearchInput from '@revenge-mod/components/SearchInput'
import { ActionSheetActionCreators } from '@revenge-mod/discord/actions'
import { Design } from '@revenge-mod/discord/design'
import { ReactNavigationNative } from '@revenge-mod/externals/react-navigation'
import { FlashList } from '@revenge-mod/externals/shopify'
import { InternalPluginFlags, pList, pMetadata } from '@revenge-mod/plugins/_'
import { PluginFlags } from '@revenge-mod/plugins/constants'
import { debounce } from '@revenge-mod/utils/callback'
import { useCallback, useMemo, useState } from 'react'
import { useWindowDimensions, View } from 'react-native'
import { ClickOutsideProvider } from 'react-native-click-outside'
import RevengeIcon from '~assets/RevengeIcon'
import { InstalledPluginCard } from '../components/PluginCard'
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 { InternalPlugin } from '@revenge-mod/plugins/_'
import type { FilterAndSortActionSheetProps } from '../components/FilterAndSortActionSheet'
import type { RouteNames, Setting } from '../constants'
const { Text, Stack, IconButton, LayerScope } = Design
const FiltersHorizontalIcon = getAssetIdByName('FiltersHorizontalIcon', 'png')!
export default function RevengePluginsSettingScreen() {
return (
<LayerScope>
<ClickOutsideProvider>
<Page spacing={16}>
<EssentialPluginTooltipProvider>
<EnablePluginTooltipProvider>
<Screen />
</EnablePluginTooltipProvider>
</EssentialPluginTooltipProvider>
</Page>
</ClickOutsideProvider>
</LayerScope>
)
}
const SearchDebounceTime = 100
const Filters: FilterAndSortActionSheetProps['filters'] = {
Enabled: [
getAssetIdByName('CircleCheckIcon')!,
plugin => Boolean(plugin.flags & PluginFlags.Enabled),
],
Disabled: [
getAssetIdByName('CircleXIcon')!,
plugin => !(plugin.flags & PluginFlags.Enabled),
],
Internal: [
RevengeIcon,
plugin => Boolean(plugin.flags & InternalPluginFlags.Internal),
],
Essential: [
getAssetIdByName('StarIcon')!,
(_, iflags) => Boolean(iflags & InternalPluginFlags.Essential),
],
} satisfies FilterAndSortActionSheetProps['filters']
const DefaultFilters: FilterAndSortActionSheetProps['filter'] = []
const DefaultSort: keyof typeof Sorts = 'Name'
const Sorts = {
Name: [
getAssetIdByName('IdIcon')!,
(a, b) => a.manifest.name.localeCompare(b.manifest.name),
],
'Enabled first': [
getAssetIdByName('CircleCheckIcon')!,
(a, b) =>
(b.flags & PluginFlags.Enabled) - (a.flags & PluginFlags.Enabled),
],
} satisfies FilterAndSortActionSheetProps['sorts']
function Screen() {
useUpdateOnPluginStatesChange()
const navigation = ReactNavigationNative.useNavigation()
const route =
ReactNavigationNative.useRoute<
RouteProp<
ReactNavigationParamList,
(typeof RouteNames)[typeof Setting.RevengePlugins]
>
>()
const [search, setSearch] = useState('')
const debouncedSetSearch = useCallback(
debounce(setSearch, SearchDebounceTime),
[],
)
const [filter, setFilter] = useState(route.params?.filter ?? DefaultFilters)
const [matchAll, setMatchAll] = useState(route.params?.matchAll ?? false)
const [reverse, setReverse] = useState(route.params?.reverse ?? false)
const [sort, setSort] = useState(route.params?.sort ?? DefaultSort)
const allPlugins = useMemo(
() =>
[...pList.values()].map(
plugin =>
[plugin, pMetadata.get(plugin.manifest.id)![2]] as const,
),
[],
)
const plugins = allPlugins
.filter(([plugin, iflags]) => {
if (filter.length === 0) return true
if (matchAll)
return filter.every(f => Filters[f][1](plugin, iflags))
return filter.some(f => Filters[f][1](plugin, iflags))
})
.filter(([plugin]) => {
const { name, description, author } = plugin.manifest
const query = search.toLowerCase()
return (
name.toLowerCase().includes(query) ||
description.toLowerCase().includes(query) ||
author.toLowerCase().includes(query)
)
})
.sort(([a], [b]) => {
const result = Sorts[sort as keyof typeof Sorts][1](a, b)
if (reverse) return -result
return result
})
return (
<>
<Stack direction="horizontal">
<View style={styles.grow}>
<SearchInput
onChange={(v: string) => {
debouncedSetSearch(v)
}}
size="md"
/>
</View>
<IconButton
icon={FiltersHorizontalIcon}
variant="tertiary"
onPress={() =>
ActionSheetActionCreators.openLazy(
import('../components/FilterAndSortActionSheet'),
'filter-and-sort-plugins',
{
filters: Filters,
filter,
setFilter: filter => {
navigation.setParams({ filter })
setFilter(filter)
},
matchAll,
setMatchAll: matchAll => {
navigation.setParams({ matchAll })
setMatchAll(matchAll)
},
reverse,
setReverse: reverse => {
navigation.setParams({ reverse })
setReverse(reverse)
},
sorts: Sorts,
sort,
setSort: sort => {
navigation.setParams({ sort })
setSort(sort)
},
},
)
}
/>
</Stack>
<PluginMasonryFlashList plugins={plugins} />
</>
)
}
function PluginMasonryFlashList({
plugins,
}: {
plugins: (readonly [InternalPlugin, number])[]
}) {
const { width, height } = useWindowDimensions()
const numColumns = Math.floor((width - 16) / 448)
return (
<FlashList.MasonryFlashList
data={plugins}
onScrollBeginDrag={resetTooltips}
fadingEdgeLength={16}
keyExtractor={([plugin]) => plugin.manifest.id}
estimatedListSize={{ width: width - 32, height: height - 160 }}
estimatedItemSize={116}
numColumns={numColumns}
ListEmptyComponent={NoPlugins}
renderItem={({ item: [plugin, iflags], columnIndex }) => (
<InstalledPluginCard
iflags={iflags}
key={plugin.manifest.id}
plugin={plugin}
rightGap={columnIndex + 1 < numColumns}
/>
)}
/>
)
}
function NoPlugins() {
return (
<Text variant="heading-md/medium" style={{ textAlign: 'center' }}>
No plugins found. Try changing your query or filters.
</Text>
)
}