feat!: plugins page, tons of API changes, and bug fixes
Plugins - Settings: Plugins page is now semi-functional - No Track: Added new plugin - Developer Settings: Clean up properly - Logging: Log in preInit stage - Staff Settings: Add ReloadRequired flags when late-running @revenge-mod/init - Moved to the top-level src/index.ts instead @revenge-mod/modules - Switch `exports` and `id` parameters in finders APIs - Lookups now return an array of exports and ID instead of just exports - Lookup ID APIs have been removed - Add `finders.get` helper, a new alternative for `finders.find` - Corrected documentation for subscription APIs @revenge-mod/plugins - Added new APIs for plugins to allow running during pre-init stages - Added `unscoped.modules.finders.get*` - Added `unscoped.react.native` @revenge-mod/react - native: Added `onRunApplication` and `onRunApplicationFinished` @revenge-mod/utils - callbacks: Add `noopFalse` - promises: Fix `allSettled` - promises: Add `sleep`
This commit is contained in:
parent
4d31b7016c
commit
a43cd5d564
56 changed files with 921 additions and 441 deletions
|
|
@ -12,7 +12,7 @@ export let AssetRegistry: ReactNative.AssetsRegistry
|
|||
export let AssetRegistryModuleId: number
|
||||
|
||||
// Tracking/caching assets
|
||||
const unsubAR = waitForModules(byProps<ReactNative.AssetsRegistry>('registerAsset'), (id, exports) => {
|
||||
const unsubAR = waitForModules(byProps<ReactNative.AssetsRegistry>('registerAsset'), (exports, id) => {
|
||||
AssetRegistryModuleId = id
|
||||
AssetRegistry = exports as ReactNative.AssetsRegistry
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ const unsubRAS = waitForModules(
|
|||
byName<{
|
||||
addCustomSourceTransformer: (transformer: (arg: { asset: Asset }) => ReactNativeAsset) => void
|
||||
}>('resolveAssetSource'),
|
||||
(_, rAS) => {
|
||||
rAS => {
|
||||
unsubRAS()
|
||||
|
||||
// Custom assets
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import type { DiscordModules } from './types'
|
|||
|
||||
export let ActionSheetActionCreators: DiscordModules.Actions.ActionSheetActionCreators = proxify(
|
||||
() => {
|
||||
const module = lookupModule(
|
||||
const [module] = lookupModule(
|
||||
preferExports(
|
||||
byProps<DiscordModules.Actions.ActionSheetActionCreators>('hideActionSheet'),
|
||||
byDependencies([
|
||||
|
|
@ -45,7 +45,7 @@ export let ActionSheetActionCreators: DiscordModules.Actions.ActionSheetActionCr
|
|||
|
||||
export let AlertActionCreators: DiscordModules.Actions.AlertActionCreators = proxify(
|
||||
() => {
|
||||
const module = lookupModule(
|
||||
const [module] = lookupModule(
|
||||
preferExports(
|
||||
byProps<DiscordModules.Actions.AlertActionCreators>('openAlert', 'dismissAlert'),
|
||||
byDependencies([
|
||||
|
|
@ -81,5 +81,5 @@ export let ToastActionCreators: DiscordModules.Actions.ToastActionCreators = pro
|
|||
},
|
||||
)
|
||||
|
||||
for (const module of generator) if (module.open.length === 1) return (ToastActionCreators = module)
|
||||
for (const [module] of generator) if (module.open.length === 1) return (ToastActionCreators = module)
|
||||
})!
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { byDependencies, byProps, preferExports, relativeDep } from '@revenge-mod/modules/finders/filters'
|
||||
import { lookupModuleId } from '@revenge-mod/modules/finders/lookup'
|
||||
import { lookupModule } from '@revenge-mod/modules/finders/lookup'
|
||||
import { waitForModules } from '@revenge-mod/modules/finders/wait'
|
||||
|
||||
import type { DiscordModules } from '../types'
|
||||
import type { Metro } from '@revenge-mod/modules/types'
|
||||
|
||||
// ../discord_common/js/packages/flux
|
||||
|
||||
export const DispatcherModuleId = lookupModuleId(
|
||||
export const [Dispatcher, DispatcherModuleId] = lookupModule(
|
||||
preferExports(
|
||||
byProps<DiscordModules.Flux.Dispatcher>('_interceptors'),
|
||||
byDependencies([relativeDep(1), undefined, undefined, undefined, undefined, 2]),
|
||||
|
|
@ -14,11 +15,9 @@ export const DispatcherModuleId = lookupModuleId(
|
|||
{
|
||||
includeUninitialized: true,
|
||||
},
|
||||
)
|
||||
) as [DiscordModules.Flux.Dispatcher, Metro.ModuleID]
|
||||
|
||||
export const Dispatcher = __r(DispatcherModuleId!) as DiscordModules.Flux.Dispatcher
|
||||
|
||||
waitForModules(byProps<DiscordModules.Flux.Store>('_dispatchToken'), (_, store) => {
|
||||
waitForModules(byProps<DiscordModules.Flux.Store>('_dispatchToken'), store => {
|
||||
const name = store.getName()
|
||||
_stores[name] = store
|
||||
})
|
||||
|
|
@ -27,5 +26,5 @@ const _stores: Record<string, any> = {}
|
|||
|
||||
export const Stores = new Proxy(_stores, {
|
||||
ownKeys: () => Reflect.ownKeys(_stores),
|
||||
get: (_, name) => Stores[name as string],
|
||||
get: (_, name) => _stores[name as string],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { byName, byProps } from '@revenge-mod/modules/finders/filters'
|
||||
import { lookupModule, lookupModuleId } from '@revenge-mod/modules/finders/lookup'
|
||||
import { lookupModule } from '@revenge-mod/modules/finders/lookup'
|
||||
|
||||
import type { DiscordModules } from '../types'
|
||||
import type { Metro } from '@revenge-mod/modules/types'
|
||||
|
||||
export { AppStartPerformance } from '../preinit'
|
||||
|
||||
export * as Flux from './flux'
|
||||
|
||||
// ../discord_common/js/shared/Logger.tsx
|
||||
export const Logger = lookupModule(byName<DiscordModules.Logger>('Logger'))!
|
||||
export const [Logger] = lookupModule(byName<DiscordModules.Logger>('Logger')) as [DiscordModules.Logger, Metro.ModuleID]
|
||||
|
||||
// ../discord_common/js/packages/tokens/native.tsx
|
||||
export const TokensModuleId = lookupModuleId(byProps('RawColor'))!
|
||||
export const Tokens = __r(TokensModuleId)
|
||||
export const [Tokens, TokensModuleId] = lookupModule(byProps('RawColor')) as [any, Metro.ModuleID]
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export let Design: Design = proxify(
|
|||
() => {
|
||||
// ID: 3236
|
||||
// [3237, 1366, 3238, 3239, 2, ...];
|
||||
const module = lookupModule(
|
||||
const [module] = lookupModule(
|
||||
preferExports(
|
||||
byProps<Design>('TableRow', 'Button'),
|
||||
byDependencies(looseDeps([relativeDep(1), undefined, relativeDep(2), relativeDep(3), 2])),
|
||||
|
|
@ -43,7 +43,7 @@ export let FormSwitch: DiscordModules.Components.FormSwitch = proxify(() => {
|
|||
// ID: 46
|
||||
// Deps: [47, 48, 35, 49]
|
||||
|
||||
const module = lookupModule(
|
||||
const [module] = lookupModule(
|
||||
preferExports(
|
||||
bySingleProp<{ FormSwitch: DiscordModules.Components.FormSwitch }>('FormSwitch'),
|
||||
byDependencies([
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export function isSettingsModulesLoaded() {
|
|||
* @returns A function to unsubscribe from the event.
|
||||
* @throws Throws an error if the settings modules are already loaded. Check with {@link isSettingsModulesLoaded} first.
|
||||
*/
|
||||
export function onceSettingsModulesLoaded(subcription: SettingsModulesLoadedSubscription) {
|
||||
export function onSettingsModulesLoaded(subcription: SettingsModulesLoadedSubscription) {
|
||||
if (_data[2]) throw new Error('Settings modules already loaded')
|
||||
|
||||
_subs.add(subcription)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export type SettingListRenderer = DiscordModules.Modules.Settings.SettingListRen
|
|||
|
||||
export let SettingListRenderer: SettingListRenderer = proxify(
|
||||
() => {
|
||||
const module = lookupModule(
|
||||
const [module] = lookupModule(
|
||||
preferExports(
|
||||
byProps<SettingListRenderer>('SettingsList'),
|
||||
byDependencies(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { byDependencies, byProps, preferExports, relativeDep } from '@revenge-mod/modules/finders/filters'
|
||||
import { lookupModule } from '@revenge-mod/modules/finders/lookup'
|
||||
|
||||
import type { Metro } from '@revenge-mod/modules/types'
|
||||
import type { DiscordModules } from './types'
|
||||
|
||||
// ../discord_common/js/shared/AppStartPerformance.tsx
|
||||
export const AppStartPerformance = lookupModule(
|
||||
export const [AppStartPerformance] = lookupModule(
|
||||
preferExports(
|
||||
byProps<DiscordModules.AppStartPerformance>('markAndLog'),
|
||||
byDependencies([relativeDep(-1), undefined, undefined, 2]),
|
||||
|
|
@ -12,6 +13,6 @@ export const AppStartPerformance = lookupModule(
|
|||
{
|
||||
includeUninitialized: true,
|
||||
},
|
||||
)!
|
||||
) as [DiscordModules.AppStartPerformance, Metro.ModuleID]
|
||||
|
||||
AppStartPerformance.mark('👊', 'Preinit')
|
||||
AppStartPerformance.mark('👊', 'Pre-init')
|
||||
|
|
|
|||
|
|
@ -262,8 +262,8 @@ export namespace DiscordModules {
|
|||
onBlur?: () => void
|
||||
onFocus?: () => void
|
||||
|
||||
leadingIcon?: () => JSX.Element
|
||||
trailingIcon?: () => JSX.Element
|
||||
leadingIcon?: FC
|
||||
trailingIcon?: FC
|
||||
leadingText?: string
|
||||
trailingText?: string
|
||||
description?: string
|
||||
|
|
|
|||
2
lib/externals/src/browserify.ts
vendored
2
lib/externals/src/browserify.ts
vendored
|
|
@ -4,7 +4,7 @@ import { proxify } from '@revenge-mod/utils/proxy'
|
|||
|
||||
export let nodeUtil: typeof import('node:util') = proxify(
|
||||
() => {
|
||||
const module = lookupModule(
|
||||
const [module] = lookupModule(
|
||||
preferExports(byProps<typeof nodeUtil>('inspect'), byDependencies([relativeDep(1), [], []])),
|
||||
{
|
||||
includeUninitialized: true,
|
||||
|
|
|
|||
2
lib/externals/src/react-native-clipboard.ts
vendored
2
lib/externals/src/react-native-clipboard.ts
vendored
|
|
@ -16,7 +16,7 @@ let ClipboardModule: typeof import('@react-native-clipboard/clipboard') = proxif
|
|||
// r6 = 4792;
|
||||
// r5 = [27, 4793];
|
||||
|
||||
const module = lookupModule(
|
||||
const [module] = lookupModule(
|
||||
preferExports(
|
||||
byProps<typeof ClipboardModule>('useClipboard'),
|
||||
every(
|
||||
|
|
|
|||
10
lib/externals/src/react-navigation.ts
vendored
10
lib/externals/src/react-navigation.ts
vendored
|
|
@ -1,13 +1,13 @@
|
|||
import { byDependencies, byName, byProps, looseDeps, preferExports } from '@revenge-mod/modules/finders/filters'
|
||||
import { lookupModule, lookupModuleId } from '@revenge-mod/modules/finders/lookup'
|
||||
import { lookupModule } from '@revenge-mod/modules/finders/lookup'
|
||||
import { proxify } from '@revenge-mod/utils/proxy'
|
||||
|
||||
const _createClassModuleId = lookupModuleId(byName('_createClass'))
|
||||
const _classCallCheckModuleId = lookupModuleId(byName('_classCallCheck'))
|
||||
const [, _createClassModuleId] = lookupModule(byName('_createClass'))
|
||||
const [, _classCallCheckModuleId] = lookupModule(byName('_classCallCheck'))
|
||||
|
||||
export let ReactNavigationNative: typeof import('@react-navigation/native') = proxify(
|
||||
() => {
|
||||
const module = lookupModule(
|
||||
const [module] = lookupModule(
|
||||
preferExports(
|
||||
byProps<typeof ReactNavigationNative>('useLinkTo'),
|
||||
byDependencies(looseDeps([[], looseDeps([[_createClassModuleId, _classCallCheckModuleId]])])),
|
||||
|
|
@ -26,7 +26,7 @@ export let ReactNavigationNative: typeof import('@react-navigation/native') = pr
|
|||
|
||||
export let ReactNavigationStack: typeof import('@react-navigation/stack') = proxify(
|
||||
() => {
|
||||
const module = lookupModule(
|
||||
const [module] = lookupModule(
|
||||
preferExports(
|
||||
byProps<typeof ReactNavigationStack>('createStackNavigator'),
|
||||
byDependencies(looseDeps([[], looseDeps([[_createClassModuleId, _classCallCheckModuleId]])])),
|
||||
|
|
|
|||
2
lib/externals/src/shopify.ts
vendored
2
lib/externals/src/shopify.ts
vendored
|
|
@ -6,7 +6,7 @@ import { ReactJsxRuntimeModuleId, ReactModuleId, ReactNativeModuleId } from '@re
|
|||
|
||||
export let FlashList: typeof import('@shopify/flash-list') = proxify(
|
||||
() => {
|
||||
const module = lookupModule(
|
||||
const [module] = lookupModule(
|
||||
preferExports(
|
||||
byProps<typeof FlashList>('FlashList'),
|
||||
byDependencies([
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"name": "@revenge-mod/init",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"default": "./src/index.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
// Note that both Object.freeze and Proxy patches are implicitly run by requiring the two libraries below.
|
||||
import { patchMetroDefine } from '@revenge-mod/modules/_/metro'
|
||||
import { interceptProperty } from '@revenge-mod/utils/objects'
|
||||
|
||||
// By doing this, we are assuming __METRO_GLOBAL_PREFIX__ is an empty string and globalThis.__d isn't already set.
|
||||
// But both should be true either way.
|
||||
const unintercept = interceptProperty('__d', (_, mDefine) => (unintercept(), patchMetroDefine(mDefine)))
|
||||
|
||||
import { onModuleFirstRequired, onModuleInitialized } from '@revenge-mod/modules/metro/subscriptions'
|
||||
import { getErrorStack } from '@revenge-mod/utils/errors'
|
||||
|
||||
onModuleFirstRequired(0, function onceIndexRequired() {
|
||||
runCatching(() => {
|
||||
if (__BUILD_FLAG_LOG_PROMISE_REJECTIONS__) require('./patches/log-promise-rejections')
|
||||
|
||||
// Initialize preinit libraries
|
||||
require('@revenge-mod/modules/preinit')
|
||||
require('@revenge-mod/react/preinit')
|
||||
require('@revenge-mod/assets/preinit')
|
||||
require('@revenge-mod/discord/preinit')
|
||||
|
||||
onModuleInitialized(0, function onceIndexInitialized() {
|
||||
runCatching(() => {
|
||||
// Register all plugins
|
||||
require('~/plugins/all')
|
||||
|
||||
// Initialize init libraries
|
||||
require('@revenge-mod/plugins/init')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function runCatching(fn: () => void) {
|
||||
try {
|
||||
fn()
|
||||
} catch (e) {
|
||||
// TODO(init): Move to use native provided alert function, which will accept a string stack trace
|
||||
// Above will reduce the need for runCatching() to exist, as the code won't be able to be deduped any further
|
||||
const { ClientInfoModule, DeviceModule } = require('@revenge-mod/discord/native') as
|
||||
// biome-ignore format: Don't format this please
|
||||
typeof import('@revenge-mod/discord/native')
|
||||
|
||||
alert(
|
||||
// biome-ignore lint/style/useTemplate: I want this to be readable, thank you
|
||||
`Failed to load Revenge (${__BUILD_VERSION__}-${__BUILD_COMMIT__}-${__BUILD_BRANCH__} (${__BUILD_ENV__}))\n` +
|
||||
`Discord: ${ClientInfoModule.Version} (${ClientInfoModule.Build})\n` +
|
||||
`Device: ${DeviceModule.deviceManufacturer} ${DeviceModule.deviceModel}\n\n` +
|
||||
getErrorStack(e),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,9 @@
|
|||
"./finders/filters": {
|
||||
"default": "./src/finders/filters.ts"
|
||||
},
|
||||
"./finders/get": {
|
||||
"default": "./src/finders/get.ts"
|
||||
},
|
||||
"./finders/lookup": {
|
||||
"default": "./src/finders/lookup.ts"
|
||||
},
|
||||
|
|
|
|||
101
lib/modules/src/finders/get.ts
Normal file
101
lib/modules/src/finders/get.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { noopFalse } from '@revenge-mod/utils/callbacks'
|
||||
|
||||
import { type LookupModulesOptions, lookupModule, lookupModuleByImportedPath } from './lookup'
|
||||
import { type WaitForModulesOptions, waitForModuleByImportedPath, waitForModules } from './wait'
|
||||
|
||||
import type { MaybeDefaultExportMatched } from '../types'
|
||||
import type { Metro } from '../types'
|
||||
import type { RunFilterReturnExportsOptions } from './_internal'
|
||||
import type { Filter, FilterResult } from './filters'
|
||||
|
||||
export type GetModuleOptions<
|
||||
ReturnNamespace extends boolean = boolean,
|
||||
IncludeUninitialized extends boolean = boolean,
|
||||
> = WaitForModulesOptions<ReturnNamespace> & LookupModulesOptions<ReturnNamespace, IncludeUninitialized>
|
||||
|
||||
export type GetModuleResult<
|
||||
F extends Filter,
|
||||
O extends GetModuleOptions,
|
||||
> = O extends RunFilterReturnExportsOptions<true> ? MaybeDefaultExportMatched<FilterResult<F>> : FilterResult<F>
|
||||
|
||||
export type GetModuleCallback<T> = (exports: T, id: Metro.ModuleID) => any
|
||||
|
||||
export type GetModuleUnsubscribeFunction = () => boolean
|
||||
|
||||
/**
|
||||
* Get a module matching the filter.
|
||||
*
|
||||
* This is a combination of {@link lookupModule} and {@link waitForModules}.
|
||||
*
|
||||
* @param filter The filter to use to find the module.
|
||||
* @param options The options to use for the find.
|
||||
* @returns A promise that resolves to the module's exports or rejects if the find is aborted before a module is found.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* getModule(byProps<typeof import('react')>('createElement'), React => {
|
||||
* // Immediately called because React is always initialized when plugins are loaded
|
||||
* })
|
||||
*
|
||||
* getModule(byProps<typeof import('@shopify/flash-list')>('FlashList'), FlashList => {
|
||||
* // Called when the module is initialized
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function getModule<F extends Filter>(
|
||||
filter: F,
|
||||
callback: GetModuleCallback<FilterResult<F>>,
|
||||
): GetModuleUnsubscribeFunction
|
||||
|
||||
export function getModule<
|
||||
F extends O extends GetModuleOptions<boolean, true> ? Filter<any, false> : Filter,
|
||||
O extends GetModuleOptions,
|
||||
>(filter: F, callback: GetModuleCallback<FilterResult<F>>, options: O): GetModuleUnsubscribeFunction
|
||||
|
||||
export function getModule(filter: Filter, callback: GetModuleCallback<any>, options?: GetModuleOptions) {
|
||||
const [exports, id] = lookupModule(filter, options!)
|
||||
if (id != null) {
|
||||
callback(exports, id)
|
||||
return noopFalse
|
||||
}
|
||||
|
||||
const unsub = waitForModules(
|
||||
filter,
|
||||
(id, exports) => {
|
||||
unsub()
|
||||
callback(exports, id)
|
||||
},
|
||||
options!,
|
||||
)
|
||||
|
||||
return unsub
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a module by its imported path.
|
||||
*
|
||||
* @param path The path to find the module by.
|
||||
* @param options The options to use for the find.
|
||||
* @returns A promise that resolves to the module's exports or rejects if the find is aborted before the module is found.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* getModuleByImportedPath('modules/main_tabs_v2/native/settings/SettingsConstants.tsx', SettingsConstants => {
|
||||
* console.log('Settings page opened') // Logs once the module is initialized
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function getModuleByImportedPath<T>(path: string, callback: GetModuleCallback<T>): GetModuleUnsubscribeFunction {
|
||||
const [exports, id] = lookupModuleByImportedPath(path)
|
||||
if (id != null) {
|
||||
callback(exports, id)
|
||||
return noopFalse
|
||||
}
|
||||
|
||||
const unsub = waitForModuleByImportedPath(path, (exports, id) => {
|
||||
unsub()
|
||||
callback(exports, id)
|
||||
})
|
||||
|
||||
return unsub
|
||||
}
|
||||
|
|
@ -37,10 +37,12 @@ export type LookupModulesOptions<
|
|||
IncludeUninitialized extends boolean = boolean,
|
||||
> = RunFilterReturnExportsOptions<ReturnNamespace> & BaseLookupModulesOptions<IncludeUninitialized>
|
||||
|
||||
export type LookupModulesResult<
|
||||
F extends Filter,
|
||||
O extends LookupModulesOptions,
|
||||
> = O extends RunFilterReturnExportsOptions<true> ? MaybeDefaultExportMatched<FilterResult<F>> : FilterResult<F>
|
||||
export type LookupModulesResult<F extends Filter, O extends LookupModulesOptions> = [
|
||||
exports: O extends RunFilterReturnExportsOptions<true>
|
||||
? MaybeDefaultExportMatched<FilterResult<F>>
|
||||
: FilterResult<F>,
|
||||
id: Metro.ModuleID,
|
||||
]
|
||||
|
||||
export type LookupModuleIdsOptions<
|
||||
IncludeUninitialized extends boolean = boolean,
|
||||
|
|
@ -63,47 +65,6 @@ export type LookupModuleIdsOptions<
|
|||
} & BaseLookupModulesOptions<false>)
|
||||
)
|
||||
|
||||
/**
|
||||
* Lookup module IDs.
|
||||
*
|
||||
* You can lookup uninitialized module IDs by passing `options.includeUninitialized` when filtering via without-exports filters (eg. `byDependencies`).
|
||||
* Use the `moduleStateAware` helper to filter dynamically based on whether the module is initialized or not.
|
||||
*
|
||||
* You can also lookup all modules (even ones with bad exports) by passing `options.includeAll`.
|
||||
* Use the `preferExports` helper to filter dynamically based on whether the module has good exports or not.
|
||||
*
|
||||
* @param filter The filter to use.
|
||||
* @param options The options to use for the lookup.
|
||||
* @returns A generator that yields the module exports that match the filter.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const lookup = lookupModuleIds(byProps('createElement'))
|
||||
* // Log all module IDs that export React
|
||||
* for (const id of lookup) console.log(id)
|
||||
* ```
|
||||
*
|
||||
* @example Lookup uninitialized modules
|
||||
* ```ts
|
||||
* const lookup = lookupModuleIds(byDependencies([...]), { includeUninitialized: true })
|
||||
* // Log all module IDs that has those dependencies
|
||||
* for (const id of lookup) console.log(id)
|
||||
* ```
|
||||
*/
|
||||
export function* lookupModuleIds<O extends LookupModuleIdsOptions>(
|
||||
filter: O extends LookupModuleIdsOptions<true> ? Filter<any, false> : Filter,
|
||||
options?: O,
|
||||
): Generator<Metro.ModuleID, undefined> {
|
||||
if (options?.includeAll) {
|
||||
for (const id of _metas.keys()) if (runFilter(filter, id, getInitializedModuleExports(id), options)) yield id
|
||||
} else {
|
||||
if (options?.includeInitialized ?? true)
|
||||
for (const id of _inits) if (runFilter(filter, id, getInitializedModuleExports(id), options)) yield id
|
||||
|
||||
if (options?.includeUninitialized) for (const id of _uninits) if (runFilter(filter, id)) yield id
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup modules.
|
||||
*
|
||||
|
|
@ -121,7 +82,7 @@ export function* lookupModuleIds<O extends LookupModuleIdsOptions>(
|
|||
* for (const exports of lookup) console.log(exports)
|
||||
* ```
|
||||
*/
|
||||
export function lookupModules<F extends Filter>(filter: F): Generator<FilterResult<F>, undefined>
|
||||
export function lookupModules<F extends Filter>(filter: F): Generator<LookupModulesResult<F, never>, undefined>
|
||||
|
||||
export function lookupModules<
|
||||
F extends O extends BaseLookupModulesOptions<true> ? Filter<any, false> : Filter,
|
||||
|
|
@ -133,7 +94,7 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) {
|
|||
for (const id of _inits) {
|
||||
const exports = getInitializedModuleExports(id)
|
||||
const flag = runFilter(filter, id, exports, options)
|
||||
if (flag) yield exportsFromFilterResultFlag(flag, exports, options)
|
||||
if (flag) yield [exportsFromFilterResultFlag(flag, exports, options), id]
|
||||
}
|
||||
|
||||
if (options?.includeUninitialized)
|
||||
|
|
@ -142,48 +103,11 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) {
|
|||
// Run the filter again to ensure we have the correct exports
|
||||
const exports = __r(id)
|
||||
const flag = runFilter(filter, id, exports, options)
|
||||
if (flag) yield exportsFromFilterResultFlag(flag, exports, options)
|
||||
warnDeveloperAboutAPartialFilterMatch(id, filter.key)
|
||||
if (flag) yield [exportsFromFilterResultFlag(flag, exports, options), id]
|
||||
else warnDeveloperAboutAPartialFilterMatch(id, filter.key)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup a module ID. Skipping creating a `Generator`.
|
||||
*
|
||||
* @see {@link lookupModuleIds} for more documentation.
|
||||
*
|
||||
* @param filter The filter to use.
|
||||
* @param options The options to use for the lookup.
|
||||
* @returns A generator that yields the module exports that match the filter.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const id = lookupModuleId(byProps('createElement'))
|
||||
* // Log the first initialized module ID that exports React
|
||||
* console.log(id)
|
||||
* ```
|
||||
*
|
||||
* @example Lookup uninitialized modules
|
||||
* ```ts
|
||||
* const id = lookupModuleId(byDependencies([...]), { includeUninitialized: true })
|
||||
* // Log the first initialized module ID that has those dependencies
|
||||
* console.log(id)
|
||||
* ```
|
||||
*/
|
||||
export function lookupModuleId<O extends LookupModuleIdsOptions>(
|
||||
filter: O extends BaseLookupModulesOptions<true> ? Filter<any, false> : Filter,
|
||||
options?: O,
|
||||
): Metro.ModuleID | undefined {
|
||||
if (options?.includeAll) {
|
||||
for (const id of _metas.keys()) if (runFilter(filter, id, getInitializedModuleExports(id), options)) return id
|
||||
} else {
|
||||
if (options?.includeInitialized ?? true)
|
||||
for (const id of _inits) if (runFilter(filter, id, getInitializedModuleExports(id), options)) return id
|
||||
|
||||
if (options?.includeUninitialized) for (const id of _uninits) if (runFilter(filter, id)) return id
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup a module. Skipping creating a `Generator`.
|
||||
*
|
||||
|
|
@ -198,19 +122,19 @@ export function lookupModuleId<O extends LookupModuleIdsOptions>(
|
|||
* const React = lookupModule(byProps<typeof import('react')>('createElement'))
|
||||
* ```
|
||||
*/
|
||||
export function lookupModule<F extends Filter>(filter: F): FilterResult<F> | undefined
|
||||
export function lookupModule<F extends Filter>(filter: F): LookupModulesResult<F, never> | []
|
||||
|
||||
export function lookupModule<F extends Filter, O extends LookupModulesOptions>(
|
||||
filter: F,
|
||||
options: O,
|
||||
): LookupModulesResult<F, O> | undefined
|
||||
): LookupModulesResult<F, O> | []
|
||||
|
||||
export function lookupModule(filter: Filter, options?: LookupModulesOptions) {
|
||||
if (options?.includeInitialized ?? true)
|
||||
for (const id of _inits) {
|
||||
const exports = getInitializedModuleExports(id)
|
||||
const flag = runFilter(filter, id, exports, options)
|
||||
if (flag) return exportsFromFilterResultFlag(flag, exports, options)
|
||||
if (flag) return [exportsFromFilterResultFlag(flag, exports, options), id]
|
||||
}
|
||||
|
||||
if (options?.includeUninitialized)
|
||||
|
|
@ -219,24 +143,11 @@ export function lookupModule(filter: Filter, options?: LookupModulesOptions) {
|
|||
const exports = __r(id)
|
||||
// Run the filter again to ensure we have the correct exports
|
||||
const flag = runFilter(filter, id, exports, options)
|
||||
if (flag) return exportsFromFilterResultFlag(flag, exports, options)
|
||||
if (flag) return [exportsFromFilterResultFlag(flag, exports, options), id]
|
||||
warnDeveloperAboutAPartialFilterMatch(id, filter.key)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup a module ID by its imported path. **The module may have bad exports.**
|
||||
*
|
||||
* @param path The path to lookup the module ID by.
|
||||
* @returns The module ID if the module is initialized, or `undefined` if the module is not found or not initialized.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const LoggerId = lookupModuleIdByImportedPath('modules/debug/Logger.tsx')
|
||||
* ```
|
||||
*/
|
||||
export function lookupModuleIdByImportedPath(path: string): Metro.ModuleID | undefined {
|
||||
return _paths.get(path)
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -249,12 +160,13 @@ export function lookupModuleIdByImportedPath(path: string): Metro.ModuleID | und
|
|||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const { default: Logger } = lookupModuleByImportedPath<{ default: typeof DiscordModules.Logger }>('modules/debug/Logger.tsx')
|
||||
* const [{ default: Logger }] = lookupModuleByImportedPath<{ default: typeof DiscordModules.Logger }>('modules/debug/Logger.tsx')
|
||||
* ```
|
||||
*/
|
||||
export function lookupModuleByImportedPath<T = any>(path: string): T | undefined {
|
||||
// undefined will also return undefined either way, so we can just non-nullish assert it
|
||||
return getInitializedModuleExports(lookupModuleIdByImportedPath(path)!)
|
||||
export function lookupModuleByImportedPath<T = any>(path: string): [exports: T, id: Metro.ModuleID] | [] {
|
||||
const id = _paths.get(path)
|
||||
if (id == null) return []
|
||||
return [getInitializedModuleExports(id), id]
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ export interface BaseWaitForModulesOptions<IncludeAll extends boolean = boolean>
|
|||
includeAll?: IncludeAll
|
||||
}
|
||||
|
||||
export type WaitForModulesUnsubscribeFunction = () => boolean
|
||||
|
||||
export type WaitForModulesCallback<T> = (exports: T, id: Metro.ModuleID) => any
|
||||
|
||||
export type WaitForModulesOptions<
|
||||
ReturnNamespace extends boolean = boolean,
|
||||
IncludeBadExports extends boolean = boolean,
|
||||
|
|
@ -38,7 +42,7 @@ export type WaitForModulesResult<
|
|||
* const unsub = waitForModules(
|
||||
* byName<typeof import('@shopify/flash-list')>('FlashList'),
|
||||
* // (exports: typeof import('@shopify/flash-list'), id: Metro.ModuleID) => any
|
||||
* (id, exports) => {
|
||||
* (exports, id) => {
|
||||
* unsub()
|
||||
* // Do something with the module...
|
||||
* }
|
||||
|
|
@ -47,29 +51,29 @@ export type WaitForModulesResult<
|
|||
*/
|
||||
export function waitForModules<F extends Filter>(
|
||||
filter: F,
|
||||
callback: (id: Metro.ModuleID, exports: FilterResult<F>) => any,
|
||||
): () => void
|
||||
callback: WaitForModulesCallback<WaitForModulesResult<F, object>>,
|
||||
): WaitForModulesUnsubscribeFunction
|
||||
|
||||
export function waitForModules<
|
||||
F extends O extends WaitForModulesOptions<boolean, true> ? Filter<any, false> : Filter,
|
||||
O extends WaitForModulesOptions,
|
||||
>(filter: F, callback: (id: Metro.ModuleID, exports: WaitForModulesResult<F, O>) => any, options: O): () => void
|
||||
>(
|
||||
filter: F,
|
||||
callback: WaitForModulesCallback<WaitForModulesResult<F, O>>,
|
||||
options: O,
|
||||
): WaitForModulesUnsubscribeFunction
|
||||
|
||||
export function waitForModules(
|
||||
filter: Filter,
|
||||
callback: (id: Metro.ModuleID, exports: Metro.ModuleExports) => any,
|
||||
options?: WaitForModulesOptions,
|
||||
) {
|
||||
export function waitForModules(filter: Filter, callback: WaitForModulesCallback<any>, options?: WaitForModulesOptions) {
|
||||
return onAnyModuleInitialized(
|
||||
options?.includeAll
|
||||
? (id, exports) => {
|
||||
const flag = runFilter(filter, id, exports, options)
|
||||
if (flag) callback(id, exportsFromFilterResultFlag(flag, exports, options))
|
||||
if (flag) callback(exportsFromFilterResultFlag(flag, exports, options), id)
|
||||
}
|
||||
: (id, exports) => {
|
||||
if (initializedModuleHasBadExports(id)) return
|
||||
const flag = runFilter(filter, id, exports, options)
|
||||
if (flag) callback(id, exportsFromFilterResultFlag(flag, exports, options))
|
||||
if (flag) callback(exportsFromFilterResultFlag(flag, exports, options), id)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -90,7 +94,7 @@ export function waitForModules(
|
|||
* ```ts
|
||||
* waitForModuleByImportedPath(
|
||||
* 'utils/PlatformUtils.tsx',
|
||||
* (id, exports) => {
|
||||
* (exports, id) => {
|
||||
* // Do something with the module...
|
||||
* }
|
||||
* )
|
||||
|
|
@ -98,7 +102,7 @@ export function waitForModules(
|
|||
*/
|
||||
export function waitForModuleByImportedPath<T = any>(
|
||||
path: string,
|
||||
callback: (id: Metro.ModuleID, exports: T) => any,
|
||||
callback: WaitForModulesCallback<T>,
|
||||
options?: BaseWaitForModulesOptions,
|
||||
) {
|
||||
const unsub = onModuleFinishedImporting(
|
||||
|
|
@ -106,14 +110,14 @@ export function waitForModuleByImportedPath<T = any>(
|
|||
? (id, cmpPath) => {
|
||||
if (path === cmpPath) {
|
||||
unsub()
|
||||
callback(id, getInitializedModuleExports(id))
|
||||
callback(getInitializedModuleExports(id), id)
|
||||
}
|
||||
}
|
||||
: (id, cmpPath) => {
|
||||
if (path === cmpPath) {
|
||||
unsub()
|
||||
if (initializedModuleHasBadExports(id)) return
|
||||
callback(id, getInitializedModuleExports(id))
|
||||
callback(getInitializedModuleExports(id), id)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { _execPathSubs } from './subscriptions/_internal'
|
|||
import { byProps } from '../finders/filters'
|
||||
import { waitForModules } from '../finders/wait'
|
||||
|
||||
const unsubFFI = waitForModules(byProps('fileFinishedImporting'), (_, exports) => {
|
||||
const unsubFFI = waitForModules(byProps('fileFinishedImporting'), exports => {
|
||||
unsubFFI()
|
||||
|
||||
const orig = exports.fileFinishedImporting
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
import type {
|
||||
ModuleFirstRequiredCallback,
|
||||
ModuleInitializedCallback,
|
||||
ModuleWithImportedPathInitializedCallback,
|
||||
} from '.'
|
||||
import type { ModuleFirstRequiredCallback, ModuleInitializedCallback, ModuleFinishedImportingCallback } from '.'
|
||||
import type { Metro } from '../../types'
|
||||
|
||||
export const _reqAll = new Set<ModuleFirstRequiredCallback>()
|
||||
export const _req = new Map<Metro.ModuleID, Set<ModuleFirstRequiredCallback>>()
|
||||
export const _initAll = new Set<ModuleInitializedCallback>()
|
||||
export const _init = new Map<Metro.ModuleID, Set<ModuleInitializedCallback>>()
|
||||
export const _path = new Set<ModuleWithImportedPathInitializedCallback>()
|
||||
export const _path = new Set<ModuleFinishedImportingCallback>()
|
||||
|
||||
export function _execReqSubs(id: Metro.ModuleID) {
|
||||
for (const cb of _reqAll) cb(id)
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import type { initializedModuleHasBadExports } from '../utils'
|
|||
|
||||
export type ModuleFirstRequiredCallback = (id: Metro.ModuleID) => void
|
||||
export type ModuleInitializedCallback = (id: Metro.ModuleID, exports: Metro.ModuleExports) => void
|
||||
export type ModuleWithImportedPathInitializedCallback = (id: Metro.ModuleID, path: string) => void
|
||||
export type ModuleFinishedImportingCallback = (id: Metro.ModuleID, path: string) => void
|
||||
|
||||
/**
|
||||
* Registers a callback to be called when any module is initialized.
|
||||
* During this time, the module factory is not yet executed.
|
||||
*
|
||||
* This runs after the module factory has been executed, but before the module is considered initialized by Metro.
|
||||
* However, Revenge APIs will consider the module initialized at this point.
|
||||
*
|
||||
* @see {@link initializedModuleHasBadExports} to avoid bad module exports.
|
||||
*
|
||||
|
|
@ -25,7 +27,7 @@ export function onAnyModuleInitialized(callback: ModuleInitializedCallback) {
|
|||
* Registers a callback to be called when a specific module is initialized.
|
||||
*
|
||||
* This runs after the module factory has been executed, but before the module is considered initialized by Metro.
|
||||
* However, Revenge APIs will consider the module initialized at this poi
|
||||
* However, Revenge APIs will consider the module initialized at this point.
|
||||
*
|
||||
* @see {@link initializedModuleHasBadExports} to avoid bad module exports.
|
||||
*
|
||||
|
|
@ -52,16 +54,15 @@ export function onModuleInitialized(id: Metro.ModuleID, callback: ModuleInitiali
|
|||
* @param callback The callback to be called.
|
||||
* @returns A function that unregisters the callback.
|
||||
*/
|
||||
export function onModuleFinishedImporting(callback: ModuleWithImportedPathInitializedCallback) {
|
||||
export function onModuleFinishedImporting(callback: ModuleFinishedImportingCallback) {
|
||||
_path.add(callback)
|
||||
return () => _path.delete(callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback to be called when any module is required the first time.
|
||||
* Registers a callback to be called when any module is being initialized.
|
||||
*
|
||||
* This runs after the module factory has been executed, but before the module is considered initialized by Metro.
|
||||
* However, Revenge APIs will consider the module initialized at this point.
|
||||
* This runs before the module factory is executed.
|
||||
*
|
||||
* @see {@link initializedModuleHasBadExports} to avoid bad module exports.
|
||||
*
|
||||
|
|
@ -74,8 +75,9 @@ export function onAnyModuleFirstRequired(callback: ModuleFirstRequiredCallback)
|
|||
}
|
||||
|
||||
/**
|
||||
* Registers a callback to be called when a specific module is required the first time.
|
||||
* During this time, the module factory is not yet executed.
|
||||
* Registers a callback to be called when a specific module is being initialized.
|
||||
*
|
||||
* This runs before the module factory is executed.
|
||||
*
|
||||
* @see {@link initializedModuleHasBadExports} to avoid bad module exports.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@
|
|||
"./init": {
|
||||
"default": "./src/init.ts"
|
||||
},
|
||||
"./preinit": {
|
||||
"default": "./src/preinit.ts"
|
||||
},
|
||||
"./types": {
|
||||
"default": "./src/types.ts"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Logger } from '@revenge-mod/discord/common'
|
||||
import { allSettled, sleep } from '@revenge-mod/utils/promises'
|
||||
|
||||
import { _uapi } from './apis'
|
||||
|
||||
import { PluginFlags as Flag, PluginStatus as Status } from './constants'
|
||||
|
||||
import type {
|
||||
|
|
@ -8,23 +9,23 @@ import type {
|
|||
Plugin,
|
||||
PluginApi,
|
||||
PluginCleanup,
|
||||
PluginFlags,
|
||||
PluginLifecycles,
|
||||
PluginManifest,
|
||||
PreInitPluginApi,
|
||||
} from './types'
|
||||
|
||||
const _plugins = new Map<PluginManifest['id'], InternalPlugin>()
|
||||
const _metas = new Map<
|
||||
export const _plugins = new Map<PluginManifest['id'], InternalPlugin>()
|
||||
export const _metas = new Map<
|
||||
PluginManifest['id'],
|
||||
[api: InitPluginApi | PluginApi, promises: Promise<void>[], iflags: InternalPluginFlags]
|
||||
[
|
||||
api: PreInitPluginApi | InitPluginApi | PluginApi | undefined,
|
||||
promises: Promise<void>[],
|
||||
iflags: number,
|
||||
apiLevel: number,
|
||||
]
|
||||
>()
|
||||
|
||||
export function registerPlugin(
|
||||
manifest: PluginManifest,
|
||||
lifecycles: PluginLifecycles,
|
||||
flags: PluginFlags,
|
||||
iflags: InternalPluginFlags,
|
||||
) {
|
||||
export function registerPlugin(manifest: PluginManifest, lifecycles: PluginLifecycles, flags: number, iflags: number) {
|
||||
// TODO(plugins): verify plugin manifest
|
||||
if (_plugins.has(manifest.id)) throw new Error(`Plugin with ID "${manifest.id}" already registered`)
|
||||
|
||||
|
|
@ -35,64 +36,124 @@ export function registerPlugin(
|
|||
lifecycles,
|
||||
status: 0,
|
||||
flags,
|
||||
async disable() {
|
||||
// If plugin is not stopped, and is also not stopping, we need to stop it
|
||||
if (plugin.status && !(plugin.status & Status.Stopping)) await stopPlugin(plugin)
|
||||
|
||||
plugin.flags &= ~Flag.Enabled
|
||||
},
|
||||
disable: () => disablePlugin(plugin),
|
||||
stop: () => stopPlugin(plugin),
|
||||
}
|
||||
|
||||
_metas.set(manifest.id, [
|
||||
{
|
||||
cleanup: (...items) => {
|
||||
plugin.cleanups.push(...items)
|
||||
},
|
||||
get logger() {
|
||||
return (this.logger = new Logger(`Revenge > ${manifest.name}`))
|
||||
},
|
||||
plugin,
|
||||
unscoped: _uapi,
|
||||
},
|
||||
[],
|
||||
iflags,
|
||||
])
|
||||
|
||||
_metas.set(manifest.id, [undefined, [], iflags, PluginApiLevel.None])
|
||||
_plugins.set(manifest.id, plugin)
|
||||
}
|
||||
|
||||
function handlePluginError(e: unknown, plugin: InternalPlugin) {
|
||||
plugin.errors.push(e)
|
||||
plugin.flags |= Flag.Errored
|
||||
if (plugin.flags & Flag.Enabled) return plugin.disable()
|
||||
const [api, , iflags] = _metas.get(plugin.manifest.id)!
|
||||
|
||||
const log = (api as InitPluginApi).logger ?? console
|
||||
log.error('Plugin encountered an error', e)
|
||||
|
||||
if (!(iflags & InternalPluginFlags.Essential)) return plugin.disable()
|
||||
}
|
||||
|
||||
// function preparePluginStart(id: PluginManifest['id']) {
|
||||
// const api = _metas.get(id)?.[1]!
|
||||
// api.settings ??= ...
|
||||
// }
|
||||
function preparePluginPreInit(id: PluginManifest['id']) {
|
||||
const plugin = _plugins.get(id)!
|
||||
const meta = _metas.get(id)!
|
||||
|
||||
export function enablePlugin(plugin: InternalPlugin) {
|
||||
// Clear errors from previous runs
|
||||
plugin.errors = []
|
||||
plugin.status &= ~Flag.Errored
|
||||
|
||||
meta[0] = {
|
||||
cleanup: (...items) => {
|
||||
plugin.cleanups.push(...items)
|
||||
},
|
||||
plugin,
|
||||
unscoped: _uapi,
|
||||
}
|
||||
|
||||
meta[3] = PluginApiLevel.PreInit
|
||||
}
|
||||
|
||||
function preparePluginInit(id: PluginManifest['id']) {
|
||||
const meta = _metas.get(id)!
|
||||
const api = meta[0] as InitPluginApi
|
||||
|
||||
api.logger = new api.unscoped.discord.common.Logger(`Revenge > Plugins (${id})`)
|
||||
|
||||
meta[3] = PluginApiLevel.Init
|
||||
}
|
||||
|
||||
function preparePluginStart(id: PluginManifest['id']) {
|
||||
const meta = _metas.get(id)!
|
||||
|
||||
// const api = meta[0] as PluginApi
|
||||
// api.settings = ...
|
||||
|
||||
meta[3] = PluginApiLevel.Start
|
||||
}
|
||||
|
||||
async function disablePlugin(plugin: InternalPlugin) {
|
||||
const iflags = _metas.get(plugin.manifest.id)?.[2] ?? 0
|
||||
if (iflags & InternalPluginFlags.Essential)
|
||||
throw new Error(`Plugin "${plugin.manifest.id}" is essential and cannot be disabled`)
|
||||
|
||||
// If plugin is not stopped, and is also not stopping, we need to stop it
|
||||
if (plugin.status && !(plugin.status & Status.Stopping)) await stopPlugin(plugin)
|
||||
|
||||
// TODO(plugins): write to storage
|
||||
plugin.flags &= ~Flag.Enabled
|
||||
}
|
||||
|
||||
export function enablePlugin(plugin: InternalPlugin, late: boolean) {
|
||||
// TODO(plugins): write to storage
|
||||
plugin.flags |= Flag.Enabled
|
||||
if (late) plugin.flags |= Flag.EnabledLate
|
||||
}
|
||||
|
||||
export async function preInitPlugin(plugin: InternalPlugin) {
|
||||
const { manifest, lifecycles } = plugin
|
||||
const meta = _metas.get(manifest.id)!
|
||||
const [, promises] = meta
|
||||
|
||||
preparePluginPreInit(manifest.id)
|
||||
|
||||
if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${manifest.id}" is not enabled`)
|
||||
if (plugin.status & (Status.PreIniting | Status.PreInited))
|
||||
throw new Error(`Plugin preInit lifecycle for "${manifest.id}" is already running`)
|
||||
|
||||
plugin.status |= Status.PreIniting
|
||||
|
||||
try {
|
||||
const prom = lifecycles.preInit?.(meta[0] 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 handlePluginError(e, plugin)
|
||||
}
|
||||
}
|
||||
|
||||
export async function initPlugin(plugin: InternalPlugin) {
|
||||
const { manifest, lifecycles } = plugin
|
||||
const [api, promises] = _metas.get(manifest.id)!
|
||||
const meta = _metas.get(manifest.id)!
|
||||
const [, promises, , apiLevel] = meta
|
||||
|
||||
if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${manifest.id}" is not enabled`)
|
||||
if (plugin.status & (Status.Initing | Status.Inited))
|
||||
throw new Error(`Plugin "${manifest.id}" is initializing or already initialized`)
|
||||
throw new Error(`Plugin init lifecycle for "${manifest.id}" is already running`)
|
||||
|
||||
plugin.status |= Status.Initing
|
||||
if (apiLevel < PluginApiLevel.PreInit) preparePluginPreInit(manifest.id)
|
||||
if (apiLevel < PluginApiLevel.Init) preparePluginInit(manifest.id)
|
||||
|
||||
try {
|
||||
const prom = lifecycles.init?.(api)
|
||||
const prom = lifecycles.init?.(meta[0] as InitPluginApi)
|
||||
promises.push(prom)
|
||||
await prom
|
||||
|
||||
// plugin.disable() already handles, so it's the try block
|
||||
// plugin.disable() already handles, so it's in the try block
|
||||
plugin.status |= Status.Inited
|
||||
plugin.status &= ~Status.Initing
|
||||
} catch (e) {
|
||||
|
|
@ -102,22 +163,21 @@ export async function initPlugin(plugin: InternalPlugin) {
|
|||
|
||||
export async function startPlugin(plugin: InternalPlugin) {
|
||||
const { manifest, lifecycles } = plugin
|
||||
const [api, promises] = _metas.get(manifest.id)!
|
||||
const meta = _metas.get(manifest.id)!
|
||||
const [, promises, , apiLevel] = meta
|
||||
|
||||
if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${manifest.id}" is not enabled`)
|
||||
if (plugin.status & (Status.Starting | Status.Started))
|
||||
throw new Error(`Plugin "${manifest.id}" is starting or already started`)
|
||||
|
||||
// Clear errors from previous runs
|
||||
plugin.errors = []
|
||||
plugin.status &= ~Flag.Errored
|
||||
throw new Error(`Plugin start lifecycle for "${manifest.id}" is already running`)
|
||||
|
||||
plugin.status |= Status.Starting
|
||||
|
||||
// preparePluginStart(manifest.id)
|
||||
if (apiLevel < PluginApiLevel.PreInit) preparePluginPreInit(manifest.id)
|
||||
if (apiLevel < PluginApiLevel.Init) preparePluginInit(manifest.id)
|
||||
if (apiLevel < PluginApiLevel.Start) preparePluginStart(manifest.id)
|
||||
|
||||
try {
|
||||
const prom = lifecycles.start?.(api as PluginApi)
|
||||
const prom = lifecycles.start?.(meta[0] as PluginApi)
|
||||
promises.push(prom)
|
||||
await prom
|
||||
|
||||
|
|
@ -129,31 +189,42 @@ export async function startPlugin(plugin: InternalPlugin) {
|
|||
}
|
||||
}
|
||||
|
||||
async function stopPlugin(plugin: InternalPlugin) {
|
||||
const MaxLifecycleWaitTime = 5000
|
||||
|
||||
export async function stopPlugin(plugin: InternalPlugin) {
|
||||
const { manifest, lifecycles } = plugin
|
||||
const meta = _metas.get(manifest.id)!
|
||||
const [api, promises] = meta
|
||||
const [, promises, iflags, apiLevel] = meta
|
||||
|
||||
if (iflags & InternalPluginFlags.Essential)
|
||||
throw new Error(`Plugin "${manifest.id}" is essential and cannot be stopped`)
|
||||
|
||||
if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${manifest.id}" is not enabled`)
|
||||
if (plugin.status & Status.Stopping) throw new Error(`Plugin "${manifest.id}" is stopping`)
|
||||
|
||||
// If the plugin is initializing or starting, we need to wait for it to finish, then we'll stop it
|
||||
if (plugin.status & (Status.Initing | Status.Starting)) await Promise.all(promises)
|
||||
if (plugin.status & (Status.Inited | Status.Started))
|
||||
throw new Error(`Plugin "${manifest.id}" is not initialized started`)
|
||||
// If the plugin is running its lifecycles, we need to wait for it to finish, then we'll stop it
|
||||
// We want to wait at max 5 seconds for the lifecycles to finish
|
||||
if (plugin.status & (Status.PreIniting | Status.Initing | Status.Starting))
|
||||
await Promise.race([Promise.all(promises), sleep(MaxLifecycleWaitTime)]).then(finished => {
|
||||
// If the lifecycles don't finish in 5 seconds, a reload is probably required to unapply the changes
|
||||
if (!finished) plugin.flags |= Flag.ReloadRequired
|
||||
})
|
||||
else if (!(plugin.status & (Status.PreInited | Status.Inited | Status.Started)))
|
||||
throw new Error(`Plugin "${manifest.id}" is not running`)
|
||||
|
||||
// In case the plugin only has init() and stop()
|
||||
// preparePluginStart(manifest.id)
|
||||
if (apiLevel < PluginApiLevel.PreInit) preparePluginPreInit(manifest.id)
|
||||
if (apiLevel < PluginApiLevel.Init) preparePluginInit(manifest.id)
|
||||
if (apiLevel < PluginApiLevel.Start) preparePluginStart(manifest.id)
|
||||
|
||||
plugin.status |= Status.Stopping
|
||||
|
||||
try {
|
||||
await lifecycles.stop?.(api as PluginApi)
|
||||
await lifecycles.stop?.(meta[0] as PluginApi)
|
||||
} catch (e) {
|
||||
await handlePluginError(e, plugin)
|
||||
} finally {
|
||||
// Run cleanups
|
||||
const results = await Promise.allSettled(plugin.cleanups.map(cleanup => cleanup()))
|
||||
const results = await allSettled(plugin.cleanups.map(cleanup => cleanup()))
|
||||
for (const result of results)
|
||||
if (result.status === 'rejected') {
|
||||
await handlePluginError(result.reason, plugin)
|
||||
|
|
@ -161,33 +232,19 @@ async function stopPlugin(plugin: InternalPlugin) {
|
|||
plugin.flags |= Flag.ReloadRequired
|
||||
}
|
||||
|
||||
// Clear unnecessary data
|
||||
// Reset APIs
|
||||
meta[0] = undefined
|
||||
meta[3] = PluginApiLevel.None
|
||||
|
||||
// Clear temp data
|
||||
meta[1] = []
|
||||
plugin.cleanups = []
|
||||
|
||||
// Reset status
|
||||
plugin.status = 0
|
||||
}
|
||||
}
|
||||
|
||||
export function getEnabledPluginsCount() {
|
||||
let count = 0
|
||||
for (const plugin of _plugins.values()) if (plugin.flags & Flag.Enabled) count++
|
||||
return count
|
||||
}
|
||||
|
||||
export async function initPlugins() {
|
||||
const promises: Promise<void>[] = []
|
||||
for (const plugin of _plugins.values()) if (plugin.flags & Flag.Enabled) promises.push(initPlugin(plugin))
|
||||
// These Promises never throw, so it is fine
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
export async function startPlugins() {
|
||||
const promises: Promise<void>[] = []
|
||||
for (const plugin of _plugins.values()) if (plugin.flags & Flag.Enabled) promises.push(startPlugin(plugin))
|
||||
// These Promises never throw, so it is fine
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
export interface InternalPlugin extends Plugin {
|
||||
cleanups: PluginCleanup[]
|
||||
}
|
||||
|
|
@ -203,4 +260,9 @@ export const InternalPluginFlags = {
|
|||
Essential: 1 << 1,
|
||||
}
|
||||
|
||||
export type InternalPluginFlags = (typeof InternalPluginFlags)[keyof typeof InternalPluginFlags]
|
||||
const PluginApiLevel = {
|
||||
None: 0,
|
||||
PreInit: 1,
|
||||
Init: 2,
|
||||
Start: 3,
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
import * as PatcherLibrary from '@revenge-mod/patcher'
|
||||
import type { UnscopedInitPluginApi, UnscopedPluginApi } from '../types'
|
||||
import type { UnscopedInitPluginApi, UnscopedPluginApi, UnscopedPreInitPluginApi } from '../types'
|
||||
|
||||
export const _uapi: UnscopedInitPluginApi | UnscopedPluginApi = {
|
||||
discord: require('./discord').discord,
|
||||
externals: require('./externals').externals,
|
||||
export const _uapi: UnscopedPreInitPluginApi | UnscopedInitPluginApi | UnscopedPluginApi = {
|
||||
modules: require('./modules').modules,
|
||||
plugins: require('./plugins').plugins,
|
||||
patcher: PatcherLibrary,
|
||||
react: require('./react').react,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export type PluginApiModulesMetro =
|
|||
|
||||
export type PluginApiModulesFinders =
|
||||
// biome-ignore format: Don't
|
||||
typeof import('@revenge-mod/modules/finders/get') &
|
||||
typeof import('@revenge-mod/modules/finders/lookup') &
|
||||
typeof import('@revenge-mod/modules/finders/wait') & {
|
||||
filters: typeof import('@revenge-mod/modules/finders/filters')
|
||||
|
|
@ -20,6 +21,7 @@ export type PluginApiModulesFinders =
|
|||
|
||||
export const modules: PluginApiModules = {
|
||||
finders: {
|
||||
...require('@revenge-mod/modules/finders/get'),
|
||||
...require('@revenge-mod/modules/finders/lookup'),
|
||||
...require('@revenge-mod/modules/finders/wait'),
|
||||
filters: require('@revenge-mod/modules/finders/filters'),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
export type PluginApiReact = typeof import('@revenge-mod/react') & {
|
||||
jsxRuntime: typeof import('@revenge-mod/react/jsx-runtime')
|
||||
native: typeof import('@revenge-mod/react/native')
|
||||
}
|
||||
|
||||
export const react: PluginApiReact = {
|
||||
...require('@revenge-mod/react'),
|
||||
jsxRuntime: require('@revenge-mod/react/jsx-runtime'),
|
||||
native: require('@revenge-mod/react/native'),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,12 +11,19 @@ export const PluginFlags = {
|
|||
* 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,
|
||||
}
|
||||
|
||||
export const PluginStatus = {
|
||||
Initing: 1 << 0,
|
||||
Inited: 1 << 1,
|
||||
Starting: 1 << 2,
|
||||
Started: 1 << 3,
|
||||
Stopping: 1 << 4,
|
||||
PreIniting: 1 << 0,
|
||||
PreInited: 1 << 1,
|
||||
Initing: 1 << 2,
|
||||
Inited: 1 << 3,
|
||||
Starting: 1 << 4,
|
||||
Started: 1 << 5,
|
||||
Stopping: 1 << 6,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,32 @@
|
|||
import * as AssetsLibrary from '@revenge-mod/assets'
|
||||
// import * as UiLibrary from '@revenge-mod/ui'
|
||||
import { onRunApplication } from '@revenge-mod/react/native'
|
||||
|
||||
import { after } from '@revenge-mod/patcher'
|
||||
import { ReactNative } from '@revenge-mod/react'
|
||||
|
||||
import { initPlugins, startPlugins } from './_internal'
|
||||
import { _uapi } from './apis'
|
||||
|
||||
import type { UnscopedPluginApi } from './types'
|
||||
import * as AssetsLibrary from '@revenge-mod/assets'
|
||||
import { discord } from './apis/discord'
|
||||
import { externals } from './apis/externals'
|
||||
import { react } from './apis/react'
|
||||
|
||||
// Setup non-limited APIs
|
||||
const uapi = _uapi as UnscopedPluginApi
|
||||
import { _plugins, initPlugin, startPlugin } from './_internal'
|
||||
import { PluginFlags } from './constants'
|
||||
|
||||
import type { UnscopedInitPluginApi } from './types'
|
||||
|
||||
// Setup init plugin APIs
|
||||
const uapi = _uapi as UnscopedInitPluginApi
|
||||
uapi.assets = AssetsLibrary
|
||||
// uapi.ui = UiLibrary
|
||||
uapi.discord = discord
|
||||
uapi.externals = externals
|
||||
uapi.react = react
|
||||
|
||||
initPlugins()
|
||||
for (const plugin of _plugins.values()) if (plugin.flags & PluginFlags.Enabled) initPlugin(plugin)
|
||||
|
||||
// TODO(plugins/init): move to @revenge-mod/react/native onRunApplicationCalled
|
||||
const unpatch = after(ReactNative.AppRegistry, 'runApplication', ret => {
|
||||
unpatch()
|
||||
// Don't block rendering
|
||||
setImmediate(startPlugins)
|
||||
return ret
|
||||
const unsub = onRunApplication(() => {
|
||||
unsub()
|
||||
|
||||
// TODO(plugins/start): init start APIs
|
||||
// const UILibrary = require('@revenge-mod/ui')
|
||||
// uapi.ui = UiLibrary
|
||||
|
||||
for (const plugin of _plugins.values()) if (plugin.flags & PluginFlags.Enabled) startPlugin(plugin)
|
||||
})
|
||||
|
|
|
|||
4
lib/plugins/src/preinit.ts
Normal file
4
lib/plugins/src/preinit.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { _plugins, preInitPlugin } from './_internal'
|
||||
import { PluginFlags } from './constants'
|
||||
|
||||
for (const plugin of _plugins.values()) if (plugin.flags & PluginFlags.Enabled) preInitPlugin(plugin)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import type { DiscordModules } from '@revenge-mod/discord/types'
|
||||
import type { PluginStatus, PluginFlags } from './constants'
|
||||
|
||||
import type { PluginFlags as PF, PluginStatus as PS } from './constants'
|
||||
import type { DiscordModules } from '@revenge-mod/discord/types'
|
||||
|
||||
import type { PluginApiDiscord } from './apis/discord'
|
||||
import type { PluginApiExternals } from './apis/externals'
|
||||
|
|
@ -8,21 +8,26 @@ import type { PluginApiModules } from './apis/modules'
|
|||
import type { PluginApiReact } from './apis/react'
|
||||
import type { PluginApiPlugins } from './apis/plugins'
|
||||
|
||||
export type PluginFlags = (typeof PF)[keyof typeof PF]
|
||||
export type PluginStatus = (typeof PS)[keyof typeof PS]
|
||||
/**
|
||||
* The unscoped plugin API (very limited). This API is available as a global for plugins.
|
||||
* Available in the `preInit` phase.
|
||||
*/
|
||||
export interface UnscopedPreInitPluginApi {
|
||||
modules: PluginApiModules
|
||||
patcher: typeof import('@revenge-mod/patcher')
|
||||
plugins: PluginApiPlugins
|
||||
// utils: PluginApiUtils
|
||||
}
|
||||
|
||||
/**
|
||||
* The unscoped plugin API (limited). This API is available as a global for plugins.
|
||||
* Available in the `init` phase.
|
||||
*/
|
||||
export interface UnscopedInitPluginApi {
|
||||
export interface UnscopedInitPluginApi extends UnscopedPreInitPluginApi {
|
||||
assets: typeof import('@revenge-mod/assets')
|
||||
discord: PluginApiDiscord
|
||||
externals: PluginApiExternals
|
||||
modules: PluginApiModules
|
||||
patcher: typeof import('@revenge-mod/patcher')
|
||||
plugins: PluginApiPlugins
|
||||
react: PluginApiReact
|
||||
// utils: PluginApiUtils
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -30,22 +35,29 @@ export interface UnscopedInitPluginApi {
|
|||
* Available in the `start` and `stop` phase.
|
||||
*/
|
||||
export interface UnscopedPluginApi extends UnscopedInitPluginApi {
|
||||
assets: typeof import('@revenge-mod/assets')
|
||||
// ui: typeof import('@revenge-mod/ui')
|
||||
}
|
||||
|
||||
export type PluginCleanup = () => any
|
||||
export type PluginCleanupApi = (...fns: PluginCleanup[]) => void
|
||||
|
||||
/**
|
||||
* The plugin API (very limited).
|
||||
* Available in the `preInit` phase.
|
||||
*/
|
||||
export interface PreInitPluginApi {
|
||||
unscoped: UnscopedPreInitPluginApi
|
||||
cleanup: PluginCleanupApi
|
||||
plugin: Plugin
|
||||
}
|
||||
|
||||
/**
|
||||
* The plugin API (limited).
|
||||
* Available in the `init` phase.
|
||||
*/
|
||||
export interface InitPluginApi {
|
||||
plugin: Plugin
|
||||
logger: InstanceType<DiscordModules.Logger>
|
||||
export interface InitPluginApi extends PreInitPluginApi {
|
||||
unscoped: UnscopedInitPluginApi
|
||||
cleanup: PluginCleanupApi
|
||||
logger: InstanceType<DiscordModules.Logger>
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -98,24 +110,29 @@ export interface PluginManifest {
|
|||
|
||||
export interface PluginLifecycles {
|
||||
/**
|
||||
* Runs immediately as soon as possible with limited APIs.
|
||||
* Runs as soon as possible with very limited APIs.
|
||||
* Before the index module (module 0)'s factory is run.
|
||||
*
|
||||
* @param api Plugin API (very limited).
|
||||
*/
|
||||
preInit?: (api: PreInitPluginApi) => any
|
||||
/**
|
||||
* Runs as soon as all important modules are initialized.
|
||||
* After the index module (module 0)'s factory is run.
|
||||
*
|
||||
* @param api Plugin API (limited).
|
||||
* @param context Plugin context.
|
||||
*/
|
||||
init?: (api: InitPluginApi) => any
|
||||
/**
|
||||
* Runs when the plugin can be started with all APIs available.
|
||||
*
|
||||
* @param api Plugin API.
|
||||
* @param context Plugin context.
|
||||
*/
|
||||
start?: (api: PluginApi) => any
|
||||
/**
|
||||
* Runs when the plugin is stopped.
|
||||
*
|
||||
* @param api Plugin API.
|
||||
* @param context Plugin context.
|
||||
*/
|
||||
stop?: (api: PluginApi) => any
|
||||
}
|
||||
|
|
@ -137,12 +154,14 @@ export interface Plugin {
|
|||
|
||||
/**
|
||||
* The plugin flags.
|
||||
* @see {@link PluginFlags}
|
||||
*/
|
||||
flags: PluginFlags
|
||||
flags: number
|
||||
/**
|
||||
* The plugin status.
|
||||
* @see {@link PluginStatus}
|
||||
*/
|
||||
status: PluginStatus
|
||||
status: number
|
||||
/**
|
||||
* Errors encountered during the plugin lifecycles.
|
||||
*/
|
||||
|
|
@ -152,4 +171,8 @@ export interface Plugin {
|
|||
* Disable the plugin. This will also stop the plugin if it is running.
|
||||
*/
|
||||
disable(): Promise<void>
|
||||
/**
|
||||
* Stop the plugin.
|
||||
*/
|
||||
stop(): Promise<void>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@
|
|||
"./jsx-runtime": {
|
||||
"default": "./src/jsx-runtime/index.ts"
|
||||
},
|
||||
"./native": {
|
||||
"default": "./src/native/index.ts"
|
||||
},
|
||||
"./preinit": {
|
||||
"default": "./src/preinit.ts"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,11 +1,37 @@
|
|||
import { byProps } from '@revenge-mod/modules/finders/filters'
|
||||
import { lookupModuleId, lookupModuleIds } from '@revenge-mod/modules/finders/lookup'
|
||||
import { yielded } from '@revenge-mod/utils/generators'
|
||||
import { waitForModules } from '@revenge-mod/modules/finders/wait'
|
||||
|
||||
export const ReactModuleId = yielded(lookupModuleIds(byProps<typeof import('react')>('useState')), 1)!
|
||||
export const ReactNativeModuleId = lookupModuleId(byProps<typeof import('react-native')>('AppRegistry'))!
|
||||
export const ReactJsxRuntimeModuleId = yielded(lookupModuleIds(byProps<typeof import('react/jsx-runtime')>('jsx'))!, 1)
|
||||
import type { Metro } from '@revenge-mod/modules/types'
|
||||
|
||||
export const React = __r(ReactModuleId) as typeof import('react')
|
||||
export const ReactNative = __r(ReactNativeModuleId) as typeof import('react-native')
|
||||
export const ReactJsxRuntime = __r(ReactJsxRuntimeModuleId) as typeof import('react/jsx-runtime')
|
||||
export let ReactModuleId: Metro.ModuleID
|
||||
export let ReactNativeModuleId: Metro.ModuleID
|
||||
export let ReactJsxRuntimeModuleId: Metro.ModuleID
|
||||
|
||||
export let React: typeof import('react')
|
||||
export let ReactNative: typeof import('react-native')
|
||||
export let ReactJsxRuntime: typeof import('react/jsx-runtime')
|
||||
|
||||
let RtCount = 0
|
||||
|
||||
const unsubRt = waitForModules(byProps<typeof React>('useState'), (exports, id) => {
|
||||
if (RtCount++ === 2) return unsubRt()
|
||||
|
||||
ReactModuleId = id
|
||||
React = exports
|
||||
})
|
||||
|
||||
const unsubRN = waitForModules(byProps<typeof ReactNative>('AppRegistry'), (exports, id) => {
|
||||
unsubRN()
|
||||
|
||||
ReactNativeModuleId = id
|
||||
ReactNative = exports
|
||||
})
|
||||
|
||||
let RJsxRCount = 0
|
||||
|
||||
const unsubRJsxR = waitForModules(byProps<typeof ReactJsxRuntime>('jsxs'), (exports, id) => {
|
||||
if (RJsxRCount++ === 2) return unsubRJsxR()
|
||||
|
||||
ReactJsxRuntimeModuleId = id
|
||||
ReactJsxRuntime = exports
|
||||
})
|
||||
|
|
|
|||
27
lib/react/src/native/index.ts
Normal file
27
lib/react/src/native/index.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { _ara, _bra } from './patches'
|
||||
|
||||
import type { AppRegistry } from 'react-native'
|
||||
|
||||
/**
|
||||
* Registers a callback to be run when a call to {@link AppRegistry.runApplication} is made.
|
||||
*
|
||||
* @param callback The callback to be called.
|
||||
* @returns A function to unregister the callback.
|
||||
*/
|
||||
export function onRunApplication(callback: RunApplicationCallback) {
|
||||
_bra.add(callback)
|
||||
return () => _bra.delete(callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback to be run when a call to {@link AppRegistry.runApplication} is finished.
|
||||
*
|
||||
* @param callback The callback to be called.
|
||||
* @returns A function to unregister the callback.
|
||||
*/
|
||||
export function onRunApplicationFinished(callback: RunApplicationCallback) {
|
||||
_ara.add(callback)
|
||||
return () => _ara.delete(callback)
|
||||
}
|
||||
|
||||
export type RunApplicationCallback = () => any
|
||||
24
lib/react/src/native/patches.ts
Normal file
24
lib/react/src/native/patches.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { after, before } from '@revenge-mod/patcher'
|
||||
import { ReactNative } from '..'
|
||||
|
||||
import { waitForModules } from '@revenge-mod/modules/finders/wait'
|
||||
import { byProps } from '@revenge-mod/modules/finders/filters'
|
||||
|
||||
import type { RunApplicationCallback } from '.'
|
||||
|
||||
export const _bra = new Set<RunApplicationCallback>()
|
||||
export const _ara = new Set<RunApplicationCallback>()
|
||||
|
||||
const unsub = waitForModules(byProps('AppRegistry'), () => {
|
||||
unsub()
|
||||
|
||||
before(ReactNative.AppRegistry, 'runApplication', args => {
|
||||
for (const cb of _bra) cb()
|
||||
return args
|
||||
})
|
||||
|
||||
after(ReactNative.AppRegistry, 'runApplication', res => {
|
||||
for (const cb of _ara) cb()
|
||||
return res
|
||||
})
|
||||
})
|
||||
|
|
@ -1 +1,5 @@
|
|||
// Initialize the wait immediately, so we don't miss the actual modules before the re-exports
|
||||
import '.'
|
||||
|
||||
import './native/patches'
|
||||
import './jsx-runtime/patches'
|
||||
|
|
|
|||
6
lib/storage/package.json
Normal file
6
lib/storage/package.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@revenge-mod/storage",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {}
|
||||
}
|
||||
1
lib/storage/src/index.ts
Normal file
1
lib/storage/src/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
// TODO(storage): Implement from NativeModules interop instead
|
||||
|
|
@ -8,3 +8,5 @@ export function debounce<F extends (...args: any[]) => any>(func: F, timeout: nu
|
|||
}, timeout)
|
||||
})
|
||||
}
|
||||
|
||||
export const noopFalse = () => false
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const MultiIconComponentFilterBase = [
|
|||
|
||||
// TODO(utils/discord/lookupGeneratedIconComponent): Make this into a filter?
|
||||
export function lookupGeneratedIconComponent<N extends string>(compName: N, ...assetNames: string[]) {
|
||||
const icon = lookupModule(byProps(compName))?.[compName]
|
||||
const icon = lookupModule(byProps(compName))[0]?.[compName]
|
||||
if (icon) return icon
|
||||
|
||||
let filter = IconComponentFilter
|
||||
|
|
@ -51,5 +51,5 @@ export function lookupGeneratedIconComponent<N extends string>(compName: N, ...a
|
|||
return lookupModule(byDependencies<{ [K in N]: FC }>(filter), {
|
||||
includeInitialized: false,
|
||||
includeUninitialized: true,
|
||||
})?.[compName]
|
||||
})[0]?.[compName]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,30 @@
|
|||
export function allSettled(promises: Promise<any>[]): Promise<PromiseSettledResult<any>[]> {
|
||||
const mapped = promises.map(p =>
|
||||
p
|
||||
?.then(
|
||||
value =>
|
||||
({
|
||||
status: 'fulfilled',
|
||||
value,
|
||||
}) as const,
|
||||
)
|
||||
.catch(
|
||||
reason =>
|
||||
({
|
||||
status: 'rejected',
|
||||
reason,
|
||||
}) as const,
|
||||
),
|
||||
p instanceof Promise
|
||||
? p
|
||||
.then(
|
||||
value =>
|
||||
({
|
||||
status: 'fulfilled',
|
||||
value,
|
||||
}) as const,
|
||||
)
|
||||
.catch(
|
||||
reason =>
|
||||
({
|
||||
status: 'rejected',
|
||||
reason,
|
||||
}) as const,
|
||||
)
|
||||
: ({
|
||||
status: 'fulfilled',
|
||||
value: p,
|
||||
} as const),
|
||||
)
|
||||
|
||||
return Promise.all(mapped)
|
||||
}
|
||||
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
|
|
|||
21
src/components/FormSwitch.tsx
Normal file
21
src/components/FormSwitch.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { FormSwitch as Switch } from '@revenge-mod/discord/design'
|
||||
import { ReactNative } from '@revenge-mod/react'
|
||||
|
||||
import type { DiscordModules } from '@revenge-mod/discord/types'
|
||||
|
||||
/**
|
||||
* A switch component that is styled to match Discord's configuration
|
||||
*/
|
||||
export default function FormSwitch(props: DiscordModules.Components.FormSwitchProps) {
|
||||
return (
|
||||
<ReactNative.View style={props.disabled && styles.disabled}>
|
||||
<Switch {...props} />
|
||||
</ReactNative.View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = ReactNative.StyleSheet.create({
|
||||
disabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
})
|
||||
59
src/index.ts
59
src/index.ts
|
|
@ -1 +1,58 @@
|
|||
import '@revenge-mod/init'
|
||||
// Note that both Object.freeze and Proxy patches are implicitly run by requiring the two libraries below.
|
||||
import { patchMetroDefine } from '@revenge-mod/modules/_/metro'
|
||||
import { interceptProperty } from '@revenge-mod/utils/objects'
|
||||
|
||||
// By doing this, we are assuming __METRO_GLOBAL_PREFIX__ is an empty string and globalThis.__d isn't already set.
|
||||
// But both should be true either way.
|
||||
const unintercept = interceptProperty('__d', (_, mDefine) => (unintercept(), patchMetroDefine(mDefine)))
|
||||
|
||||
import { onModuleFirstRequired, onModuleInitialized } from '@revenge-mod/modules/metro/subscriptions'
|
||||
import { getErrorStack } from '@revenge-mod/utils/errors'
|
||||
|
||||
const IndexModuleId = 0
|
||||
|
||||
onModuleFirstRequired(IndexModuleId, function onIndexRequired() {
|
||||
runCatching(() => {
|
||||
if (__BUILD_FLAG_LOG_PROMISE_REJECTIONS__) require('./patches/log-promise-rejections')
|
||||
|
||||
// Initialize preinit libraries
|
||||
require('@revenge-mod/modules/preinit')
|
||||
require('@revenge-mod/react/preinit')
|
||||
require('@revenge-mod/assets/preinit')
|
||||
require('@revenge-mod/discord/preinit')
|
||||
|
||||
// Run all preinit plugins
|
||||
require('~/plugins/_preinit')
|
||||
require('@revenge-mod/plugins/preinit')
|
||||
|
||||
onModuleInitialized(IndexModuleId, function onIndexInitialized() {
|
||||
runCatching(() => {
|
||||
// Initialize init libraries
|
||||
|
||||
// Run all init plugins
|
||||
require('~/plugins/_all')
|
||||
require('@revenge-mod/plugins/init')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function runCatching(fn: () => void) {
|
||||
try {
|
||||
fn()
|
||||
} catch (e) {
|
||||
// TODO(init): Move to use native provided alert function, which will accept a string stack trace
|
||||
// Above will reduce the need for runCatching() to exist, as the code won't be able to be deduped any further
|
||||
const { ClientInfoModule, DeviceModule } = require('@revenge-mod/discord/native') as
|
||||
// biome-ignore format: Don't format this please
|
||||
typeof import('@revenge-mod/discord/native')
|
||||
|
||||
alert(
|
||||
// biome-ignore lint/style/useTemplate: I want this to be readable, thank you
|
||||
`Failed to load Revenge (${__BUILD_VERSION__}-${__BUILD_COMMIT__}-${__BUILD_BRANCH__} (${__BUILD_ENV__}))\n` +
|
||||
`Discord: ${ClientInfoModule.Version} (${ClientInfoModule.Build})\n` +
|
||||
`Device: ${DeviceModule.deviceManufacturer} ${DeviceModule.deviceModel}\n\n` +
|
||||
getErrorStack(e),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
import { getErrorStack } from '@revenge-mod/utils/errors'
|
||||
import { createLogger } from '@revenge-mod/utils/logger'
|
||||
|
||||
const logger = createLogger('revenge.init.promise-rejections')
|
||||
const logger = createLogger('revenge.promise-rejections')
|
||||
|
||||
const ErrorTypeWhitelist = [ReferenceError, TypeError, RangeError]
|
||||
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import './logging'
|
||||
import './staff-settings'
|
||||
import './settings'
|
||||
import './developer-settings'
|
||||
2
src/plugins/_preinit.ts
Normal file
2
src/plugins/_preinit.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import './logging'
|
||||
import './no-track'
|
||||
|
|
@ -9,7 +9,7 @@ import { getErrorStack } from '@revenge-mod/utils/errors'
|
|||
|
||||
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
|
||||
|
||||
import { pluginApi } from '..'
|
||||
import { api } from '..'
|
||||
import { MobileSetting } from '../constants'
|
||||
|
||||
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
|
||||
|
|
@ -98,14 +98,14 @@ function EvaluateJavaScriptAlert() {
|
|||
setLoading(true)
|
||||
|
||||
try {
|
||||
if (!pluginApi) {
|
||||
if (!api) {
|
||||
alert('Unable to provide plugin API. Running snippet in a second...')
|
||||
await new Promise(rs => setTimeout(rs, 1000))
|
||||
} else {
|
||||
// @ts-expect-error
|
||||
globalThis.revenge = pluginApi.unscoped
|
||||
globalThis.revenge = api.unscoped
|
||||
// @ts-expect-error
|
||||
globalThis.api = pluginApi
|
||||
globalThis.api = api
|
||||
}
|
||||
|
||||
// Do a no-local-scope-access eval
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { ToastActionCreators } from '@revenge-mod/discord/actions'
|
|||
import { React } from '@revenge-mod/react'
|
||||
import { getErrorStack } from '@revenge-mod/utils/errors'
|
||||
import { useReRender } from '@revenge-mod/utils/react'
|
||||
import { pluginApi } from '.'
|
||||
import { api } from '.'
|
||||
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
|
||||
|
||||
export const DevToolsContext: {
|
||||
|
|
@ -26,9 +26,7 @@ subscriptions.add((e, err) => {
|
|||
if (e === 3) {
|
||||
const actualError = (err as { message: string }).message ?? getErrorStack(err)
|
||||
|
||||
pluginApi.logger.error('React DevTools error:', actualError)
|
||||
|
||||
pluginApi.logger.log(CircleXIcon)
|
||||
api.logger.error('React DevTools error:', actualError)
|
||||
|
||||
ToastActionCreators.open({
|
||||
key: 'DEVTOOLS_ERROR',
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { PluginFlags } from '@revenge-mod/plugins/constants'
|
|||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
|
||||
import { _data } from '@revenge-mod/discord/_/modules/settings'
|
||||
import { onceSettingsModulesLoaded } from '@revenge-mod/discord/modules/settings'
|
||||
import { isSettingsModulesLoaded, onSettingsModulesLoaded } from '@revenge-mod/discord/modules/settings'
|
||||
|
||||
import type { PluginApi } from '@revenge-mod/plugins/types'
|
||||
|
||||
|
|
@ -15,9 +15,10 @@ registerPlugin(
|
|||
icon: 'WrenchIcon',
|
||||
},
|
||||
{
|
||||
start(api) {
|
||||
pluginApi = api
|
||||
onceSettingsModulesLoaded(() => require('./register'))
|
||||
start(api_) {
|
||||
api = api_
|
||||
if (isSettingsModulesLoaded()) require('./register').register()
|
||||
else onSettingsModulesLoaded(() => require('./register').register())
|
||||
},
|
||||
},
|
||||
PluginFlags.Enabled,
|
||||
|
|
@ -25,4 +26,4 @@ registerPlugin(
|
|||
)
|
||||
|
||||
// Expose to EvaluateJavaScriptSetting
|
||||
export let pluginApi: PluginApi
|
||||
export let api: PluginApi
|
||||
|
|
|
|||
|
|
@ -9,13 +9,18 @@ import ReactDevToolsVersionSetting from './definitions/ReactDevToolsVersionSetti
|
|||
import RevengeDeveloperSetting from './definitions/RevengeDeveloperSetting'
|
||||
import AssetBrowserSetting from './definitions/AssetBrowserSetting'
|
||||
|
||||
registerSettingsItems({
|
||||
[MobileSetting.REVENGE_DEVELOPER]: RevengeDeveloperSetting,
|
||||
[MobileSetting.REACT_DEVTOOLS_VERSION]: ReactDevToolsVersionSetting,
|
||||
[MobileSetting.REACT_DEVTOOLS_CONNECT]: ReactDevToolsConnectSetting,
|
||||
[MobileSetting.REACT_DEVTOOLS_DISCONNECT]: ReactDevToolsDisconnectSetting,
|
||||
[MobileSetting.EVALUATE_JAVASCRIPT]: EvaluateJavaScriptSetting,
|
||||
[MobileSetting.ASSET_BROWSER]: AssetBrowserSetting,
|
||||
})
|
||||
import { api } from '.'
|
||||
|
||||
addSettingsItemToSection('REVENGE', MobileSetting.REVENGE_DEVELOPER)
|
||||
export function register() {
|
||||
api.cleanup(
|
||||
registerSettingsItems({
|
||||
[MobileSetting.REVENGE_DEVELOPER]: RevengeDeveloperSetting,
|
||||
[MobileSetting.REACT_DEVTOOLS_VERSION]: ReactDevToolsVersionSetting,
|
||||
[MobileSetting.REACT_DEVTOOLS_CONNECT]: ReactDevToolsConnectSetting,
|
||||
[MobileSetting.REACT_DEVTOOLS_DISCONNECT]: ReactDevToolsDisconnectSetting,
|
||||
[MobileSetting.EVALUATE_JAVASCRIPT]: EvaluateJavaScriptSetting,
|
||||
[MobileSetting.ASSET_BROWSER]: AssetBrowserSetting,
|
||||
}),
|
||||
addSettingsItemToSection('REVENGE', MobileSetting.REVENGE_DEVELOPER),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { AppStartPerformance } from '@revenge-mod/discord/common'
|
||||
import { AppStartPerformance } from '@revenge-mod/discord/preinit'
|
||||
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
|
|
@ -6,6 +6,7 @@ import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
|||
AppStartPerformance.mark('👊', 'Plugins register')
|
||||
|
||||
const tsReg = performance.now()
|
||||
let tsPreInit: number
|
||||
let tsInit: number
|
||||
|
||||
registerPlugin(
|
||||
|
|
@ -17,9 +18,13 @@ registerPlugin(
|
|||
icon: 'PaperIcon',
|
||||
},
|
||||
{
|
||||
preInit() {
|
||||
tsPreInit = performance.now()
|
||||
AppStartPerformance.mark('👊', 'Plugins preInit', tsPreInit - tsReg)
|
||||
},
|
||||
init() {
|
||||
tsInit = performance.now()
|
||||
AppStartPerformance.mark('👊', 'Plugins init', tsInit - tsReg)
|
||||
AppStartPerformance.mark('👊', 'Plugins init', tsInit - tsPreInit)
|
||||
},
|
||||
start({ logger }) {
|
||||
AppStartPerformance.mark('👊', 'Plugins start', performance.now() - tsInit)
|
||||
|
|
|
|||
69
src/plugins/no-track/index.ts
Normal file
69
src/plugins/no-track/index.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { byProps } from '@revenge-mod/modules/finders/filters'
|
||||
import { waitForModules } from '@revenge-mod/modules/finders/wait'
|
||||
|
||||
import { instead } from '@revenge-mod/patcher'
|
||||
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
|
||||
// TODO(plugins/no-track): Block Sentry native-side
|
||||
registerPlugin(
|
||||
{
|
||||
id: 'revenge.no-track',
|
||||
name: 'No Track',
|
||||
description: "Disables Discord's analytics/tracking, and Sentry.",
|
||||
author: 'Revenge',
|
||||
icon: 'AnalyticsIcon',
|
||||
},
|
||||
{
|
||||
preInit({ cleanup }) {
|
||||
// modules/errors/native/SentryInitUtils.tsx
|
||||
const unsubSIU = waitForModules(byProps('initSentry'), SentryInitUtils => {
|
||||
unsubSIU()
|
||||
|
||||
console.log('Patching SentryInitUtils...')
|
||||
cleanup(instead(SentryInitUtils, 'initSentry', () => {}))
|
||||
cleanup(() => SentryInitUtils.initSentry())
|
||||
})
|
||||
},
|
||||
init({ cleanup, logger }) {
|
||||
// utils/AnalyticsUtils.tsx
|
||||
const unsubAU = waitForModules(
|
||||
byProps<{
|
||||
trackNetworkAction: () => void
|
||||
default: {
|
||||
track: () => void
|
||||
AnalyticsActionHandlers: Record<string, () => void>
|
||||
}
|
||||
}>('trackNetworkAction'),
|
||||
AnalyticsUtils => {
|
||||
unsubAU()
|
||||
|
||||
logger.info('Patching AnalyticsUtils...')
|
||||
cleanup(instead(AnalyticsUtils.default, 'track', () => {}))
|
||||
cleanup(instead(AnalyticsUtils, 'trackNetworkAction', () => {}))
|
||||
|
||||
for (const key in AnalyticsUtils.default.AnalyticsActionHandlers)
|
||||
cleanup(instead(AnalyticsUtils.default.AnalyticsActionHandlers, key, () => {}))
|
||||
},
|
||||
)
|
||||
// modules/app_analytics/useTrackImpression.tsx
|
||||
const unsubTI = waitForModules(
|
||||
byProps<{
|
||||
default: () => void
|
||||
trackImpression: () => void
|
||||
}>('trackImpression'),
|
||||
useTrackImpression => {
|
||||
unsubTI()
|
||||
|
||||
logger.info('Patching useTrackImpression...')
|
||||
cleanup(instead(useTrackImpression, 'trackImpression', () => {}))
|
||||
cleanup(instead(useTrackImpression, 'default', () => {}))
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
PluginFlags.Enabled,
|
||||
// TODO(plugins/no-track): is it essential?
|
||||
InternalPluginFlags.Internal | InternalPluginFlags.Essential,
|
||||
)
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { getEnabledPluginsCount } from '@revenge-mod/plugins/_'
|
||||
import { _plugins } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
|
||||
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
|
||||
|
||||
|
|
@ -11,7 +12,11 @@ const RevengePluginsSetting: SettingsItem = {
|
|||
type: 'route',
|
||||
IconComponent: () => <TableRowAssetIcon name="PuzzlePieceIcon" />,
|
||||
title: () => 'Plugins',
|
||||
useTrailing: () => `${getEnabledPluginsCount()} enabled`,
|
||||
useTrailing: () => {
|
||||
let count = 0
|
||||
for (const plugin of _plugins.values()) if (plugin.flags & PluginFlags.Enabled) count++
|
||||
return `${count} enabled`
|
||||
},
|
||||
screen: {
|
||||
route: UserSettingsSections[MobileSetting.REVENGE_PLUGINS],
|
||||
getComponent: () => require('../screens/RevengePluginsSettingScreen').default,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { byName, byProps } from '@revenge-mod/modules/finders/filters'
|
|||
import { waitForModules } from '@revenge-mod/modules/finders/wait'
|
||||
|
||||
import { _data, _subs } from '@revenge-mod/discord/_/modules/settings'
|
||||
import { type SettingsItem, onceSettingsModulesLoaded } from '@revenge-mod/discord/modules/settings'
|
||||
import { type SettingsItem, onSettingsModulesLoaded } from '@revenge-mod/discord/modules/settings'
|
||||
|
||||
import { after } from '@revenge-mod/patcher'
|
||||
|
||||
|
|
@ -21,13 +21,13 @@ registerPlugin(
|
|||
},
|
||||
{
|
||||
start({ logger }) {
|
||||
onceSettingsModulesLoaded(() => require('./register'))
|
||||
onSettingsModulesLoaded(() => require('./register'))
|
||||
|
||||
const unsubForRendererConfig = waitForModules(
|
||||
byProps<{
|
||||
SETTING_RENDERER_CONFIG: Record<string, SettingsItem>
|
||||
}>('SETTING_RENDERER_CONFIG'),
|
||||
(_, SettingRendererConfig) => {
|
||||
SettingRendererConfig => {
|
||||
unsubForRendererConfig()
|
||||
|
||||
logger.info('Settings modules loaded, running subscriptions and patching...')
|
||||
|
|
@ -52,16 +52,22 @@ registerPlugin(
|
|||
|
||||
const unsubForSettingsOverviewScreen = waitForModules(
|
||||
byName('SettingsOverviewScreen'),
|
||||
(_, exports) => {
|
||||
exports => {
|
||||
unsubForSettingsOverviewScreen()
|
||||
|
||||
after(exports as { default: FC }, 'default', tree => {
|
||||
const sections = (tree as Extract<typeof tree, { props: unknown }>).props.sections as Array<{
|
||||
label: string
|
||||
settings: string[]
|
||||
}>
|
||||
const customSections = _data[0]
|
||||
|
||||
const customSections = _data[0]
|
||||
after(exports as { default: FC }, 'default', tree => {
|
||||
const {
|
||||
props: { sections },
|
||||
} = tree as {
|
||||
props: {
|
||||
sections: Array<{
|
||||
label: string
|
||||
settings: string[]
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we even have custom sections
|
||||
const firstCustomSection = customSections[Object.keys(customSections)[0]]
|
||||
|
|
|
|||
|
|
@ -1,15 +1,154 @@
|
|||
import { SettingListRenderer } from '@revenge-mod/discord/modules/settings/renderer'
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { FlashList } from '@revenge-mod/externals/shopify'
|
||||
import { React, ReactNative } from '@revenge-mod/react'
|
||||
import {
|
||||
_metas,
|
||||
_plugins,
|
||||
enablePlugin,
|
||||
initPlugin,
|
||||
InternalPluginFlags,
|
||||
preInitPlugin,
|
||||
startPlugin,
|
||||
type InternalPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
|
||||
import { MobileSetting } from '../constants'
|
||||
import FormSwitch from '~/components/FormSwitch'
|
||||
import Page from '~/components/Page'
|
||||
import SearchInput from '~/components/SearchInput'
|
||||
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import { Tokens } from '@revenge-mod/discord/common'
|
||||
import { getAssetByName, getAssetId } from '@revenge-mod/assets'
|
||||
import { useReRender } from '@revenge-mod/utils/react'
|
||||
|
||||
const { Image, useWindowDimensions } = ReactNative
|
||||
const { Card, Text, Stack } = Design
|
||||
|
||||
export default function RevengePluginsSettingScreen() {
|
||||
const [search, setSearch] = React.useState('')
|
||||
|
||||
const { width } = useWindowDimensions()
|
||||
const numColumns = Math.floor((width - 16) / 448)
|
||||
|
||||
const plugins = React.useMemo(
|
||||
() => [..._plugins.values()].map(plugin => [plugin, _metas.get(plugin.manifest.id)![2]] as const),
|
||||
[_plugins],
|
||||
)
|
||||
|
||||
const filteredPlugins = React.useMemo(
|
||||
() =>
|
||||
plugins.filter(([plugin]) => {
|
||||
const { name, description, author } = plugin.manifest
|
||||
const query = search.toLowerCase()
|
||||
return (
|
||||
name.toLowerCase().includes(query) ||
|
||||
description.toLowerCase().includes(query) ||
|
||||
author.toLowerCase().includes(query)
|
||||
)
|
||||
}),
|
||||
[plugins, search],
|
||||
)
|
||||
|
||||
export default function RevengeFontsSettingScreen() {
|
||||
return (
|
||||
<SettingListRenderer.SettingsList
|
||||
sections={[
|
||||
{
|
||||
settings: [MobileSetting.REVENGE_NOT_IMPLEMENTED],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Page spacing={16}>
|
||||
<SearchInput size="md" onChange={(v: string) => setSearch(v)} />
|
||||
<FlashList.MasonryFlashList
|
||||
data={filteredPlugins}
|
||||
numColumns={numColumns}
|
||||
estimatedItemSize={108}
|
||||
renderItem={({ item: [plugin, iflags], columnIndex }) => (
|
||||
<InstalledPluginCard
|
||||
key={plugin.manifest.id}
|
||||
plugin={plugin}
|
||||
iflags={iflags}
|
||||
rightGap={columnIndex + 1 < numColumns}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
const usePluginCardStyles = Design.createStyles({
|
||||
icon: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
tintColor: Tokens.default.colors.TEXT_NORMAL,
|
||||
},
|
||||
card: {
|
||||
flexGrow: 1,
|
||||
marginBottom: 12,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12,
|
||||
gap: 4,
|
||||
},
|
||||
rightGap: {
|
||||
marginRight: 12,
|
||||
},
|
||||
topContainer: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
alignedContainer: {
|
||||
paddingLeft: 28,
|
||||
},
|
||||
grow: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
autoSize: {
|
||||
flex: 1,
|
||||
},
|
||||
})
|
||||
|
||||
function InstalledPluginCard({
|
||||
plugin,
|
||||
iflags,
|
||||
rightGap,
|
||||
}: { plugin: InternalPlugin; iflags: number; rightGap?: boolean }) {
|
||||
const {
|
||||
manifest: { name, description, author, icon },
|
||||
flags,
|
||||
} = plugin
|
||||
|
||||
const reRender = useReRender()
|
||||
const essential = Boolean(iflags & InternalPluginFlags.Essential)
|
||||
const enabled = Boolean(flags & PluginFlags.Enabled)
|
||||
const styles = usePluginCardStyles()
|
||||
|
||||
return (
|
||||
<Card style={[styles.card, rightGap && styles.rightGap]}>
|
||||
<Stack direction="horizontal" style={[styles.grow, styles.topContainer]}>
|
||||
<Stack spacing={8} direction="horizontal" style={[styles.topContainer, styles.autoSize]}>
|
||||
<Image source={getAssetId(getAssetByName(icon ?? 'PuzzlePieceIcon')!)!} style={styles.icon} />
|
||||
<Text variant="heading-lg/semibold">{name}</Text>
|
||||
</Stack>
|
||||
<FormSwitch
|
||||
disabled={essential}
|
||||
value={enabled}
|
||||
// biome-ignore lint/complexity/useArrowFunction: Async arrows are not supported
|
||||
onValueChange={async function (v) {
|
||||
if (v) {
|
||||
await enablePlugin(plugin, true)
|
||||
await preInitPlugin(plugin)
|
||||
await initPlugin(plugin)
|
||||
await startPlugin(plugin)
|
||||
} else await plugin.disable()
|
||||
|
||||
reRender()
|
||||
|
||||
// TODO(plugins/settings): show ReloadRequired modal
|
||||
// make an event based system for this, so we can register a listener for when plugins are disabled
|
||||
// and check its flags afterwards
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack spacing={4} style={[styles.alignedContainer, styles.grow]}>
|
||||
<Text style={styles.grow} variant="heading-md/medium" color="text-muted">
|
||||
by {author}
|
||||
</Text>
|
||||
<Text style={styles.grow} variant="text-md/medium">
|
||||
{description}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { byProps } from '@revenge-mod/modules/finders/filters'
|
||||
import { waitForModules } from '@revenge-mod/modules/finders/wait'
|
||||
|
||||
import { after } from '@revenge-mod/patcher'
|
||||
import { instead } from '@revenge-mod/patcher'
|
||||
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
|
|
@ -16,10 +16,18 @@ registerPlugin(
|
|||
},
|
||||
{
|
||||
init({ cleanup }) {
|
||||
const unsub = waitForModules(byProps('isStaffEnv'), (_, UserStoreUtils) => {
|
||||
const unsub = waitForModules(byProps('isStaffEnv'), UserStoreUtils => {
|
||||
unsub()
|
||||
cleanup(after(UserStoreUtils, 'isStaffEnv', () => true))
|
||||
cleanup(instead(UserStoreUtils, 'isStaffEnv', () => true))
|
||||
})
|
||||
|
||||
cleanup(unsub)
|
||||
},
|
||||
start({ plugin }) {
|
||||
if (plugin.flags & PluginFlags.EnabledLate) plugin.stop()
|
||||
},
|
||||
stop({ plugin }) {
|
||||
plugin.flags |= PluginFlags.ReloadRequired
|
||||
},
|
||||
},
|
||||
PluginFlags.Enabled,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue