193 lines
7 KiB
TypeScript
193 lines
7 KiB
TypeScript
import { useNavigation, useRoute } from '@react-navigation/native'
|
|
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 {
|
|
getInternalPluginMeta,
|
|
InternalPluginFlags,
|
|
isPluginEnabled,
|
|
isPluginEssential,
|
|
isPluginInternal,
|
|
pList,
|
|
} 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 { View } from 'react-native'
|
|
import { ClickOutsideProvider } from 'react-native-click-outside'
|
|
import RevengeIcon from '~assets/RevengeIcon'
|
|
import { InstalledPluginMasonryFlashList } from '../components/PluginList'
|
|
import PluginStatesProvider from '../components/PluginStateProvider'
|
|
import {
|
|
EnablePluginTooltipProvider,
|
|
EssentialPluginTooltipProvider,
|
|
} from '../components/TooltipProvider'
|
|
import type { RouteProp } from '@react-navigation/core'
|
|
import type { ReactNavigationParamList } from '@revenge-mod/externals/react-navigation'
|
|
import type { FilterAndSortActionSheetProps } from '../components/FilterAndSortActionSheet'
|
|
import type { RouteNames, Setting } from '../constants'
|
|
|
|
const { Stack, IconButton, LayerScope } = Design
|
|
|
|
const FiltersHorizontalIcon = getAssetIdByName('FiltersHorizontalIcon', 'png')!
|
|
|
|
export default function RevengePluginsSettingScreen() {
|
|
return (
|
|
<LayerScope>
|
|
<ClickOutsideProvider>
|
|
<PluginStatesProvider>
|
|
<Page spacing={16}>
|
|
<EssentialPluginTooltipProvider>
|
|
<EnablePluginTooltipProvider>
|
|
<Screen />
|
|
</EnablePluginTooltipProvider>
|
|
</EssentialPluginTooltipProvider>
|
|
</Page>
|
|
</PluginStatesProvider>
|
|
</ClickOutsideProvider>
|
|
</LayerScope>
|
|
)
|
|
}
|
|
|
|
const SearchDebounceTime = 100
|
|
|
|
const Filters: FilterAndSortActionSheetProps['filters'] = {
|
|
Enabled: {
|
|
icon: getAssetIdByName('CircleCheckIcon')!,
|
|
filter: plugin => isPluginEnabled(plugin),
|
|
},
|
|
Disabled: {
|
|
icon: getAssetIdByName('CircleXIcon')!,
|
|
filter: plugin => !isPluginEnabled(plugin),
|
|
},
|
|
'Has Errors': {
|
|
icon: getAssetIdByName('CircleXIcon')!,
|
|
filter: plugin => plugin.errors.length > 0,
|
|
},
|
|
Internal: {
|
|
icon: RevengeIcon,
|
|
desc: 'Included with Revenge.',
|
|
filter: (_, meta) => isPluginInternal(meta),
|
|
},
|
|
Essential: {
|
|
icon: getAssetIdByName('StarIcon')!,
|
|
desc: 'Required for Revenge to function properly.',
|
|
filter: (_, meta) => isPluginEssential(meta),
|
|
},
|
|
'Non-APIs': {
|
|
icon: getAssetIdByName('PaperIcon')!,
|
|
desc: 'Exclude essential plugins that provide APIs for other plugins.',
|
|
filter: (_, meta) => !(meta.iflags & InternalPluginFlags.API),
|
|
},
|
|
} satisfies FilterAndSortActionSheetProps['filters']
|
|
const DefaultFilters: FilterAndSortActionSheetProps['filter'] = ['Non-APIs']
|
|
|
|
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() {
|
|
const navigation = useNavigation()
|
|
const route =
|
|
useRoute<
|
|
RouteProp<
|
|
ReactNavigationParamList,
|
|
(typeof RouteNames)[typeof Setting.RevengePlugins]
|
|
>
|
|
>()
|
|
|
|
const [search, setSearch] = useState('')
|
|
const debouncedSetSearch = useCallback(
|
|
debounce(setSearch, SearchDebounceTime),
|
|
[],
|
|
)
|
|
|
|
const filter = route.params?.filter ?? DefaultFilters
|
|
const matchAll = route.params?.matchAll ?? true
|
|
const reverse = route.params?.reverse ?? false
|
|
const sort = route.params?.sort ?? DefaultSort
|
|
|
|
const allPlugins = useMemo(
|
|
() =>
|
|
[...pList.values()].map(
|
|
plugin => [plugin, getInternalPluginMeta(plugin)!] as const,
|
|
),
|
|
[],
|
|
)
|
|
|
|
const plugins = useMemo(
|
|
() =>
|
|
allPlugins
|
|
.filter(([plugin, meta]) => {
|
|
if (filter.length === 0) return true
|
|
if (matchAll)
|
|
return filter.every(f =>
|
|
Filters[f].filter(plugin, meta),
|
|
)
|
|
|
|
return filter.some(f => Filters[f].filter(plugin, meta))
|
|
})
|
|
.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)
|
|
return reverse ? -result : result
|
|
}),
|
|
[allPlugins, filter, matchAll, reverse, sort, search],
|
|
)
|
|
|
|
return (
|
|
<>
|
|
<Stack direction="horizontal">
|
|
<View style={styles.grow}>
|
|
<SearchInput onChange={debouncedSetSearch} 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 }),
|
|
matchAll,
|
|
setMatchAll: matchAll =>
|
|
navigation.setParams({ matchAll }),
|
|
reverse,
|
|
setReverse: reverse =>
|
|
navigation.setParams({ reverse }),
|
|
sorts: Sorts,
|
|
sort,
|
|
setSort: sort => navigation.setParams({ sort }),
|
|
},
|
|
)
|
|
}
|
|
/>
|
|
</Stack>
|
|
<InstalledPluginMasonryFlashList plugins={plugins} />
|
|
</>
|
|
)
|
|
}
|