Merge remote-tracking branch 'origin/main' into plugin-ux-fix
This commit is contained in:
commit
4c57f5c257
25 changed files with 831 additions and 585 deletions
|
|
@ -120,3 +120,5 @@ bun run types
|
||||||
<br>
|
<br>
|
||||||
<sub>Bundlers will need to map imports to property access on `revenge` turning `kebab-case` and `snake_case` to `camelCase`.</sub>
|
<sub>Bundlers will need to map imports to property access on `revenge` turning `kebab-case` and `snake_case` to `camelCase`.</sub>
|
||||||
<sub>Example: `@revenge-mod/discord/modules/main_tabs_v2` to `revenge.discord.modules.mainTabsV2`</sub>
|
<sub>Example: `@revenge-mod/discord/modules/main_tabs_v2` to `revenge.discord.modules.mainTabsV2`</sub>
|
||||||
|
<br>
|
||||||
|
<sub>Exception for `@revenge-mod/externals` which property accesses should turn into `PascalCase` instead of `camelCase`.</sub>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { lookupModule, lookupModules } from '@revenge-mod/modules/finders'
|
import { lookupModule, lookupModules } from '@revenge-mod/modules/finders'
|
||||||
import { withDependencies } from '@revenge-mod/modules/finders/filters'
|
import { withDependencies } from '@revenge-mod/modules/finders/filters'
|
||||||
import { getModuleDependencies } from '@revenge-mod/modules/metro/utils'
|
|
||||||
import {
|
import {
|
||||||
ReactJSXRuntimeModuleId,
|
ReactJSXRuntimeModuleId,
|
||||||
ReactModuleId,
|
ReactModuleId,
|
||||||
|
|
@ -10,7 +9,7 @@ import { proxify } from '@revenge-mod/utils/proxy'
|
||||||
import { ImportTrackerModuleId } from './common/import-tracker'
|
import { ImportTrackerModuleId } from './common/import-tracker'
|
||||||
import type { DiscordModules } from './types'
|
import type { DiscordModules } from './types'
|
||||||
|
|
||||||
const { loose } = withDependencies
|
const { atLeast, last, loose, relative } = withDependencies
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The lowest amount of dependencies `design/native.tsx` is expected to have.
|
* The lowest amount of dependencies `design/native.tsx` is expected to have.
|
||||||
|
|
@ -29,13 +28,12 @@ export let Design: Design = proxify(
|
||||||
// [3909, 4608, 5182, 13172, 2, ...] (141 dependencies)
|
// [3909, 4608, 5182, 13172, 2, ...] (141 dependencies)
|
||||||
const [, id] = lookupModule(
|
const [, id] = lookupModule(
|
||||||
withDependencies<Design>(
|
withDependencies<Design>(
|
||||||
loose([
|
atLeast(DesignMinimumDependencies, [
|
||||||
[ImportTrackerModuleId],
|
[ImportTrackerModuleId],
|
||||||
[ImportTrackerModuleId],
|
[ImportTrackerModuleId],
|
||||||
[ReactNativeModuleId, ImportTrackerModuleId],
|
[ReactNativeModuleId, ImportTrackerModuleId],
|
||||||
[ImportTrackerModuleId],
|
[ImportTrackerModuleId],
|
||||||
ImportTrackerModuleId,
|
ImportTrackerModuleId,
|
||||||
...Array<null>(DesignMinimumDependencies - 5).fill(null),
|
|
||||||
]),
|
]),
|
||||||
).keyAs('revenge.discord.design.Design'),
|
).keyAs('revenge.discord.design.Design'),
|
||||||
{
|
{
|
||||||
|
|
@ -55,9 +53,6 @@ export let Design: Design = proxify(
|
||||||
|
|
||||||
// design/components/Forms/native/FormSwitch.native.tsx
|
// design/components/Forms/native/FormSwitch.native.tsx
|
||||||
export let FormSwitch: DiscordModules.Components.FormSwitch = proxify(() => {
|
export let FormSwitch: DiscordModules.Components.FormSwitch = proxify(() => {
|
||||||
// TODO: Possibly come up with a better dependency fingerprinting API
|
|
||||||
// to not have to deal with this bullshit
|
|
||||||
|
|
||||||
for (const [, id] of lookupModules(
|
for (const [, id] of lookupModules(
|
||||||
withDependencies(
|
withDependencies(
|
||||||
loose([
|
loose([
|
||||||
|
|
@ -66,17 +61,25 @@ export let FormSwitch: DiscordModules.Components.FormSwitch = proxify(() => {
|
||||||
ReactNativeModuleId,
|
ReactNativeModuleId,
|
||||||
ReactJSXRuntimeModuleId,
|
ReactJSXRuntimeModuleId,
|
||||||
]),
|
]),
|
||||||
).keyAs('revenge.discord.design.FormSwitch'),
|
)
|
||||||
|
.and(
|
||||||
|
withDependencies(
|
||||||
|
last([
|
||||||
|
relative(1),
|
||||||
|
relative(2),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
ImportTrackerModuleId,
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.keyAs('revenge.discord.design.FormSwitch'),
|
||||||
{
|
{
|
||||||
initialize: false,
|
initialize: false,
|
||||||
},
|
},
|
||||||
)) {
|
)) {
|
||||||
const deps = getModuleDependencies(id)!
|
const FormSwitch_ = __r(id)!.FormSwitch
|
||||||
if (deps.at(-1) !== ImportTrackerModuleId) continue
|
if (FormSwitch_) return (FormSwitch = FormSwitch_)
|
||||||
if (deps.at(-4) === id + 2 && deps.at(-5) === id + 1) {
|
|
||||||
const FormSwitch_ = __r(id)!.FormSwitch
|
|
||||||
if (FormSwitch_) return (FormSwitch = FormSwitch_)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})!
|
})!
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { DispatcherModuleId } from '@revenge-mod/discord/common/flux'
|
||||||
import { ImportTrackerModuleId } from '@revenge-mod/discord/common/import-tracker'
|
import { ImportTrackerModuleId } from '@revenge-mod/discord/common/import-tracker'
|
||||||
import {
|
import {
|
||||||
lookupModule,
|
lookupModule,
|
||||||
|
|
@ -7,9 +8,8 @@ import {
|
||||||
import {
|
import {
|
||||||
createFilterGenerator,
|
createFilterGenerator,
|
||||||
withDependencies,
|
withDependencies,
|
||||||
withName,
|
|
||||||
} from '@revenge-mod/modules/finders/filters'
|
} from '@revenge-mod/modules/finders/filters'
|
||||||
import { getModuleDependencies } from '@revenge-mod/modules/metro/utils'
|
import { isModuleExportBad } from '@revenge-mod/modules/metro/utils'
|
||||||
import { asap, noop } from '@revenge-mod/utils/callback'
|
import { asap, noop } from '@revenge-mod/utils/callback'
|
||||||
import {
|
import {
|
||||||
cache,
|
cache,
|
||||||
|
|
@ -17,10 +17,7 @@ import {
|
||||||
Uncached,
|
Uncached,
|
||||||
} from '../../../modules/src/caches'
|
} from '../../../modules/src/caches'
|
||||||
import { FilterResultFlags } from '../../../modules/src/finders/_internal'
|
import { FilterResultFlags } from '../../../modules/src/finders/_internal'
|
||||||
import {
|
import { FilterScopes } from '../../../modules/src/finders/filters/constants'
|
||||||
FilterFlag,
|
|
||||||
FilterScopes,
|
|
||||||
} from '../../../modules/src/finders/filters/constants'
|
|
||||||
import type {
|
import type {
|
||||||
Filter,
|
Filter,
|
||||||
FilterGenerator,
|
FilterGenerator,
|
||||||
|
|
@ -66,32 +63,16 @@ export function getStore<T>(
|
||||||
|
|
||||||
/// STORE FILTERING
|
/// STORE FILTERING
|
||||||
|
|
||||||
/*
|
const { last, includes } = withDependencies
|
||||||
Flux Store dependencies: [
|
|
||||||
_classCallCheck
|
|
||||||
_createClass
|
|
||||||
_possibleConstructorReturn
|
|
||||||
bound getPrototypeOf
|
|
||||||
_inherits
|
|
||||||
(...), // (any amount of dependencies)
|
|
||||||
ImportTracker
|
|
||||||
]
|
|
||||||
*/
|
|
||||||
|
|
||||||
const withLeadingFluxStoreDeps = withDependencies(
|
// The import tracker is checked first, as it is far cheaper than resolving includes
|
||||||
withDependencies.loose([
|
const withFluxStoreDeps = withDependencies(last([ImportTrackerModuleId])).and(
|
||||||
withName('_classCallCheck'),
|
withDependencies(includes([DispatcherModuleId])),
|
||||||
withName('_createClass'),
|
|
||||||
withName('_possibleConstructorReturn'),
|
|
||||||
withName('bound getPrototypeOf'),
|
|
||||||
withName('_inherits'),
|
|
||||||
]),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
export type WithStore = FilterGenerator<
|
export type WithStore = FilterGenerator<
|
||||||
<T>() => Filter<{
|
<T>() => Filter<{
|
||||||
Result: DiscordModules.Flux.Store<T>
|
Result: DiscordModules.Flux.Store<T>
|
||||||
RequiresExports: boolean
|
|
||||||
Scopes: [
|
Scopes: [
|
||||||
typeof FilterScopes.Uninitialized,
|
typeof FilterScopes.Uninitialized,
|
||||||
typeof FilterScopes.Initialized,
|
typeof FilterScopes.Initialized,
|
||||||
|
|
@ -103,27 +84,22 @@ export type WithStore = FilterGenerator<
|
||||||
* A dynamic filter that matches all Flux stores.
|
* A dynamic filter that matches all Flux stores.
|
||||||
*/
|
*/
|
||||||
export const withStore = createFilterGenerator(
|
export const withStore = createFilterGenerator(
|
||||||
(_, id, exports) => {
|
(_, id, exports, initialized) => {
|
||||||
if (exports) return Boolean(exports._dispatchToken)
|
if (initialized)
|
||||||
else {
|
return (
|
||||||
if (!withLeadingFluxStoreDeps(id)) return false
|
!isModuleExportBad(exports) && Boolean(exports._dispatchToken)
|
||||||
const deps = getModuleDependencies(id)!
|
)
|
||||||
return deps[deps.length - 1] === ImportTrackerModuleId
|
|
||||||
}
|
return withFluxStoreDeps(id, undefined, false)
|
||||||
},
|
},
|
||||||
() => 'revenge.discord.store',
|
() => 'revenge.discord.store',
|
||||||
FilterFlag.Dynamic,
|
|
||||||
FilterScopes.Uninitialized | FilterScopes.Initialized,
|
FilterScopes.Uninitialized | FilterScopes.Initialized,
|
||||||
) as WithStore
|
) as WithStore
|
||||||
|
|
||||||
export type WithStoreName = FilterGenerator<
|
export type WithStoreName = FilterGenerator<
|
||||||
<T>(name: string) => Filter<{
|
<T>(name: string) => Filter<{
|
||||||
Result: DiscordModules.Flux.Store<T>
|
Result: DiscordModules.Flux.Store<T>
|
||||||
RequiresExports: true
|
Scopes: [typeof FilterScopes.Initialized]
|
||||||
Scopes: [
|
|
||||||
typeof FilterScopes.Uninitialized,
|
|
||||||
typeof FilterScopes.Initialized,
|
|
||||||
]
|
|
||||||
}>
|
}>
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|
@ -131,11 +107,21 @@ export type WithStoreName = FilterGenerator<
|
||||||
* A with-exports filter that matches a Flux store by its name.
|
* A with-exports filter that matches a Flux store by its name.
|
||||||
*/
|
*/
|
||||||
export const withStoreName = createFilterGenerator(
|
export const withStoreName = createFilterGenerator(
|
||||||
([name], _, exports) =>
|
([name], _, exports) => {
|
||||||
exports?.getName?.length === 0 && exports.getName() === name,
|
if (isModuleExportBad(exports)) return false
|
||||||
|
|
||||||
|
const getNameMethod = exports.getName
|
||||||
|
|
||||||
|
return (
|
||||||
|
typeof getNameMethod === 'function' &&
|
||||||
|
getNameMethod.length === 0 &&
|
||||||
|
// Needs to be bound to exports since it's a class
|
||||||
|
exports.getName() === name
|
||||||
|
)
|
||||||
|
},
|
||||||
([name]) => `revenge.discord.storeName(${name})`,
|
([name]) => `revenge.discord.storeName(${name})`,
|
||||||
FilterFlag.RequiresExports,
|
// Uninitialized to make sure cached results resolve immediately
|
||||||
FilterScopes.Uninitialized | FilterScopes.Initialized,
|
FilterScopes.Initialized | FilterScopes.Uninitialized,
|
||||||
) as WithStoreName
|
) as WithStoreName
|
||||||
|
|
||||||
/// STORE CACHING
|
/// STORE CACHING
|
||||||
|
|
@ -155,5 +141,6 @@ if (cache === Uncached)
|
||||||
asap(() => {
|
asap(() => {
|
||||||
const lookup = lookupModules(withStore())
|
const lookup = lookupModules(withStore())
|
||||||
|
|
||||||
while (lookup.next().done);
|
// Initialize all stores
|
||||||
|
for (const _ of lookup);
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -263,6 +263,7 @@ export namespace DiscordModules {
|
||||||
| 'tertiary'
|
| 'tertiary'
|
||||||
| 'destructive'
|
| 'destructive'
|
||||||
| 'active'
|
| 'active'
|
||||||
|
| 'expressive'
|
||||||
| 'primary-overlay'
|
| 'primary-overlay'
|
||||||
| 'secondary-overlay'
|
| 'secondary-overlay'
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { getCurrentStack } from '@revenge-mod/utils/error'
|
||||||
import { cacheFilterResultForId } from '../caches'
|
import { cacheFilterResultForId } from '../caches'
|
||||||
import { mInitialized } from '../metro/patches'
|
import { mInitialized } from '../metro/patches'
|
||||||
import { metroRequire } from '../metro/runtime'
|
import { metroRequire } from '../metro/runtime'
|
||||||
import { FilterFlag } from './filters'
|
import { isModuleInitialized } from '../metro/utils'
|
||||||
import type { If } from '@revenge-mod/utils/types'
|
import type { If } from '@revenge-mod/utils/types'
|
||||||
import type { Metro } from '../types'
|
import type { Metro } from '../types'
|
||||||
import type { Filter } from './filters'
|
import type { Filter } from './filters'
|
||||||
|
|
@ -72,34 +72,14 @@ const noDefaultExportsCache = new Set<Metro.ModuleID>()
|
||||||
// Currently, we only have options that are relevant for checking exports
|
// Currently, we only have options that are relevant for checking exports
|
||||||
// If we add more options later on, do NOT forget about
|
// If we add more options later on, do NOT forget about
|
||||||
// adding them here, and passing them in the lookup* functions
|
// adding them here, and passing them in the lookup* functions
|
||||||
export function runFilter(
|
|
||||||
filter: Filter<{ Result: any; RequiresExports: false; Scopes: any[] }>,
|
|
||||||
id: Metro.ModuleID,
|
|
||||||
): FilterResultFlag | undefined
|
|
||||||
|
|
||||||
export function runFilter(
|
|
||||||
filter: Filter,
|
|
||||||
id: Metro.ModuleID,
|
|
||||||
exports: Metro.ModuleExports,
|
|
||||||
options?: RunFilterOptions,
|
|
||||||
): FilterResultFlag | undefined
|
|
||||||
|
|
||||||
export function runFilter(
|
export function runFilter(
|
||||||
filter: Filter,
|
filter: Filter,
|
||||||
id: Metro.ModuleID,
|
id: Metro.ModuleID,
|
||||||
exports?: Metro.ModuleExports,
|
exports?: Metro.ModuleExports,
|
||||||
options?: RunFilterOptions,
|
options?: RunFilterOptions,
|
||||||
): FilterResultFlag | undefined {
|
): FilterResultFlag | undefined {
|
||||||
if (exports === undefined) {
|
if (!isModuleInitialized(id)) {
|
||||||
if (filter.flags & FilterFlag.RequiresExports) return
|
if (filter(id, undefined, false)) {
|
||||||
|
|
||||||
const filter_ = filter as Filter<{
|
|
||||||
Result: any
|
|
||||||
RequiresExports: false
|
|
||||||
Scopes: any[]
|
|
||||||
}>
|
|
||||||
|
|
||||||
if (filter_(id)) {
|
|
||||||
if (options?.initialize ?? true) {
|
if (options?.initialize ?? true) {
|
||||||
const module = metroRequire(id)
|
const module = metroRequire(id)
|
||||||
// Check if the required module is not blacklisted
|
// Check if the required module is not blacklisted
|
||||||
|
|
@ -118,13 +98,7 @@ export function runFilter(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const filter_ = filter as Filter<{
|
if (filter(id, exports, true))
|
||||||
Result: any
|
|
||||||
RequiresExports: true
|
|
||||||
Scopes: any[]
|
|
||||||
}>
|
|
||||||
|
|
||||||
if (filter_(id, exports))
|
|
||||||
return cacheFilterResultForId(
|
return cacheFilterResultForId(
|
||||||
filter.key,
|
filter.key,
|
||||||
id,
|
id,
|
||||||
|
|
@ -133,13 +107,15 @@ export function runFilter(
|
||||||
|
|
||||||
if (options?.skipDefault) return
|
if (options?.skipDefault) return
|
||||||
if (noDefaultExportsCache.has(id)) return
|
if (noDefaultExportsCache.has(id)) return
|
||||||
if (!('default' in exports)) {
|
|
||||||
|
const type = typeof exports
|
||||||
|
if (type !== 'object' || exports === null || !('default' in exports)) {
|
||||||
noDefaultExportsCache.add(id)
|
noDefaultExportsCache.add(id)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { default: defaultExport } = exports
|
const { default: defaultExport } = exports
|
||||||
if (filter_(id, defaultExport))
|
if (filter(id, defaultExport, true))
|
||||||
return cacheFilterResultForId(filter.key, id, FilterResultFlags.Default)
|
return cacheFilterResultForId(filter.key, id, FilterResultFlags.Default)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { FilterFlag } from './constants'
|
import { FilterScopes } from '.'
|
||||||
import { createFilterGenerator } from './utils'
|
import { createFilterGenerator } from './utils'
|
||||||
import type { Metro } from '@revenge-mod/modules/types'
|
import type { Metro } from '@revenge-mod/modules/types'
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -6,33 +6,48 @@ import type {
|
||||||
FilterBase,
|
FilterBase,
|
||||||
FilterGenerator,
|
FilterGenerator,
|
||||||
FilterInfoOf,
|
FilterInfoOf,
|
||||||
|
FilterScopeValue,
|
||||||
MergeFilterInfo,
|
MergeFilterInfo,
|
||||||
UnionFilterInfo,
|
UnionFilterInfo,
|
||||||
} from '.'
|
} from '.'
|
||||||
|
|
||||||
|
/** {@link FilterScopes.All} sets no predicate assumptions. Only {@link FilterScopes.Uninitialized} implies exportless. */
|
||||||
|
const canFilterUninitialized = (scopes: FilterScopeValue) =>
|
||||||
|
Boolean(scopes & FilterScopes.Uninitialized)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If both or neither can filter uninitialized, positional order is preserved.
|
||||||
|
*
|
||||||
|
* If only one can, the one that can will be used as a prefilter for uninitialized modules,
|
||||||
|
* and the other will only run on initialized modules.
|
||||||
|
*/
|
||||||
const compositeHandler = <G extends (a: FilterBase, b: FilterBase) => any>(
|
const compositeHandler = <G extends (a: FilterBase, b: FilterBase) => any>(
|
||||||
sameHandler: G,
|
directHandler: G,
|
||||||
dynamicHandler: G,
|
prefilteredHandler: G,
|
||||||
) =>
|
) =>
|
||||||
((a, b) =>
|
((a, b) => {
|
||||||
a.flags === b.flags
|
const ea = canFilterUninitialized(a.scopes)
|
||||||
? sameHandler(a, b)
|
return ea === canFilterUninitialized(b.scopes)
|
||||||
: a.flags === FilterFlag.RequiresExports
|
? directHandler(a, b)
|
||||||
? dynamicHandler(a, b)
|
: ea
|
||||||
: dynamicHandler(b, a)) as G
|
? prefilteredHandler(b, a)
|
||||||
|
: prefilteredHandler(a, b)
|
||||||
|
}) as G
|
||||||
|
|
||||||
const compositeArrayHandler = <
|
const compositeArrayHandler = <
|
||||||
G extends (args: [a: FilterBase, b: FilterBase]) => any,
|
G extends (args: [a: FilterBase, b: FilterBase]) => any,
|
||||||
>(
|
>(
|
||||||
sameHandler: G,
|
directHandler: G,
|
||||||
dynamicHandler: G,
|
prefilteredHandler: G,
|
||||||
) =>
|
) =>
|
||||||
(([a, b]) =>
|
(([a, b]) => {
|
||||||
a.flags === b.flags
|
const ea = canFilterUninitialized(a.scopes)
|
||||||
? sameHandler([a, b])
|
return ea === canFilterUninitialized(b.scopes)
|
||||||
: a.flags === FilterFlag.RequiresExports
|
? directHandler([a, b])
|
||||||
? dynamicHandler([a, b])
|
: ea
|
||||||
: dynamicHandler([b, a])) as G
|
? prefilteredHandler([b, a])
|
||||||
|
: prefilteredHandler([a, b])
|
||||||
|
}) as G
|
||||||
|
|
||||||
export type And = FilterGenerator<
|
export type And = FilterGenerator<
|
||||||
<F1 extends FilterBase, F2 extends FilterBase>(
|
<F1 extends FilterBase, F2 extends FilterBase>(
|
||||||
|
|
@ -46,49 +61,49 @@ const andKeyGenerator = ([a, b]: Parameters<And>) =>
|
||||||
|
|
||||||
const andScopesGenerator = ([a, b]: Parameters<And>) => a.scopes | b.scopes
|
const andScopesGenerator = ([a, b]: Parameters<And>) => a.scopes | b.scopes
|
||||||
|
|
||||||
const sameFlagsAnd = createFilterGenerator(
|
const directAnd = createFilterGenerator(
|
||||||
([a, b], id, exports) => a(id, exports) && b(id, exports),
|
([a, b], id, exports, initialized) =>
|
||||||
|
a(id, exports, initialized) && b(id, exports, initialized),
|
||||||
andKeyGenerator,
|
andKeyGenerator,
|
||||||
([a]) => a.flags,
|
|
||||||
andScopesGenerator,
|
andScopesGenerator,
|
||||||
) as And
|
) as And
|
||||||
|
|
||||||
const andFallbackCache = new WeakMap<FilterBase, Set<Metro.ModuleID>>()
|
const andPrefilterCache = new WeakMap<FilterBase, Set<Metro.ModuleID>>()
|
||||||
|
|
||||||
const dynamicFlagsAnd = createFilterGenerator(
|
const prefilteredAnd = createFilterGenerator(
|
||||||
([filter, fallbackFilter], id, exports) => {
|
([filter, prefilter], id, exports, initialized) => {
|
||||||
if (exports) {
|
if (initialized) {
|
||||||
if (filter(id, exports)) {
|
if (filter(id, exports, true)) {
|
||||||
// Avoid running the fallback filter if we already know the first one passed
|
// Avoid running the prefilter again if we already know it passed
|
||||||
const cache = andFallbackCache.get(fallbackFilter)
|
const cache = andPrefilterCache.get(prefilter)
|
||||||
return (
|
return (
|
||||||
// biome-ignore lint/complexity/useOptionalChain: Hot path should be optimized
|
// biome-ignore lint/complexity/useOptionalChain: Hot path should be optimized
|
||||||
(cache && cache.has(id)) || fallbackFilter(id, exports)
|
(cache && cache.has(id)) || prefilter(id, exports, true)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = fallbackFilter(id)
|
const result = prefilter(id, undefined, false)
|
||||||
if (result) {
|
if (result) {
|
||||||
// Cache fallback hits to avoid calling the fallback filter again
|
// Cache prefilter hits to avoid calling the prefilter again
|
||||||
// Fallback filters are usually more expensive
|
// Prefilters are usually more expensive
|
||||||
let set = andFallbackCache.get(fallbackFilter)
|
let set = andPrefilterCache.get(prefilter)
|
||||||
if (!set) andFallbackCache.set(fallbackFilter, (set = new Set()))
|
if (!set) andPrefilterCache.set(prefilter, (set = new Set()))
|
||||||
set.add(id)
|
set.add(id)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
andKeyGenerator,
|
andKeyGenerator,
|
||||||
FilterFlag.Dynamic,
|
|
||||||
andScopesGenerator,
|
andScopesGenerator,
|
||||||
) as And
|
) as And
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combines two filters into one, returning true if **every** filter matches.
|
* Combines two filters into one, returning true if **every** filter matches.
|
||||||
*
|
*
|
||||||
* If each filter has different flags,
|
* If only one of the filters can run on uninitialized modules ({@link FilterScopes.Uninitialized}),
|
||||||
|
* it is used as the prefilter for uninitialized modules, and the other only runs once a candidate is initialized.
|
||||||
*
|
*
|
||||||
* @param filters The filters to combine.
|
* @param filters The filters to combine.
|
||||||
*
|
*
|
||||||
|
|
@ -111,23 +126,13 @@ const dynamicFlagsAnd = createFilterGenerator(
|
||||||
* )
|
* )
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const and = Object.assign(
|
export const and = Object.assign(compositeHandler(directAnd, prefilteredAnd), {
|
||||||
compositeHandler(sameFlagsAnd, dynamicFlagsAnd),
|
keyFor: compositeArrayHandler(directAnd.keyFor, prefilteredAnd.keyFor),
|
||||||
{
|
defaultScopesFor: compositeArrayHandler(
|
||||||
keyFor: compositeArrayHandler(
|
directAnd.defaultScopesFor,
|
||||||
sameFlagsAnd.keyFor,
|
prefilteredAnd.defaultScopesFor,
|
||||||
dynamicFlagsAnd.keyFor,
|
),
|
||||||
),
|
}) satisfies And
|
||||||
flagsFor: compositeArrayHandler(
|
|
||||||
sameFlagsAnd.flagsFor,
|
|
||||||
dynamicFlagsAnd.flagsFor,
|
|
||||||
),
|
|
||||||
defaultScopesFor: compositeArrayHandler(
|
|
||||||
sameFlagsAnd.defaultScopesFor,
|
|
||||||
dynamicFlagsAnd.defaultScopesFor,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
) satisfies And
|
|
||||||
|
|
||||||
export type Or = FilterGenerator<
|
export type Or = FilterGenerator<
|
||||||
<F1 extends FilterBase, F2 extends FilterBase>(
|
<F1 extends FilterBase, F2 extends FilterBase>(
|
||||||
|
|
@ -141,21 +146,21 @@ const orKeyGenerator = ([a, b]: Parameters<Or>) =>
|
||||||
|
|
||||||
const orScopesGenerator = ([a, b]: Parameters<Or>) => a.scopes | b.scopes
|
const orScopesGenerator = ([a, b]: Parameters<Or>) => a.scopes | b.scopes
|
||||||
|
|
||||||
const sameFlagOr = createFilterGenerator(
|
const directOr = createFilterGenerator(
|
||||||
([a, b], id, exports) => a(id, exports) || b(id, exports),
|
([a, b], id, exports, initialized) =>
|
||||||
|
a(id, exports, initialized) || b(id, exports, initialized),
|
||||||
orKeyGenerator,
|
orKeyGenerator,
|
||||||
([a]) => a.flags,
|
|
||||||
orScopesGenerator,
|
orScopesGenerator,
|
||||||
) as Or
|
) as Or
|
||||||
|
|
||||||
const dynamicFlagOr = createFilterGenerator(
|
const prefilteredOr = createFilterGenerator(
|
||||||
([filter, fallbackFilter], id, exports) => {
|
([filter, prefilter], id, exports, initialized) => {
|
||||||
// TODO(PalmDevs): Potential optimization: Add fallback cache here too?
|
// TODO(PalmDevs): Potential optimization: Add prefilter cache here too?
|
||||||
if (exports) return filter(id, exports) || fallbackFilter(id, exports)
|
if (initialized)
|
||||||
return fallbackFilter(id)
|
return filter(id, exports, true) || prefilter(id, exports, true)
|
||||||
|
return prefilter(id, undefined, false)
|
||||||
},
|
},
|
||||||
orKeyGenerator,
|
orKeyGenerator,
|
||||||
FilterFlag.Dynamic,
|
|
||||||
orScopesGenerator,
|
orScopesGenerator,
|
||||||
) as Or
|
) as Or
|
||||||
|
|
||||||
|
|
@ -183,17 +188,10 @@ const dynamicFlagOr = createFilterGenerator(
|
||||||
* )
|
* )
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const or = Object.assign(
|
export const or = Object.assign(compositeHandler<Or>(directOr, prefilteredOr), {
|
||||||
compositeHandler<Or>(sameFlagOr, dynamicFlagOr),
|
keyFor: compositeArrayHandler(directOr.keyFor, prefilteredOr.keyFor),
|
||||||
{
|
defaultScopesFor: compositeArrayHandler(
|
||||||
keyFor: compositeArrayHandler(sameFlagOr.keyFor, dynamicFlagOr.keyFor),
|
directOr.defaultScopesFor,
|
||||||
flagsFor: compositeArrayHandler(
|
prefilteredOr.defaultScopesFor,
|
||||||
sameFlagOr.flagsFor,
|
),
|
||||||
dynamicFlagOr.flagsFor,
|
}) satisfies Or
|
||||||
),
|
|
||||||
defaultScopesFor: compositeArrayHandler(
|
|
||||||
sameFlagOr.defaultScopesFor,
|
|
||||||
dynamicFlagOr.defaultScopesFor,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
) satisfies Or
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,3 @@
|
||||||
export const FilterFlag = {
|
|
||||||
/**
|
|
||||||
* This filter works with and without module exports.
|
|
||||||
* Allowing for both initialized and uninitialized modules to be matched.
|
|
||||||
*/
|
|
||||||
Dynamic: 0,
|
|
||||||
/**
|
|
||||||
* This filter requires module exports to work.
|
|
||||||
* Only initialized modules will be matched.
|
|
||||||
*/
|
|
||||||
RequiresExports: 1,
|
|
||||||
} as const
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see {@link FilterFlag}
|
|
||||||
*/
|
|
||||||
export type FilterFlag = number
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scopes to limit filters to certain module states.
|
* Scopes to limit filters to certain module states.
|
||||||
*/
|
*/
|
||||||
|
|
@ -23,10 +5,16 @@ export const FilterScopes = {
|
||||||
/**
|
/**
|
||||||
* Include all modules (both initialized and uninitialized, including blacklisted).
|
* Include all modules (both initialized and uninitialized, including blacklisted).
|
||||||
* This overrides {@link FilterScopes.Uninitialized} and {@link FilterScopes.Initialized}.
|
* This overrides {@link FilterScopes.Uninitialized} and {@link FilterScopes.Initialized}.
|
||||||
|
*
|
||||||
|
* **Filter generators generally don't need this scope.**
|
||||||
|
*
|
||||||
|
* When combining multiple filters with composite filters, the {@link FilterScopes.All} scope doesn't set assumptions for the filter predicate.
|
||||||
|
* It only decides which modules to run the predicate against.
|
||||||
|
* **Filter generators must include {@link FilterScopes.Uninitialized} and/or {@link FilterScopes.Initialized} as well.**
|
||||||
*/
|
*/
|
||||||
All: 1,
|
All: 1,
|
||||||
/**
|
/**
|
||||||
* Include uninitialized modules in the search.
|
* Include uninitialized modules in the search. Implies the predicate can run without exports.
|
||||||
*/
|
*/
|
||||||
Uninitialized: 2,
|
Uninitialized: 2,
|
||||||
/**
|
/**
|
||||||
|
|
@ -47,10 +35,6 @@ export interface FilterInfo {
|
||||||
* The result type of the filter.
|
* The result type of the filter.
|
||||||
*/
|
*/
|
||||||
Result: any
|
Result: any
|
||||||
/**
|
|
||||||
* Whether the filter requires exports to work.
|
|
||||||
*/
|
|
||||||
RequiresExports: boolean
|
|
||||||
/**
|
/**
|
||||||
* Scopes the filter matches modules in.
|
* Scopes the filter matches modules in.
|
||||||
*/
|
*/
|
||||||
|
|
@ -59,6 +43,5 @@ export interface FilterInfo {
|
||||||
|
|
||||||
export interface DefaultFilterInfo extends FilterInfo {
|
export interface DefaultFilterInfo extends FilterInfo {
|
||||||
Result: any
|
Result: any
|
||||||
RequiresExports: boolean
|
|
||||||
Scopes: FilterScope[]
|
Scopes: FilterScope[]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import {
|
||||||
getModuleDependencies,
|
getModuleDependencies,
|
||||||
} from '../../metro/utils'
|
} from '../../metro/utils'
|
||||||
import { runFilter } from '../_internal'
|
import { runFilter } from '../_internal'
|
||||||
import { FilterFlag, FilterScopes } from './constants'
|
import { FilterScopes } from './constants'
|
||||||
import { createFilterGenerator } from './utils'
|
import { createFilterGenerator } from './utils'
|
||||||
import type { Metro } from '../../types'
|
import type { Metro } from '../../types'
|
||||||
import type { Filter, FilterGenerator } from './utils'
|
import type { Filter, FilterGenerator } from './utils'
|
||||||
|
|
@ -18,8 +18,18 @@ export interface ComparableDependencyMap
|
||||||
| ComparableDependencyMap
|
| ComparableDependencyMap
|
||||||
| Filter
|
| Filter
|
||||||
> {
|
> {
|
||||||
|
// loose
|
||||||
l?: boolean
|
l?: boolean
|
||||||
|
// relative
|
||||||
r?: number
|
r?: number
|
||||||
|
// skip
|
||||||
|
s?: number
|
||||||
|
// atLeast
|
||||||
|
n?: number
|
||||||
|
// atMost
|
||||||
|
x?: number
|
||||||
|
// includes
|
||||||
|
i?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -30,6 +40,9 @@ export interface ComparableDependencyMap
|
||||||
* To do proper fingerprinting for modules:
|
* To do proper fingerprinting for modules:
|
||||||
* @see {@link withDependencies.loose} to loosen the checks.
|
* @see {@link withDependencies.loose} to loosen the checks.
|
||||||
* @see {@link withDependencies.relative} to compare dependencies relatively.
|
* @see {@link withDependencies.relative} to compare dependencies relatively.
|
||||||
|
* @see {@link withDependencies.skip} and {@link withDependencies.last} to move where comparison starts.
|
||||||
|
* @see {@link withDependencies.atLeast} and {@link withDependencies.atMost} to bound the dependency count.
|
||||||
|
* @see {@link withDependencies.includes} to compare without caring about order.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
|
|
@ -75,30 +88,30 @@ export interface ComparableDependencyMap
|
||||||
*/
|
*/
|
||||||
const withDependencies_ = createFilterGenerator<Parameters<WithDependencies>>(
|
const withDependencies_ = createFilterGenerator<Parameters<WithDependencies>>(
|
||||||
([deps], id) => depCompare(getModuleDependencies(id)!, deps, id, id),
|
([deps], id) => depCompare(getModuleDependencies(id)!, deps, id, id),
|
||||||
|
// The dep map is keyed as a nested set, so its modifiers are already part of the key
|
||||||
deps => `revenge.deps(${depGenFilterKey(deps)})`,
|
deps => `revenge.deps(${depGenFilterKey(deps)})`,
|
||||||
FilterFlag.Dynamic,
|
|
||||||
FilterScopes.Uninitialized | FilterScopes.Initialized,
|
FilterScopes.Uninitialized | FilterScopes.Initialized,
|
||||||
) as WithDependencies
|
) as WithDependencies
|
||||||
|
|
||||||
export const withDependencies = __DEV__
|
export const withDependencies = __DEV__
|
||||||
? (((deps: ComparableDependencyMap) => {
|
? (Object.assign((deps: ComparableDependencyMap) => {
|
||||||
// Warn about using undefined in deps, which is likely a mistake
|
DEBUG_validateWithDependenciesFilter(deps)
|
||||||
for (let i = 0; i < deps.length; i++) {
|
|
||||||
if (deps[i] === undefined)
|
|
||||||
DEBUG_warnBadWithDependenciesFilter(deps, i)
|
|
||||||
}
|
|
||||||
|
|
||||||
return withDependencies_(deps)
|
return withDependencies_(deps)
|
||||||
}) as WithDependencies)
|
}, withDependencies_) as WithDependencies)
|
||||||
: withDependencies_
|
: withDependencies_
|
||||||
|
|
||||||
withDependencies.loose = loose
|
withDependencies.loose = loose
|
||||||
withDependencies.relative = relative
|
withDependencies.relative = relative
|
||||||
|
withDependencies.skip = skip
|
||||||
|
withDependencies.last = last
|
||||||
|
withDependencies.atLeast = atLeast
|
||||||
|
withDependencies.atMost = atMost
|
||||||
|
withDependencies.includes = includes
|
||||||
|
|
||||||
type WithDependencies = FilterGenerator<
|
type WithDependencies = FilterGenerator<
|
||||||
<T>(deps: ComparableDependencyMap) => Filter<{
|
<T>(deps: ComparableDependencyMap) => Filter<{
|
||||||
Result: T
|
Result: T
|
||||||
RequiresExports: false
|
|
||||||
Scopes: [
|
Scopes: [
|
||||||
typeof FilterScopes.Uninitialized,
|
typeof FilterScopes.Uninitialized,
|
||||||
typeof FilterScopes.Initialized,
|
typeof FilterScopes.Initialized,
|
||||||
|
|
@ -107,6 +120,11 @@ type WithDependencies = FilterGenerator<
|
||||||
> & {
|
> & {
|
||||||
loose: typeof loose
|
loose: typeof loose
|
||||||
relative: typeof relative
|
relative: typeof relative
|
||||||
|
skip: typeof skip
|
||||||
|
last: typeof last
|
||||||
|
atLeast: typeof atLeast
|
||||||
|
atMost: typeof atMost
|
||||||
|
includes: typeof includes
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -123,6 +141,96 @@ function loose(deps: ComparableDependencyMap) {
|
||||||
return deps
|
return deps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skip a number of dependencies before comparing positionally.
|
||||||
|
*
|
||||||
|
* Passing `Infinity` anchors the set to the end, matching the **last** `deps.length` dependencies.
|
||||||
|
* Anything before them is unconstrained.
|
||||||
|
*
|
||||||
|
* @param amount The amount of dependencies to skip from the start, or `Infinity` to anchor to the end.
|
||||||
|
* @param deps The dependency map to skip in. This permanently modifies the array.
|
||||||
|
* @returns The modified dependency map.
|
||||||
|
*
|
||||||
|
* @see {@link withDependencies.last} for the `Infinity` shorthand.
|
||||||
|
*/
|
||||||
|
function skip(amount: number, deps: ComparableDependencyMap = []) {
|
||||||
|
deps.s = amount
|
||||||
|
return deps
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match the **last** `deps.length` dependencies, leaving anything before them unconstrained.
|
||||||
|
*
|
||||||
|
* Shorthand for {@link withDependencies.skip} with `Infinity`.
|
||||||
|
* Prefer this over leading comparisons when a module's trailing dependencies are the stable part of its fingerprint.
|
||||||
|
*
|
||||||
|
* @param deps The dependency map to anchor to the end. This permanently modifies the array.
|
||||||
|
* @returns The modified dependency map.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const { last, relative } = withDependencies
|
||||||
|
*
|
||||||
|
* // Matches modules whose last three dependencies are [Any, module ID + 1, 2]
|
||||||
|
* withDependencies(last([null, relative(1), 2]))
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
function last(deps: ComparableDependencyMap = []) {
|
||||||
|
deps.s = Infinity
|
||||||
|
return deps
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require the module to have at least `count` dependencies.
|
||||||
|
*
|
||||||
|
* This implies {@link withDependencies.loose}, as an exact length check would never pass alongside a bound.
|
||||||
|
*
|
||||||
|
* @param count The minimum amount of dependencies.
|
||||||
|
* @param deps The dependency map to bound. This permanently modifies the array.
|
||||||
|
* @returns The modified dependency map.
|
||||||
|
*/
|
||||||
|
function atLeast(count: number, deps: ComparableDependencyMap = []) {
|
||||||
|
deps.n = count
|
||||||
|
deps.l = true
|
||||||
|
return deps
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require the module to have at most `count` dependencies.
|
||||||
|
*
|
||||||
|
* This implies {@link withDependencies.loose}, as an exact length check would never pass alongside a bound.
|
||||||
|
*
|
||||||
|
* @param count The maximum amount of dependencies.
|
||||||
|
* @param deps The dependency map to bound. This permanently modifies the array.
|
||||||
|
* @returns The modified dependency map.
|
||||||
|
*/
|
||||||
|
function atMost(count: number, deps: ComparableDependencyMap = []) {
|
||||||
|
deps.x = count
|
||||||
|
deps.l = true
|
||||||
|
return deps
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare the set without caring about order or position, only that every dependency exists somewhere.
|
||||||
|
*
|
||||||
|
* Entries are matched independently, so two identical entries can both match the same dependency.
|
||||||
|
* Dynamic (`null`) entries are meaningless here and are ignored.
|
||||||
|
*
|
||||||
|
* **This is much more expensive than positional comparison**, as every entry is compared against every dependency.
|
||||||
|
* Bound it with {@link withDependencies.atLeast} or {@link withDependencies.atMost} where possible, as those are checked first.
|
||||||
|
*
|
||||||
|
* @param deps The dependency map to compare unordered. This permanently modifies the array.
|
||||||
|
* @returns The modified dependency map.
|
||||||
|
*/
|
||||||
|
function includes(deps: ComparableDependencyMap) {
|
||||||
|
if (__DEV__)
|
||||||
|
for (let i = 0; i < deps.length; i++)
|
||||||
|
if (deps[i] == null) DEBUG_warnBadIncludesDependency(deps, i)
|
||||||
|
|
||||||
|
deps.i = true
|
||||||
|
return deps
|
||||||
|
}
|
||||||
|
|
||||||
const RelativeSignBit = 1 << 30
|
const RelativeSignBit = 1 << 30
|
||||||
const RelativeBit = 1 << 29
|
const RelativeBit = 1 << 29
|
||||||
const RelativeRootBit = 1 << 28
|
const RelativeRootBit = 1 << 28
|
||||||
|
|
@ -192,6 +300,70 @@ function DEBUG_warnBadWithDependenciesFilter(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warns the developer about a dynamic dependency in a `withDependencies.includes` set, which matches anything and is therefore a no-op.
|
||||||
|
*/
|
||||||
|
function DEBUG_warnBadIncludesDependency(
|
||||||
|
deps: ComparableDependencyMap,
|
||||||
|
index: number,
|
||||||
|
) {
|
||||||
|
nativeLoggingHook(
|
||||||
|
`\u001b[33mBad withDependencies.includes set, dynamic ID at index ${index} matches anything: [${depGenFilterKey(deps)}]\n${getCurrentStack()}\u001b[0m`,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warns the developer about dependency count bounds that contradict, or are already implied by, the set being compared.
|
||||||
|
*/
|
||||||
|
function DEBUG_warnBadDependencyBounds(
|
||||||
|
deps: ComparableDependencyMap,
|
||||||
|
reason: string,
|
||||||
|
) {
|
||||||
|
nativeLoggingHook(
|
||||||
|
`\u001b[33mBad withDependencies filter, ${reason}: [${depGenFilterKey(deps)}]\n${getCurrentStack()}\u001b[0m`,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a dependency map and its nested sets, warning about mistakes that would otherwise silently never match.
|
||||||
|
*/
|
||||||
|
function DEBUG_validateWithDependenciesFilter(deps: ComparableDependencyMap) {
|
||||||
|
const { n: min, x: max, s: skip } = deps
|
||||||
|
|
||||||
|
// The lowest dependency count the set itself can match.
|
||||||
|
// Unordered sets have no window, so they only ever constrain the count through bounds.
|
||||||
|
const floor = deps.i
|
||||||
|
? 0
|
||||||
|
: (skip === undefined || skip === Infinity ? 0 : skip) + deps.length
|
||||||
|
|
||||||
|
if (max !== undefined && max < floor)
|
||||||
|
DEBUG_warnBadDependencyBounds(
|
||||||
|
deps,
|
||||||
|
`atMost(${max}) can never match, the set requires at least ${floor} dependencies`,
|
||||||
|
)
|
||||||
|
else if (min !== undefined && max !== undefined && min > max)
|
||||||
|
DEBUG_warnBadDependencyBounds(
|
||||||
|
deps,
|
||||||
|
`atLeast(${min}) can never match, atMost(${max}) is lower`,
|
||||||
|
)
|
||||||
|
else if (min !== undefined && min <= floor)
|
||||||
|
DEBUG_warnBadDependencyBounds(
|
||||||
|
deps,
|
||||||
|
`atLeast(${min}) does nothing, the set already requires at least ${floor} dependencies`,
|
||||||
|
)
|
||||||
|
|
||||||
|
for (let i = 0; i < deps.length; i++) {
|
||||||
|
const dep = deps[i]
|
||||||
|
|
||||||
|
// Warn about using undefined in deps, which is likely a mistake
|
||||||
|
if (dep === undefined) DEBUG_warnBadWithDependenciesFilter(deps, i)
|
||||||
|
else if (typeof dep === 'object' && dep !== null)
|
||||||
|
DEBUG_validateWithDependenciesFilter(dep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function depCompare(
|
function depCompare(
|
||||||
a: Metro.ModuleID[],
|
a: Metro.ModuleID[],
|
||||||
b: ComparableDependencyMap,
|
b: ComparableDependencyMap,
|
||||||
|
|
@ -200,7 +372,29 @@ function depCompare(
|
||||||
): boolean {
|
): boolean {
|
||||||
const lenA = a.length
|
const lenA = a.length
|
||||||
const lenB = b.length
|
const lenB = b.length
|
||||||
if (b.l ? lenA < lenB : lenA !== lenB) return false
|
|
||||||
|
const min = b.n
|
||||||
|
if (min !== undefined && lenA < min) return false
|
||||||
|
|
||||||
|
const max = b.x
|
||||||
|
if (max !== undefined && lenA > max) return false
|
||||||
|
|
||||||
|
if (b.i) return depIncludesCompare(a, b, root, parent)
|
||||||
|
|
||||||
|
// Index in a where positional comparison starts
|
||||||
|
let start = 0
|
||||||
|
const skip = b.s
|
||||||
|
|
||||||
|
if (skip === undefined) {
|
||||||
|
if (b.l ? lenA < lenB : lenA !== lenB) return false
|
||||||
|
} else if (skip === Infinity) {
|
||||||
|
// Anchor to the end, so everything before the last lenB dependencies is unconstrained
|
||||||
|
start = lenA - lenB
|
||||||
|
if (start < 0) return false
|
||||||
|
} else {
|
||||||
|
start = skip
|
||||||
|
if (b.l ? lenA < start + lenB : lenA !== start + lenB) return false
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < lenB; i++) {
|
for (let i = 0; i < lenB; i++) {
|
||||||
const compare = b[i]
|
const compare = b[i]
|
||||||
|
|
@ -211,43 +405,71 @@ function depCompare(
|
||||||
// Skip dynamic
|
// Skip dynamic
|
||||||
if (compare == null) continue
|
if (compare == null) continue
|
||||||
|
|
||||||
const id = a[i]
|
const id = a[start + i]
|
||||||
|
|
||||||
// Very rare case where a module dependency has an unused `null` dependency
|
// Very rare case where a module dependency has an unused `null` dependency
|
||||||
// TODO: Probably a Metro/Discord bug?
|
// TODO: Probably a Metro/Discord bug?
|
||||||
if (id === null) return false
|
if (id === null) return false
|
||||||
|
|
||||||
switch (typeof compare) {
|
if (!depMatches(compare, id, root, parent)) return false
|
||||||
case 'function': {
|
|
||||||
const filter = compare
|
|
||||||
if (!runFilter(filter, id, getInitializedModuleExports(id)))
|
|
||||||
return false
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
case 'object': {
|
|
||||||
const nested = compare as ComparableDependencyMap
|
|
||||||
|
|
||||||
// relative.withDependencies?
|
|
||||||
if (nested.r && !depShallowCompare(nested.r, id, root, parent))
|
|
||||||
return false
|
|
||||||
|
|
||||||
if (depCompare(getModuleDependencies(id)!, nested, root, id))
|
|
||||||
continue
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
if (depShallowCompare(compare as number, id, root, parent))
|
|
||||||
continue
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function depIncludesCompare(
|
||||||
|
a: Metro.ModuleID[],
|
||||||
|
b: ComparableDependencyMap,
|
||||||
|
root: Metro.ModuleID,
|
||||||
|
parent: Metro.ModuleID,
|
||||||
|
): boolean {
|
||||||
|
const lenA = a.length
|
||||||
|
|
||||||
|
for (let i = 0; i < b.length; i++) {
|
||||||
|
const compare = b[i]
|
||||||
|
|
||||||
|
// Dynamic dependencies match anything, so they carry no meaning when unordered
|
||||||
|
if (compare == null) continue
|
||||||
|
|
||||||
|
let found = false
|
||||||
|
|
||||||
|
for (let j = 0; j < lenA; j++) {
|
||||||
|
const id = a[j]
|
||||||
|
if (id !== null && depMatches(compare, id, root, parent)) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function depMatches(
|
||||||
|
compare: NonNullable<ComparableDependencyMap[number]>,
|
||||||
|
id: Metro.ModuleID,
|
||||||
|
root: Metro.ModuleID,
|
||||||
|
parent: Metro.ModuleID,
|
||||||
|
): boolean {
|
||||||
|
switch (typeof compare) {
|
||||||
|
case 'function':
|
||||||
|
return !!runFilter(compare, id, getInitializedModuleExports(id))
|
||||||
|
case 'object': {
|
||||||
|
const nested = compare
|
||||||
|
|
||||||
|
// relative.withDependencies?
|
||||||
|
if (nested.r && !depShallowCompare(nested.r, id, root, parent))
|
||||||
|
return false
|
||||||
|
|
||||||
|
return depCompare(getModuleDependencies(id)!, nested, root, id)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return depShallowCompare(compare, id, root, parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function depShallowCompare(
|
function depShallowCompare(
|
||||||
compare: number,
|
compare: number,
|
||||||
id: Metro.ModuleID,
|
id: Metro.ModuleID,
|
||||||
|
|
@ -291,11 +513,7 @@ function depGenFilterKey(deps: ComparableDependencyMap): string {
|
||||||
case 'object': {
|
case 'object': {
|
||||||
// It's a nested dependency array
|
// It's a nested dependency array
|
||||||
const nested = dep as ComparableDependencyMap
|
const nested = dep as ComparableDependencyMap
|
||||||
if (nested.l) key += '#'
|
key += `${depGenModifierKey(nested)}[${depGenFilterKey(nested)}],`
|
||||||
// relative.withDependencies?
|
|
||||||
if (nested.r) key += `${depGenRelativeKeyPart(nested.r)}:`
|
|
||||||
|
|
||||||
key += `[${depGenFilterKey(nested)}],`
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
|
@ -311,6 +529,20 @@ function depGenFilterKey(deps: ComparableDependencyMap): string {
|
||||||
return key.substring(0, key.length - 1)
|
return key.substring(0, key.length - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function depGenModifierKey(deps: ComparableDependencyMap): string {
|
||||||
|
let key = ''
|
||||||
|
|
||||||
|
if (deps.i) key += '?'
|
||||||
|
if (deps.l) key += '#'
|
||||||
|
if (deps.n !== undefined) key += `>${deps.n}`
|
||||||
|
if (deps.x !== undefined) key += `<${deps.x}`
|
||||||
|
if (deps.s !== undefined) key += `+${deps.s === Infinity ? '*' : deps.s}`
|
||||||
|
// relative.withDependencies?
|
||||||
|
if (deps.r) key += `${depGenRelativeKeyPart(deps.r)}:`
|
||||||
|
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
function depGenRelativeKeyPart(dep: number) {
|
function depGenRelativeKeyPart(dep: number) {
|
||||||
const magnitude = depGetRelMagnitude(dep)
|
const magnitude = depGetRelMagnitude(dep)
|
||||||
const prefix = dep & RelativeRootBit ? '~' : '^'
|
const prefix = dep & RelativeRootBit ? '~' : '^'
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { isModuleExportBad } from '@revenge-mod/modules/metro/utils'
|
import { isModuleExportBad } from '@revenge-mod/modules/metro/utils'
|
||||||
import { FilterFlag, FilterScopes } from './constants'
|
import { FilterScopes } from './constants'
|
||||||
import { createFilterGenerator } from './utils'
|
import { createFilterGenerator } from './utils'
|
||||||
import type { Filter, FilterGenerator } from './utils'
|
import type { Filter, FilterGenerator } from './utils'
|
||||||
|
|
||||||
|
|
@ -8,7 +8,6 @@ export * from './utils'
|
||||||
|
|
||||||
type FilterRequiringExports<T> = Filter<{
|
type FilterRequiringExports<T> = Filter<{
|
||||||
Result: T
|
Result: T
|
||||||
RequiresExports: true
|
|
||||||
Scopes: [typeof FilterScopes.Initialized]
|
Scopes: [typeof FilterScopes.Initialized]
|
||||||
}>
|
}>
|
||||||
|
|
||||||
|
|
@ -41,7 +40,6 @@ export const withProps = createFilterGenerator<Parameters<WithProps>>(
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
props => `revenge.props(${props.join(',')})`,
|
props => `revenge.props(${props.join(',')})`,
|
||||||
FilterFlag.RequiresExports,
|
|
||||||
FilterScopes.Initialized,
|
FilterScopes.Initialized,
|
||||||
) as WithProps
|
) as WithProps
|
||||||
|
|
||||||
|
|
@ -67,7 +65,6 @@ export const withoutProps = createFilterGenerator<Parameters<WithoutProps>>(
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
props => `revenge.withoutProps(${props.join(',')})`,
|
props => `revenge.withoutProps(${props.join(',')})`,
|
||||||
FilterFlag.RequiresExports,
|
|
||||||
FilterScopes.Initialized,
|
FilterScopes.Initialized,
|
||||||
) as WithoutProps
|
) as WithoutProps
|
||||||
|
|
||||||
|
|
@ -97,7 +94,6 @@ export const withSingleProp = createFilterGenerator<Parameters<WithSingleProp>>(
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
([prop]) => `revenge.singleProp(${prop})`,
|
([prop]) => `revenge.singleProp(${prop})`,
|
||||||
FilterFlag.RequiresExports,
|
|
||||||
FilterScopes.Initialized,
|
FilterScopes.Initialized,
|
||||||
) as WithSingleProp
|
) as WithSingleProp
|
||||||
|
|
||||||
|
|
@ -138,7 +134,6 @@ export type WithSingleProp = FilterGenerator<
|
||||||
export const withName = createFilterGenerator<Parameters<WithName>>(
|
export const withName = createFilterGenerator<Parameters<WithName>>(
|
||||||
([name], _, exports) => exports?.name === name,
|
([name], _, exports) => exports?.name === name,
|
||||||
([name]) => `revenge.name(${name})`,
|
([name]) => `revenge.name(${name})`,
|
||||||
FilterFlag.RequiresExports,
|
|
||||||
FilterScopes.Initialized,
|
FilterScopes.Initialized,
|
||||||
) as WithName
|
) as WithName
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,32 @@
|
||||||
|
import { isModuleInitialized } from '@revenge-mod/modules/metro/utils'
|
||||||
import { getErrorStack } from '@revenge-mod/utils/error'
|
import { getErrorStack } from '@revenge-mod/utils/error'
|
||||||
import { withProps } from '.'
|
import { withProps } from '.'
|
||||||
import { and, or } from './composite'
|
import { and, or } from './composite'
|
||||||
import { FilterScopes } from './constants'
|
import { FilterScopes } from './constants'
|
||||||
import type { Metro } from '@revenge-mod/modules/types'
|
import type { Metro } from '@revenge-mod/modules/types'
|
||||||
import type { If, LogicalAnd } from '@revenge-mod/utils/types'
|
|
||||||
import type {
|
import type {
|
||||||
DefaultFilterInfo,
|
DefaultFilterInfo,
|
||||||
FilterFlag,
|
|
||||||
FilterInfo,
|
FilterInfo,
|
||||||
FilterScope,
|
FilterScope,
|
||||||
FilterScopes as FilterScopesObject,
|
|
||||||
FilterScopeValue,
|
FilterScopeValue,
|
||||||
} from './constants'
|
} from './constants'
|
||||||
|
|
||||||
export type FilterResult<F> = F extends Filter<infer I> ? I['Result'] : never
|
export type FilterResult<F> = F extends Filter<infer I> ? I['Result'] : never
|
||||||
|
|
||||||
export type FilterRequiresExports<F> =
|
|
||||||
F extends Filter<infer I> ? I['RequiresExports'] : never
|
|
||||||
|
|
||||||
export type FilterInfoOf<F> = F extends Filter<infer I> ? I : FilterInfo
|
export type FilterInfoOf<F> = F extends Filter<infer I> ? I : FilterInfo
|
||||||
|
|
||||||
|
declare const FilterInfoBrand: unique symbol
|
||||||
|
|
||||||
export interface FilterBase<Info extends FilterInfo = DefaultFilterInfo> {
|
export interface FilterBase<Info extends FilterInfo = DefaultFilterInfo> {
|
||||||
(
|
(
|
||||||
...args: If<
|
id: Metro.ModuleID,
|
||||||
Info['RequiresExports'],
|
exports?: Metro.ModuleExports,
|
||||||
[id: Metro.ModuleID, exports: Metro.ModuleExports],
|
initialized?: boolean,
|
||||||
[id: Metro.ModuleID, exports?: never]
|
|
||||||
>
|
|
||||||
): boolean
|
): boolean
|
||||||
key: string
|
key: string
|
||||||
flags: If<
|
|
||||||
Info['RequiresExports'],
|
|
||||||
(typeof FilterFlag)['RequiresExports'],
|
|
||||||
FilterFlag
|
|
||||||
>
|
|
||||||
scopes: FilterScopeValue
|
scopes: FilterScopeValue
|
||||||
|
/** @internal */
|
||||||
|
readonly [FilterInfoBrand]?: Info
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Filter<Info extends FilterInfo = DefaultFilterInfo> =
|
export type Filter<Info extends FilterInfo = DefaultFilterInfo> =
|
||||||
|
|
@ -42,13 +34,11 @@ export type Filter<Info extends FilterInfo = DefaultFilterInfo> =
|
||||||
|
|
||||||
export type MergeFilterInfo<I1 extends FilterInfo, I2 extends FilterInfo> = {
|
export type MergeFilterInfo<I1 extends FilterInfo, I2 extends FilterInfo> = {
|
||||||
Result: I1['Result'] & I2['Result']
|
Result: I1['Result'] & I2['Result']
|
||||||
RequiresExports: LogicalAnd<I1['RequiresExports'], I2['RequiresExports']>
|
|
||||||
Scopes: [...I1['Scopes'], ...I2['Scopes']]
|
Scopes: [...I1['Scopes'], ...I2['Scopes']]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UnionFilterInfo<I1 extends FilterInfo, I2 extends FilterInfo> = {
|
export type UnionFilterInfo<I1 extends FilterInfo, I2 extends FilterInfo> = {
|
||||||
Result: I1['Result'] | I2['Result']
|
Result: I1['Result'] | I2['Result']
|
||||||
RequiresExports: LogicalAnd<I1['RequiresExports'], I2['RequiresExports']>
|
|
||||||
Scopes: [...I1['Scopes'], ...I2['Scopes']]
|
Scopes: [...I1['Scopes'], ...I2['Scopes']]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,25 +81,12 @@ export interface FilterHelpers<Info extends FilterInfo = DefaultFilterInfo> {
|
||||||
*/
|
*/
|
||||||
scope<T extends Filter<Info>, const S extends FilterScope[]>(
|
scope<T extends Filter<Info>, const S extends FilterScope[]>(
|
||||||
this: T,
|
this: T,
|
||||||
...scopes: If<
|
...scopes: S
|
||||||
Info['RequiresExports'],
|
): Filter<Info & { Scopes: S }>
|
||||||
[typeof FilterScopesObject.Initialized],
|
|
||||||
S
|
|
||||||
>
|
|
||||||
): Filter<
|
|
||||||
Info & {
|
|
||||||
Scopes: If<
|
|
||||||
Info['RequiresExports'],
|
|
||||||
[typeof FilterScopesObject.Initialized],
|
|
||||||
S
|
|
||||||
>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FilterGenerator<G extends (...args: any[]) => Filter> = G & {
|
export type FilterGenerator<G extends (...args: any[]) => Filter> = G & {
|
||||||
keyFor(args: Parameters<G>): string
|
keyFor(args: Parameters<G>): string
|
||||||
flagsFor(args: Parameters<G>): FilterFlag
|
|
||||||
defaultScopesFor(args: Parameters<G>): FilterScopeValue
|
defaultScopesFor(args: Parameters<G>): FilterScopeValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,7 +106,8 @@ const Helpers: FilterHelpers = Object.setPrototypeOf(
|
||||||
const newFilter = ((
|
const newFilter = ((
|
||||||
id: Metro.ModuleID,
|
id: Metro.ModuleID,
|
||||||
exports?: Metro.ModuleExports,
|
exports?: Metro.ModuleExports,
|
||||||
) => this(id, exports)) as typeof this
|
initialized?: boolean,
|
||||||
|
) => this(id, exports, initialized)) as typeof this
|
||||||
|
|
||||||
Object.assign(newFilter, this)
|
Object.assign(newFilter, this)
|
||||||
|
|
||||||
|
|
@ -139,7 +117,9 @@ const Helpers: FilterHelpers = Object.setPrototypeOf(
|
||||||
const filter = this.new()
|
const filter = this.new()
|
||||||
filter.scopes = 0
|
filter.scopes = 0
|
||||||
for (const scope of scopes) filter.scopes |= scope
|
for (const scope of scopes) filter.scopes |= scope
|
||||||
return filter
|
return filter as Filter<
|
||||||
|
FilterInfoOf<typeof filter> & { Scopes: typeof scopes }
|
||||||
|
>
|
||||||
},
|
},
|
||||||
} satisfies FilterHelpers,
|
} satisfies FilterHelpers,
|
||||||
Function.prototype,
|
Function.prototype,
|
||||||
|
|
@ -150,15 +130,14 @@ const Helpers: FilterHelpers = Object.setPrototypeOf(
|
||||||
*
|
*
|
||||||
* @param filter The function that filters the modules.
|
* @param filter The function that filters the modules.
|
||||||
* @param keyFor The function that generates the key for the filter.
|
* @param keyFor The function that generates the key for the filter.
|
||||||
* @param flagFor The function that generates the flags for the filter, or a static flag.
|
|
||||||
* @param defaultScopesFor The function that generates the default scopes for the filter, or static scopes. Defaults to {@link FilterScopes.Initialized}.
|
* @param defaultScopesFor The function that generates the default scopes for the filter, or static scopes. Defaults to {@link FilterScopes.Initialized}.
|
||||||
* @returns A function that generates a filter with the specified arguments.
|
* @returns A function that generates a filter with the specified arguments.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* const custom = createFilterGenerator<[arg1: number, arg2: string]>(
|
* const custom = createFilterGenerator<[arg1: number, arg2: string]>(
|
||||||
* ([arg1, arg2], id, exports) => {
|
* ([arg1, arg2], id, exports, initialized) => {
|
||||||
* // WARNING: exports can be Proxy or nullish, so be careful when using it
|
* // WARNING: exports can be a Proxy, nullish, or a primitive, so be careful when using it
|
||||||
* // filter logic
|
* // filter logic
|
||||||
* return true
|
* return true
|
||||||
* },
|
* },
|
||||||
|
|
@ -173,16 +152,15 @@ export function createFilterGenerator<A extends any[]>(
|
||||||
args: A,
|
args: A,
|
||||||
id: Metro.ModuleID,
|
id: Metro.ModuleID,
|
||||||
exports: Metro.ModuleExports,
|
exports: Metro.ModuleExports,
|
||||||
|
initialized: boolean,
|
||||||
) => boolean,
|
) => boolean,
|
||||||
keyFor: (args: A) => string,
|
keyFor: (args: A) => string,
|
||||||
flagFor: ((args: A) => FilterFlag) | FilterFlag,
|
|
||||||
defaultScopesFor?: ((args: A) => FilterScopeValue) | FilterScopeValue,
|
defaultScopesFor?: ((args: A) => FilterScopeValue) | FilterScopeValue,
|
||||||
): FilterGenerator<(...args: A) => Filter>
|
): FilterGenerator<(...args: A) => Filter>
|
||||||
|
|
||||||
export function createFilterGenerator<A extends any[]>(
|
export function createFilterGenerator<A extends any[]>(
|
||||||
filter: (args: A, id: Metro.ModuleID) => boolean,
|
filter: (args: A, id: Metro.ModuleID) => boolean,
|
||||||
keyFor: (args: A) => string,
|
keyFor: (args: A) => string,
|
||||||
flagFor: ((args: A) => FilterFlag) | FilterFlag,
|
|
||||||
defaultScopesFor?: ((args: A) => FilterScopeValue) | FilterScopeValue,
|
defaultScopesFor?: ((args: A) => FilterScopeValue) | FilterScopeValue,
|
||||||
): FilterGenerator<(...args: A) => Filter>
|
): FilterGenerator<(...args: A) => Filter>
|
||||||
|
|
||||||
|
|
@ -190,16 +168,14 @@ export function createFilterGenerator<A extends any[]>(
|
||||||
filter: (
|
filter: (
|
||||||
args: A,
|
args: A,
|
||||||
id: Metro.ModuleID,
|
id: Metro.ModuleID,
|
||||||
exports?: Metro.ModuleExports,
|
exports: Metro.ModuleExports,
|
||||||
|
initialized: boolean,
|
||||||
) => boolean,
|
) => boolean,
|
||||||
keyFor: (args: A) => string,
|
keyFor: (args: A) => string,
|
||||||
flagFor: ((args: A) => FilterFlag) | FilterFlag,
|
|
||||||
defaultScopesFor?: ((args: A) => FilterScopeValue) | FilterScopeValue,
|
defaultScopesFor?: ((args: A) => FilterScopeValue) | FilterScopeValue,
|
||||||
): FilterGenerator<(...args: A) => Filter> {
|
): FilterGenerator<(...args: A) => Filter> {
|
||||||
type GeneratorType = ReturnType<typeof createFilterGenerator<A>>
|
type GeneratorType = ReturnType<typeof createFilterGenerator<A>>
|
||||||
|
|
||||||
const isFlagsStatic = typeof flagFor === 'number'
|
|
||||||
|
|
||||||
const defaultScopes = defaultScopesFor ?? FilterScopes.Initialized
|
const defaultScopes = defaultScopesFor ?? FilterScopes.Initialized
|
||||||
const isDefaultScopesStatic = typeof defaultScopes === 'number'
|
const isDefaultScopesStatic = typeof defaultScopes === 'number'
|
||||||
|
|
||||||
|
|
@ -207,9 +183,10 @@ export function createFilterGenerator<A extends any[]>(
|
||||||
const filter_ = ((
|
const filter_ = ((
|
||||||
id: Metro.ModuleID,
|
id: Metro.ModuleID,
|
||||||
exports?: Metro.ModuleExports,
|
exports?: Metro.ModuleExports,
|
||||||
|
initialized: boolean = Boolean(isModuleInitialized(id)),
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return filter(args, id, exports)
|
return filter(args, id, exports, initialized)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
DEBUG_warnFilterThrown(id, keyFor(args), e)
|
DEBUG_warnFilterThrown(id, keyFor(args), e)
|
||||||
return false
|
return false
|
||||||
|
|
@ -217,17 +194,12 @@ export function createFilterGenerator<A extends any[]>(
|
||||||
}) as ReturnType<GeneratorType>
|
}) as ReturnType<GeneratorType>
|
||||||
|
|
||||||
filter_.key = keyFor(args)
|
filter_.key = keyFor(args)
|
||||||
filter_.flags = isFlagsStatic ? flagFor : flagFor(args)
|
|
||||||
filter_.scopes = isDefaultScopesStatic
|
filter_.scopes = isDefaultScopesStatic
|
||||||
? defaultScopes
|
? defaultScopes
|
||||||
: defaultScopes(args)
|
: defaultScopes(args)
|
||||||
return Object.setPrototypeOf(filter_, Helpers)
|
return Object.setPrototypeOf(filter_, Helpers)
|
||||||
}
|
}
|
||||||
|
|
||||||
generator.flagsFor = isFlagsStatic
|
|
||||||
? () => flagFor
|
|
||||||
: (args: A) => flagFor(args)
|
|
||||||
|
|
||||||
generator.defaultScopesFor = isDefaultScopesStatic
|
generator.defaultScopesFor = isDefaultScopesStatic
|
||||||
? () => defaultScopes
|
? () => defaultScopes
|
||||||
: defaultScopes
|
: defaultScopes
|
||||||
|
|
|
||||||
|
|
@ -183,11 +183,19 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeUninit)
|
if (includeUninit)
|
||||||
for (const id of mUninitialized) {
|
// Must snapshot because partial matches can cause modules that haven't been looped over to be initialized
|
||||||
|
// which would remove them from the Set
|
||||||
|
for (const id of Array.from(mUninitialized)) {
|
||||||
// biome-ignore lint/complexity/useOptionalChain: Hot path should be optimized
|
// biome-ignore lint/complexity/useOptionalChain: Hot path should be optimized
|
||||||
if (cached && cached.has(id)) continue
|
if (cached && cached.has(id)) continue
|
||||||
|
|
||||||
const flag = runFilter(filter, id, undefined, options)
|
const flag = runFilter(
|
||||||
|
filter,
|
||||||
|
id,
|
||||||
|
// Could be initialized by a previous partial match
|
||||||
|
getInitializedModuleExports(id),
|
||||||
|
options,
|
||||||
|
)
|
||||||
if (flag) {
|
if (flag) {
|
||||||
notFound = false
|
notFound = false
|
||||||
|
|
||||||
|
|
@ -308,8 +316,15 @@ export function lookupModule(filter: Filter, options?: LookupModulesOptions) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeUninit)
|
if (includeUninit)
|
||||||
for (const id of mUninitialized) {
|
// See equivalent code in `lookupModules` for why this is snapshotted
|
||||||
const flag = runFilter(filter, id, undefined, options)
|
for (const id of [...mUninitialized]) {
|
||||||
|
const flag = runFilter(
|
||||||
|
filter,
|
||||||
|
id,
|
||||||
|
// See equivalent code in `lookupModules` for why this isn't undefined
|
||||||
|
getInitializedModuleExports(id),
|
||||||
|
options,
|
||||||
|
)
|
||||||
if (flag) {
|
if (flag) {
|
||||||
if (__BUILD_FLAG_DEBUG_MODULE_LOOKUPS__)
|
if (__BUILD_FLAG_DEBUG_MODULE_LOOKUPS__)
|
||||||
DEBUG_logLookupMatched(filter.key, flag, id)
|
DEBUG_logLookupMatched(filter.key, flag, id)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
import { callNativeMethodSync } from '@revenge-mod/modules/native'
|
import { callNativeMethodSync } from '@revenge-mod/modules/native'
|
||||||
import { getErrorStack } from '@revenge-mod/utils/error'
|
import { getErrorStack } from '@revenge-mod/utils/error'
|
||||||
import { FullVersion } from '~constants'
|
import { FullVersion } from '~constants'
|
||||||
import { loadModuleFromSegment, mInitializingId, mList } from './patches'
|
import { loadModuleFromSegment, mList } from './patches'
|
||||||
import {
|
import {
|
||||||
executeInitializeSubscriptions,
|
executeInitializeSubscriptions,
|
||||||
executeRequireSubscriptions,
|
executeRequireSubscriptions,
|
||||||
|
|
@ -62,7 +62,7 @@ export const metroRequire = (moduleId => {
|
||||||
mod.flags = (flags & NotInitializedOrInitializingMask) | HasError
|
mod.flags = (flags & NotInitializedOrInitializingMask) | HasError
|
||||||
mod.error = e
|
mod.error = e
|
||||||
|
|
||||||
const msg = `Module ${mInitializingId} failed to initialize:\n\n${getErrorStack(e)}`
|
const msg = `Module ${moduleId} failed to initialize:\n\n${getErrorStack(e)}`
|
||||||
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
callNativeMethodSync('revenge.alertError', [msg, FullVersion])
|
callNativeMethodSync('revenge.alertError', [msg, FullVersion])
|
||||||
|
|
@ -71,7 +71,7 @@ export const metroRequire = (moduleId => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some modifications can cause modules to fail by initializing them in the wrong order, we can't just blacklist them
|
// Some modifications can cause modules to fail by initializing them in the wrong order, we can't just blacklist them
|
||||||
// cacheBlacklistedModule(mInitializingId)
|
// cacheBlacklistedModule(moduleId)
|
||||||
|
|
||||||
// So... it wasn't a great idea to throw, Discord has pushed a broken build that has some failing modules
|
// So... it wasn't a great idea to throw, Discord has pushed a broken build that has some failing modules
|
||||||
// Vanilla Metro would swallow the error and just return an empty object as the exports..., insanity
|
// Vanilla Metro would swallow the error and just return an empty object as the exports..., insanity
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,11 @@ export function isModuleExportBad(
|
||||||
return (
|
return (
|
||||||
// Nullish?
|
// Nullish?
|
||||||
exp == null ||
|
exp == null ||
|
||||||
// Does it have some non-existent key? (Turbo modules)
|
// Does it have some non-existent key? (proxies)
|
||||||
(typeof exp === 'object' && CATCH_ALL_TEST in exp)
|
(typeof exp === 'object' &&
|
||||||
|
// Implementing Reflect.get
|
||||||
|
(exp[CATCH_ALL_TEST] !== undefined ||
|
||||||
|
// Implementing Reflect.ownKeys
|
||||||
|
CATCH_ALL_TEST in exp))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,42 +7,50 @@ import {
|
||||||
withDependencies,
|
withDependencies,
|
||||||
} from '@revenge-mod/modules/finders/filters'
|
} from '@revenge-mod/modules/finders/filters'
|
||||||
import { isModuleExportBad } from '@revenge-mod/modules/metro/utils'
|
import { isModuleExportBad } from '@revenge-mod/modules/metro/utils'
|
||||||
import {
|
import { FilterScopes } from '../../modules/src/finders/filters/constants'
|
||||||
ReactJSXRuntimeModuleId,
|
|
||||||
ReactModuleId,
|
|
||||||
ReactNativeModuleId,
|
|
||||||
} from '@revenge-mod/react'
|
|
||||||
import {
|
|
||||||
FilterFlag,
|
|
||||||
FilterScopes,
|
|
||||||
} from '../../modules/src/finders/filters/constants'
|
|
||||||
import type {
|
import type {
|
||||||
Filter,
|
Filter,
|
||||||
FilterGenerator,
|
FilterGenerator,
|
||||||
} from '@revenge-mod/modules/finders/filters'
|
} from '@revenge-mod/modules/finders/filters'
|
||||||
|
import type { Metro } from '@revenge-mod/modules/types'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
|
|
||||||
// [React, ReactJsxRuntime, (Tokens), (BaseIconImage), (Asset), ImportTracker]
|
const { last } = withDependencies
|
||||||
// Asset = relativeDep(1)
|
|
||||||
const IconComponentFilter = [
|
|
||||||
ReactModuleId,
|
|
||||||
ReactJSXRuntimeModuleId,
|
|
||||||
TokensModuleId,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
ImportTrackerModuleId,
|
|
||||||
]
|
|
||||||
|
|
||||||
// Generated IconComponents that have multiple assets, there is no specific length, so the filter has to be generated on-the-fly.
|
const depsFilters: Record<string, Filter> = {}
|
||||||
// [React, ReactNative, ReactJsxRuntime, (Tokens), (BaseIconImage), (...Assets), ImportTracker]
|
|
||||||
// ...Assets = relativeDep(1..n)
|
/**
|
||||||
const MultiIconComponentFilterBase = [
|
* Builds a dependency filter for an icon component.
|
||||||
ReactModuleId,
|
*
|
||||||
ReactNativeModuleId,
|
* `[React, (ReactNative), ReactJsxRuntime, Tokens, (BaseIconImage), (...Assets), ImportTracker]`
|
||||||
ReactJSXRuntimeModuleId,
|
*
|
||||||
TokensModuleId,
|
* `ReactNative` is only present on components with multiple assets, and it sits right after `React`,
|
||||||
null,
|
* so everything from `ReactJsxRuntime` onwards is at a fixed distance from the end on both shapes.
|
||||||
]
|
*
|
||||||
|
* @param names The component name, then the asset names if the component has multiple assets.
|
||||||
|
* @returns The filter, or `undefined` if any of the assets aren't registered yet.
|
||||||
|
*/
|
||||||
|
function depsFilterFor(names: string[]) {
|
||||||
|
const key = names.join(',')
|
||||||
|
const cached = depsFilters[key]
|
||||||
|
if (cached) return cached
|
||||||
|
|
||||||
|
// Components with multiple assets are named separately from their assets
|
||||||
|
const assets = names.length > 1 ? names.slice(1) : names
|
||||||
|
const mids: Metro.ModuleID[] = []
|
||||||
|
|
||||||
|
for (const name of assets) {
|
||||||
|
const mid = getAssetByName(name)?.moduleId
|
||||||
|
// Module ID can never be zero, so if it's falsy, it means the asset was not found.
|
||||||
|
if (!mid) return
|
||||||
|
|
||||||
|
mids.push(mid)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (depsFilters[key] = withDependencies(
|
||||||
|
last([TokensModuleId, null, ...mids, ImportTrackerModuleId]),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
export type WithGeneratedIconComponent = FilterGenerator<
|
export type WithGeneratedIconComponent = FilterGenerator<
|
||||||
<N extends string>(
|
<N extends string>(
|
||||||
|
|
@ -50,7 +58,6 @@ export type WithGeneratedIconComponent = FilterGenerator<
|
||||||
...assets: string[]
|
...assets: string[]
|
||||||
) => Filter<{
|
) => Filter<{
|
||||||
Result: { [K in N]: FC<any> }
|
Result: { [K in N]: FC<any> }
|
||||||
RequiresExports: boolean
|
|
||||||
Scopes: [
|
Scopes: [
|
||||||
typeof FilterScopes.Uninitialized,
|
typeof FilterScopes.Uninitialized,
|
||||||
typeof FilterScopes.Initialized,
|
typeof FilterScopes.Initialized,
|
||||||
|
|
@ -86,47 +93,13 @@ export type WithGeneratedIconComponent = FilterGenerator<
|
||||||
export const withGeneratedIconComponent = createFilterGenerator<
|
export const withGeneratedIconComponent = createFilterGenerator<
|
||||||
Parameters<WithGeneratedIconComponent>
|
Parameters<WithGeneratedIconComponent>
|
||||||
>(
|
>(
|
||||||
(names, id, exports) => {
|
(names, id, exports, initialized) => {
|
||||||
if (typeof exports === 'object' && isModuleExportBad(exports)) {
|
// Icon components always export their component, so unusable exports can never match
|
||||||
if (typeof exports[names[0]] === 'function')
|
if (initialized && isModuleExportBad(exports)) return false
|
||||||
return Object.keys(exports).length === 1
|
|
||||||
} else {
|
|
||||||
let filter = IconComponentFilter
|
|
||||||
|
|
||||||
if (names.length > 1) {
|
return depsFilterFor(names)?.(id, exports, initialized) ?? false
|
||||||
const mids = []
|
|
||||||
|
|
||||||
for (let i = 1; i < names.length; i++) {
|
|
||||||
const name = names[i]
|
|
||||||
const mid = getAssetByName(name)?.moduleId
|
|
||||||
// Module ID can never be zero, so if it's falsy, it means the asset was not found.
|
|
||||||
if (!mid) return false
|
|
||||||
|
|
||||||
mids.push(mid)
|
|
||||||
}
|
|
||||||
|
|
||||||
filter = [
|
|
||||||
...MultiIconComponentFilterBase,
|
|
||||||
...mids,
|
|
||||||
ImportTrackerModuleId,
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
const [name] = names
|
|
||||||
|
|
||||||
const mid = getAssetByName(name)?.moduleId
|
|
||||||
// Module ID can never be zero, so if it's falsy, it means the asset was not found.
|
|
||||||
if (!mid) return false
|
|
||||||
|
|
||||||
IconComponentFilter[4] = mid
|
|
||||||
}
|
|
||||||
|
|
||||||
return withDependencies(filter)(id, exports)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
},
|
},
|
||||||
names => `revenge.utils.discord.generatedIconComponent(${names.join(',')})`,
|
names => `revenge.utils.discord.generatedIconComponent(${names.join(',')})`,
|
||||||
FilterFlag.Dynamic,
|
|
||||||
FilterScopes.Uninitialized | FilterScopes.Initialized,
|
FilterScopes.Uninitialized | FilterScopes.Initialized,
|
||||||
) as WithGeneratedIconComponent
|
) as WithGeneratedIconComponent
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,8 @@ export default async function build(dev = Dev, log = true) {
|
||||||
.trim(),
|
.trim(),
|
||||||
),
|
),
|
||||||
__DEV__: String(dev),
|
__DEV__: String(dev),
|
||||||
// __BUILD_DEFAULT_PLUGIN_REPOSITORY_URL__: ...,
|
__BUILD_DEFAULT_PLUGIN_REPOSITORY_URL__: 'undefined',
|
||||||
|
__BUILD_DONATE_URL__: 'undefined',
|
||||||
|
|
||||||
// See types/build.d.ts for what these flags do
|
// See types/build.d.ts for what these flags do
|
||||||
__BUILD_FLAG_DEBUG_MODULE_LOOKUPS__: String(dev),
|
__BUILD_FLAG_DEBUG_MODULE_LOOKUPS__: String(dev),
|
||||||
|
|
|
||||||
BIN
src/assets/RevengeDonate.webp
Normal file
BIN
src/assets/RevengeDonate.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -21,11 +21,7 @@ import {
|
||||||
} from '../utils/sheets'
|
} from '../utils/sheets'
|
||||||
import { PluginIcon } from './PluginIcon'
|
import { PluginIcon } from './PluginIcon'
|
||||||
import { usePluginEnabled } from './PluginStateProvider'
|
import { usePluginEnabled } from './PluginStateProvider'
|
||||||
import {
|
import { PluginTooltip, usePluginTooltip } from './TooltipProvider'
|
||||||
useClickOutsideTooltip,
|
|
||||||
useEnablePluginTooltip,
|
|
||||||
useEssentialPluginTooltip,
|
|
||||||
} from './TooltipProvider'
|
|
||||||
import type { AnyPlugin, InternalPluginMeta } from '@revenge-mod/plugins/_'
|
import type { AnyPlugin, InternalPluginMeta } from '@revenge-mod/plugins/_'
|
||||||
import type { RepoPluginListing } from '@revenge-mod/plugins/_/repositories'
|
import type { RepoPluginListing } from '@revenge-mod/plugins/_/repositories'
|
||||||
|
|
||||||
|
|
@ -152,14 +148,12 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({
|
||||||
|
|
||||||
const toggleDisabled = essential || pendingUpdate
|
const toggleDisabled = essential || pendingUpdate
|
||||||
|
|
||||||
const enableTooltip = useEnablePluginTooltip()
|
const [settingsRef, showEnableTooltip] = usePluginTooltip(
|
||||||
const essentialTooltip = useEssentialPluginTooltip()
|
PluginTooltip.Enable,
|
||||||
|
)
|
||||||
|
|
||||||
const settingsRef = useClickOutsideTooltip(useEnablePluginTooltip, () => {})
|
const [switchRef, showToggleTooltip] = usePluginTooltip(
|
||||||
|
essential ? PluginTooltip.Essential : PluginTooltip.PendingUpdate,
|
||||||
const switchRef = useClickOutsideTooltip(
|
|
||||||
useEssentialPluginTooltip,
|
|
||||||
() => {},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -182,12 +176,7 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({
|
||||||
{plugin.SettingsComponent && (
|
{plugin.SettingsComponent && (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (!startable)
|
if (!startable) showEnableTooltip()
|
||||||
requestAnimationFrame(() => {
|
|
||||||
enableTooltip.targetRef.current =
|
|
||||||
settingsRef.current
|
|
||||||
enableTooltip.setVisible(true)
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|
@ -204,12 +193,7 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({
|
||||||
)}
|
)}
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (toggleDisabled)
|
if (toggleDisabled) showToggleTooltip()
|
||||||
requestAnimationFrame(() => {
|
|
||||||
essentialTooltip.targetRef.current =
|
|
||||||
switchRef.current
|
|
||||||
essentialTooltip.setVisible(true)
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
ref={switchRef}
|
ref={switchRef}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
PLUGIN_CARD_HALF_GUTTER,
|
PLUGIN_CARD_HALF_GUTTER,
|
||||||
PluginCard,
|
PluginCard,
|
||||||
} from './PluginCard'
|
} from './PluginCard'
|
||||||
import { useResetTooltips } from './TooltipProvider'
|
import { useHidePluginTooltips } from './TooltipProvider'
|
||||||
import type { AnyPlugin, InternalPluginMeta } from '@revenge-mod/plugins/_'
|
import type { AnyPlugin, InternalPluginMeta } from '@revenge-mod/plugins/_'
|
||||||
import type { RepoPluginListing } from '@revenge-mod/plugins/_/repositories'
|
import type { RepoPluginListing } from '@revenge-mod/plugins/_/repositories'
|
||||||
import type { FlashListProps } from '@shopify/flash-list'
|
import type { FlashListProps } from '@shopify/flash-list'
|
||||||
|
|
@ -25,14 +25,14 @@ export function PluginFlashList({
|
||||||
FlashListProps<AnyPlugin>,
|
FlashListProps<AnyPlugin>,
|
||||||
'onContentSizeChange'
|
'onContentSizeChange'
|
||||||
>) {
|
>) {
|
||||||
const resetTooltips = useResetTooltips()
|
const hideTooltips = useHidePluginTooltips()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlashList
|
<FlashList
|
||||||
style={gutterCompensation}
|
style={gutterCompensation}
|
||||||
onContentSizeChange={onContentSizeChange}
|
onContentSizeChange={onContentSizeChange}
|
||||||
data={plugins}
|
data={plugins}
|
||||||
onScrollBeginDrag={resetTooltips}
|
onScrollBeginDrag={hideTooltips}
|
||||||
fadingEdgeLength={plugins.length === 1 ? 0 : 16}
|
fadingEdgeLength={plugins.length === 1 ? 0 : 16}
|
||||||
keyExtractor={plugin => plugin.manifest.id}
|
keyExtractor={plugin => plugin.manifest.id}
|
||||||
renderItem={({
|
renderItem={({
|
||||||
|
|
@ -60,7 +60,7 @@ export function InstalledPluginMasonryFlashList({
|
||||||
plugins: (readonly [AnyPlugin, InternalPluginMeta])[]
|
plugins: (readonly [AnyPlugin, InternalPluginMeta])[]
|
||||||
}) {
|
}) {
|
||||||
const numColumns = useNumColumns()
|
const numColumns = useNumColumns()
|
||||||
const resetTooltips = useResetTooltips()
|
const hideTooltips = useHidePluginTooltips()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlashList
|
<FlashList
|
||||||
|
|
@ -69,7 +69,7 @@ export function InstalledPluginMasonryFlashList({
|
||||||
// FAB is 56px tall, plus 16px spacing on top and bottom
|
// FAB is 56px tall, plus 16px spacing on top and bottom
|
||||||
contentContainerStyle={{ paddingBottom: 56 + 2 * 16 }}
|
contentContainerStyle={{ paddingBottom: 56 + 2 * 16 }}
|
||||||
data={plugins}
|
data={plugins}
|
||||||
onScrollBeginDrag={resetTooltips}
|
onScrollBeginDrag={hideTooltips}
|
||||||
fadingEdgeLength={16}
|
fadingEdgeLength={16}
|
||||||
keyExtractor={([plugin]) => plugin.manifest.id}
|
keyExtractor={([plugin]) => plugin.manifest.id}
|
||||||
numColumns={numColumns}
|
numColumns={numColumns}
|
||||||
|
|
@ -128,7 +128,7 @@ export function BrowsePluginMasonryFlashList({
|
||||||
) => void
|
) => void
|
||||||
}) {
|
}) {
|
||||||
const numColumns = useNumColumns()
|
const numColumns = useNumColumns()
|
||||||
const resetTooltips = useResetTooltips()
|
const hideTooltips = useHidePluginTooltips()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlashList
|
<FlashList
|
||||||
|
|
@ -136,7 +136,7 @@ export function BrowsePluginMasonryFlashList({
|
||||||
style={gutterCompensation}
|
style={gutterCompensation}
|
||||||
contentContainerStyle={{ paddingBottom: 16 }}
|
contentContainerStyle={{ paddingBottom: 16 }}
|
||||||
data={entries}
|
data={entries}
|
||||||
onScrollBeginDrag={resetTooltips}
|
onScrollBeginDrag={hideTooltips}
|
||||||
fadingEdgeLength={16}
|
fadingEdgeLength={16}
|
||||||
keyExtractor={entry => entry.key}
|
keyExtractor={entry => entry.key}
|
||||||
numColumns={numColumns}
|
numColumns={numColumns}
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,9 @@ import {
|
||||||
import { messageOf, showErrorToast } from '../utils/repos'
|
import { messageOf, showErrorToast } from '../utils/repos'
|
||||||
import { InstalledPluginSwitch, PluginInfo } from './PluginCard'
|
import { InstalledPluginSwitch, PluginInfo } from './PluginCard'
|
||||||
import { usePluginEnabled, usePluginStatus } from './PluginStateProvider'
|
import { usePluginEnabled, usePluginStatus } from './PluginStateProvider'
|
||||||
import {
|
import PluginTooltipsProvider, {
|
||||||
EnablePluginTooltipProvider,
|
PluginTooltip,
|
||||||
useClickOutsideTooltip,
|
usePluginTooltip,
|
||||||
useEnablePluginTooltip,
|
|
||||||
} from './TooltipProvider'
|
} from './TooltipProvider'
|
||||||
import type { AnyPlugin } from '@revenge-mod/plugins/_'
|
import type { AnyPlugin } from '@revenge-mod/plugins/_'
|
||||||
|
|
||||||
|
|
@ -61,23 +60,44 @@ export default function PluginOptionsActionSheet({
|
||||||
plugin,
|
plugin,
|
||||||
sheetKey,
|
sheetKey,
|
||||||
}: PluginOptionsActionSheetProps) {
|
}: PluginOptionsActionSheetProps) {
|
||||||
|
return (
|
||||||
|
<ActionSheet>
|
||||||
|
<ClickOutsideProvider>
|
||||||
|
<PluginTooltipsProvider>
|
||||||
|
<PluginOptions plugin={plugin} sheetKey={sheetKey} />
|
||||||
|
</PluginTooltipsProvider>
|
||||||
|
</ClickOutsideProvider>
|
||||||
|
</ActionSheet>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PluginOptions({ plugin, sheetKey }: PluginOptionsActionSheetProps) {
|
||||||
const enabled = usePluginEnabled(plugin)
|
const enabled = usePluginEnabled(plugin)
|
||||||
const meta = getInternalPluginMeta(plugin)
|
const meta = getInternalPluginMeta(plugin)
|
||||||
const essential = isPluginEssential(meta)
|
const essential = isPluginEssential(meta)
|
||||||
const pendingUpdate = isPluginPendingUpdate(plugin)
|
const pendingUpdate = isPluginPendingUpdate(plugin)
|
||||||
const { name, author, description, icon, version } = plugin.manifest
|
const { name, author, description, icon, version } = plugin.manifest
|
||||||
|
|
||||||
|
const [switchRef, showPendingUpdateTooltip] = usePluginTooltip(
|
||||||
|
PluginTooltip.PendingUpdate,
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionSheet>
|
<Stack spacing={24} style={{ paddingTop: 8 }}>
|
||||||
<Stack spacing={24} style={{ paddingTop: 8 }}>
|
<PluginInfo
|
||||||
<PluginInfo
|
name={name}
|
||||||
name={name}
|
author={author}
|
||||||
author={author}
|
version={formatVersion(version)}
|
||||||
version={formatVersion(version)}
|
description={description}
|
||||||
description={description}
|
icon={icon}
|
||||||
icon={icon}
|
actions={
|
||||||
actions={
|
!essential && (
|
||||||
!essential && (
|
<Pressable
|
||||||
|
onPress={() => {
|
||||||
|
if (pendingUpdate) showPendingUpdateTooltip()
|
||||||
|
}}
|
||||||
|
ref={switchRef}
|
||||||
|
>
|
||||||
<InstalledPluginSwitch
|
<InstalledPluginSwitch
|
||||||
enabled={enabled}
|
enabled={enabled}
|
||||||
plugin={plugin}
|
plugin={plugin}
|
||||||
|
|
@ -86,25 +106,19 @@ export default function PluginOptionsActionSheet({
|
||||||
)}
|
)}
|
||||||
toggleDisabled={pendingUpdate}
|
toggleDisabled={pendingUpdate}
|
||||||
/>
|
/>
|
||||||
)
|
</Pressable>
|
||||||
}
|
)
|
||||||
/>
|
}
|
||||||
<ClickOutsideProvider>
|
/>
|
||||||
<EnablePluginTooltipProvider>
|
<PluginActions
|
||||||
<PluginActions
|
plugin={plugin}
|
||||||
plugin={plugin}
|
closeSheet={() => {
|
||||||
closeSheet={() => {
|
ActionSheetActionCreators.hideActionSheet(sheetKey)
|
||||||
ActionSheetActionCreators.hideActionSheet(
|
}}
|
||||||
sheetKey,
|
/>
|
||||||
)
|
<StatusSection plugin={plugin} />
|
||||||
}}
|
<AdvancedSection plugin={plugin} />
|
||||||
/>
|
</Stack>
|
||||||
</EnablePluginTooltipProvider>
|
|
||||||
</ClickOutsideProvider>
|
|
||||||
<StatusSection plugin={plugin} />
|
|
||||||
<AdvancedSection plugin={plugin} />
|
|
||||||
</Stack>
|
|
||||||
</ActionSheet>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -292,8 +306,9 @@ function PluginActions({
|
||||||
plugin: AnyPlugin
|
plugin: AnyPlugin
|
||||||
closeSheet: () => void
|
closeSheet: () => void
|
||||||
}) {
|
}) {
|
||||||
const enableTooltip = useEnablePluginTooltip()
|
const [settingsRef, showEnableTooltip] = usePluginTooltip(
|
||||||
const settingsRef = useClickOutsideTooltip(useEnablePluginTooltip, () => {})
|
PluginTooltip.Enable,
|
||||||
|
)
|
||||||
const meta = getInternalPluginMeta(plugin)
|
const meta = getInternalPluginMeta(plugin)
|
||||||
const startable = isPluginStartable(plugin)
|
const startable = isPluginStartable(plugin)
|
||||||
const enabled = usePluginEnabled(plugin)
|
const enabled = usePluginEnabled(plugin)
|
||||||
|
|
@ -347,12 +362,7 @@ function PluginActions({
|
||||||
{plugin.SettingsComponent && (
|
{plugin.SettingsComponent && (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (!startable)
|
if (!startable) showEnableTooltip()
|
||||||
requestAnimationFrame(() => {
|
|
||||||
enableTooltip.targetRef.current =
|
|
||||||
settingsRef.current
|
|
||||||
enableTooltip.setVisible(true)
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|
|
||||||
|
|
@ -1,109 +1,134 @@
|
||||||
import { Design } from '@revenge-mod/discord/design'
|
import { Design } from '@revenge-mod/discord/design'
|
||||||
import { createContext, useContext, useRef, useState } from 'react'
|
import {
|
||||||
import { useClickOutside } from 'react-native-click-outside'
|
createContext,
|
||||||
import type { RefObject } from 'react'
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import {
|
||||||
|
ClickOutsideProvider,
|
||||||
|
useClickOutside,
|
||||||
|
} from 'react-native-click-outside'
|
||||||
import type { View } from 'react-native'
|
import type { View } from 'react-native'
|
||||||
|
|
||||||
const { useTooltip } = Design
|
const { useTooltip } = Design
|
||||||
|
|
||||||
interface TooltipContextValue {
|
export const PluginTooltip = {
|
||||||
targetRef: RefObject<View | null>
|
/** Shown when an action requires the plugin to be enabled first. */
|
||||||
setVisible: (visible: boolean) => void
|
Enable: 'enable',
|
||||||
|
/** Shown when an action is blocked because the plugin is essential. */
|
||||||
|
Essential: 'essential',
|
||||||
|
/** Shown when an action is blocked because the plugin has a pending update. */
|
||||||
|
PendingUpdate: 'pendingUpdate',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type PluginTooltip = (typeof PluginTooltip)[keyof typeof PluginTooltip]
|
||||||
|
|
||||||
|
const Labels: Record<PluginTooltip, string> = {
|
||||||
|
[PluginTooltip.Enable]: 'Plugin must be enabled first',
|
||||||
|
[PluginTooltip.Essential]: 'Plugin needed for Revenge to function properly',
|
||||||
|
[PluginTooltip.PendingUpdate]:
|
||||||
|
'Plugin has a pending update, reload to apply it',
|
||||||
}
|
}
|
||||||
|
|
||||||
const EnablePluginTooltipContext = createContext<TooltipContextValue | null>(
|
interface PluginTooltipsContextValue {
|
||||||
null,
|
show: (tooltip: PluginTooltip, target: View | null) => void
|
||||||
)
|
hide: () => void
|
||||||
const EssentialPluginTooltipContext = createContext<TooltipContextValue | null>(
|
}
|
||||||
|
|
||||||
|
const PluginTooltipsContext = createContext<PluginTooltipsContextValue | null>(
|
||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
|
|
||||||
export function EnablePluginTooltipProvider({
|
export default function PluginTooltipsProvider({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const [visible, setVisible] = useState(false)
|
const [visible, setVisible] = useState<PluginTooltip | null>(null)
|
||||||
const targetRef = useRef<View | null>(null)
|
|
||||||
|
|
||||||
useTooltip(targetRef, {
|
// Targets are assigned by `show`, right before the tooltip becomes visible
|
||||||
label: 'Plugin must be enabled first',
|
const enableTarget = useRef<View | null>(null)
|
||||||
|
const essentialTarget = useRef<View | null>(null)
|
||||||
|
const pendingUpdateTarget = useRef<View | null>(null)
|
||||||
|
|
||||||
|
useTooltip(enableTarget, {
|
||||||
|
label: Labels[PluginTooltip.Enable],
|
||||||
position: 'top',
|
position: 'top',
|
||||||
visible,
|
visible: visible === PluginTooltip.Enable,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
useTooltip(essentialTarget, {
|
||||||
<EnablePluginTooltipContext.Provider value={{ targetRef, setVisible }}>
|
label: Labels[PluginTooltip.Essential],
|
||||||
{children}
|
|
||||||
</EnablePluginTooltipContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EssentialPluginTooltipProvider({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
const [visible, setVisible] = useState(false)
|
|
||||||
const targetRef = useRef<View | null>(null)
|
|
||||||
|
|
||||||
useTooltip(targetRef, {
|
|
||||||
label: 'Plugin needed for Revenge to function properly',
|
|
||||||
position: 'top',
|
position: 'top',
|
||||||
visible,
|
visible: visible === PluginTooltip.Essential,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
useTooltip(pendingUpdateTarget, {
|
||||||
<EssentialPluginTooltipContext.Provider
|
label: Labels[PluginTooltip.PendingUpdate],
|
||||||
value={{ targetRef, setVisible }}
|
position: 'top',
|
||||||
>
|
visible: visible === PluginTooltip.PendingUpdate,
|
||||||
{children}
|
})
|
||||||
</EssentialPluginTooltipContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useEnablePluginTooltip() {
|
const value = useMemo<PluginTooltipsContextValue>(() => {
|
||||||
const context = useContext(EnablePluginTooltipContext)
|
const targets = {
|
||||||
if (!context) {
|
[PluginTooltip.Enable]: enableTarget,
|
||||||
throw new Error(
|
[PluginTooltip.Essential]: essentialTarget,
|
||||||
'useEnablePluginTooltip must be used within EnablePluginTooltipProvider',
|
[PluginTooltip.PendingUpdate]: pendingUpdateTarget,
|
||||||
)
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useEssentialPluginTooltip() {
|
|
||||||
const context = useContext(EssentialPluginTooltipContext)
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(
|
|
||||||
'useEssentialPluginTooltip must be used within EssentialPluginTooltipProvider',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useResetTooltips() {
|
|
||||||
const enableTooltip = useContext(EnablePluginTooltipContext)
|
|
||||||
const essentialTooltip = useContext(EssentialPluginTooltipContext)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
enableTooltip?.setVisible(false)
|
|
||||||
essentialTooltip?.setVisible(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useClickOutsideTooltip(
|
|
||||||
contextHook: () => TooltipContextValue,
|
|
||||||
onClickOutside: () => void,
|
|
||||||
) {
|
|
||||||
const { targetRef, setVisible } = contextHook()
|
|
||||||
|
|
||||||
const ref = useClickOutside<View>(() => {
|
|
||||||
setVisible(false)
|
|
||||||
if (targetRef.current === ref.current) {
|
|
||||||
onClickOutside()
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
return ref
|
return {
|
||||||
|
show: (tooltip, target) => {
|
||||||
|
targets[tooltip].current = target
|
||||||
|
setVisible(tooltip)
|
||||||
|
},
|
||||||
|
hide: () => setVisible(null),
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClickOutsideProvider>
|
||||||
|
<PluginTooltipsContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</PluginTooltipsContext.Provider>
|
||||||
|
</ClickOutsideProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds an element to a plugin tooltip.
|
||||||
|
*
|
||||||
|
* @param tooltip The tooltip to show for the element.
|
||||||
|
* @returns The ref to attach to the element, and a callback showing the tooltip on it.
|
||||||
|
*/
|
||||||
|
export function usePluginTooltip(tooltip: PluginTooltip) {
|
||||||
|
const context = useContext(PluginTooltipsContext)
|
||||||
|
if (!context)
|
||||||
|
throw new Error(
|
||||||
|
'usePluginTooltip must be used within PluginTooltipsProvider',
|
||||||
|
)
|
||||||
|
|
||||||
|
const { show, hide } = context
|
||||||
|
|
||||||
|
// Tapping anywhere else dismisses the tooltip
|
||||||
|
const ref = useClickOutside<View>(hide)
|
||||||
|
|
||||||
|
const showTooltip = useCallback(() => {
|
||||||
|
// Tapping the element itself also counts as a tap outside of it (the
|
||||||
|
// press target wraps the ref), so showing waits for the next frame
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
show(tooltip, ref.current)
|
||||||
|
})
|
||||||
|
}, [show, tooltip, ref])
|
||||||
|
|
||||||
|
return [ref, showTooltip] as const
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hides the visible plugin tooltip, if any.. */
|
||||||
|
export function useHidePluginTooltips() {
|
||||||
|
const context = useContext(PluginTooltipsContext)
|
||||||
|
|
||||||
|
return useCallback(() => context?.hide(), [context])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export interface Storage {
|
||||||
defaultRepoRestored?: boolean
|
defaultRepoRestored?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const OUTDATED_THRESHOLD = 24 * 60 * 60 * 1000 // 1 day
|
const OUTDATED_THRESHOLD = 60 * 60 * 1000 // 1 hour
|
||||||
const AUTO_UPDATE_CHECK_DELAY = 10_000
|
const AUTO_UPDATE_CHECK_DELAY = 10_000
|
||||||
|
|
||||||
const CircleXIconComponent = lookupGeneratedIconComponent(
|
const CircleXIconComponent = lookupGeneratedIconComponent(
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,9 @@ import {
|
||||||
import { debounce } from '@revenge-mod/utils/callback'
|
import { debounce } from '@revenge-mod/utils/callback'
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
import { ClickOutsideProvider } from 'react-native-click-outside'
|
|
||||||
import { BrowsePluginMasonryFlashList } from '../components/PluginList'
|
import { BrowsePluginMasonryFlashList } from '../components/PluginList'
|
||||||
import PluginStatesProvider from '../components/PluginStateProvider'
|
import PluginStatesProvider from '../components/PluginStateProvider'
|
||||||
import {
|
import PluginTooltipsProvider from '../components/TooltipProvider'
|
||||||
EnablePluginTooltipProvider,
|
|
||||||
EssentialPluginTooltipProvider,
|
|
||||||
} from '../components/TooltipProvider'
|
|
||||||
import { runInstallFlow } from '../utils/repos'
|
import { runInstallFlow } from '../utils/repos'
|
||||||
import type { RepoPluginListing } from '@revenge-mod/plugins/_/repositories'
|
import type { RepoPluginListing } from '@revenge-mod/plugins/_/repositories'
|
||||||
import type { BrowseSortKey } from '../components/BrowseFilterAndSortActionSheet'
|
import type { BrowseSortKey } from '../components/BrowseFilterAndSortActionSheet'
|
||||||
|
|
@ -34,17 +30,13 @@ const SearchDebounceTime = 100
|
||||||
export default function RevengePluginsBrowseSettingScreen() {
|
export default function RevengePluginsBrowseSettingScreen() {
|
||||||
return (
|
return (
|
||||||
<LayerScope>
|
<LayerScope>
|
||||||
<ClickOutsideProvider>
|
<PluginStatesProvider>
|
||||||
<PluginStatesProvider>
|
<Page spacing={16}>
|
||||||
<Page spacing={16}>
|
<PluginTooltipsProvider>
|
||||||
<EssentialPluginTooltipProvider>
|
<Screen />
|
||||||
<EnablePluginTooltipProvider>
|
</PluginTooltipsProvider>
|
||||||
<Screen />
|
</Page>
|
||||||
</EnablePluginTooltipProvider>
|
</PluginStatesProvider>
|
||||||
</EssentialPluginTooltipProvider>
|
|
||||||
</Page>
|
|
||||||
</PluginStatesProvider>
|
|
||||||
</ClickOutsideProvider>
|
|
||||||
</LayerScope>
|
</LayerScope>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,14 +28,10 @@ import {
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { Image, View } from 'react-native'
|
import { Image, View } from 'react-native'
|
||||||
import { ClickOutsideProvider } from 'react-native-click-outside'
|
|
||||||
import RevengeIcon from '~assets/RevengeIcon'
|
import RevengeIcon from '~assets/RevengeIcon'
|
||||||
import { InstalledPluginMasonryFlashList } from '../components/PluginList'
|
import { InstalledPluginMasonryFlashList } from '../components/PluginList'
|
||||||
import PluginStatesProvider from '../components/PluginStateProvider'
|
import PluginStatesProvider from '../components/PluginStateProvider'
|
||||||
import {
|
import PluginTooltipsProvider from '../components/TooltipProvider'
|
||||||
EnablePluginTooltipProvider,
|
|
||||||
EssentialPluginTooltipProvider,
|
|
||||||
} from '../components/TooltipProvider'
|
|
||||||
import { RouteNames, Setting } from '../constants'
|
import { RouteNames, Setting } from '../constants'
|
||||||
import type { NavigationProp, RouteProp } from '@react-navigation/core'
|
import type { NavigationProp, RouteProp } from '@react-navigation/core'
|
||||||
import type { ReactNavigationParamList } from '@revenge-mod/externals/react-navigation'
|
import type { ReactNavigationParamList } from '@revenge-mod/externals/react-navigation'
|
||||||
|
|
@ -51,17 +47,13 @@ const PlusLargeIcon = getAssetIdByName('PlusLargeIcon')!
|
||||||
export default function RevengePluginsSettingScreen() {
|
export default function RevengePluginsSettingScreen() {
|
||||||
return (
|
return (
|
||||||
<LayerScope>
|
<LayerScope>
|
||||||
<ClickOutsideProvider>
|
<PluginStatesProvider>
|
||||||
<PluginStatesProvider>
|
<PluginTooltipsProvider>
|
||||||
<Page spacing={16}>
|
<Page spacing={16}>
|
||||||
<EssentialPluginTooltipProvider>
|
<Screen />
|
||||||
<EnablePluginTooltipProvider>
|
|
||||||
<Screen />
|
|
||||||
</EnablePluginTooltipProvider>
|
|
||||||
</EssentialPluginTooltipProvider>
|
|
||||||
</Page>
|
</Page>
|
||||||
</PluginStatesProvider>
|
</PluginTooltipsProvider>
|
||||||
</ClickOutsideProvider>
|
</PluginStatesProvider>
|
||||||
</LayerScope>
|
</LayerScope>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,139 @@
|
||||||
|
import { getAssetIdByName } from '@revenge-mod/assets'
|
||||||
|
import { styles } from '@revenge-mod/components/_'
|
||||||
|
import { Design } from '@revenge-mod/discord/design'
|
||||||
import { SettingListRenderer } from '@revenge-mod/discord/modules/settings/renderer'
|
import { SettingListRenderer } from '@revenge-mod/discord/modules/settings/renderer'
|
||||||
|
import { Image, Linking, ScrollView, StyleSheet, View } from 'react-native'
|
||||||
|
import RevengeDonate from '~assets/RevengeDonate'
|
||||||
import { Setting } from '../constants'
|
import { Setting } from '../constants'
|
||||||
|
|
||||||
|
const { Button, Card, Stack, Text } = Design
|
||||||
|
|
||||||
export default function RevengeSettingScreen() {
|
export default function RevengeSettingScreen() {
|
||||||
return (
|
return (
|
||||||
<SettingListRenderer.SettingsList
|
<ScrollView style={{ flex: 1 }}>
|
||||||
node={{
|
<Stack spacing={0}>
|
||||||
type: 'list',
|
<View style={{ padding: 16, paddingBottom: 0 }}>
|
||||||
sections: [
|
<DonateCard />
|
||||||
{
|
</View>
|
||||||
label: 'Revenge',
|
<View>
|
||||||
settings: [
|
<SettingListRenderer.SettingsList
|
||||||
Setting.RevengeVersion,
|
node={{
|
||||||
Setting.LoaderVersion,
|
type: 'list',
|
||||||
Setting.RevengeDiscord,
|
sections: [
|
||||||
Setting.RevengeSourceRepository,
|
{
|
||||||
Setting.RevengeLicense,
|
label: 'Revenge',
|
||||||
],
|
settings: [
|
||||||
},
|
Setting.RevengeVersion,
|
||||||
{
|
Setting.LoaderVersion,
|
||||||
label: 'Versions',
|
Setting.RevengeDiscord,
|
||||||
settings: [
|
Setting.RevengeSourceRepository,
|
||||||
Setting.ReactVersion,
|
Setting.RevengeLicense,
|
||||||
Setting.ReactNativeVersion,
|
],
|
||||||
Setting.HermesVersion,
|
},
|
||||||
],
|
{
|
||||||
},
|
label: 'Versions',
|
||||||
{
|
settings: [
|
||||||
label: 'Actions',
|
Setting.ReactVersion,
|
||||||
settings: [
|
Setting.ReactNativeVersion,
|
||||||
Setting.Reload,
|
Setting.HermesVersion,
|
||||||
Setting.RevengeEnterRecoveryMode,
|
],
|
||||||
],
|
},
|
||||||
},
|
{
|
||||||
],
|
label: 'Actions',
|
||||||
}}
|
settings: [
|
||||||
/>
|
Setting.Reload,
|
||||||
|
Setting.RevengeEnterRecoveryMode,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</Stack>
|
||||||
|
</ScrollView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardStyles = StyleSheet.create({
|
||||||
|
background: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
illustration: {
|
||||||
|
width: 96,
|
||||||
|
height: 96,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function DonateCard() {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
padding: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
cardStyles.background,
|
||||||
|
{
|
||||||
|
opacity: 0.5,
|
||||||
|
experimental_backgroundImage:
|
||||||
|
'linear-gradient(166deg, rgba(255, 131, 29, 0.33), rgba(145, 47, 47, 0)), ' +
|
||||||
|
'linear-gradient(45deg, rgba(145, 47, 47, 0), rgba(255, 22, 22, 0.5))',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<View style={{ padding: 16 }}>
|
||||||
|
<Stack style={styles.grow} direction="horizontal" spacing={16}>
|
||||||
|
<Stack spacing={16} style={styles.flex}>
|
||||||
|
<Stack spacing={6} style={styles.flex}>
|
||||||
|
<Text
|
||||||
|
variant="heading-lg/semibold"
|
||||||
|
color="text-strong"
|
||||||
|
>
|
||||||
|
Support Revenge
|
||||||
|
</Text>
|
||||||
|
<Text variant="text-md/medium">
|
||||||
|
You can support the development of Revenge by
|
||||||
|
donating or contributing!
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Stack spacing={12} direction="horizontal">
|
||||||
|
{__BUILD_DONATE_URL__ && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
icon={getAssetIdByName('HeartIcon')}
|
||||||
|
text="Donate"
|
||||||
|
variant="expressive"
|
||||||
|
onPress={() => {
|
||||||
|
Linking.openURL(__BUILD_DONATE_URL__)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
icon={getAssetIdByName('HandRequestSpeakIcon')}
|
||||||
|
text="Contribute"
|
||||||
|
variant="primary-overlay"
|
||||||
|
onPress={() => {
|
||||||
|
Linking.openURL(
|
||||||
|
__BUILD_SOURCE_REPOSITORY_URL__,
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<Image
|
||||||
|
source={RevengeDonate}
|
||||||
|
style={cardStyles.illustration}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</View>
|
||||||
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
types/build.d.ts
vendored
1
types/build.d.ts
vendored
|
|
@ -3,6 +3,7 @@
|
||||||
declare global {
|
declare global {
|
||||||
const __DEV__: boolean
|
const __DEV__: boolean
|
||||||
const __BUILD_DISCORD_SERVER_URL__: string
|
const __BUILD_DISCORD_SERVER_URL__: string
|
||||||
|
const __BUILD_DONATE_URL__: string | undefined
|
||||||
const __BUILD_SOURCE_REPOSITORY_URL__: string
|
const __BUILD_SOURCE_REPOSITORY_URL__: string
|
||||||
const __BUILD_DEFAULT_PLUGIN_REPOSITORY_URL__: string | undefined
|
const __BUILD_DEFAULT_PLUGIN_REPOSITORY_URL__: string | undefined
|
||||||
const __BUILD_LICENSE_URL__: string
|
const __BUILD_LICENSE_URL__: string
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue