feat: plugin dependencies and dependent dialogs, refactor plugins library and settings plugin
- Refactored the plugins library - Refactored the settings plugin
This commit is contained in:
parent
5d7ac417ba
commit
8eea0d12ea
13 changed files with 618 additions and 307 deletions
|
|
@ -2,11 +2,7 @@ import { TypedEventEmitter } from '@revenge-mod/discord/common/utils'
|
|||
import { getErrorStack } from '@revenge-mod/utils/error'
|
||||
import { sleepReject } from '@revenge-mod/utils/promise'
|
||||
import { pUnscopedApi as uapi } from '../apis'
|
||||
import {
|
||||
PluginFlags as Flag,
|
||||
PluginFlags,
|
||||
PluginStatus as Status,
|
||||
} from '../constants'
|
||||
import { PluginFlags as Flag, PluginStatus as Status } from '../constants'
|
||||
import {
|
||||
addPluginApiDecorator,
|
||||
decoratePluginApi,
|
||||
|
|
@ -128,7 +124,7 @@ export function registerPlugin<O extends PluginApiExtensionsOptions>(
|
|||
pImplicitDeps.add(plugin)
|
||||
}
|
||||
// Only add to pending if the plugin is enabled
|
||||
else if (flags & PluginFlags.Enabled) pPending.add(plugin)
|
||||
else if (isPluginEnabled(plugin)) pPending.add(plugin)
|
||||
|
||||
pEmitter.emit('register', plugin, options)
|
||||
|
||||
|
|
@ -150,7 +146,7 @@ export function getPluginDependencies(plugin: AnyPlugin): AnyPlugin[] {
|
|||
const dep = pList.get(depId)
|
||||
|
||||
if (dep) {
|
||||
if (dep.flags & PluginFlags.Enabled) deps.push(dep)
|
||||
if (isPluginEnabled(dep)) deps.push(dep)
|
||||
else
|
||||
throw new Error(
|
||||
`Plugin "${id}" depends on disabled plugin "${depId}"`,
|
||||
|
|
@ -169,13 +165,29 @@ export function getPluginDependencies(plugin: AnyPlugin): AnyPlugin[] {
|
|||
return (meta.dependencies = deps)
|
||||
}
|
||||
|
||||
export function isPluginEnabled({ flags }: AnyPlugin): boolean {
|
||||
return Boolean(flags & Flag.Enabled)
|
||||
}
|
||||
|
||||
export function isPluginEssential({ iflags }: InternalPluginMeta): boolean {
|
||||
return Boolean(iflags & InternalPluginFlags.Essential)
|
||||
}
|
||||
|
||||
export function isPluginInternal({ iflags }: InternalPluginMeta): boolean {
|
||||
return Boolean(iflags & InternalPluginFlags.Internal)
|
||||
}
|
||||
|
||||
function guardPluginEnabled(plugin: AnyPlugin) {
|
||||
if (!isPluginEnabled(plugin))
|
||||
throw new Error(`Plugin "${plugin.manifest.id}" is not enabled`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles errors that occur in plugins.
|
||||
*/
|
||||
async function handlePluginError(e: unknown, plugin: AnyPlugin) {
|
||||
plugin.errors.push(e)
|
||||
plugin.flags |= Flag.Errored
|
||||
const { iflags } = pMetadata.get(plugin)!
|
||||
|
||||
nativeLoggingHook(
|
||||
`\u001b[31mPlugin "${plugin.manifest.id}" encountered an error: ${getErrorStack(e)}\u001b[0m`,
|
||||
|
|
@ -185,14 +197,15 @@ async function handlePluginError(e: unknown, plugin: AnyPlugin) {
|
|||
plugin.api?.logger?.error('Plugin encountered an error', e)
|
||||
pEmitter.emit('errored', plugin, e)
|
||||
|
||||
if (!(iflags & InternalPluginFlags.Essential)) await plugin.disable()
|
||||
if (!isPluginEssential(pMetadata.get(plugin)!)) await plugin.disable()
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the plugin API for the preInit lifecycle.
|
||||
*/
|
||||
function preparePluginPreInit(plugin: AnyPlugin) {
|
||||
function tryPreparePluginPreInit(plugin: AnyPlugin) {
|
||||
const meta = pMetadata.get(plugin)!
|
||||
if (meta.apiLevel >= PluginApiLevel.PreInit) return
|
||||
|
||||
// Clear errors from previous runs
|
||||
plugin.errors = []
|
||||
|
|
@ -216,8 +229,10 @@ function preparePluginPreInit(plugin: AnyPlugin) {
|
|||
/**
|
||||
* Prepares the plugin API for the init lifecycle.
|
||||
*/
|
||||
function preparePluginInit(plugin: AnyPlugin) {
|
||||
function tryPreparePluginInit(plugin: AnyPlugin) {
|
||||
const meta = pMetadata.get(plugin)!
|
||||
if (meta.apiLevel >= PluginApiLevel.Init) return
|
||||
|
||||
const api = plugin.api as InitPluginApi
|
||||
|
||||
api.decorate = decorator => {
|
||||
|
|
@ -231,8 +246,10 @@ function preparePluginInit(plugin: AnyPlugin) {
|
|||
/**
|
||||
* Prepares the plugin API for the start lifecycle.
|
||||
*/
|
||||
function preparePluginStart(plugin: AnyPlugin) {
|
||||
function tryPreparePluginStart(plugin: AnyPlugin) {
|
||||
const meta = pMetadata.get(plugin)!
|
||||
if (meta.apiLevel >= PluginApiLevel.Start) return
|
||||
|
||||
const api = plugin.api as PluginApi
|
||||
|
||||
api.decorate = decorator => {
|
||||
|
|
@ -244,18 +261,26 @@ function preparePluginStart(plugin: AnyPlugin) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Disables a plugin.
|
||||
* Disables a plugin, as well as all its dependents.
|
||||
*/
|
||||
export async function disablePlugin(plugin: AnyPlugin) {
|
||||
if (!(plugin.flags & Flag.Enabled))
|
||||
throw new Error(`Plugin "${plugin.manifest.id}" is not enabled`)
|
||||
guardPluginEnabled(plugin)
|
||||
|
||||
const iflags = pMetadata.get(plugin)!.iflags ?? 0
|
||||
if (iflags & InternalPluginFlags.Essential)
|
||||
const meta = pMetadata.get(plugin)!
|
||||
|
||||
if (isPluginEssential(meta))
|
||||
throw new Error(
|
||||
`Plugin "${plugin.manifest.id}" is essential and cannot be disabled`,
|
||||
)
|
||||
|
||||
const { dependents } = meta
|
||||
|
||||
await Promise.all(
|
||||
dependents.map(dep => {
|
||||
if (dep.flags & Flag.Enabled) return disablePlugin(dep)
|
||||
}),
|
||||
)
|
||||
|
||||
// Stop the plugin if needed
|
||||
if (plugin.status && !(plugin.status & Status.Stopping))
|
||||
await stopPlugin(plugin)
|
||||
|
|
@ -266,55 +291,72 @@ export async function disablePlugin(plugin: AnyPlugin) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Enables a plugin.
|
||||
* Enables a plugin, as well as all its dependencies.
|
||||
*/
|
||||
export function enablePlugin(plugin: AnyPlugin, late: boolean) {
|
||||
if (plugin.flags & Flag.Enabled)
|
||||
export async function enablePlugin(plugin: AnyPlugin) {
|
||||
if (isPluginEnabled(plugin))
|
||||
throw new Error(`Plugin "${plugin.manifest.id}" is already enabled`)
|
||||
|
||||
await Promise.all(
|
||||
getPluginDependencies(plugin).map(dep => {
|
||||
if (dep.flags & Flag.Enabled) return
|
||||
return runPluginLate(dep)
|
||||
}),
|
||||
)
|
||||
|
||||
// TODO(plugins): write to storage
|
||||
plugin.flags |= Flag.Enabled
|
||||
if (late) plugin.flags |= Flag.EnabledLate
|
||||
|
||||
pEmitter.emit('enabled', plugin)
|
||||
}
|
||||
|
||||
export async function runPluginLate(plugin: AnyPlugin) {
|
||||
guardPluginEnabled(plugin)
|
||||
|
||||
if (plugin.status & Status.Started)
|
||||
throw new Error(`Plugin "${plugin.manifest.id}" is already started`)
|
||||
|
||||
plugin.flags |= Flag.EnabledLate
|
||||
|
||||
await preInitPlugin(plugin)
|
||||
await initPlugin(plugin)
|
||||
await startPlugin(plugin)
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the preInit lifecycle of a plugin.
|
||||
*/
|
||||
export async function preInitPlugin(plugin: AnyPlugin) {
|
||||
guardPluginEnabled(plugin)
|
||||
|
||||
const {
|
||||
manifest: { id },
|
||||
lifecycles,
|
||||
} = plugin
|
||||
|
||||
if (plugin.status & (Status.PreIniting | Status.PreInited))
|
||||
throw new Error(
|
||||
`Plugin preInit lifecycle for "${id}" is already running`,
|
||||
)
|
||||
|
||||
tryPreparePluginPreInit(plugin)
|
||||
|
||||
const { lifecycles } = plugin
|
||||
const { promises, handleError } = pMetadata.get(plugin)!
|
||||
|
||||
try {
|
||||
if (!lifecycles.preInit) return
|
||||
|
||||
const meta = pMetadata.get(plugin)!
|
||||
const { promises, handleError } = meta
|
||||
|
||||
if (!(plugin.flags & Flag.Enabled))
|
||||
throw new Error(`Plugin "${id}" is not enabled`)
|
||||
|
||||
if (plugin.status & (Status.PreIniting | Status.PreInited))
|
||||
throw new Error(
|
||||
`Plugin preInit lifecycle for "${id}" is already running`,
|
||||
)
|
||||
|
||||
preparePluginPreInit(plugin)
|
||||
plugin.status |= Status.PreIniting
|
||||
|
||||
try {
|
||||
const prom = lifecycles.preInit(plugin.api as PreInitPluginApi)
|
||||
promises.push(prom)
|
||||
await prom
|
||||
|
||||
// plugin.disable() already handles, so it's in the try block
|
||||
plugin.status |= Status.PreInited
|
||||
plugin.status &= ~Status.PreIniting
|
||||
} catch (e) {
|
||||
await handleError(e)
|
||||
} finally {
|
||||
plugin.status |= Status.PreInited
|
||||
plugin.status &= ~Status.PreIniting
|
||||
}
|
||||
} finally {
|
||||
pEmitter.emit('preInited', plugin)
|
||||
|
|
@ -325,40 +367,37 @@ export async function preInitPlugin(plugin: AnyPlugin) {
|
|||
* Runs the init lifecycle of a plugin.
|
||||
*/
|
||||
export async function initPlugin(plugin: AnyPlugin) {
|
||||
guardPluginEnabled(plugin)
|
||||
|
||||
const {
|
||||
manifest: { id },
|
||||
lifecycles,
|
||||
} = plugin
|
||||
|
||||
const meta = pMetadata.get(plugin)!
|
||||
|
||||
if (plugin.status & (Status.Initing | Status.Inited))
|
||||
throw new Error(`Plugin init lifecycle for "${id}" is already running`)
|
||||
|
||||
tryPreparePluginPreInit(plugin)
|
||||
tryPreparePluginInit(plugin)
|
||||
|
||||
const { lifecycles } = plugin
|
||||
const { promises, handleError } = meta
|
||||
|
||||
try {
|
||||
if (!lifecycles.init) return
|
||||
|
||||
const meta = pMetadata.get(plugin)!
|
||||
const { promises, apiLevel, handleError } = meta
|
||||
|
||||
if (!(plugin.flags & Flag.Enabled))
|
||||
throw new Error(`Plugin "${id}" is not enabled`)
|
||||
|
||||
if (plugin.status & (Status.Initing | Status.Inited))
|
||||
throw new Error(
|
||||
`Plugin init lifecycle for "${id}" is already running`,
|
||||
)
|
||||
|
||||
if (apiLevel < PluginApiLevel.PreInit) preparePluginPreInit(plugin)
|
||||
if (apiLevel < PluginApiLevel.Init) preparePluginInit(plugin)
|
||||
|
||||
plugin.status |= Status.Initing
|
||||
|
||||
try {
|
||||
const prom = lifecycles.init(plugin.api as InitPluginApi)
|
||||
promises.push(prom)
|
||||
await prom
|
||||
|
||||
// plugin.disable() already handles, so it's in the try block
|
||||
plugin.status |= Status.Inited
|
||||
plugin.status &= ~Status.Initing
|
||||
} catch (e) {
|
||||
await handleError(e)
|
||||
} finally {
|
||||
plugin.status |= Status.Inited
|
||||
plugin.status &= ~Status.Initing
|
||||
}
|
||||
} finally {
|
||||
pEmitter.emit('inited', plugin)
|
||||
|
|
@ -369,40 +408,36 @@ export async function initPlugin(plugin: AnyPlugin) {
|
|||
* Starts a plugin by running its start lifecycle.
|
||||
*/
|
||||
export async function startPlugin(plugin: AnyPlugin) {
|
||||
guardPluginEnabled(plugin)
|
||||
|
||||
const {
|
||||
manifest: { id },
|
||||
lifecycles,
|
||||
} = plugin
|
||||
|
||||
if (plugin.status & (Status.Starting | Status.Started))
|
||||
throw new Error(`Plugin start lifecycle for "${id}" is already running`)
|
||||
|
||||
tryPreparePluginPreInit(plugin)
|
||||
tryPreparePluginInit(plugin)
|
||||
tryPreparePluginStart(plugin)
|
||||
|
||||
const { lifecycles } = plugin
|
||||
const { promises, handleError } = pMetadata.get(plugin)!
|
||||
|
||||
try {
|
||||
if (!lifecycles.start) return
|
||||
|
||||
const meta = pMetadata.get(plugin)!
|
||||
const { promises, apiLevel, handleError } = meta
|
||||
|
||||
if (!(plugin.flags & Flag.Enabled))
|
||||
throw new Error(`Plugin "${id}" is not enabled`)
|
||||
|
||||
if (plugin.status & (Status.Starting | Status.Started))
|
||||
throw new Error(
|
||||
`Plugin start lifecycle for "${id}" is already running`,
|
||||
)
|
||||
|
||||
if (apiLevel < PluginApiLevel.PreInit) preparePluginPreInit(plugin)
|
||||
if (apiLevel < PluginApiLevel.Init) preparePluginInit(plugin)
|
||||
if (apiLevel < PluginApiLevel.Start) preparePluginStart(plugin)
|
||||
|
||||
plugin.status |= Status.Starting
|
||||
|
||||
try {
|
||||
const prom = lifecycles.start(plugin.api as PluginApi)
|
||||
promises.push(prom)
|
||||
await prom
|
||||
|
||||
plugin.status |= Status.Started
|
||||
plugin.status &= ~Status.Starting
|
||||
} catch (e) {
|
||||
await handleError(e)
|
||||
} finally {
|
||||
plugin.status |= Status.Started
|
||||
plugin.status &= ~Status.Starting
|
||||
}
|
||||
} finally {
|
||||
pEmitter.emit('started', plugin)
|
||||
|
|
@ -413,22 +448,23 @@ export async function startPlugin(plugin: AnyPlugin) {
|
|||
* Stops a plugin by running its stop lifecycle and cleanup functions.
|
||||
*/
|
||||
export async function stopPlugin(plugin: AnyPlugin) {
|
||||
guardPluginEnabled(plugin)
|
||||
|
||||
const {
|
||||
manifest: { id },
|
||||
lifecycles,
|
||||
} = plugin
|
||||
|
||||
const meta = pMetadata.get(plugin)!
|
||||
const { promises, iflags, apiLevel } = meta
|
||||
|
||||
if (iflags & InternalPluginFlags.Essential)
|
||||
if (isPluginEssential(meta))
|
||||
throw new Error(`Plugin "${id}" is essential and cannot be stopped`)
|
||||
|
||||
if (!(plugin.flags & Flag.Enabled))
|
||||
throw new Error(`Plugin "${id}" is not enabled`)
|
||||
|
||||
if (plugin.status & Status.Stopping)
|
||||
throw new Error(`Plugin "${id}" is already stopping`)
|
||||
|
||||
const { lifecycles } = plugin
|
||||
const { promises, handleError } = meta
|
||||
|
||||
// Wait for in-progress lifecycles to finish or timeout
|
||||
if (plugin.status & (Status.PreIniting | Status.Initing | Status.Starting))
|
||||
await Promise.race([
|
||||
|
|
@ -439,18 +475,13 @@ export async function stopPlugin(plugin: AnyPlugin) {
|
|||
),
|
||||
]).catch(e => {
|
||||
plugin.flags |= Flag.ReloadRequired
|
||||
handlePluginError(e, plugin)
|
||||
return handlePluginError(e, plugin)
|
||||
})
|
||||
else if (
|
||||
!(plugin.status & (Status.PreInited | Status.Inited | Status.Started))
|
||||
)
|
||||
throw new Error(`Plugin "${id}" is not running`)
|
||||
|
||||
// Prepare APIs if needed
|
||||
if (apiLevel < PluginApiLevel.PreInit) preparePluginPreInit(plugin)
|
||||
if (apiLevel < PluginApiLevel.Init) preparePluginInit(plugin)
|
||||
if (apiLevel < PluginApiLevel.Start) preparePluginStart(plugin)
|
||||
|
||||
plugin.status |= Status.Stopping
|
||||
|
||||
try {
|
||||
|
|
@ -463,7 +494,7 @@ export async function stopPlugin(plugin: AnyPlugin) {
|
|||
),
|
||||
])
|
||||
} catch (e) {
|
||||
await handlePluginError(e, plugin)
|
||||
await handleError(e)
|
||||
} finally {
|
||||
// Run cleanups
|
||||
await cleanupPlugin(plugin, meta)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { initPlugin } from './_internal'
|
||||
import { initPlugin, isPluginEnabled } from './_internal'
|
||||
import { computePendingNodes, pListOrdered } from './_internal/dependency-graph'
|
||||
import { PluginFlags } from './constants'
|
||||
|
||||
computePendingNodes()
|
||||
|
||||
for (const plugin of pListOrdered)
|
||||
if (plugin.flags & PluginFlags.Enabled) initPlugin(plugin)
|
||||
if (isPluginEnabled(plugin)) initPlugin(plugin)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { preInitPlugin } from './_internal'
|
||||
import { isPluginEnabled, preInitPlugin } from './_internal'
|
||||
import { computePendingNodes, pListOrdered } from './_internal/dependency-graph'
|
||||
import { PluginFlags } from './constants'
|
||||
|
||||
computePendingNodes()
|
||||
|
||||
for (const plugin of pListOrdered)
|
||||
if (plugin.flags & PluginFlags.Enabled) preInitPlugin(plugin)
|
||||
if (isPluginEnabled(plugin)) preInitPlugin(plugin)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { startPlugin } from './_internal'
|
||||
import { isPluginEnabled, startPlugin } from './_internal'
|
||||
import { computePendingNodes, pListOrdered } from './_internal/dependency-graph'
|
||||
import { PluginFlags } from './constants'
|
||||
|
||||
computePendingNodes()
|
||||
|
||||
|
|
@ -11,4 +10,4 @@ if (__DEV__)
|
|||
)
|
||||
|
||||
for (const plugin of pListOrdered)
|
||||
if (plugin.flags & PluginFlags.Enabled) startPlugin(plugin)
|
||||
if (isPluginEnabled(plugin)) startPlugin(plugin)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,11 @@
|
|||
import { getAssetIdByName } from '@revenge-mod/assets'
|
||||
import { Tokens } from '@revenge-mod/discord/common'
|
||||
import { Stores } from '@revenge-mod/discord/common/flux'
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { Image } from 'react-native'
|
||||
import type { DiscordModules } from '@revenge-mod/discord/types'
|
||||
|
||||
const { NavigatorHeader } = Design
|
||||
const { NavigatorHeader, createStyles } = Design
|
||||
const { default: tokens } = Tokens
|
||||
|
||||
const ThemeStore = Stores.ThemeStore as DiscordModules.Flux.Store<{
|
||||
theme: string
|
||||
}>
|
||||
|
||||
interface NavigatorHeaderWithIconProps {
|
||||
title: string
|
||||
icon: string
|
||||
|
|
@ -21,23 +15,23 @@ export default function NavigatorHeaderWithIcon({
|
|||
title,
|
||||
icon,
|
||||
}: NavigatorHeaderWithIconProps) {
|
||||
const styles = useNavigatorHeaderStyles()
|
||||
|
||||
return (
|
||||
<NavigatorHeader
|
||||
icon={
|
||||
<Image
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
marginEnd: 8,
|
||||
tintColor: tokens.internal.resolveSemanticColor(
|
||||
ThemeStore.theme,
|
||||
tokens.colors.HEADER_PRIMARY,
|
||||
),
|
||||
}}
|
||||
source={getAssetIdByName(icon!)}
|
||||
/>
|
||||
<Image style={styles.header} source={getAssetIdByName(icon!)} />
|
||||
}
|
||||
title={title}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const useNavigatorHeaderStyles = createStyles({
|
||||
header: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
marginEnd: 8,
|
||||
tintColor: tokens.colors.HEADER_PRIMARY,
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,66 +1,71 @@
|
|||
import { useNavigation } from '@react-navigation/native'
|
||||
import { getAssetIdByName } from '@revenge-mod/assets'
|
||||
import { styles } from '@revenge-mod/components/_'
|
||||
import FormSwitch from '@revenge-mod/components/FormSwitch'
|
||||
import { AlertActionCreators } 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 {
|
||||
enablePlugin,
|
||||
InternalPluginFlags,
|
||||
initPlugin,
|
||||
preInitPlugin,
|
||||
startPlugin,
|
||||
getPluginDependencies,
|
||||
isPluginEnabled,
|
||||
isPluginEssential,
|
||||
runPluginLate,
|
||||
} 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 {
|
||||
Image,
|
||||
PixelRatio,
|
||||
Pressable,
|
||||
useWindowDimensions,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import { PluginFlashList } from './PluginList'
|
||||
import PluginStatesProvider, { usePluginEnabled } from './PluginStateProvider'
|
||||
import {
|
||||
enableTooltipTarget,
|
||||
essentialTooltipTarget,
|
||||
resetTooltips,
|
||||
setEnablePluginTooltipVisible,
|
||||
setEssentialPluginTooltipVisible,
|
||||
useClickOutsideTooltip,
|
||||
} from './TooltipProvider'
|
||||
import type { AnyPlugin, InternalPluginMeta } 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 const InstalledPluginCard = memo(function InstalledPluginCard({
|
||||
plugin,
|
||||
meta,
|
||||
rightGap,
|
||||
export const PLUGIN_CARD_ESTIMATED_SIZE = 116
|
||||
|
||||
export const PluginCard = memo(function PluginCard({
|
||||
name,
|
||||
description,
|
||||
author,
|
||||
icon,
|
||||
actions,
|
||||
rowEnd,
|
||||
columnEnd,
|
||||
}: {
|
||||
plugin: AnyPlugin
|
||||
meta: InternalPluginMeta
|
||||
rightGap?: boolean
|
||||
name: string
|
||||
description: string
|
||||
author: string
|
||||
icon?: string
|
||||
actions?: React.ReactNode
|
||||
rowEnd?: boolean
|
||||
columnEnd?: boolean
|
||||
}) {
|
||||
const navigation = ReactNavigationNative.useNavigation()
|
||||
const enabled = usePluginEnabled(plugin)
|
||||
|
||||
const {
|
||||
manifest: { name, description, author, icon },
|
||||
} = plugin
|
||||
|
||||
const essential = Boolean(meta.iflags & InternalPluginFlags.Essential)
|
||||
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]}>
|
||||
<Card
|
||||
style={[
|
||||
styles_.card,
|
||||
styles.grow,
|
||||
columnEnd && styles_.columnEnd,
|
||||
rowEnd && styles_.rowEnd,
|
||||
]}
|
||||
>
|
||||
<Stack
|
||||
direction="horizontal"
|
||||
style={[styles.grow, styles_.topContainer]}
|
||||
|
|
@ -76,62 +81,7 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({
|
|||
/>
|
||||
<Text variant="heading-lg/semibold">{name}</Text>
|
||||
</Stack>
|
||||
{plugin.SettingsComponent && (
|
||||
<Pressable
|
||||
onPress={e => {
|
||||
if (enabled) return
|
||||
|
||||
e.stopPropagation()
|
||||
resetTooltips()
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
enableTooltipTarget.current =
|
||||
settingsRef.current
|
||||
setEnablePluginTooltipVisible?.(true)
|
||||
})
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
ref={settingsRef}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={SettingsIcon}
|
||||
onPress={() => {
|
||||
navigation.navigate(plugin.manifest.id)
|
||||
}}
|
||||
disabled={!enabled}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
<Pressable
|
||||
onPress={e => {
|
||||
if (essential) {
|
||||
e.stopPropagation()
|
||||
resetTooltips()
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
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()
|
||||
}}
|
||||
value={enabled}
|
||||
/>
|
||||
</Pressable>
|
||||
{actions}
|
||||
</Stack>
|
||||
<Stack spacing={4} style={[styles_.alignedContainer, styles.grow]}>
|
||||
<Text
|
||||
|
|
@ -149,6 +99,265 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({
|
|||
)
|
||||
})
|
||||
|
||||
export const InstalledPluginCard = memo(function InstalledPluginCard({
|
||||
plugin,
|
||||
meta,
|
||||
rowEnd,
|
||||
columnEnd,
|
||||
}: {
|
||||
plugin: AnyPlugin
|
||||
meta: InternalPluginMeta
|
||||
rowEnd?: boolean
|
||||
columnEnd?: boolean
|
||||
}) {
|
||||
const navigation = useNavigation()
|
||||
const enabled = usePluginEnabled(plugin)
|
||||
|
||||
const {
|
||||
manifest: { name, description, author, icon },
|
||||
} = plugin
|
||||
|
||||
const dependencies = getPluginDependencies(plugin)
|
||||
const essential = isPluginEssential(meta)
|
||||
const { dependents } = meta
|
||||
|
||||
const settingsRef = useClickOutsideTooltip(enableTooltipTarget, () => {
|
||||
setEnablePluginTooltipVisible?.(false)
|
||||
})
|
||||
|
||||
const switchRef = useClickOutsideTooltip(essentialTooltipTarget, () => {
|
||||
setEssentialPluginTooltipVisible?.(false)
|
||||
})
|
||||
|
||||
return (
|
||||
<PluginCard
|
||||
name={name}
|
||||
description={description}
|
||||
author={author}
|
||||
icon={icon}
|
||||
rowEnd={rowEnd}
|
||||
columnEnd={columnEnd}
|
||||
actions={
|
||||
<>
|
||||
{plugin.SettingsComponent && (
|
||||
<Pressable
|
||||
onPress={e => {
|
||||
if (enabled) return
|
||||
|
||||
e.stopPropagation()
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
enableTooltipTarget.current =
|
||||
settingsRef.current
|
||||
setEnablePluginTooltipVisible!(true)
|
||||
})
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
ref={settingsRef}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={SettingsIcon}
|
||||
onPress={() => {
|
||||
navigation.navigate(plugin.manifest.id)
|
||||
}}
|
||||
disabled={!enabled}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
<Pressable
|
||||
onPress={e => {
|
||||
if (essential) {
|
||||
e.stopPropagation()
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
essentialTooltipTarget.current =
|
||||
switchRef.current
|
||||
setEssentialPluginTooltipVisible!(true)
|
||||
})
|
||||
}
|
||||
}}
|
||||
ref={switchRef}
|
||||
>
|
||||
<FormSwitch
|
||||
disabled={essential}
|
||||
onValueChange={enabled => {
|
||||
if (enabled)
|
||||
return handleEnablePlugin(
|
||||
plugin,
|
||||
dependencies,
|
||||
)
|
||||
else
|
||||
return handleDisablePlugin(
|
||||
plugin,
|
||||
dependents,
|
||||
)
|
||||
}}
|
||||
value={enabled}
|
||||
/>
|
||||
</Pressable>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
async function handleEnablePlugin(
|
||||
plugin: AnyPlugin,
|
||||
dependencies: AnyPlugin[],
|
||||
) {
|
||||
const disabledDeps = dependencies.filter(dep => !isPluginEnabled(dep))
|
||||
|
||||
async function action() {
|
||||
await enablePlugin(plugin)
|
||||
await runPluginLate(plugin)
|
||||
}
|
||||
|
||||
if (disabledDeps.length)
|
||||
AlertActionCreators.openAlert(
|
||||
'plugin-has-dependencies',
|
||||
<PluginStatesProvider>
|
||||
<PluginHasDependenciesAlert
|
||||
plugin={plugin}
|
||||
dependencies={disabledDeps}
|
||||
action={action}
|
||||
/>
|
||||
</PluginStatesProvider>,
|
||||
)
|
||||
else await action()
|
||||
}
|
||||
|
||||
async function handleDisablePlugin(plugin: AnyPlugin, dependents: AnyPlugin[]) {
|
||||
const action = plugin.disable
|
||||
|
||||
const enabledDeps = dependents.filter(isPluginEnabled)
|
||||
|
||||
if (enabledDeps.length)
|
||||
AlertActionCreators.openAlert(
|
||||
'plugin-has-dependents',
|
||||
<PluginStatesProvider>
|
||||
<PluginHasDependentsAlert
|
||||
plugin={plugin}
|
||||
dependents={enabledDeps}
|
||||
action={action}
|
||||
/>
|
||||
</PluginStatesProvider>,
|
||||
)
|
||||
else await action()
|
||||
}
|
||||
|
||||
function PluginHasDependenciesAlert({
|
||||
plugin,
|
||||
dependencies,
|
||||
action,
|
||||
}: {
|
||||
plugin: AnyPlugin
|
||||
dependencies: AnyPlugin[]
|
||||
action: () => Promise<void>
|
||||
}) {
|
||||
const { height: windowHeight } = useWindowDimensions()
|
||||
const maxHeight = PixelRatio.get() * windowHeight * 0.35 - 64
|
||||
|
||||
return (
|
||||
<Design.AlertModal
|
||||
title="Plugin has dependencies"
|
||||
content={
|
||||
<Text color="header-secondary">
|
||||
Plugin{' '}
|
||||
<Text variant="text-md/semibold" color="text-normal">
|
||||
{plugin.manifest.name}
|
||||
</Text>{' '}
|
||||
depends on other plugins to function.
|
||||
{'\n'}
|
||||
Enabling it will also enable the following plugins:
|
||||
</Text>
|
||||
}
|
||||
extraContent={
|
||||
<View
|
||||
style={{
|
||||
height: Math.min(
|
||||
PLUGIN_CARD_ESTIMATED_SIZE * dependencies.length,
|
||||
maxHeight,
|
||||
),
|
||||
maxHeight,
|
||||
}}
|
||||
>
|
||||
<PluginFlashList plugins={dependencies} />
|
||||
</View>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<Design.AlertActionButton
|
||||
onPress={action}
|
||||
text="Enable all"
|
||||
variant="primary"
|
||||
/>
|
||||
<Design.AlertActionButton
|
||||
text="Cancel"
|
||||
variant="secondary"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PluginHasDependentsAlert({
|
||||
plugin,
|
||||
dependents,
|
||||
action,
|
||||
}: {
|
||||
plugin: AnyPlugin
|
||||
dependents: AnyPlugin[]
|
||||
action: () => Promise<void>
|
||||
}) {
|
||||
const { height: windowHeight } = useWindowDimensions()
|
||||
const maxHeight = PixelRatio.get() * windowHeight * 0.35 - 64
|
||||
|
||||
return (
|
||||
<Design.AlertModal
|
||||
title="Plugin has dependents"
|
||||
content={
|
||||
<Text color="header-secondary">
|
||||
Other plugins depend on{' '}
|
||||
<Text variant="text-md/semibold" color="text-normal">
|
||||
{plugin.manifest.name}
|
||||
</Text>{' '}
|
||||
to function.
|
||||
{'\n'}
|
||||
Disabling it will also disable the following plugins:
|
||||
</Text>
|
||||
}
|
||||
extraContent={
|
||||
<View
|
||||
style={{
|
||||
height: Math.min(
|
||||
PLUGIN_CARD_ESTIMATED_SIZE * dependents.length,
|
||||
maxHeight,
|
||||
),
|
||||
maxHeight,
|
||||
}}
|
||||
>
|
||||
<PluginFlashList plugins={dependents} />
|
||||
</View>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<Design.AlertActionButton
|
||||
text="Disable all"
|
||||
variant="destructive"
|
||||
onPress={action}
|
||||
/>
|
||||
<Design.AlertActionButton
|
||||
text="Cancel"
|
||||
variant="secondary"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const usePluginCardStyles = createStyles({
|
||||
icon: {
|
||||
width: 20,
|
||||
|
|
@ -156,14 +365,18 @@ const usePluginCardStyles = createStyles({
|
|||
tintColor: Tokens.default.colors.TEXT_NORMAL,
|
||||
},
|
||||
card: {
|
||||
marginBottom: 12,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12,
|
||||
gap: 4,
|
||||
},
|
||||
rightGap: {
|
||||
marginBottom: 12,
|
||||
marginRight: 12,
|
||||
},
|
||||
rowEnd: {
|
||||
marginBottom: 0,
|
||||
},
|
||||
columnEnd: {
|
||||
marginRight: 0,
|
||||
},
|
||||
topContainer: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
|
|
|||
80
src/plugins/start/settings/components/PluginList.tsx
Normal file
80
src/plugins/start/settings/components/PluginList.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { FlashList, MasonryFlashList } from '@shopify/flash-list'
|
||||
import { useWindowDimensions } from 'react-native'
|
||||
import {
|
||||
InstalledPluginCard,
|
||||
PLUGIN_CARD_ESTIMATED_SIZE,
|
||||
PluginCard,
|
||||
} from './PluginCard'
|
||||
import { resetTooltips } from './TooltipProvider'
|
||||
import type { AnyPlugin, InternalPluginMeta } from '@revenge-mod/plugins/_'
|
||||
|
||||
const { Text } = Design
|
||||
|
||||
export function PluginFlashList({ plugins }: { plugins: AnyPlugin[] }) {
|
||||
return (
|
||||
<FlashList
|
||||
data={plugins}
|
||||
onScrollBeginDrag={resetTooltips}
|
||||
fadingEdgeLength={plugins.length === 1 ? 0 : 16}
|
||||
keyExtractor={plugin => plugin.manifest.id}
|
||||
estimatedItemSize={PLUGIN_CARD_ESTIMATED_SIZE}
|
||||
renderItem={({
|
||||
item: {
|
||||
manifest: { name, description, author, icon },
|
||||
},
|
||||
index,
|
||||
}) => (
|
||||
<PluginCard
|
||||
name={name}
|
||||
description={description}
|
||||
author={author}
|
||||
icon={icon}
|
||||
rowEnd={index === plugins.length - 1}
|
||||
columnEnd
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function InstalledPluginMasonryFlashList({
|
||||
plugins,
|
||||
}: {
|
||||
plugins: (readonly [AnyPlugin, InternalPluginMeta])[]
|
||||
}) {
|
||||
const { width } = useWindowDimensions()
|
||||
const numColumns = Math.floor((width - 16) / 448)
|
||||
|
||||
return (
|
||||
<MasonryFlashList
|
||||
contentContainerStyle={{ paddingBottom: 16 }}
|
||||
data={plugins}
|
||||
onScrollBeginDrag={resetTooltips}
|
||||
fadingEdgeLength={16}
|
||||
keyExtractor={([plugin]) => plugin.manifest.id}
|
||||
estimatedItemSize={PLUGIN_CARD_ESTIMATED_SIZE}
|
||||
numColumns={numColumns}
|
||||
ListEmptyComponent={NoPlugins}
|
||||
renderItem={({ item: [plugin, meta], columnIndex, index }) => (
|
||||
<InstalledPluginCard
|
||||
plugin={plugin}
|
||||
meta={meta}
|
||||
rowEnd={
|
||||
index > plugins.length - numColumns &&
|
||||
columnIndex <= plugins.length % numColumns
|
||||
}
|
||||
columnEnd={columnIndex === numColumns - 1}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function NoPlugins() {
|
||||
return (
|
||||
<Text variant="heading-md/medium" style={{ textAlign: 'center' }}>
|
||||
No plugins found. Try changing your query or filters.
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { pEmitter, pList } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import { isPluginEnabled, pEmitter, pList } from '@revenge-mod/plugins/_'
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import type { AnyPlugin } from '@revenge-mod/plugins/_'
|
||||
import type { ReactNode } from 'react'
|
||||
|
|
@ -20,9 +19,7 @@ export default function PluginStatesProvider({
|
|||
const [enabled, setEnabled] = useState<PluginEnabledStates>(() => {
|
||||
const states: PluginEnabledStates = {}
|
||||
for (const plugin of pList.values()) {
|
||||
states[plugin.manifest.id] = Boolean(
|
||||
plugin.flags & PluginFlags.Enabled,
|
||||
)
|
||||
states[plugin.manifest.id] = isPluginEnabled(plugin)
|
||||
}
|
||||
return states
|
||||
})
|
||||
|
|
@ -70,7 +67,7 @@ export function usePluginEnabled(plugin: AnyPlugin) {
|
|||
|
||||
if (!context) {
|
||||
console.warn('usePluginEnabled not used within a PluginStateProvider')
|
||||
return Boolean(plugin.flags & PluginFlags.Enabled)
|
||||
return isPluginEnabled(plugin)
|
||||
}
|
||||
|
||||
return context.enabled[plugin.manifest.id]
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useClickOutside } from 'react-native-click-outside'
|
||||
import type { RefObject } from 'react'
|
||||
import type { View } from 'react-native'
|
||||
|
||||
|
|
@ -72,3 +73,15 @@ export function resetTooltips() {
|
|||
setEnablePluginTooltipVisible?.(false)
|
||||
setEssentialPluginTooltipVisible?.(false)
|
||||
}
|
||||
|
||||
export function useClickOutsideTooltip(
|
||||
target: RefObject<View | null>,
|
||||
onClickOutside: () => void,
|
||||
) {
|
||||
const ref = useClickOutside<View>(() => {
|
||||
resetTooltips()
|
||||
if (target.current === ref.current) onClickOutside()
|
||||
})
|
||||
|
||||
return ref
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
|
||||
import { Setting } from '../constants'
|
||||
import { CopyableSetting } from './shared'
|
||||
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
|
||||
|
||||
// @ts-expect-error
|
||||
const props = HermesInternal.getRuntimeProperties()
|
||||
|
||||
const HermesVersionSetting: SettingsItem = {
|
||||
parent: Setting.Revenge,
|
||||
IconComponent: () => <TableRowAssetIcon name="TranscriptOutlineIcon" />,
|
||||
title: () => 'Hermes',
|
||||
useDescription: () => `${props['Bytecode Version']} (${props.Build})`,
|
||||
type: 'static',
|
||||
}
|
||||
const HermesVersionSetting: SettingsItem = CopyableSetting(
|
||||
{
|
||||
parent: Setting.Revenge,
|
||||
IconComponent: () => <TableRowAssetIcon name="TranscriptOutlineIcon" />,
|
||||
title: () => 'Hermes',
|
||||
},
|
||||
() => `${props['Bytecode Version']} (${props.Build})`,
|
||||
)
|
||||
|
||||
export default HermesVersionSetting
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
|
|||
import { AlertActionCreators } from '@revenge-mod/discord/actions'
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { BundleUpdaterManager } from '@revenge-mod/discord/native'
|
||||
import { pEmitter, pList } from '@revenge-mod/plugins/_'
|
||||
import { isPluginEnabled, pEmitter, pList } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import { useReRender } from '@revenge-mod/utils/react'
|
||||
import { useEffect } from 'react'
|
||||
|
|
@ -25,8 +25,7 @@ const RevengePluginsSetting: SettingsItem = {
|
|||
|
||||
let enabledCount = 0
|
||||
|
||||
for (const plugin of pList.values())
|
||||
if (plugin.flags & PluginFlags.Enabled) enabledCount++
|
||||
for (const plugin of pList.values()) if (isPluginEnabled(plugin)) enabledCount++
|
||||
|
||||
pEmitter.on('disabled', () => {
|
||||
enabledCount--
|
||||
|
|
@ -58,34 +57,54 @@ pEmitter.on('started', showReloadRequiredAlertIfNeeded)
|
|||
pEmitter.on('stopped', showReloadRequiredAlertIfNeeded)
|
||||
|
||||
function showReloadRequiredAlertIfNeeded(plugin: AnyPlugin) {
|
||||
if (plugin.flags & PluginFlags.ReloadRequired)
|
||||
if (plugin.flags & PluginFlags.ReloadRequired) {
|
||||
const plugins = [...pList.values()].filter(
|
||||
plugin => plugin.flags & PluginFlags.ReloadRequired,
|
||||
)
|
||||
|
||||
AlertActionCreators.openAlert(
|
||||
'plugin-reload-required',
|
||||
<AlertModal
|
||||
title="Reload required"
|
||||
content={
|
||||
<Text variant="text-md/medium" color="header-secondary">
|
||||
Plugin{' '}
|
||||
<Text variant="text-md/bold" color="header-secondary">
|
||||
{plugin.manifest.name}
|
||||
</Text>{' '}
|
||||
requires a reload to apply changes.
|
||||
</Text>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<AlertActionButton
|
||||
variant="destructive"
|
||||
text="Reload"
|
||||
onPress={() => {
|
||||
BundleUpdaterManager.reload()
|
||||
}}
|
||||
/>
|
||||
<AlertActionButton variant="secondary" text="Not now" />
|
||||
</>
|
||||
}
|
||||
/>,
|
||||
<PluginReloadRequiredAlert plugins={plugins} />,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function PluginReloadRequiredAlert({ plugins }: { plugins: AnyPlugin[] }) {
|
||||
return (
|
||||
<AlertModal
|
||||
title="Reload required"
|
||||
content={
|
||||
<Text variant="text-md/medium" color="header-secondary">
|
||||
The following plugins require a reload to apply changes:
|
||||
{'\n'}
|
||||
{plugins.map(plugin => (
|
||||
<>
|
||||
<Text
|
||||
key={plugin.manifest.id}
|
||||
variant="text-md/bold"
|
||||
color="text-normal"
|
||||
>
|
||||
{plugin.manifest.name}
|
||||
</Text>
|
||||
{', '}
|
||||
</>
|
||||
))}
|
||||
</Text>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<AlertActionButton
|
||||
variant="destructive"
|
||||
text="Reload"
|
||||
onPress={() => {
|
||||
BundleUpdaterManager.reload()
|
||||
}}
|
||||
/>
|
||||
<AlertActionButton variant="secondary" text="Not now" />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default RevengePluginsSetting
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ const pluginSettings = registerPlugin(
|
|||
},
|
||||
},
|
||||
PluginFlags.Enabled,
|
||||
InternalPluginFlags.Internal | InternalPluginFlags.Essential,
|
||||
InternalPluginFlags.Internal,
|
||||
)
|
||||
|
||||
function patchSettingsNavigator(exports: any) {
|
||||
|
|
|
|||
|
|
@ -1,32 +1,35 @@
|
|||
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 { ReactNavigationNative } from '@revenge-mod/externals/react-navigation'
|
||||
import { FlashList } from '@revenge-mod/externals/shopify'
|
||||
import { InternalPluginFlags, pList, pMetadata } from '@revenge-mod/plugins/_'
|
||||
import {
|
||||
isPluginEnabled,
|
||||
isPluginEssential,
|
||||
isPluginInternal,
|
||||
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 { View } from 'react-native'
|
||||
import { ClickOutsideProvider } from 'react-native-click-outside'
|
||||
import RevengeIcon from '~assets/RevengeIcon'
|
||||
import { InstalledPluginCard } from '../components/PluginCard'
|
||||
import { InstalledPluginMasonryFlashList } from '../components/PluginList'
|
||||
import PluginStatesProvider from '../components/PluginStateProvider'
|
||||
import {
|
||||
EnablePluginTooltipProvider,
|
||||
EssentialPluginTooltipProvider,
|
||||
resetTooltips,
|
||||
} from '../components/TooltipProvider'
|
||||
import type { RouteProp } from '@react-navigation/core'
|
||||
import type { ReactNavigationParamList } from '@revenge-mod/externals/react-navigation'
|
||||
import type { AnyPlugin, InternalPluginMeta } from '@revenge-mod/plugins/_'
|
||||
import type { FilterAndSortActionSheetProps } from '../components/FilterAndSortActionSheet'
|
||||
import type { RouteNames, Setting } from '../constants'
|
||||
|
||||
const { Text, Stack, IconButton, LayerScope } = Design
|
||||
const { Stack, IconButton, LayerScope } = Design
|
||||
|
||||
const FiltersHorizontalIcon = getAssetIdByName('FiltersHorizontalIcon', 'png')!
|
||||
|
||||
|
|
@ -53,20 +56,19 @@ const SearchDebounceTime = 100
|
|||
const Filters: FilterAndSortActionSheetProps['filters'] = {
|
||||
Enabled: {
|
||||
icon: getAssetIdByName('CircleCheckIcon')!,
|
||||
filter: plugin => Boolean(plugin.flags & PluginFlags.Enabled),
|
||||
filter: plugin => isPluginEnabled(plugin),
|
||||
},
|
||||
Disabled: {
|
||||
icon: getAssetIdByName('CircleXIcon')!,
|
||||
filter: plugin => !(plugin.flags & PluginFlags.Enabled),
|
||||
filter: plugin => !isPluginEnabled(plugin),
|
||||
},
|
||||
Internal: {
|
||||
icon: RevengeIcon,
|
||||
filter: plugin => Boolean(plugin.flags & InternalPluginFlags.Internal),
|
||||
filter: (_, meta) => isPluginInternal(meta),
|
||||
},
|
||||
Essential: {
|
||||
icon: getAssetIdByName('StarIcon')!,
|
||||
filter: (_, meta) =>
|
||||
Boolean(meta.iflags & InternalPluginFlags.Essential),
|
||||
filter: (_, meta) => isPluginEssential(meta),
|
||||
},
|
||||
} satisfies FilterAndSortActionSheetProps['filters']
|
||||
const DefaultFilters: FilterAndSortActionSheetProps['filter'] = []
|
||||
|
|
@ -85,9 +87,9 @@ const Sorts = {
|
|||
} satisfies FilterAndSortActionSheetProps['sorts']
|
||||
|
||||
function Screen() {
|
||||
const navigation = ReactNavigationNative.useNavigation()
|
||||
const navigation = useNavigation()
|
||||
const route =
|
||||
ReactNavigationNative.useRoute<
|
||||
useRoute<
|
||||
RouteProp<
|
||||
ReactNavigationParamList,
|
||||
(typeof RouteNames)[typeof Setting.RevengePlugins]
|
||||
|
|
@ -182,44 +184,7 @@ function Screen() {
|
|||
}
|
||||
/>
|
||||
</Stack>
|
||||
<PluginMasonryFlashList plugins={plugins} />
|
||||
<InstalledPluginMasonryFlashList plugins={plugins} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function PluginMasonryFlashList({
|
||||
plugins,
|
||||
}: {
|
||||
plugins: (readonly [AnyPlugin, InternalPluginMeta])[]
|
||||
}) {
|
||||
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, meta], columnIndex }) => (
|
||||
<InstalledPluginCard
|
||||
plugin={plugin}
|
||||
meta={meta}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue