feat(lib/plugins): external plugins support
This is the first draft. Things may change in the future. Loader changes are required as well.
This commit is contained in:
parent
00047da9b3
commit
bf8b99efb9
21 changed files with 460 additions and 111 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { getInternalPluginMeta, getPluginDependencies } from '.'
|
||||
import type { Plugin, PluginApiDecorator } from '../types'
|
||||
import type { PluginApiDecorator } from '../types'
|
||||
import type { AnyPlugin, InternalPluginMeta } from '.'
|
||||
|
||||
export type PluginApiDecoratorStore<T extends 'PreInit' | 'Init' | 'Start'> =
|
||||
|
|
@ -29,7 +29,7 @@ export function addPluginApiDecorator(
|
|||
|
||||
export function decoratePluginApi(
|
||||
store: PluginApiDecoratorStore<any>,
|
||||
plugin: Plugin<any, any>,
|
||||
plugin: AnyPlugin,
|
||||
meta: InternalPluginMeta,
|
||||
) {
|
||||
// Don't decorate API plugins with API plugins...
|
||||
|
|
|
|||
101
lib/plugins/src/_internal/external-plugins.ts
Normal file
101
lib/plugins/src/_internal/external-plugins.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import {
|
||||
callBridgeMethod,
|
||||
callBridgeMethodSync,
|
||||
registerJSMethod,
|
||||
} from '@revenge-mod/modules/native'
|
||||
import { getErrorStack } from '@revenge-mod/utils/error'
|
||||
import { pUnscopedApi } from '../apis'
|
||||
import {
|
||||
disablePlugin,
|
||||
InternalPluginFlags,
|
||||
isPluginEnabled,
|
||||
PluginFlags,
|
||||
pEmitter,
|
||||
pList,
|
||||
registerPlugin,
|
||||
runPluginLate,
|
||||
} from '.'
|
||||
import type { PluginManifest, PluginOptionsFactory } from '../types'
|
||||
import type { AnyPlugin } from '.'
|
||||
|
||||
interface ExternalPlugin {
|
||||
manifest: PluginManifest
|
||||
script?: string
|
||||
internal?: boolean
|
||||
essential?: boolean
|
||||
enabledByDefault?: boolean
|
||||
}
|
||||
|
||||
export function registerExternalPlugins() {
|
||||
registerJSMethod(
|
||||
'revenge.plugins.loader.pluginInstalled',
|
||||
(external: ExternalPlugin) => {
|
||||
loadExternalPlugin(external)
|
||||
},
|
||||
)
|
||||
|
||||
const externals = callBridgeMethodSync(
|
||||
'revenge.plugins.loader.getPlugins',
|
||||
[],
|
||||
)
|
||||
if (!externals) return
|
||||
|
||||
for (const external of externals)
|
||||
try {
|
||||
// Skip plugins whose JS counterpart is already registered
|
||||
if (pList.has(external.manifest.id)) continue
|
||||
registerExternalPlugin(external)
|
||||
} catch (e) {
|
||||
nativeLoggingHook(
|
||||
`\u001b[31mFailed to register external plugin "${external.manifest.id}": ${getErrorStack(e)}\u001b[0m`,
|
||||
2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function registerExternalPlugin({
|
||||
manifest,
|
||||
script,
|
||||
internal,
|
||||
essential,
|
||||
enabledByDefault,
|
||||
}: ExternalPlugin) {
|
||||
return registerPlugin(
|
||||
manifest,
|
||||
createOptionsFactory(script),
|
||||
essential || enabledByDefault ? PluginFlags.Enabled : 0,
|
||||
(internal ? InternalPluginFlags.Internal : 0) |
|
||||
(essential ? InternalPluginFlags.Essential : 0),
|
||||
)
|
||||
}
|
||||
|
||||
export async function loadExternalPlugin(external: ExternalPlugin) {
|
||||
const { id } = registerExternalPlugin(external)
|
||||
const plugin = pList.get(id)
|
||||
|
||||
if (plugin && isPluginEnabled(plugin)) await runPluginLate(plugin)
|
||||
}
|
||||
|
||||
export async function uninstallExternalPlugin(plugin: AnyPlugin) {
|
||||
if (isPluginEnabled(plugin)) await disablePlugin(plugin)
|
||||
|
||||
callBridgeMethod('revenge.plugins.loader.uninstallPlugin', [
|
||||
plugin.manifest.id,
|
||||
])
|
||||
|
||||
pList.delete(plugin.manifest.id)
|
||||
pEmitter.emit('unregister', plugin)
|
||||
}
|
||||
|
||||
function createOptionsFactory(script?: string): PluginOptionsFactory {
|
||||
if (!script) return () => ({})
|
||||
return () => new Function('revenge', `return ${script}`)(pUnscopedApi)
|
||||
}
|
||||
|
||||
declare module '@revenge-mod/modules/native' {
|
||||
interface Methods {
|
||||
'revenge.plugins.loader.getPlugins': [[], ExternalPlugin[] | null]
|
||||
'revenge.plugins.loader.installPlugin': [[], null]
|
||||
'revenge.plugins.loader.uninstallPlugin': [[string], null]
|
||||
}
|
||||
}
|
||||
|
|
@ -2,15 +2,12 @@ import { TypedEventEmitter } from '@revenge-mod/discord/common/utils'
|
|||
import {
|
||||
callBridgeMethod,
|
||||
callBridgeMethodSync,
|
||||
registerJSMethod,
|
||||
} from '@revenge-mod/modules/native'
|
||||
import { getErrorStack } from '@revenge-mod/utils/error'
|
||||
import { sleepReject } from '@revenge-mod/utils/promise'
|
||||
import { pUnscopedApi as uapi } from '../apis'
|
||||
import {
|
||||
PluginFlags as Flag,
|
||||
PersistentPluginFlags,
|
||||
PluginStatus as Status,
|
||||
} from '../constants'
|
||||
import { PluginStatus as Status } from '../constants'
|
||||
import {
|
||||
addPluginApiDecorator,
|
||||
decoratePluginApi,
|
||||
|
|
@ -34,13 +31,28 @@ import type {
|
|||
PluginDependency,
|
||||
PluginManifest,
|
||||
PluginOptions,
|
||||
PluginOptionsFactory,
|
||||
PreInitPluginApi,
|
||||
} from '../types'
|
||||
|
||||
export type AnyPlugin = Plugin<any, any>
|
||||
export type AnyPlugin = Plugin<any, any> & {
|
||||
/** @internal */
|
||||
flags: number
|
||||
}
|
||||
|
||||
const MaxWaitTime = 5000
|
||||
|
||||
// TODO: Maybe remove this later?
|
||||
// This is never used in bridge, but still used internally by JS.
|
||||
export const PluginFlags = {
|
||||
Enabled: 1 << 0,
|
||||
ReloadRequired: 1 << 1,
|
||||
Errored: 1 << 2,
|
||||
EnabledLate: 1 << 3,
|
||||
}
|
||||
|
||||
const Flag = PluginFlags
|
||||
|
||||
const PluginApiLevel = {
|
||||
None: 0,
|
||||
PreInit: 1,
|
||||
|
|
@ -73,12 +85,14 @@ export interface InternalPluginMeta {
|
|||
dependents: AnyPlugin[]
|
||||
dependencies?: AnyPlugin[]
|
||||
options: PluginOptions<any>
|
||||
optionsFactory?: PluginOptionsFactory<any>
|
||||
flags: number
|
||||
}
|
||||
|
||||
export const pUnscopedApi = uapi
|
||||
export const pEmitter = new TypedEventEmitter<{
|
||||
register: [AnyPlugin, PluginOptions<any>, update?: true]
|
||||
unregister: [AnyPlugin]
|
||||
disabled: [AnyPlugin]
|
||||
enabled: [AnyPlugin]
|
||||
preInited: [AnyPlugin]
|
||||
|
|
@ -92,17 +106,45 @@ export const pEmitter = new TypedEventEmitter<{
|
|||
export const pList = new Map<PluginManifest['id'], AnyPlugin>()
|
||||
const pMetadata = new WeakMap<AnyPlugin, InternalPluginMeta>()
|
||||
|
||||
const { flags: PersistedFlags }: PersistedPluginStates = callBridgeMethodSync(
|
||||
'revenge.plugins.states.read',
|
||||
[],
|
||||
) ?? {
|
||||
flags: {},
|
||||
const { states: InitialPersistedStates }: PersistedPluginStates =
|
||||
callBridgeMethodSync('revenge.plugins.states.read', []) ?? {
|
||||
states: {},
|
||||
}
|
||||
|
||||
// Native is the source of truth, we're expecting dispatches from native to sync states
|
||||
registerJSMethod('revenge.plugins.states.update', (id, state) => {
|
||||
applyPluginFlags(
|
||||
id as PluginManifest['id'],
|
||||
pluginStateToFlags(state as PluginStateObject),
|
||||
)
|
||||
})
|
||||
|
||||
function pluginStateToFlags(state: PluginStateObject): number {
|
||||
return (
|
||||
(state.enabled ? Flag.Enabled : 0) |
|
||||
(state.reloadRequired ? Flag.ReloadRequired : 0) |
|
||||
(state.errored ? Flag.Errored : 0) |
|
||||
(state.enabledLate ? Flag.EnabledLate : 0)
|
||||
)
|
||||
}
|
||||
|
||||
pEmitter.on('flagUpdate', plugin => {
|
||||
PersistedFlags[plugin.manifest.id] = plugin.flags & PersistentPluginFlags
|
||||
callBridgeMethod('revenge.plugins.states.write', [PersistedFlags])
|
||||
})
|
||||
async function applyPluginFlags(id: PluginManifest['id'], flags: number) {
|
||||
const plugin = pList.get(id)
|
||||
if (!plugin || plugin.flags === flags) return
|
||||
|
||||
const wasEnabled = plugin.flags & Flag.Enabled
|
||||
const nowEnabled = flags & Flag.Enabled
|
||||
|
||||
if (wasEnabled && !nowEnabled) {
|
||||
if (plugin.status && !(plugin.status & Status.Stopping))
|
||||
await stopPlugin(plugin)
|
||||
plugin.flags = flags
|
||||
pEmitter.emit('disabled', plugin)
|
||||
} else {
|
||||
plugin.flags = flags
|
||||
if (!wasEnabled && nowEnabled) pEmitter.emit('enabled', plugin)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new plugin with the system.
|
||||
|
|
@ -114,7 +156,7 @@ pEmitter.on('flagUpdate', plugin => {
|
|||
*/
|
||||
export function registerPlugin<O extends PluginApiExtensionsOptions>(
|
||||
manifest: PluginManifest,
|
||||
options: PluginOptions<O>,
|
||||
options: PluginOptions<O> | PluginOptionsFactory<O>,
|
||||
defflags: number,
|
||||
iflags: number,
|
||||
) {
|
||||
|
|
@ -122,19 +164,25 @@ export function registerPlugin<O extends PluginApiExtensionsOptions>(
|
|||
if (pList.has(manifest.id))
|
||||
throw new Error(`Plugin with ID "${manifest.id}" already registered`)
|
||||
|
||||
const factory = typeof options === 'function' ? options : undefined
|
||||
const resolved = typeof options === 'function' ? undefined : options
|
||||
|
||||
const plugin = {
|
||||
errors: [],
|
||||
manifest,
|
||||
lifecycles: {
|
||||
preInit: options.preInit,
|
||||
init: options.init,
|
||||
start: options.start,
|
||||
stop: options.stop,
|
||||
preInit: resolved?.preInit,
|
||||
init: resolved?.init,
|
||||
start: resolved?.start,
|
||||
stop: resolved?.stop,
|
||||
},
|
||||
SettingsComponent: options.SettingsComponent,
|
||||
SettingsComponent: resolved?.SettingsComponent,
|
||||
status: 0,
|
||||
disable: () => disablePlugin(plugin),
|
||||
stop: () => stopPlugin(plugin),
|
||||
requireReload: () => {
|
||||
plugin.flags |= Flag.ReloadRequired
|
||||
},
|
||||
api: undefined,
|
||||
set flags(flags: number) {
|
||||
if (meta.flags === flags) return
|
||||
|
|
@ -153,8 +201,11 @@ export function registerPlugin<O extends PluginApiExtensionsOptions>(
|
|||
apiLevel: PluginApiLevel.None,
|
||||
dependents: [],
|
||||
handleError: e => handlePluginError(e, plugin),
|
||||
options,
|
||||
flags: PersistedFlags[manifest.id] ?? defflags,
|
||||
options: resolved ?? {},
|
||||
optionsFactory: factory,
|
||||
flags: InitialPersistedStates[manifest.id]
|
||||
? pluginStateToFlags(InitialPersistedStates[manifest.id])
|
||||
: defflags,
|
||||
}
|
||||
|
||||
pMetadata.set(plugin, meta)
|
||||
|
|
@ -167,7 +218,7 @@ export function registerPlugin<O extends PluginApiExtensionsOptions>(
|
|||
// Only add to pending if the plugin is enabled
|
||||
else if (isPluginEnabled(plugin)) pPending.add(plugin)
|
||||
|
||||
pEmitter.emit('register', plugin, options)
|
||||
pEmitter.emit('register', plugin, meta.options)
|
||||
|
||||
return { id: manifest.id } satisfies PluginDependency
|
||||
}
|
||||
|
|
@ -206,8 +257,16 @@ export function getPluginDependencies(plugin: AnyPlugin): AnyPlugin[] {
|
|||
return (meta.dependencies = deps)
|
||||
}
|
||||
|
||||
export function isPluginEnabled({ flags }: AnyPlugin): boolean {
|
||||
return Boolean(flags & Flag.Enabled)
|
||||
export function isPluginEnabled(plugin: Plugin<any, any>): boolean {
|
||||
return Boolean((plugin as AnyPlugin).flags & Flag.Enabled)
|
||||
}
|
||||
|
||||
export function isPluginEnabledLate(plugin: Plugin<any, any>): 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 {
|
||||
|
|
@ -218,8 +277,8 @@ export function isPluginInternal({ iflags }: InternalPluginMeta): boolean {
|
|||
return Boolean(iflags & InternalPluginFlags.Internal)
|
||||
}
|
||||
|
||||
export function isPluginErrored(plugin: AnyPlugin) {
|
||||
return Boolean(plugin.flags & Flag.Errored)
|
||||
export function isPluginErrored(plugin: Plugin<any, any>): boolean {
|
||||
return Boolean((plugin as AnyPlugin).flags & Flag.Errored)
|
||||
}
|
||||
|
||||
function guardPluginEnabled(plugin: AnyPlugin) {
|
||||
|
|
@ -246,6 +305,29 @@ async function handlePluginError(e: unknown, plugin: AnyPlugin) {
|
|||
await plugin.disable()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a deferred options factory, populating the plugin's lifecycles and options.
|
||||
*/
|
||||
function resolvePluginOptions(plugin: AnyPlugin, meta: InternalPluginMeta) {
|
||||
const { optionsFactory } = meta
|
||||
if (!optionsFactory) return
|
||||
|
||||
meta.optionsFactory = undefined
|
||||
|
||||
try {
|
||||
const options = optionsFactory()
|
||||
|
||||
meta.options = options
|
||||
plugin.lifecycles.preInit = options.preInit
|
||||
plugin.lifecycles.init = options.init
|
||||
plugin.lifecycles.start = options.start
|
||||
plugin.lifecycles.stop = options.stop
|
||||
plugin.SettingsComponent = options.SettingsComponent
|
||||
} catch (e) {
|
||||
meta.handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the plugin API for the preInit lifecycle.
|
||||
*/
|
||||
|
|
@ -257,6 +339,8 @@ function tryPreparePluginPreInit(plugin: AnyPlugin) {
|
|||
plugin.errors = []
|
||||
plugin.flags &= ~Flag.Errored
|
||||
|
||||
resolvePluginOptions(plugin, meta)
|
||||
|
||||
plugin.api = {
|
||||
cleanup: (...items) => {
|
||||
meta.cleanups.push(...items)
|
||||
|
|
@ -333,6 +417,11 @@ export async function disablePlugin(plugin: AnyPlugin) {
|
|||
|
||||
plugin.flags &= ~Flag.Enabled
|
||||
pEmitter.emit('disabled', plugin)
|
||||
|
||||
callBridgeMethod('revenge.plugins.states.setEnabled', [
|
||||
plugin.manifest.id,
|
||||
false,
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -350,6 +439,11 @@ export async function enablePlugin(plugin: AnyPlugin) {
|
|||
|
||||
plugin.flags |= Flag.Enabled
|
||||
pEmitter.emit('enabled', plugin)
|
||||
|
||||
callBridgeMethod('revenge.plugins.states.setEnabled', [
|
||||
plugin.manifest.id,
|
||||
true,
|
||||
])
|
||||
}
|
||||
|
||||
export async function runPluginLate(plugin: AnyPlugin) {
|
||||
|
|
@ -370,6 +464,12 @@ export async function runPluginLate(plugin: AnyPlugin) {
|
|||
.map(async function runLate(plugin) {
|
||||
plugin.flags |= Flag.EnabledLate
|
||||
|
||||
// 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', [
|
||||
plugin.manifest.id,
|
||||
])
|
||||
|
||||
// Prepare the plugin API
|
||||
await preInitPlugin(plugin)
|
||||
await initPlugin(plugin)
|
||||
|
|
@ -603,17 +703,28 @@ export function getInternalPluginMeta(plugin: AnyPlugin): InternalPluginMeta {
|
|||
return pMetadata.get(plugin)!
|
||||
}
|
||||
|
||||
export { uninstallExternalPlugin } from './external-plugins'
|
||||
|
||||
declare module '@revenge-mod/modules/native' {
|
||||
interface Methods {
|
||||
'revenge.plugins.loader.start': [[id: PluginManifest['id']], null]
|
||||
'revenge.plugins.states.read': [[], PersistedPluginStates | null]
|
||||
'revenge.plugins.states.write': [[statuses: PersistedPluginFlags], void]
|
||||
'revenge.plugins.states.setEnabled': [
|
||||
[id: PluginManifest['id'], enabled: boolean],
|
||||
null,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
interface PersistedPluginStates {
|
||||
flags: PersistedPluginFlags
|
||||
interface PluginStateObject {
|
||||
enabled?: boolean
|
||||
reloadRequired?: boolean
|
||||
errored?: boolean
|
||||
enabledLate?: boolean
|
||||
}
|
||||
|
||||
interface PersistedPluginFlags {
|
||||
[id: PluginManifest['id']]: number
|
||||
interface PersistedPluginStates {
|
||||
states: {
|
||||
[id: PluginManifest['id']]: PluginStateObject
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,3 @@
|
|||
/**
|
||||
* The plugin flags.
|
||||
*/
|
||||
export const PluginFlags = {
|
||||
/**
|
||||
* The plugin is enabled.
|
||||
*/
|
||||
Enabled: 1 << 0,
|
||||
/**
|
||||
* The plugin requires a reload to apply changes.
|
||||
*/
|
||||
ReloadRequired: 1 << 1,
|
||||
/**
|
||||
* The plugin has errors.
|
||||
*/
|
||||
Errored: 1 << 2,
|
||||
/**
|
||||
* The plugin was enabled after the app was started.
|
||||
* This is usually caused by a newly installed plugin, or a plugin that was re-enabled.
|
||||
*/
|
||||
EnabledLate: 1 << 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* A bitmask of {@link PluginFlags} that are persisted to storage.
|
||||
*/
|
||||
export const PersistentPluginFlags = PluginFlags.Enabled
|
||||
|
||||
/**
|
||||
* The plugin status.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { isPluginEnabled, preInitPlugin } from './_internal'
|
||||
import { computePendingNodes, pListOrdered } from './_internal/dependency-graph'
|
||||
import { registerExternalPlugins } from './_internal/external-plugins'
|
||||
|
||||
registerExternalPlugins()
|
||||
computePendingNodes()
|
||||
|
||||
for (const plugin of pListOrdered)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { PluginApiExternals } from './apis/externals'
|
|||
import type { PluginApiModules } from './apis/modules'
|
||||
import type { PluginApiPlugins } from './apis/plugins'
|
||||
import type { PluginApiReact } from './apis/react'
|
||||
import type { PluginFlags, PluginStatus } from './constants'
|
||||
import type { PluginStatus } from './constants'
|
||||
|
||||
// biome-ignore lint/suspicious/noEmptyInterface: To be extended by actual extensions
|
||||
export interface PluginApiExtensionsOptions {}
|
||||
|
|
@ -200,6 +200,16 @@ export interface PluginOptions<
|
|||
SettingsComponent?: PluginSettingsComponent<O>
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory that lazily creates the plugin options.
|
||||
*
|
||||
* Only passed when the options (and lifecycles) must only be created right before the plugin runs,
|
||||
* for example to avoid evaluating external plugin code until it is actually used.
|
||||
*/
|
||||
export type PluginOptionsFactory<
|
||||
O extends PluginApiExtensionsOptions = PluginApiExtensionsOptions,
|
||||
> = () => PluginOptions<O>
|
||||
|
||||
/**
|
||||
* The plugin lifecycles.
|
||||
*/
|
||||
|
|
@ -247,10 +257,6 @@ export interface Plugin<
|
|||
manifest: PluginManifest
|
||||
lifecycles: PluginLifecycles<O>
|
||||
|
||||
/**
|
||||
* @see {@link PluginFlags}
|
||||
*/
|
||||
flags: number
|
||||
/**
|
||||
* @see {@link PluginStatus}
|
||||
*/
|
||||
|
|
@ -271,6 +277,10 @@ export interface Plugin<
|
|||
* Stop the plugin.
|
||||
*/
|
||||
stop(this: Plugin<O, S>): Promise<void>
|
||||
/**
|
||||
* Marks this plugin as requiring a reload to apply changes.
|
||||
*/
|
||||
requireReload(this: Plugin<O, S>): void
|
||||
|
||||
/**
|
||||
* The plugin API.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
PluginFlags,
|
||||
registerPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { defineLazyProperty } from '@revenge-mod/utils/object'
|
||||
|
||||
registerPlugin(
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
PluginFlags,
|
||||
PluginsStorageDirectory,
|
||||
} from '@revenge-mod/plugins/constants'
|
||||
registerPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { PluginsStorageDirectory } from '@revenge-mod/plugins/constants'
|
||||
import * as storage from '@revenge-mod/storage'
|
||||
import { getStorage } from '@revenge-mod/storage'
|
||||
import { defineLazyProperty } from '@revenge-mod/utils/object'
|
||||
import type { AnyPlugin } from '@revenge-mod/plugins/_'
|
||||
import type { InitPluginApi, Plugin } from '@revenge-mod/plugins/types'
|
||||
import type { StorageOptions } from '@revenge-mod/storage'
|
||||
import type { AnyObject } from '@revenge-mod/utils/types'
|
||||
|
||||
const storageOptions = new WeakMap<AnyPlugin, StorageOptions>()
|
||||
const storageOptions = new WeakMap<Plugin<any, any>, StorageOptions>()
|
||||
|
||||
registerPlugin(
|
||||
{
|
||||
|
|
@ -55,11 +55,11 @@ registerPlugin(
|
|||
InternalPluginFlags.API,
|
||||
)
|
||||
|
||||
function getStoragePathForPlugin(plugin: AnyPlugin) {
|
||||
function getStoragePathForPlugin(plugin: Plugin<any, any>) {
|
||||
return `${PluginsStorageDirectory}/${plugin.manifest.id}.json`
|
||||
}
|
||||
|
||||
export async function deleteStorageForPlugin(plugin: AnyPlugin) {
|
||||
export async function deleteStorageForPlugin(plugin: Plugin<any, any>) {
|
||||
const api = plugin.api as InitPluginApi<{ storage: AnyObject }> | undefined
|
||||
const storage = api?.storage ?? getStorage(getStoragePathForPlugin(plugin))
|
||||
console.log(api?.storage, storage, getStoragePathForPlugin(plugin))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
PluginFlags,
|
||||
registerPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import defer * as UtilsCallback from '@revenge-mod/utils/callback'
|
||||
import defer * as UtilsDiscord from '@revenge-mod/utils/discord'
|
||||
import defer * as UtilsError from '@revenge-mod/utils/error'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { AppStartPerformance } from '@revenge-mod/discord/preinit'
|
||||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
PluginFlags,
|
||||
registerPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { BuildEnvironment, FullVersion } from '~/constants'
|
||||
|
||||
if (__DEV__) {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,12 @@ import { waitForModules } from '@revenge-mod/modules/finders'
|
|||
import { withProps } from '@revenge-mod/modules/finders/filters'
|
||||
import { getModuleDependencies } from '@revenge-mod/modules/metro/utils'
|
||||
import { instead } from '@revenge-mod/patcher'
|
||||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
isPluginEnabledLate,
|
||||
PluginFlags,
|
||||
registerPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { noop } from '@revenge-mod/utils/callback'
|
||||
import { getCurrentStack } from '@revenge-mod/utils/error'
|
||||
|
||||
|
|
@ -45,8 +49,7 @@ registerPlugin(
|
|||
},
|
||||
{
|
||||
preInit({ cleanup, plugin }) {
|
||||
if (plugin.flags & PluginFlags.EnabledLate)
|
||||
plugin.flags |= PluginFlags.ReloadRequired
|
||||
if (isPluginEnabledLate(plugin)) plugin.requireReload()
|
||||
|
||||
// utils/SentryUtils.native.tsx
|
||||
const unsubSU = waitForModules(
|
||||
|
|
@ -206,7 +209,7 @@ registerPlugin(
|
|||
cleanup(unsubTI, unsubATAC, unsubHTTPUtils)
|
||||
},
|
||||
stop({ plugin }) {
|
||||
plugin.flags |= PluginFlags.ReloadRequired
|
||||
plugin.requireReload()
|
||||
},
|
||||
},
|
||||
PluginFlags.Enabled,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,12 @@ import {
|
|||
refreshSettingsNavigator,
|
||||
refreshSettingsOverviewScreen,
|
||||
} from '@revenge-mod/discord/modules/settings'
|
||||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
isPluginEnabledLate,
|
||||
PluginFlags,
|
||||
registerPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import pluginSettings from '../settings'
|
||||
import * as dt from './devtools'
|
||||
import defer * as rdt from './react-devtools'
|
||||
|
|
@ -52,7 +56,7 @@ registerPlugin<{ storage: Storage }>(
|
|||
|
||||
onSettingsModulesLoaded(utils.register)
|
||||
|
||||
if (api_.plugin.flags & PluginFlags.EnabledLate) {
|
||||
if (isPluginEnabledLate(api_.plugin)) {
|
||||
refreshSettingsOverviewScreen()
|
||||
refreshSettingsNavigator()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import { waitForModules } from '@revenge-mod/modules/finders'
|
||||
import { withName } from '@revenge-mod/modules/finders/filters'
|
||||
import { instead } from '@revenge-mod/patcher'
|
||||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
PluginFlags,
|
||||
registerPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import ErrorBoundaryScreen from './components/ErrorBoundaryScreen'
|
||||
import type { Component, ReactNode } from 'react'
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ import {
|
|||
getInternalPluginMeta,
|
||||
InternalPluginFlags,
|
||||
isPluginEssential,
|
||||
isPluginInternal,
|
||||
PluginFlags,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags, PluginStatus } from '@revenge-mod/plugins/constants'
|
||||
import { PluginStatus } from '@revenge-mod/plugins/constants'
|
||||
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
|
||||
import { getErrorStack } from '@revenge-mod/utils/error'
|
||||
import { Pressable } from 'react-native'
|
||||
|
|
@ -20,6 +22,7 @@ import { handleDisablePlugin, handleEnablePlugin } from '../utils/actions'
|
|||
import {
|
||||
openPluginSettings,
|
||||
showPluginClearDataConfirmation,
|
||||
showPluginUninstallConfirmation,
|
||||
} from '../utils/alerts'
|
||||
import { PluginInfo } from './PluginCard'
|
||||
import { usePluginEnabled, usePluginStatus } from './PluginStateProvider'
|
||||
|
|
@ -39,6 +42,7 @@ const { ActionSheet, IconButton, TableRowGroup, TableRow, Stack } = Design
|
|||
|
||||
const FileWarningIcon = getAssetIdByName('FileWarningIcon', 'png')!
|
||||
const SettingsIcon = getAssetIdByName('SettingsIcon', 'png')!
|
||||
const TrashIcon = getAssetIdByName('TrashIcon', 'png')!
|
||||
|
||||
export default function PluginOptionsActionSheet({
|
||||
plugin,
|
||||
|
|
@ -179,6 +183,7 @@ function PluginActions({
|
|||
const enabled = usePluginEnabled(plugin)
|
||||
const enableTooltip = useEnablePluginTooltip()
|
||||
const settingsRef = useClickOutsideTooltip(useEnablePluginTooltip, () => {})
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
|
||||
return (
|
||||
<Stack
|
||||
|
|
@ -195,6 +200,17 @@ function PluginActions({
|
|||
showPluginClearDataConfirmation(plugin, closeSheet)
|
||||
}}
|
||||
/>
|
||||
{!isPluginInternal(meta) && (
|
||||
<IconButton
|
||||
variant="destructive"
|
||||
size="lg"
|
||||
icon={TrashIcon}
|
||||
label="Uninstall"
|
||||
onPress={() => {
|
||||
showPluginUninstallConfirmation(plugin, closeSheet)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{plugin.SettingsComponent && (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
import { Design } from '@revenge-mod/discord/design'
|
||||
import type { AnyPlugin } from '@revenge-mod/plugins/_'
|
||||
|
||||
const { AlertModal, AlertActionButton, Text } = Design
|
||||
|
||||
export default function PluginUninstallConfirmationAlert({
|
||||
plugin,
|
||||
action,
|
||||
}: {
|
||||
plugin: AnyPlugin
|
||||
action: () => Promise<void>
|
||||
}) {
|
||||
return (
|
||||
<AlertModal
|
||||
title="Uninstall plugin?"
|
||||
content={
|
||||
<Text color="text-default">
|
||||
<Text variant="text-md/semibold" color="text-default">
|
||||
{plugin.manifest.name}
|
||||
</Text>{' '}
|
||||
and all of its data will be removed. This cannot be undone.
|
||||
</Text>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<AlertActionButton
|
||||
onPress={action}
|
||||
text="Uninstall"
|
||||
variant="destructive"
|
||||
/>
|
||||
<AlertActionButton text="Cancel" variant="secondary" />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -4,8 +4,11 @@ import { waitForModuleWithImportedPath } from '@revenge-mod/discord/utils/module
|
|||
import { waitForModules } from '@revenge-mod/modules/finders'
|
||||
import { withName } from '@revenge-mod/modules/finders/filters'
|
||||
import { instead } from '@revenge-mod/patcher'
|
||||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
PluginFlags,
|
||||
registerPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { React } from '@revenge-mod/react'
|
||||
import { asap, noop } from '@revenge-mod/utils/callback'
|
||||
import { getCurrentStack } from '@revenge-mod/utils/error'
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ import {
|
|||
refreshSettingsNavigator,
|
||||
registerSettingsItem,
|
||||
} from '@revenge-mod/discord/modules/settings'
|
||||
import { pEmitter, pList } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import { PluginFlags, pEmitter, pList } from '@revenge-mod/plugins/_'
|
||||
import { useLayoutEffect } from 'react'
|
||||
import defer * as NavigatorHeaderWithIcon from './components/NavigatorHeaderWithIcon'
|
||||
import PluginsFailedToStartAlert from './components/PluginsFailedToStartAlert'
|
||||
|
|
|
|||
|
|
@ -5,17 +5,25 @@ 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 { callBridgeMethod } from '@revenge-mod/modules/native'
|
||||
import {
|
||||
getInternalPluginMeta,
|
||||
InternalPluginFlags,
|
||||
isPluginEnabled,
|
||||
isPluginEssential,
|
||||
isPluginInternal,
|
||||
PluginFlags,
|
||||
pEmitter,
|
||||
pList,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import { debounce } from '@revenge-mod/utils/callback'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { View } from 'react-native'
|
||||
import { ClickOutsideProvider } from 'react-native-click-outside'
|
||||
import RevengeIcon from '~assets/RevengeIcon'
|
||||
|
|
@ -30,9 +38,10 @@ import type { ReactNavigationParamList } from '@revenge-mod/externals/react-navi
|
|||
import type { FilterAndSortActionSheetProps } from '../components/FilterAndSortActionSheet'
|
||||
import type { RouteNames, Setting } from '../constants'
|
||||
|
||||
const { Stack, IconButton, LayerScope } = Design
|
||||
const { Stack, IconButton, LayerScope, ContextMenu } = Design
|
||||
|
||||
const FiltersHorizontalIcon = getAssetIdByName('FiltersHorizontalIcon', 'png')!
|
||||
const MoreHorizontalIcon = getAssetIdByName('MoreHorizontalIcon')!
|
||||
|
||||
export default function RevengePluginsSettingScreen() {
|
||||
return (
|
||||
|
|
@ -98,6 +107,39 @@ const Sorts = {
|
|||
],
|
||||
} satisfies FilterAndSortActionSheetProps['sorts']
|
||||
|
||||
function HeaderContextMenu() {
|
||||
return (
|
||||
<ContextMenu
|
||||
title="More options"
|
||||
align="below"
|
||||
items={[
|
||||
{
|
||||
label: 'Install from file',
|
||||
action: () =>
|
||||
callBridgeMethod(
|
||||
'revenge.plugins.loader.installPlugin',
|
||||
[],
|
||||
),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{props => (
|
||||
<IconButton
|
||||
{...props}
|
||||
icon={MoreHorizontalIcon}
|
||||
variant="tertiary"
|
||||
/>
|
||||
)}
|
||||
</ContextMenu>
|
||||
)
|
||||
}
|
||||
|
||||
function snapshotPlugins() {
|
||||
return [...pList.values()].map(
|
||||
plugin => [plugin, getInternalPluginMeta(plugin)!] as const,
|
||||
)
|
||||
}
|
||||
|
||||
function Screen() {
|
||||
const navigation = useNavigation()
|
||||
const route =
|
||||
|
|
@ -114,18 +156,30 @@ function Screen() {
|
|||
[],
|
||||
)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: () => <HeaderContextMenu />,
|
||||
})
|
||||
}, [navigation])
|
||||
|
||||
const filter = route.params?.filter ?? DefaultFilters
|
||||
const matchAll = route.params?.matchAll ?? true
|
||||
const reverse = route.params?.reverse ?? false
|
||||
const sort = route.params?.sort ?? DefaultSort
|
||||
|
||||
const allPlugins = useMemo(
|
||||
() =>
|
||||
[...pList.values()].map(
|
||||
plugin => [plugin, getInternalPluginMeta(plugin)!] as const,
|
||||
),
|
||||
[],
|
||||
)
|
||||
const [allPlugins, setAllPlugins] = useState(snapshotPlugins)
|
||||
|
||||
useEffect(() => {
|
||||
const handleUpdate = () => setAllPlugins(snapshotPlugins())
|
||||
|
||||
pEmitter.on('register', handleUpdate)
|
||||
pEmitter.on('unregister', handleUpdate)
|
||||
|
||||
return () => {
|
||||
pEmitter.off('register', handleUpdate)
|
||||
pEmitter.off('unregister', handleUpdate)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const plugins = useMemo(
|
||||
() =>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { AlertActionCreators } from '@revenge-mod/discord/actions'
|
||||
import { RootNavigationRef } from '@revenge-mod/discord/modules/main_tabs_v2'
|
||||
import { uninstallExternalPlugin } from '@revenge-mod/plugins/_'
|
||||
import { deleteStorageForPlugin } from '~plugins/preinit/api.storage'
|
||||
import PluginClearDataConfirmationAlert from '../components/PluginClearDataConfirmationAlert'
|
||||
import PluginHasDependenciesAlert from '../components/PluginHasDependenciesAlert'
|
||||
import PluginHasDependentsAlert from '../components/PluginHasDependentsAlert'
|
||||
import PluginStatesProvider from '../components/PluginStateProvider'
|
||||
import PluginUninstallConfirmationAlert from '../components/PluginUninstallConfirmationAlert'
|
||||
import type { AnyPlugin } from '@revenge-mod/plugins/_'
|
||||
|
||||
export function showPluginClearDataConfirmation(
|
||||
|
|
@ -24,6 +26,24 @@ export function showPluginClearDataConfirmation(
|
|||
)
|
||||
}
|
||||
|
||||
export function showPluginUninstallConfirmation(
|
||||
plugin: AnyPlugin,
|
||||
callback: () => void,
|
||||
) {
|
||||
const KEY = 'plugin-uninstall-confirmation'
|
||||
|
||||
async function action() {
|
||||
await deleteStorageForPlugin(plugin)
|
||||
await uninstallExternalPlugin(plugin)
|
||||
callback()
|
||||
}
|
||||
|
||||
AlertActionCreators.openAlert(
|
||||
KEY,
|
||||
<PluginUninstallConfirmationAlert plugin={plugin} action={action} />,
|
||||
)
|
||||
}
|
||||
|
||||
export function openPluginSettings(plugin: AnyPlugin) {
|
||||
if (!plugin.SettingsComponent) return
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@ import { getStore, Stores } from '@revenge-mod/discord/flux'
|
|||
import { getModules } from '@revenge-mod/modules/finders'
|
||||
import { withProps } from '@revenge-mod/modules/finders/filters'
|
||||
import { instead } from '@revenge-mod/patcher'
|
||||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
PluginFlags,
|
||||
registerPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
|
||||
import type { DiscordModules } from '@revenge-mod/discord/types'
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@ import { AlertActionCreators } from '@revenge-mod/discord/actions'
|
|||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { getModules } from '@revenge-mod/modules/finders'
|
||||
import { withProps } from '@revenge-mod/modules/finders/filters'
|
||||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
PluginFlags,
|
||||
registerPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { afterJSX, beforeJSX } from '@revenge-mod/react/jsx-runtime'
|
||||
import { findInReactFiber } from '@revenge-mod/utils/react'
|
||||
import { Image } from 'react-native'
|
||||
|
|
@ -92,7 +95,7 @@ registerPlugin(
|
|||
cleanup(unsub)
|
||||
},
|
||||
stop({ plugin }) {
|
||||
plugin.flags |= PluginFlags.ReloadRequired
|
||||
plugin.requireReload()
|
||||
},
|
||||
},
|
||||
PluginFlags.Enabled,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue