feat: plugin system improvement
- Stop plugins prior to an update - Native calls are now awaited in relevant places - External plugin install feedback - Plugins that are pending reloads now can't be started after an update
This commit is contained in:
parent
fb1a31640f
commit
fb3692bc2d
8 changed files with 258 additions and 68 deletions
|
|
@ -9,11 +9,13 @@ import {
|
|||
disablePlugin,
|
||||
InternalPluginFlags,
|
||||
isPluginEnabled,
|
||||
isPluginPendingReload,
|
||||
PluginFlags,
|
||||
pEmitter,
|
||||
pList,
|
||||
registerPlugin,
|
||||
runPluginLate,
|
||||
stopPlugin,
|
||||
} from '.'
|
||||
import type { PluginManifest, PluginOptionsFactory } from '../types'
|
||||
import type { AnyPlugin } from '.'
|
||||
|
|
@ -24,13 +26,64 @@ interface ExternalPlugin {
|
|||
internal?: boolean
|
||||
essential?: boolean
|
||||
enabledByDefault?: boolean
|
||||
/** Errors the native side already hit (eg. at boot before JS was up, after faulty update). */
|
||||
errors?: string[]
|
||||
}
|
||||
|
||||
type PluginInstallResult =
|
||||
| { error: false; plugin: ExternalPlugin }
|
||||
| { error: string }
|
||||
|
||||
export function registerExternalPlugins() {
|
||||
registerJSMethod(
|
||||
'revenge.plugins.loader.stopPlugin',
|
||||
async function stopHandler(id: string) {
|
||||
const plugin = pList.get(id)
|
||||
if (plugin?.status) await stopPlugin(plugin)
|
||||
},
|
||||
)
|
||||
|
||||
registerJSMethod(
|
||||
'revenge.plugins.loader.pluginInstalled',
|
||||
(external: ExternalPlugin) => {
|
||||
loadExternalPlugin(external)
|
||||
async function installedHandler(result: PluginInstallResult) {
|
||||
if (result.error !== false) {
|
||||
pEmitter.emit('install', { error: result.error })
|
||||
return
|
||||
}
|
||||
|
||||
const { plugin } = result
|
||||
const existing = pList.get(plugin.manifest.id)
|
||||
|
||||
try {
|
||||
if (existing?.status) {
|
||||
// Should already be stopped by this point, but just in case the native side didn't await
|
||||
await stopPlugin(existing)
|
||||
}
|
||||
|
||||
const pendingReload =
|
||||
existing !== undefined && isPluginPendingReload(existing)
|
||||
|
||||
if (existing) pList.delete(plugin.manifest.id)
|
||||
|
||||
if (pendingReload) {
|
||||
// Since reload is required to (un)apply changes, we don't load the new plugin yet
|
||||
// We'll do the next time the app is started
|
||||
const registered = pList.get(
|
||||
registerExternalPlugin(plugin).id,
|
||||
)!
|
||||
|
||||
registered.flags |= PluginFlags.PendingUpdate
|
||||
} else await loadExternalPlugin(plugin)
|
||||
} catch (e) {
|
||||
pEmitter.emit('install', { error: getErrorStack(e) })
|
||||
return
|
||||
}
|
||||
|
||||
pEmitter.emit('install', {
|
||||
error: false,
|
||||
manifest: plugin.manifest,
|
||||
updated: existing !== undefined,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -53,20 +106,26 @@ export function registerExternalPlugins() {
|
|||
}
|
||||
}
|
||||
|
||||
export function registerExternalPlugin({
|
||||
manifest,
|
||||
script,
|
||||
internal,
|
||||
essential,
|
||||
enabledByDefault,
|
||||
}: ExternalPlugin) {
|
||||
return registerPlugin(
|
||||
export function registerExternalPlugin(external: ExternalPlugin) {
|
||||
const { manifest, script, internal, essential, enabledByDefault, errors } =
|
||||
external
|
||||
|
||||
const dep = registerPlugin(
|
||||
manifest,
|
||||
createOptionsFactory(script),
|
||||
essential || enabledByDefault ? PluginFlags.Enabled : 0,
|
||||
(internal ? InternalPluginFlags.Internal : 0) |
|
||||
(essential ? InternalPluginFlags.Essential : 0),
|
||||
)
|
||||
|
||||
// Sync errors the native side already caught
|
||||
if (errors?.length) {
|
||||
const plugin = pList.get(dep.id)!
|
||||
plugin.errors.push(...errors)
|
||||
plugin.flags |= PluginFlags.Errored
|
||||
}
|
||||
|
||||
return dep
|
||||
}
|
||||
|
||||
export async function loadExternalPlugin(external: ExternalPlugin) {
|
||||
|
|
@ -79,7 +138,7 @@ export async function loadExternalPlugin(external: ExternalPlugin) {
|
|||
export async function uninstallExternalPlugin(plugin: AnyPlugin) {
|
||||
if (isPluginEnabled(plugin)) await disablePlugin(plugin)
|
||||
|
||||
callBridgeMethod('revenge.plugins.loader.uninstallPlugin', [
|
||||
await callBridgeMethod('revenge.plugins.loader.uninstallPlugin', [
|
||||
plugin.manifest.id,
|
||||
])
|
||||
|
||||
|
|
|
|||
|
|
@ -46,9 +46,14 @@ const MaxWaitTime = 5000
|
|||
// This is never used in bridge, but still used internally by JS.
|
||||
export const PluginFlags = {
|
||||
Enabled: 1 << 0,
|
||||
ReloadRequired: 1 << 1,
|
||||
PendingReload: 1 << 1,
|
||||
Errored: 1 << 2,
|
||||
EnabledLate: 1 << 3,
|
||||
/**
|
||||
* Marks the plugin as pending an update. Set when {@link PluginFlags.PendingReload} is set when stopping the plugin to update.
|
||||
* This means for the update to be applied safely, the updated plugin can only be started after a reload.
|
||||
*/
|
||||
PendingUpdate: 1 << 4,
|
||||
}
|
||||
|
||||
const Flag = PluginFlags
|
||||
|
|
@ -101,8 +106,13 @@ export const pEmitter = new TypedEventEmitter<{
|
|||
stopped: [AnyPlugin]
|
||||
errored: [AnyPlugin, unknown]
|
||||
flagUpdate: [AnyPlugin]
|
||||
install: [PluginInstallEvent]
|
||||
}>()
|
||||
|
||||
export type PluginInstallEvent =
|
||||
| { error: false; manifest: PluginManifest; updated: boolean }
|
||||
| { error: string }
|
||||
|
||||
export const pList = new Map<PluginManifest['id'], AnyPlugin>()
|
||||
const pMetadata = new WeakMap<AnyPlugin, InternalPluginMeta>()
|
||||
|
||||
|
|
@ -122,7 +132,7 @@ registerJSMethod('revenge.plugins.states.update', (id, state) => {
|
|||
function pluginStateToFlags(state: PluginStateObject): number {
|
||||
return (
|
||||
(state.enabled ? Flag.Enabled : 0) |
|
||||
(state.reloadRequired ? Flag.ReloadRequired : 0) |
|
||||
(state.pendingReload ? Flag.PendingReload : 0) |
|
||||
(state.errored ? Flag.Errored : 0) |
|
||||
(state.enabledLate ? Flag.EnabledLate : 0)
|
||||
)
|
||||
|
|
@ -181,7 +191,7 @@ export function registerPlugin<O extends PluginApiExtensionsOptions>(
|
|||
disable: () => disablePlugin(plugin),
|
||||
stop: () => stopPlugin(plugin),
|
||||
requireReload: () => {
|
||||
plugin.flags |= Flag.ReloadRequired
|
||||
plugin.flags |= Flag.PendingReload
|
||||
},
|
||||
api: undefined,
|
||||
set flags(flags: number) {
|
||||
|
|
@ -257,18 +267,14 @@ export function getPluginDependencies(plugin: AnyPlugin): AnyPlugin[] {
|
|||
return (meta.dependencies = deps)
|
||||
}
|
||||
|
||||
export function isPluginEnabled(plugin: Plugin<any, any>): boolean {
|
||||
export function isPluginEnabled(plugin: AnyPlugin): boolean {
|
||||
return Boolean((plugin as AnyPlugin).flags & Flag.Enabled)
|
||||
}
|
||||
|
||||
export function isPluginEnabledLate(plugin: Plugin<any, any>): boolean {
|
||||
export function isPluginEnabledLate(plugin: AnyPlugin): boolean {
|
||||
return Boolean((plugin as AnyPlugin).flags & Flag.EnabledLate)
|
||||
}
|
||||
|
||||
export function isPluginReloadRequired(plugin: Plugin<any, any>): boolean {
|
||||
return Boolean((plugin as AnyPlugin).flags & Flag.ReloadRequired)
|
||||
}
|
||||
|
||||
export function isPluginEssential({ iflags }: InternalPluginMeta): boolean {
|
||||
return Boolean(iflags & InternalPluginFlags.Essential)
|
||||
}
|
||||
|
|
@ -277,13 +283,45 @@ export function isPluginInternal({ iflags }: InternalPluginMeta): boolean {
|
|||
return Boolean(iflags & InternalPluginFlags.Internal)
|
||||
}
|
||||
|
||||
export function isPluginErrored(plugin: Plugin<any, any>): boolean {
|
||||
export function isPluginErrored(plugin: AnyPlugin): boolean {
|
||||
return Boolean((plugin as AnyPlugin).flags & Flag.Errored)
|
||||
}
|
||||
|
||||
function guardPluginEnabled(plugin: AnyPlugin) {
|
||||
export function isPluginPendingReload(plugin: AnyPlugin): boolean {
|
||||
return Boolean((plugin as AnyPlugin).flags & Flag.PendingReload)
|
||||
}
|
||||
|
||||
export function isPluginPendingUpdate(plugin: AnyPlugin): boolean {
|
||||
return Boolean((plugin as AnyPlugin).flags & Flag.PendingUpdate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires that the plugin is in a state where it can be started, all of:
|
||||
* - Enabled
|
||||
* - Not pending an update
|
||||
*/
|
||||
function requirePluginStartableState(plugin: AnyPlugin) {
|
||||
if (!isPluginEnabled(plugin))
|
||||
throw new Error(`Plugin "${plugin.manifest.id}" is not enabled`)
|
||||
|
||||
if (isPluginPendingUpdate(plugin))
|
||||
throw new Error(
|
||||
`Plugin "${plugin.manifest.id}" is pending an update and cannot be started`,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a plugin is startable.
|
||||
*
|
||||
* @see {@link requirePluginStartableState}.
|
||||
*/
|
||||
export function isPluginStartable(plugin: AnyPlugin): boolean {
|
||||
try {
|
||||
requirePluginStartableState(plugin)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -394,7 +432,7 @@ function tryPreparePluginStart(plugin: AnyPlugin) {
|
|||
* Disables a plugin, as well as all its dependents.
|
||||
*/
|
||||
export async function disablePlugin(plugin: AnyPlugin) {
|
||||
guardPluginEnabled(plugin)
|
||||
requirePluginStartableState(plugin)
|
||||
|
||||
const meta = getInternalPluginMeta(plugin)!
|
||||
|
||||
|
|
@ -415,13 +453,13 @@ export async function disablePlugin(plugin: AnyPlugin) {
|
|||
if (plugin.status && !(plugin.status & Status.Stopping))
|
||||
await stopPlugin(plugin)
|
||||
|
||||
plugin.flags &= ~Flag.Enabled
|
||||
pEmitter.emit('disabled', plugin)
|
||||
|
||||
callBridgeMethod('revenge.plugins.states.setEnabled', [
|
||||
await callBridgeMethod('revenge.plugins.states.setEnabled', [
|
||||
plugin.manifest.id,
|
||||
false,
|
||||
])
|
||||
|
||||
plugin.flags &= ~Flag.Enabled
|
||||
pEmitter.emit('disabled', plugin)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -437,17 +475,17 @@ export async function enablePlugin(plugin: AnyPlugin) {
|
|||
}),
|
||||
)
|
||||
|
||||
plugin.flags |= Flag.Enabled
|
||||
pEmitter.emit('enabled', plugin)
|
||||
|
||||
callBridgeMethod('revenge.plugins.states.setEnabled', [
|
||||
await callBridgeMethod('revenge.plugins.states.setEnabled', [
|
||||
plugin.manifest.id,
|
||||
true,
|
||||
])
|
||||
|
||||
plugin.flags |= Flag.Enabled
|
||||
pEmitter.emit('enabled', plugin)
|
||||
}
|
||||
|
||||
export async function runPluginLate(plugin: AnyPlugin) {
|
||||
guardPluginEnabled(plugin)
|
||||
requirePluginStartableState(plugin)
|
||||
|
||||
if (plugin.status & Status.Started)
|
||||
throw new Error(`Plugin "${plugin.manifest.id}" is already started`)
|
||||
|
|
@ -466,7 +504,7 @@ export async function runPluginLate(plugin: AnyPlugin) {
|
|||
|
||||
// Plugin would already be started native-side if it was not started late
|
||||
// But on late starts (enable, fresh install), we must start the native side too
|
||||
callBridgeMethod('revenge.plugins.loader.start', [
|
||||
await callBridgeMethod('revenge.plugins.loader.start', [
|
||||
plugin.manifest.id,
|
||||
])
|
||||
|
||||
|
|
@ -482,7 +520,7 @@ export async function runPluginLate(plugin: AnyPlugin) {
|
|||
* Runs the preInit lifecycle of a plugin.
|
||||
*/
|
||||
export async function preInitPlugin(plugin: AnyPlugin) {
|
||||
guardPluginEnabled(plugin)
|
||||
requirePluginStartableState(plugin)
|
||||
|
||||
const {
|
||||
manifest: { id },
|
||||
|
|
@ -526,7 +564,7 @@ export async function preInitPlugin(plugin: AnyPlugin) {
|
|||
* Runs the init lifecycle of a plugin.
|
||||
*/
|
||||
export async function initPlugin(plugin: AnyPlugin) {
|
||||
guardPluginEnabled(plugin)
|
||||
requirePluginStartableState(plugin)
|
||||
|
||||
const {
|
||||
manifest: { id },
|
||||
|
|
@ -571,7 +609,7 @@ export async function initPlugin(plugin: AnyPlugin) {
|
|||
* Starts a plugin by running its start lifecycle.
|
||||
*/
|
||||
export async function startPlugin(plugin: AnyPlugin) {
|
||||
guardPluginEnabled(plugin)
|
||||
requirePluginStartableState(plugin)
|
||||
|
||||
const {
|
||||
manifest: { id },
|
||||
|
|
@ -615,7 +653,7 @@ 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)
|
||||
requirePluginStartableState(plugin)
|
||||
|
||||
const {
|
||||
manifest: { id },
|
||||
|
|
@ -641,7 +679,7 @@ export async function stopPlugin(plugin: AnyPlugin) {
|
|||
'Plugin lifecycles timed out, force stopping',
|
||||
),
|
||||
]).catch(e => {
|
||||
plugin.flags |= Flag.ReloadRequired
|
||||
plugin.flags |= Flag.PendingReload
|
||||
return handlePluginError(e, plugin)
|
||||
})
|
||||
else if (
|
||||
|
|
@ -683,7 +721,7 @@ export async function stopPlugin(plugin: AnyPlugin) {
|
|||
async function cleanupPlugin(plugin: AnyPlugin, meta: InternalPluginMeta) {
|
||||
async function handleStopError(e: unknown) {
|
||||
// Some cleanup was unsuccessful, so we need to reload the app
|
||||
plugin.flags |= Flag.ReloadRequired
|
||||
plugin.flags |= Flag.PendingReload
|
||||
return handlePluginError(e, plugin)
|
||||
}
|
||||
|
||||
|
|
@ -718,7 +756,7 @@ declare module '@revenge-mod/modules/native' {
|
|||
|
||||
interface PluginStateObject {
|
||||
enabled?: boolean
|
||||
reloadRequired?: boolean
|
||||
pendingReload?: boolean
|
||||
errored?: boolean
|
||||
enabledLate?: boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ 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 { isPluginEssential } from '@revenge-mod/plugins/_'
|
||||
import {
|
||||
isPluginEssential,
|
||||
isPluginPendingUpdate,
|
||||
isPluginStartable,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { memo } from 'react'
|
||||
import { Image, Pressable } from 'react-native'
|
||||
import { handleDisablePlugin, handleEnablePlugin } from '../utils/actions'
|
||||
|
|
@ -144,6 +148,10 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({
|
|||
} = plugin
|
||||
|
||||
const essential = isPluginEssential(meta)
|
||||
const startable = isPluginStartable(plugin)
|
||||
const pendingUpdate = isPluginPendingUpdate(plugin)
|
||||
|
||||
const toggleDisabled = essential || pendingUpdate
|
||||
|
||||
const enableTooltip = useEnablePluginTooltip()
|
||||
const essentialTooltip = useEssentialPluginTooltip()
|
||||
|
|
@ -175,13 +183,13 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({
|
|||
/>
|
||||
{plugin.SettingsComponent && (
|
||||
<Pressable
|
||||
disabled={!startable}
|
||||
onPress={() => {
|
||||
if (!enabled)
|
||||
requestAnimationFrame(() => {
|
||||
enableTooltip.targetRef.current =
|
||||
settingsRef.current
|
||||
enableTooltip.setVisible(true)
|
||||
})
|
||||
requestAnimationFrame(() => {
|
||||
enableTooltip.targetRef.current =
|
||||
settingsRef.current
|
||||
enableTooltip.setVisible(true)
|
||||
})
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
|
|
@ -189,27 +197,27 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({
|
|||
size="sm"
|
||||
variant="secondary"
|
||||
icon={SettingsIcon}
|
||||
disabled={!startable}
|
||||
onPress={() => {
|
||||
openPluginSettings(plugin)
|
||||
}}
|
||||
disabled={!enabled}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
<Pressable
|
||||
disabled={toggleDisabled}
|
||||
onPress={() => {
|
||||
if (essential)
|
||||
requestAnimationFrame(() => {
|
||||
essentialTooltip.targetRef.current =
|
||||
switchRef.current
|
||||
essentialTooltip.setVisible(true)
|
||||
})
|
||||
requestAnimationFrame(() => {
|
||||
essentialTooltip.targetRef.current =
|
||||
switchRef.current
|
||||
essentialTooltip.setVisible(true)
|
||||
})
|
||||
}}
|
||||
ref={switchRef}
|
||||
>
|
||||
<FormSwitch
|
||||
key={plugin.manifest.id}
|
||||
disabled={essential}
|
||||
disabled={toggleDisabled}
|
||||
onValueChange={enabled => {
|
||||
if (enabled) handleEnablePlugin(plugin)
|
||||
else handleDisablePlugin(plugin)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
import { Design } from '@revenge-mod/discord/design'
|
||||
|
||||
const { AlertModal, AlertActionButton, Text } = Design
|
||||
|
||||
export default function PluginInstallFailedAlert({ error }: { error: string }) {
|
||||
return (
|
||||
<AlertModal
|
||||
title="Plugin install failed"
|
||||
content={
|
||||
<Text variant="text-md/medium" color="text-feedback-critical">
|
||||
{error}
|
||||
</Text>
|
||||
}
|
||||
actions={<AlertActionButton text="Got it" />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -11,6 +11,8 @@ import {
|
|||
InternalPluginFlags,
|
||||
isPluginEssential,
|
||||
isPluginInternal,
|
||||
isPluginPendingUpdate,
|
||||
isPluginStartable,
|
||||
PluginFlags,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { PluginStatus } from '@revenge-mod/plugins/constants'
|
||||
|
|
@ -53,6 +55,7 @@ export default function PluginOptionsActionSheet({
|
|||
const meta = getInternalPluginMeta(plugin)
|
||||
const { dependencies, dependents } = meta!
|
||||
const essential = isPluginEssential(meta)
|
||||
const pendingUpdate = isPluginPendingUpdate(plugin)
|
||||
const {
|
||||
manifest: { id, name, author, description, icon },
|
||||
errors,
|
||||
|
|
@ -71,6 +74,7 @@ export default function PluginOptionsActionSheet({
|
|||
!essential && (
|
||||
<FormSwitch
|
||||
value={enabled}
|
||||
disabled={pendingUpdate}
|
||||
onValueChange={v => {
|
||||
if (v) handleEnablePlugin(plugin)
|
||||
else handleDisablePlugin(plugin)
|
||||
|
|
@ -180,10 +184,10 @@ function PluginActions({
|
|||
plugin: AnyPlugin
|
||||
closeSheet: () => void
|
||||
}) {
|
||||
const enabled = usePluginEnabled(plugin)
|
||||
const enableTooltip = useEnablePluginTooltip()
|
||||
const settingsRef = useClickOutsideTooltip(useEnablePluginTooltip, () => {})
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
const startable = isPluginStartable(plugin)
|
||||
|
||||
return (
|
||||
<Stack
|
||||
|
|
@ -213,9 +217,8 @@ function PluginActions({
|
|||
)}
|
||||
{plugin.SettingsComponent && (
|
||||
<Pressable
|
||||
disabled={!startable}
|
||||
onPress={() => {
|
||||
if (enabled) return
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
enableTooltip.targetRef.current =
|
||||
settingsRef.current
|
||||
|
|
@ -223,13 +226,13 @@ function PluginActions({
|
|||
})
|
||||
}}
|
||||
>
|
||||
<Design.IconButton
|
||||
<IconButton
|
||||
ref={settingsRef}
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
icon={SettingsIcon}
|
||||
label="Settings"
|
||||
disabled={!enabled}
|
||||
disabled={!startable}
|
||||
onPress={() => {
|
||||
openPluginSettings(plugin)
|
||||
closeSheet()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import { Design } from '@revenge-mod/discord/design'
|
||||
import {
|
||||
isPluginPendingReload,
|
||||
isPluginPendingUpdate,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import type { AnyPlugin } from '@revenge-mod/plugins/_'
|
||||
|
||||
const { AlertModal, AlertActionButton, Text } = Design
|
||||
|
|
@ -10,6 +14,9 @@ export default function PluginUninstallConfirmationAlert({
|
|||
plugin: AnyPlugin
|
||||
action: () => Promise<void>
|
||||
}) {
|
||||
const reloadPending =
|
||||
isPluginPendingReload(plugin) || isPluginPendingUpdate(plugin)
|
||||
|
||||
return (
|
||||
<AlertModal
|
||||
title="Uninstall plugin?"
|
||||
|
|
@ -21,6 +28,17 @@ export default function PluginUninstallConfirmationAlert({
|
|||
and all of its data will be removed. This cannot be undone.
|
||||
</Text>
|
||||
}
|
||||
extraContent={
|
||||
reloadPending && (
|
||||
<Text
|
||||
variant="text-md/semibold"
|
||||
color="text-feedback-critical"
|
||||
>
|
||||
This plugin is pending a reload. Uninstalling now may
|
||||
leave side unintended effects.
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<AlertActionButton
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
|
||||
import { AlertActionCreators } from '@revenge-mod/discord/actions'
|
||||
import {
|
||||
AlertActionCreators,
|
||||
ToastActionCreators,
|
||||
} from '@revenge-mod/discord/actions'
|
||||
import {
|
||||
refreshSettingsNavigator,
|
||||
registerSettingsItem,
|
||||
} from '@revenge-mod/discord/modules/settings'
|
||||
import { PluginFlags, pEmitter, pList } from '@revenge-mod/plugins/_'
|
||||
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
|
||||
import { useLayoutEffect } from 'react'
|
||||
import defer * as NavigatorHeaderWithIcon from './components/NavigatorHeaderWithIcon'
|
||||
import PluginInstallFailedAlert from './components/PluginInstallFailedAlert'
|
||||
import PluginsFailedToStartAlert from './components/PluginsFailedToStartAlert'
|
||||
import PluginsRequireReloadAlert from './components/PluginsRequireReloadAlert'
|
||||
import { Setting } from './constants'
|
||||
|
|
@ -71,9 +76,11 @@ pEmitter.on('started', plugin => {
|
|||
|
||||
/// RELOAD REQUIRED ALERT
|
||||
|
||||
function showReloadRequiredAlertIfNeeded() {
|
||||
function showPendingReloadAlertIfNeeded() {
|
||||
const plugins = [...pList.values()].filter(
|
||||
plugin => plugin.flags & PluginFlags.ReloadRequired,
|
||||
plugin =>
|
||||
plugin.flags &
|
||||
(PluginFlags.PendingReload | PluginFlags.PendingUpdate),
|
||||
)
|
||||
|
||||
if (!plugins.length) return
|
||||
|
|
@ -100,13 +107,50 @@ function showErrorAlertIfNeeded() {
|
|||
}
|
||||
|
||||
pEmitter.on('flagUpdate', plugin => {
|
||||
if (plugin.flags & PluginFlags.ReloadRequired)
|
||||
showReloadRequiredAlertIfNeeded()
|
||||
if (plugin.flags & (PluginFlags.PendingReload | PluginFlags.PendingUpdate))
|
||||
showPendingReloadAlertIfNeeded()
|
||||
})
|
||||
|
||||
pEmitter.on('stopped', plugin => {
|
||||
if (plugin.flags & PluginFlags.Errored) showErrorAlertIfNeeded()
|
||||
})
|
||||
|
||||
/// PLUGIN INSTALL FEEDBACK
|
||||
|
||||
const pluginInstallToastKeyFor = (id: string) => `REVENGE_PLUGIN_INSTALL:${id}`
|
||||
const PluginInstallFailedAlertKey = 'plugin-install-failed'
|
||||
|
||||
pEmitter.on('install', result => {
|
||||
if (result.error !== false) {
|
||||
AlertActionCreators.dismissAlert(PluginInstallFailedAlertKey)
|
||||
AlertActionCreators.openAlert(
|
||||
PluginInstallFailedAlertKey,
|
||||
<PluginInstallFailedAlert error={result.error} />,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
showInstallToast(
|
||||
result.updated
|
||||
? `Updated ${result.manifest.name}`
|
||||
: `Installed ${result.manifest.name}`,
|
||||
result.manifest.id,
|
||||
)
|
||||
})
|
||||
|
||||
const CircleCheckIcon = lookupGeneratedIconComponent(
|
||||
'CircleCheckIcon',
|
||||
'CircleCheckIcon-secondary',
|
||||
'CircleCheckIcon-primary',
|
||||
)
|
||||
|
||||
function showInstallToast(content: string, id: string) {
|
||||
ToastActionCreators.open({
|
||||
key: pluginInstallToastKeyFor(id),
|
||||
content,
|
||||
IconComponent: CircleCheckIcon,
|
||||
})
|
||||
}
|
||||
|
||||
showErrorAlertIfNeeded()
|
||||
showReloadRequiredAlertIfNeeded()
|
||||
showPendingReloadAlertIfNeeded()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { AlertActionCreators } from '@revenge-mod/discord/actions'
|
||||
import { RootNavigationRef } from '@revenge-mod/discord/modules/main_tabs_v2'
|
||||
import { uninstallExternalPlugin } from '@revenge-mod/plugins/_'
|
||||
import {
|
||||
isPluginStartable,
|
||||
uninstallExternalPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { deleteStorageForPlugin } from '~plugins/preinit/api.storage'
|
||||
import PluginClearDataConfirmationAlert from '../components/PluginClearDataConfirmationAlert'
|
||||
import PluginHasDependenciesAlert from '../components/PluginHasDependenciesAlert'
|
||||
|
|
@ -45,7 +48,7 @@ export function showPluginUninstallConfirmation(
|
|||
}
|
||||
|
||||
export function openPluginSettings(plugin: AnyPlugin) {
|
||||
if (!plugin.SettingsComponent) return
|
||||
if (!plugin.SettingsComponent || !isPluginStartable(plugin)) return
|
||||
|
||||
const navigation = RootNavigationRef.getRootNavigationRef()
|
||||
if (navigation.isReady()) navigation.navigate(plugin.manifest.id)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue