feat(plugins/settings): see commit description
- Added ability to filter plugins - Show tooltip when attempting to disable essential plugins - Moved tooltip providers to its own file - Moved PluginCard to its own file
This commit is contained in:
parent
24331dfaad
commit
a969a64832
5 changed files with 458 additions and 237 deletions
|
|
@ -1,33 +1,120 @@
|
|||
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 { InternalPlugin } from '@revenge-mod/plugins/_'
|
||||
|
||||
export interface FilterAndSortActionSheetProps<S extends string> {
|
||||
sorts: Record<S, (a: InternalPlugin, b: InternalPlugin) => number>
|
||||
selectedSort: S
|
||||
onSelectSort: (fn: S) => void
|
||||
export interface FilterAndSortActionSheetProps {
|
||||
filters: Record<
|
||||
string,
|
||||
[icon: AssetId, (p: InternalPlugin, iflags: number) => boolean]
|
||||
>
|
||||
filter: string[]
|
||||
setFilter: (filters: string[]) => void
|
||||
matchAll: boolean
|
||||
setMatchAll: (matchAll: boolean) => void
|
||||
sorts: Record<
|
||||
string,
|
||||
[icon: AssetId, (a: InternalPlugin, b: InternalPlugin) => number]
|
||||
>
|
||||
sort: string
|
||||
setSort: (fn: string) => void
|
||||
reverse: boolean
|
||||
setReverse: (reverse: boolean) => void
|
||||
}
|
||||
|
||||
const { ActionSheet, BottomSheetTitleHeader, TableRadioGroup, TableRadioRow } =
|
||||
Design
|
||||
const {
|
||||
ActionSheet,
|
||||
BottomSheetTitleHeader,
|
||||
TableCheckboxRow,
|
||||
TableRadioGroup,
|
||||
TableRadioRow,
|
||||
TableRowGroup,
|
||||
TableSwitchRow,
|
||||
} = Design
|
||||
|
||||
// TODO(plugins/settings): Add filter options
|
||||
export default function FilterAndSortActionSheet<S extends string>({
|
||||
export default function FilterAndSortActionSheet({
|
||||
filters,
|
||||
filter,
|
||||
setFilter,
|
||||
matchAll,
|
||||
setMatchAll,
|
||||
sorts,
|
||||
selectedSort,
|
||||
onSelectSort,
|
||||
}: FilterAndSortActionSheetProps<S>) {
|
||||
sort,
|
||||
setSort,
|
||||
reverse,
|
||||
setReverse,
|
||||
}: FilterAndSortActionSheetProps) {
|
||||
const [filter_, setFilter_] = useState(filter)
|
||||
const [sort_, setSort_] = useState(sort)
|
||||
const [reverse_, setReverse_] = useState(reverse)
|
||||
const [matchAll_, setMatchAll_] = useState(matchAll ?? false)
|
||||
|
||||
return (
|
||||
<ActionSheet>
|
||||
<BottomSheetTitleHeader title="Filter & Sort" />
|
||||
<TableRowGroup title="Filter by">
|
||||
{Object.entries(filters).map(([label, [icon]]) => {
|
||||
const checked = filter_.includes(label)
|
||||
|
||||
return (
|
||||
<TableCheckboxRow
|
||||
key={label}
|
||||
icon={<TableRowAssetIcon id={icon} />}
|
||||
label={label}
|
||||
value={label}
|
||||
checked={checked}
|
||||
onPress={() => {
|
||||
const v = checked
|
||||
? filter_.filter(f => f !== label)
|
||||
: [...filter_, label]
|
||||
|
||||
setFilter(v)
|
||||
setFilter_(v)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</TableRowGroup>
|
||||
<TableRowGroup>
|
||||
<TableSwitchRow
|
||||
label="Match all"
|
||||
subLabel="Plugins must match all selected filters to show up."
|
||||
value={matchAll_}
|
||||
onValueChange={v => {
|
||||
setMatchAll(v)
|
||||
setMatchAll_(v)
|
||||
}}
|
||||
/>
|
||||
</TableRowGroup>
|
||||
<TableRadioGroup
|
||||
title="Sort"
|
||||
defaultValue={selectedSort}
|
||||
onChange={onSelectSort}
|
||||
title="Sort by"
|
||||
defaultValue={sort_}
|
||||
onChange={(v: string) => {
|
||||
setSort(v)
|
||||
setSort_(v)
|
||||
}}
|
||||
>
|
||||
{Object.keys(sorts).map(label => (
|
||||
<TableRadioRow key={label} label={label} value={label} />
|
||||
{Object.entries(sorts).map(([label, [icon]]) => (
|
||||
<TableRadioRow
|
||||
key={label}
|
||||
icon={<TableRowAssetIcon id={icon} />}
|
||||
label={label}
|
||||
value={label}
|
||||
/>
|
||||
))}
|
||||
</TableRadioGroup>
|
||||
<TableRowGroup>
|
||||
<TableSwitchRow
|
||||
label="Reverse results"
|
||||
value={reverse_}
|
||||
onValueChange={v => {
|
||||
setReverse(v)
|
||||
setReverse_(v)
|
||||
}}
|
||||
/>
|
||||
</TableRowGroup>
|
||||
</ActionSheet>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
172
src/plugins/init/settings/components/PluginCard.tsx
Normal file
172
src/plugins/init/settings/components/PluginCard.tsx
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import { getAssetIdByName } from '@revenge-mod/assets'
|
||||
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 {
|
||||
enablePlugin,
|
||||
InternalPluginFlags,
|
||||
initPlugin,
|
||||
preInitPlugin,
|
||||
startPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import { useReRender } from '@revenge-mod/utils/react'
|
||||
import { Image, Pressable } from 'react-native'
|
||||
import { useClickOutside } from 'react-native-click-outside'
|
||||
import { navigatePluginSettings } from '../utils'
|
||||
import {
|
||||
enableTooltipTarget,
|
||||
essentialTooltipTarget,
|
||||
resetTooltips,
|
||||
setEnablePluginTooltipVisible,
|
||||
setEssentialPluginTooltipVisible,
|
||||
} from './TooltipProvider'
|
||||
import type { InternalPlugin } from '@revenge-mod/plugins/_'
|
||||
import type { View } from 'react-native'
|
||||
|
||||
const { Card, Text, Stack, IconButton, createStyles } = Design
|
||||
|
||||
const PuzzlePieceIcon = getAssetIdByName('PuzzlePieceIcon', 'png')!
|
||||
const SettingsIcon = getAssetIdByName('SettingsIcon', 'png')!
|
||||
|
||||
export function InstalledPluginCard({
|
||||
plugin,
|
||||
iflags,
|
||||
rightGap,
|
||||
}: {
|
||||
plugin: InternalPlugin
|
||||
iflags: number
|
||||
rightGap?: boolean
|
||||
}) {
|
||||
const {
|
||||
manifest: { name, description, author, icon },
|
||||
flags,
|
||||
} = plugin
|
||||
|
||||
const reRender = useReRender()
|
||||
const essential = Boolean(iflags & InternalPluginFlags.Essential)
|
||||
const enabled = Boolean(flags & PluginFlags.Enabled)
|
||||
const styles_ = usePluginCardStyles()
|
||||
|
||||
const settingsRef = useClickOutside<View>(() => {
|
||||
if (enableTooltipTarget.current === settingsRef.current)
|
||||
setEnablePluginTooltipVisible?.(false)
|
||||
})
|
||||
|
||||
const switchRef = useClickOutside<View>(() => {
|
||||
if (essentialTooltipTarget.current === switchRef.current)
|
||||
setEssentialPluginTooltipVisible?.(false)
|
||||
})
|
||||
|
||||
return (
|
||||
<Card style={[styles_.card, styles.grow, rightGap && styles_.rightGap]}>
|
||||
<Stack
|
||||
direction="horizontal"
|
||||
style={[styles.grow, styles_.topContainer]}
|
||||
>
|
||||
<Stack
|
||||
direction="horizontal"
|
||||
spacing={8}
|
||||
style={[styles_.topContainer, styles.flex]}
|
||||
>
|
||||
<Image
|
||||
source={icon ? getAssetIdByName(icon) : PuzzlePieceIcon}
|
||||
style={styles_.icon}
|
||||
/>
|
||||
<Text variant="heading-lg/semibold">{name}</Text>
|
||||
</Stack>
|
||||
{plugin.SettingsComponent && (
|
||||
<Pressable
|
||||
onPress={e => {
|
||||
if (enabled) return
|
||||
|
||||
e.stopPropagation()
|
||||
resetTooltips()
|
||||
|
||||
enableTooltipTarget.current = settingsRef.current
|
||||
setEnablePluginTooltipVisible?.(true)
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
ref={settingsRef}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={SettingsIcon}
|
||||
onPress={() => navigatePluginSettings(plugin)}
|
||||
disabled={!enabled}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
<Pressable
|
||||
onPress={e => {
|
||||
if (essential) {
|
||||
e.stopPropagation()
|
||||
resetTooltips()
|
||||
|
||||
essentialTooltipTarget.current = switchRef.current
|
||||
setEssentialPluginTooltipVisible?.(true)
|
||||
}
|
||||
}}
|
||||
ref={switchRef}
|
||||
>
|
||||
<FormSwitch
|
||||
disabled={essential}
|
||||
// biome-ignore lint/complexity/useArrowFunction: Async arrows are not supported
|
||||
onValueChange={async function (v) {
|
||||
if (v) {
|
||||
await enablePlugin(plugin, true)
|
||||
await preInitPlugin(plugin)
|
||||
await initPlugin(plugin)
|
||||
await startPlugin(plugin)
|
||||
} else await plugin.disable()
|
||||
|
||||
reRender()
|
||||
|
||||
// TODO(plugins/settings): handle sorting after plugin enabled/disabled
|
||||
// TODO(plugins/settings): show ReloadRequired modal
|
||||
// make an event based system for this, so we can register a listener for when plugins are disabled
|
||||
// and update the UI accordingly
|
||||
}}
|
||||
value={enabled}
|
||||
/>
|
||||
</Pressable>
|
||||
</Stack>
|
||||
<Stack spacing={4} style={[styles_.alignedContainer, styles.grow]}>
|
||||
<Text
|
||||
color="text-muted"
|
||||
style={styles.grow}
|
||||
variant="heading-md/medium"
|
||||
>
|
||||
by {author}
|
||||
</Text>
|
||||
<Text style={styles.grow} variant="text-md/medium">
|
||||
{description}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const usePluginCardStyles = createStyles({
|
||||
icon: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
tintColor: Tokens.default.colors.TEXT_NORMAL,
|
||||
},
|
||||
card: {
|
||||
marginBottom: 12,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12,
|
||||
gap: 4,
|
||||
},
|
||||
rightGap: {
|
||||
marginRight: 12,
|
||||
},
|
||||
topContainer: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
alignedContainer: {
|
||||
paddingLeft: 28,
|
||||
},
|
||||
})
|
||||
74
src/plugins/init/settings/components/TooltipProvider.tsx
Normal file
74
src/plugins/init/settings/components/TooltipProvider.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
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 type { MutableRefObject } from 'react'
|
||||
import type { View } from 'react-native'
|
||||
|
||||
const { useTooltip } = Design
|
||||
|
||||
export let setEnablePluginTooltipVisible: ((v: boolean) => void) | undefined
|
||||
export const enableTooltipTarget: MutableRefObject<View | null> = {
|
||||
current: null,
|
||||
}
|
||||
|
||||
export function EnablePluginTooltipProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setEnablePluginTooltipVisible = setVisible
|
||||
return () => {
|
||||
enableTooltipTarget.current = null
|
||||
setEnablePluginTooltipVisible = undefined
|
||||
}
|
||||
}, [])
|
||||
|
||||
useTooltip(enableTooltipTarget, {
|
||||
label: 'Plugin must be enabled first',
|
||||
position: 'top',
|
||||
visible,
|
||||
})
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
export let setEssentialPluginTooltipVisible: ((v: boolean) => void) | undefined
|
||||
export const essentialTooltipTarget: MutableRefObject<View | null> = {
|
||||
current: null,
|
||||
}
|
||||
|
||||
export function EssentialPluginTooltipProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setEssentialPluginTooltipVisible = setVisible
|
||||
return () => {
|
||||
essentialTooltipTarget.current = null
|
||||
setEssentialPluginTooltipVisible = undefined
|
||||
}
|
||||
}, [])
|
||||
|
||||
useTooltip(essentialTooltipTarget, {
|
||||
label: 'Plugin needed for Revenge to function properly',
|
||||
position: 'top',
|
||||
visible,
|
||||
})
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
export function resetTooltips() {
|
||||
setEnablePluginTooltipVisible?.(false)
|
||||
setEssentialPluginTooltipVisible?.(false)
|
||||
}
|
||||
|
|
@ -1,107 +1,86 @@
|
|||
import { getAssetIdByName } from '@revenge-mod/assets'
|
||||
import FormSwitch from '@revenge-mod/components/FormSwitch'
|
||||
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 { Tokens } from '@revenge-mod/discord/common'
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { ReactNavigationNative } from '@revenge-mod/externals/react-navigation'
|
||||
import { FlashList } from '@revenge-mod/externals/shopify'
|
||||
import {
|
||||
_metas,
|
||||
_plugins,
|
||||
enablePlugin,
|
||||
InternalPluginFlags,
|
||||
initPlugin,
|
||||
preInitPlugin,
|
||||
startPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { _metas, _plugins, InternalPluginFlags } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import { debounce } from '@revenge-mod/utils/callbacks'
|
||||
import { useReRender } from '@revenge-mod/utils/react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
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 {
|
||||
Image,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
useWindowDimensions,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {
|
||||
ClickOutsideProvider,
|
||||
useClickOutside,
|
||||
} from 'react-native-click-outside'
|
||||
import { navigatePluginSettings } from '../utils'
|
||||
EnablePluginTooltipProvider,
|
||||
EssentialPluginTooltipProvider,
|
||||
resetTooltips,
|
||||
} from '../components/TooltipProvider'
|
||||
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 { MutableRefObject } from 'react'
|
||||
import type { FilterAndSortActionSheetProps } from '../components/FilterAndSortActionSheet'
|
||||
import type { RouteNames, Setting } from '../constants'
|
||||
|
||||
const { Card, Text, Stack, IconButton, LayerScope, useTooltip, createStyles } =
|
||||
Design
|
||||
const { Text, Stack, IconButton, LayerScope } = Design
|
||||
|
||||
const FiltersHorizontalIcon = getAssetIdByName('FiltersHorizontalIcon', 'png')!
|
||||
|
||||
export default function RevengePluginsSettingScreen() {
|
||||
return (
|
||||
<LayerScope>
|
||||
<ClickOutsideProvider>
|
||||
<Page spacing={16}>
|
||||
<Screen />
|
||||
<EssentialPluginTooltipProvider>
|
||||
<EnablePluginTooltipProvider>
|
||||
<Screen />
|
||||
</EnablePluginTooltipProvider>
|
||||
</EssentialPluginTooltipProvider>
|
||||
</Page>
|
||||
</ClickOutsideProvider>
|
||||
</LayerScope>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
let setTooltipVisible: ((v: boolean) => void) | undefined
|
||||
const tooltipTargetRef: MutableRefObject<View | null> = { current: null }
|
||||
|
||||
function EnablePluginTooltipProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setTooltipVisible = setVisible
|
||||
return () => {
|
||||
setTooltipVisible = undefined
|
||||
}
|
||||
}, [])
|
||||
|
||||
useTooltip(tooltipTargetRef, {
|
||||
label: 'Enable plugin to access settings',
|
||||
position: 'top',
|
||||
visible,
|
||||
})
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
const SearchDebounceTime = 100
|
||||
|
||||
const FilterAndSortActionSheetKey = 'filter-and-sort-plugins'
|
||||
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 (A-Z)'
|
||||
const DefaultSort: keyof typeof Sorts = 'Name'
|
||||
const Sorts = {
|
||||
'Name (A-Z)': (a, b) => a.manifest.name.localeCompare(b.manifest.name),
|
||||
'Name (Z-A)': (a, b) => b.manifest.name.localeCompare(a.manifest.name),
|
||||
'Enabled first': (a, b) =>
|
||||
(b.flags & PluginFlags.Enabled) - (a.flags & PluginFlags.Enabled),
|
||||
'Disabled first': (a, b) =>
|
||||
(a.flags & PluginFlags.Enabled) - (b.flags & PluginFlags.Enabled),
|
||||
} satisfies FilterAndSortActionSheetProps<string>['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 { width } = useWindowDimensions()
|
||||
|
||||
const navigation = ReactNavigationNative.useNavigation()
|
||||
const route =
|
||||
ReactNavigationNative.useRoute<
|
||||
|
|
@ -117,9 +96,11 @@ function Screen() {
|
|||
[],
|
||||
)
|
||||
|
||||
const [sort, setSort] = useState(
|
||||
(route.params?.sort as keyof typeof Sorts) ?? DefaultSort,
|
||||
)
|
||||
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(
|
||||
() =>
|
||||
|
|
@ -132,6 +113,13 @@ function Screen() {
|
|||
const plugins = useMemo(
|
||||
() =>
|
||||
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()
|
||||
|
|
@ -141,68 +129,81 @@ function Screen() {
|
|||
author.toLowerCase().includes(query)
|
||||
)
|
||||
})
|
||||
.sort(([a], [b]) => Sorts[sort](a, b)),
|
||||
[allPlugins, search, sort],
|
||||
.sort(([a], [b]) => {
|
||||
const result = Sorts[sort as keyof typeof Sorts][1](a, b)
|
||||
if (reverse) return -result
|
||||
return result
|
||||
}),
|
||||
[search, sort, reverse, filter, matchAll, allPlugins],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setParams({ sort })
|
||||
ActionSheetActionCreators.hideActionSheet(FilterAndSortActionSheetKey)
|
||||
}, [sort, navigation.setParams])
|
||||
|
||||
return (
|
||||
<EnablePluginTooltipProvider>
|
||||
<>
|
||||
<Stack direction="horizontal">
|
||||
<View style={styles.flex}>
|
||||
<View style={styles.grow}>
|
||||
<SearchInput
|
||||
onChange={(v: string) => debouncedSetSearch(v)}
|
||||
size="md"
|
||||
/>
|
||||
</View>
|
||||
<IconButton
|
||||
icon={getAssetIdByName('ArrowsUpDownIcon')!}
|
||||
icon={FiltersHorizontalIcon}
|
||||
variant="tertiary"
|
||||
onPress={() =>
|
||||
ActionSheetActionCreators.openLazy(
|
||||
import(
|
||||
'../components/FilterAndSortActionSheet'
|
||||
) as Promise<{
|
||||
default: typeof import('../components/FilterAndSortActionSheet').default<
|
||||
keyof typeof Sorts
|
||||
>
|
||||
}>,
|
||||
FilterAndSortActionSheetKey,
|
||||
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,
|
||||
selectedSort: sort,
|
||||
onSelectSort: setSort,
|
||||
sort,
|
||||
setSort: sort => {
|
||||
navigation.setParams({ sort })
|
||||
setSort(sort)
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<PluginMasonryFlashList
|
||||
plugins={plugins}
|
||||
numColumns={Math.floor((width - 16) / 448)}
|
||||
/>
|
||||
</EnablePluginTooltipProvider>
|
||||
<PluginMasonryFlashList plugins={plugins} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function PluginMasonryFlashList({
|
||||
plugins,
|
||||
numColumns,
|
||||
}: {
|
||||
plugins: (readonly [InternalPlugin, number])[]
|
||||
numColumns: number
|
||||
}) {
|
||||
const { width, height } = useWindowDimensions()
|
||||
const numColumns = Math.floor((width - 16) / 448)
|
||||
|
||||
return (
|
||||
<FlashList.MasonryFlashList
|
||||
fadingEdgeLength={16}
|
||||
onScrollBeginDrag={() => setTooltipVisible?.(false)}
|
||||
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}
|
||||
|
|
@ -215,126 +216,10 @@ function PluginMasonryFlashList({
|
|||
)
|
||||
}
|
||||
|
||||
function InstalledPluginCard({
|
||||
plugin,
|
||||
iflags,
|
||||
rightGap,
|
||||
}: {
|
||||
plugin: InternalPlugin
|
||||
iflags: number
|
||||
rightGap?: boolean
|
||||
}) {
|
||||
const {
|
||||
manifest: { name, description, author, icon },
|
||||
flags,
|
||||
} = plugin
|
||||
|
||||
const reRender = useReRender()
|
||||
const essential = Boolean(iflags & InternalPluginFlags.Essential)
|
||||
const enabled = Boolean(flags & PluginFlags.Enabled)
|
||||
const styles_ = usePluginCardStyles()
|
||||
const ref = useClickOutside<View>(() => setTooltipVisible?.(false))
|
||||
|
||||
function NoPlugins() {
|
||||
return (
|
||||
<Card style={[styles_.card, styles.flex, rightGap && styles_.rightGap]}>
|
||||
<Stack
|
||||
direction="horizontal"
|
||||
style={[styles.flex, styles_.topContainer]}
|
||||
>
|
||||
<Stack
|
||||
direction="horizontal"
|
||||
spacing={8}
|
||||
style={[styles_.topContainer, styles.flex]}
|
||||
>
|
||||
<Image
|
||||
source={getAssetIdByName(icon ?? 'PuzzlePieceIcon')!}
|
||||
style={styles_.icon}
|
||||
/>
|
||||
<Text variant="heading-lg/semibold">{name}</Text>
|
||||
</Stack>
|
||||
{plugin.SettingsComponent && (
|
||||
<Pressable
|
||||
onPress={e => {
|
||||
if (enabled) return
|
||||
|
||||
e.stopPropagation()
|
||||
tooltipTargetRef.current = ref.current
|
||||
|
||||
setTooltipVisible?.(true)
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
ref={ref}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={getAssetIdByName('SettingsIcon')!}
|
||||
onPress={() => navigatePluginSettings(plugin)}
|
||||
disabled={!enabled}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
<FormSwitch
|
||||
disabled={essential}
|
||||
// biome-ignore lint/complexity/useArrowFunction: Async arrows are not supported
|
||||
onValueChange={async function (v) {
|
||||
if (v) {
|
||||
await enablePlugin(plugin, true)
|
||||
await preInitPlugin(plugin)
|
||||
await initPlugin(plugin)
|
||||
await startPlugin(plugin)
|
||||
} else await plugin.disable()
|
||||
|
||||
reRender()
|
||||
|
||||
// TODO(plugins/settings): handle sorting after plugin enabled/disabled
|
||||
// TODO(plugins/settings): show ReloadRequired modal
|
||||
// make an event based system for this, so we can register a listener for when plugins are disabled
|
||||
// and update the UI accordingly
|
||||
}}
|
||||
value={enabled}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack spacing={4} style={[styles_.alignedContainer, styles.flex]}>
|
||||
<Text
|
||||
color="text-muted"
|
||||
style={styles.flex}
|
||||
variant="heading-md/medium"
|
||||
>
|
||||
by {author}
|
||||
</Text>
|
||||
<Text style={styles.flex} variant="text-md/medium">
|
||||
{description}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
<Text variant="heading-md/medium" style={{ textAlign: 'center' }}>
|
||||
No plugins found. Try changing your query or filters.
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
})
|
||||
|
||||
const usePluginCardStyles = createStyles({
|
||||
icon: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
tintColor: Tokens.default.colors.TEXT_NORMAL,
|
||||
},
|
||||
card: {
|
||||
marginBottom: 12,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12,
|
||||
gap: 4,
|
||||
},
|
||||
rightGap: {
|
||||
marginRight: 12,
|
||||
},
|
||||
topContainer: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
alignedContainer: {
|
||||
paddingLeft: 28,
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -16,5 +16,8 @@ type RevengeSettingsParamList = {
|
|||
} & {
|
||||
[K in (typeof RouteNames)[(typeof Setting)['RevengePlugins']]]: {
|
||||
sort?: string
|
||||
filter?: string[]
|
||||
matchAll?: boolean
|
||||
reverse?: boolean
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue