feat: improve recovery mode + small lib additions
Improved Recovery Mode features generally: - Ability to view and modify saved states while not actually running any additional plugins - Better Recovery Mode banner display - Enter Recovery Mode option in Revenge Settings UI - Fixed freeze detection false positives New APIs: - lib/discord: `disabled` in `DiscordModules.Components.FloatingActionButtonProps` - lib/modules/native/app: new submodule with `reloadApp()` method Fixes: - Use `reloadApp()` instead of `BundleUpdaterManager.reload()`
This commit is contained in:
parent
6d7e017c0b
commit
944dbb6838
18 changed files with 270 additions and 118 deletions
|
|
@ -305,6 +305,7 @@ export namespace DiscordModules {
|
|||
export interface FloatingActionButtonProps {
|
||||
icon: number
|
||||
onPress: () => void
|
||||
disabled?: boolean
|
||||
positionBottom?: number
|
||||
accessibilityLabel?: string
|
||||
}
|
||||
|
|
@ -513,14 +514,6 @@ export namespace DiscordModules {
|
|||
|
||||
export type TableRowIconVariant =
|
||||
| 'default'
|
||||
| 'blurple'
|
||||
| 'boosting-pink'
|
||||
| 'status-online'
|
||||
| 'status-idle'
|
||||
| 'status-dnd'
|
||||
| 'status-offline'
|
||||
| 'xbox'
|
||||
| 'playstation'
|
||||
| 'danger'
|
||||
| 'secondary'
|
||||
| 'translucent'
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@
|
|||
"./native": {
|
||||
"default": "./src/native/index.ts"
|
||||
},
|
||||
"./native/app": {
|
||||
"default": "./src/native/app.ts"
|
||||
},
|
||||
"./native/fs": {
|
||||
"default": "./src/native/fs.ts"
|
||||
},
|
||||
|
|
@ -28,4 +31,4 @@
|
|||
"types": "./src/types.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
lib/modules/src/native/app.ts
Normal file
11
lib/modules/src/native/app.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { callNativeMethodSync } from '@revenge-mod/modules/native'
|
||||
|
||||
export function reloadApp() {
|
||||
callNativeMethodSync('revenge.app.reload', [])
|
||||
}
|
||||
|
||||
declare module '@revenge-mod/modules/native' {
|
||||
interface NativeMethods {
|
||||
'revenge.app.reload': [[], null]
|
||||
}
|
||||
}
|
||||
|
|
@ -232,10 +232,29 @@ const pMetadata = new WeakMap<AnyPlugin, InternalPluginMeta>()
|
|||
|
||||
/// STATE-SYNC
|
||||
|
||||
const { states: InitialPersistedStates }: PersistedPluginStates =
|
||||
callNativeMethodSync('revenge.plugins.states.read', []) ?? {
|
||||
states: {},
|
||||
}
|
||||
const { states: InitialPersistedStates, savedStates }: PersistedPluginStates =
|
||||
callNativeMethodSync('revenge.plugins.states.read', [])
|
||||
|
||||
/**
|
||||
* Whether this boot is running with default plugins only, ignoring the user's saved states.
|
||||
*/
|
||||
export const isDefaultsOnlyBoot = savedStates != null
|
||||
|
||||
/**
|
||||
* The user's real saved states, only sent when this boot ignores them (recovery/defaults-only).
|
||||
*
|
||||
* @see {@link isDefaultsOnlyBoot}
|
||||
*/
|
||||
export const SavedPluginStates = savedStates ?? null
|
||||
|
||||
/**
|
||||
* Whether a plugin is enabled in the user's saved setup, which in a defaults-only boot is not
|
||||
* what's running. Falls back to the session state on a normal boot.
|
||||
*/
|
||||
export function isPluginEnabledInSavedStates(plugin: AnyPlugin): boolean {
|
||||
const saved = SavedPluginStates?.[plugin.manifest.id]
|
||||
return saved ? Boolean(saved.enabled) : isPluginEnabled(plugin)
|
||||
}
|
||||
|
||||
export function forgetInitialPluginState(id: PluginManifest['id']) {
|
||||
delete InitialPersistedStates[id]
|
||||
|
|
@ -659,12 +678,41 @@ function tryPreparePluginStart(plugin: AnyPlugin) {
|
|||
meta.apiLevel = PluginApiLevel.Start
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the enabled state of a plugin to the native side, and updates the real saved states.
|
||||
*/
|
||||
async function writePluginEnabledState(plugin: AnyPlugin, enabled: boolean) {
|
||||
const result = await callNativeMethod('revenge.plugins.setEnabled', [
|
||||
plugin.manifest.id,
|
||||
enabled,
|
||||
])
|
||||
|
||||
if (result?.code === 'DEPENDENCIES_UNSATISFIED') {
|
||||
const details = result.problems
|
||||
.map(
|
||||
p =>
|
||||
`"${p.id}" (requires ${p.required}, installed: ${p.installed ?? 'none'}${p.installed && !p.enabled ? ', disabled' : ''})`,
|
||||
)
|
||||
.join(', ')
|
||||
|
||||
throw new Error(
|
||||
`Cannot enable plugin "${plugin.manifest.id}": unsatisfied dependencies: ${details}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (SavedPluginStates)
|
||||
SavedPluginStates[plugin.manifest.id] = {
|
||||
...SavedPluginStates[plugin.manifest.id],
|
||||
enabled,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables a plugin, as well as all its dependents.
|
||||
*/
|
||||
export async function disablePlugin(plugin: AnyPlugin) {
|
||||
// PendingReload/PendingUpdate never block disabling
|
||||
if (!isPluginEnabled(plugin))
|
||||
if (!isPluginEnabledInSavedStates(plugin))
|
||||
throw new Error(`Plugin "${plugin.manifest.id}" is not enabled`)
|
||||
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
|
|
@ -686,10 +734,7 @@ export async function disablePlugin(plugin: AnyPlugin) {
|
|||
if (plugin.status && !(plugin.status & Status.Stopping))
|
||||
await stopPlugin(plugin)
|
||||
|
||||
await callNativeMethod('revenge.plugins.setEnabled', [
|
||||
plugin.manifest.id,
|
||||
false,
|
||||
])
|
||||
await writePluginEnabledState(plugin, false)
|
||||
|
||||
meta.flags &= ~Flag.Enabled
|
||||
pEmitter.emit('disabled', plugin)
|
||||
|
|
@ -699,32 +744,16 @@ export async function disablePlugin(plugin: AnyPlugin) {
|
|||
* Enables a plugin, as well as all its dependencies.
|
||||
*/
|
||||
export async function enablePlugin(plugin: AnyPlugin) {
|
||||
if (isPluginEnabled(plugin))
|
||||
if (isPluginEnabledInSavedStates(plugin))
|
||||
throw new Error(`Plugin "${plugin.manifest.id}" is already enabled`)
|
||||
|
||||
await Promise.all(
|
||||
getPluginDependencies(plugin).map(dep => {
|
||||
if (!isPluginEnabled(dep)) return enablePlugin(dep)
|
||||
if (!isPluginEnabledInSavedStates(dep)) return enablePlugin(dep)
|
||||
}),
|
||||
)
|
||||
|
||||
const result = await callNativeMethod('revenge.plugins.setEnabled', [
|
||||
plugin.manifest.id,
|
||||
true,
|
||||
])
|
||||
|
||||
if (result?.code === 'DEPENDENCIES_UNSATISFIED') {
|
||||
const details = result.problems
|
||||
.map(
|
||||
p =>
|
||||
`"${p.id}" (requires ${p.required}, installed: ${p.installed ?? 'none'}${p.installed && !p.enabled ? ', disabled' : ''})`,
|
||||
)
|
||||
.join(', ')
|
||||
|
||||
throw new Error(
|
||||
`Cannot enable plugin "${plugin.manifest.id}": unsatisfied dependencies: ${details}`,
|
||||
)
|
||||
}
|
||||
await writePluginEnabledState(plugin, true)
|
||||
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
meta.flags |= Flag.Enabled
|
||||
|
|
@ -733,6 +762,11 @@ export async function enablePlugin(plugin: AnyPlugin) {
|
|||
}
|
||||
|
||||
export async function runPluginLate(plugin: AnyPlugin) {
|
||||
if (isDefaultsOnlyBoot)
|
||||
throw new Error(
|
||||
`Cannot start plugin "${plugin.manifest.id}" while running with default plugins. Reload to apply your changes.`,
|
||||
)
|
||||
|
||||
requirePluginStartableState(plugin)
|
||||
|
||||
if (plugin.status & Status.Started)
|
||||
|
|
@ -1013,6 +1047,13 @@ export async function deleteStorageForPlugin(plugin: Plugin<any, any>) {
|
|||
if (await exists(dir)) await rm(dir)
|
||||
}
|
||||
|
||||
export function requestNextBootDefaultsOnly() {
|
||||
callNativeMethodSync(
|
||||
'revenge.plugins.states.requestNextBootDefaultsOnly',
|
||||
[],
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
confirmInstall,
|
||||
resyncPluginSources,
|
||||
|
|
@ -1022,7 +1063,8 @@ export {
|
|||
declare module '@revenge-mod/modules/native' {
|
||||
interface NativeMethods {
|
||||
'revenge.plugins.startNative': [[id: PluginManifest['id']], null]
|
||||
'revenge.plugins.states.read': [[], PersistedPluginStates | null]
|
||||
'revenge.plugins.states.read': [[], PersistedPluginStates]
|
||||
'revenge.plugins.states.requestNextBootDefaultsOnly': [[], void]
|
||||
'revenge.plugins.setEnabled': [
|
||||
[id: PluginManifest['id'], enabled: boolean],
|
||||
SetEnabledError | null,
|
||||
|
|
@ -1055,4 +1097,7 @@ interface PersistedPluginStates {
|
|||
states: {
|
||||
[id: PluginManifest['id']]: PluginStateObject
|
||||
}
|
||||
savedStates?: {
|
||||
[id: PluginManifest['id']]: PluginStateObject
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import * as PluginApiModulesFindersFilters from '@revenge-mod/modules/finders/fi
|
|||
import * as PluginApiModulesMetroSubscriptions from '@revenge-mod/modules/metro/subscriptions'
|
||||
import * as PluginApiModulesMetroUtils from '@revenge-mod/modules/metro/utils'
|
||||
import * as PluginApiModulesNative_ from '@revenge-mod/modules/native'
|
||||
import * as PluginApiModulesNativeApp from '@revenge-mod/modules/native/app'
|
||||
import * as PluginApiModulesNativeFileSystem from '@revenge-mod/modules/native/fs'
|
||||
import { spreadDescriptors } from '.'
|
||||
|
||||
|
|
@ -13,6 +14,7 @@ export interface PluginApiModules {
|
|||
}
|
||||
|
||||
export type PluginApiModulesNative = typeof PluginApiModulesNative_ & {
|
||||
app: typeof PluginApiModulesNativeApp
|
||||
fs: typeof PluginApiModulesNativeFileSystem
|
||||
}
|
||||
|
||||
|
|
@ -34,6 +36,7 @@ export const modules: PluginApiModules = {
|
|||
spreadDescriptors(PluginApiModulesMetroSubscriptions, {}),
|
||||
),
|
||||
native: spreadDescriptors(PluginApiModulesNative_, {
|
||||
app: PluginApiModulesNativeApp,
|
||||
fs: PluginApiModulesNativeFileSystem,
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,18 +13,11 @@ export const PluginStatus = {
|
|||
Stopping: 1 << 6,
|
||||
}
|
||||
|
||||
const { storageRootPath, defaultsOnly } = callNativeMethodSync(
|
||||
const { storageRootPath } = callNativeMethodSync(
|
||||
'revenge.plugins.getConstants',
|
||||
[],
|
||||
)
|
||||
|
||||
/**
|
||||
* Whether this boot ignores saved plugin states.
|
||||
*
|
||||
* Only essential and enabled-by-default plugins run. Saved states will come back on the next reload.
|
||||
*/
|
||||
export const isDefaultsOnlyBoot = defaultsOnly
|
||||
|
||||
/**
|
||||
* Absolute path to per-plugin storage directory.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -276,6 +276,7 @@ function getLibraries() {
|
|||
['metro/subscriptions', 'metro'],
|
||||
['metro/utils', 'metro'],
|
||||
'native',
|
||||
'native/app',
|
||||
'native/fs',
|
||||
'types',
|
||||
]),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
|
||||
import { reloadApp } from '@revenge-mod/modules/native/app'
|
||||
import {
|
||||
isDefaultsOnlyBoot,
|
||||
requestNextBootDefaultsOnly,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { Setting } from '~plugins/start/settings/constants'
|
||||
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
|
||||
|
||||
const RevengeEnterRecoveryModeSetting: SettingsItem = {
|
||||
parent: Setting.Revenge,
|
||||
type: 'pressable',
|
||||
useIsDisabled: () => isDefaultsOnlyBoot,
|
||||
useTitle: () => 'Enter Recovery Mode',
|
||||
useDescription: () =>
|
||||
isDefaultsOnlyBoot
|
||||
? 'Currently in Recovery Mode. Reload to exit.'
|
||||
: 'Run Revenge with default plugins only for one boot. Use if plugins are causing issues.',
|
||||
IconComponent: () => <TableRowAssetIcon name="ShieldIcon" />,
|
||||
onPress: () => {
|
||||
requestNextBootDefaultsOnly()
|
||||
reloadApp()
|
||||
},
|
||||
}
|
||||
|
||||
export default RevengeEnterRecoveryModeSetting
|
||||
|
|
@ -2,16 +2,25 @@ import { AlertActionCreators } from '@revenge-mod/discord/actions'
|
|||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { onFluxEventDispatched } from '@revenge-mod/discord/flux'
|
||||
import { RootNavigationRef } from '@revenge-mod/discord/modules/main_tabs_v2'
|
||||
import {
|
||||
onSettingsModulesLoaded,
|
||||
registerSettingsItem,
|
||||
} from '@revenge-mod/discord/modules/settings'
|
||||
import { callNativeMethod } from '@revenge-mod/modules/native'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
isDefaultsOnlyBoot,
|
||||
PluginFlags,
|
||||
registerInternalPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { isDefaultsOnlyBoot } from '@revenge-mod/plugins/constants'
|
||||
import { asap } from '@revenge-mod/utils/callback'
|
||||
import { AppState } from 'react-native'
|
||||
import { FullVersion } from '~constants'
|
||||
import { mErrorChain } from '../../../../lib/modules/src/metro/runtime'
|
||||
import pluginSettings from '../settings'
|
||||
import { Setting } from '../settings/constants'
|
||||
import { RouteNames } from '../settings.plugins/constants'
|
||||
import RevengeEnterRecoveryModeSetting from './definitions/RevengeEnterRecoveryModeSetting'
|
||||
|
||||
registerInternalPlugin(
|
||||
{
|
||||
|
|
@ -19,28 +28,52 @@ registerInternalPlugin(
|
|||
name: 'Recovery',
|
||||
description: 'Provides troubleshooting options.',
|
||||
author: 'Revenge',
|
||||
icon: 'WrenchIcon',
|
||||
icon: 'ShieldIcon',
|
||||
dependencies: { [pluginSettings]: {} },
|
||||
},
|
||||
{
|
||||
start() {
|
||||
onSettingsModulesLoaded(() => {
|
||||
registerSettingsItem(
|
||||
Setting.RevengeEnterRecoveryMode,
|
||||
RevengeEnterRecoveryModeSetting,
|
||||
)
|
||||
})
|
||||
|
||||
// Freeze detection
|
||||
const id = setTimeout(() => {
|
||||
try {
|
||||
callNativeMethod('revenge.alertError', [
|
||||
'Application seems frozen. This is likely caused by a plugin.\n\nYou can launch with default plugins in the Recovery menu.',
|
||||
FullVersion,
|
||||
])
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'Failed to call native method "revenge.alertError":',
|
||||
e,
|
||||
)
|
||||
}
|
||||
}, 10000)
|
||||
let currentId: number
|
||||
|
||||
const setTimer = () => {
|
||||
if (currentId) clearTimeout(currentId)
|
||||
|
||||
currentId = setTimeout(() => {
|
||||
if (AppState.currentState !== 'active') return
|
||||
|
||||
try {
|
||||
callNativeMethod('revenge.alertError', [
|
||||
`App was unable to start. This is likely caused by a plugin.\n\nYou can launch Recovery Mode in the Recovery menu.\n\nErroring modules chain: ${mErrorChain.join(', ') || 'None'}`,
|
||||
FullVersion,
|
||||
])
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'Failed to call native method "revenge.alertError":',
|
||||
e,
|
||||
)
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
const sub = AppState.addEventListener('change', state => {
|
||||
if (state === 'active') setTimer()
|
||||
else clearTimeout(currentId)
|
||||
})
|
||||
|
||||
if (AppState.currentState === 'active') setTimer()
|
||||
|
||||
const unsub = onFluxEventDispatched('APP_STATE_UPDATE', e => {
|
||||
clearTimeout(id)
|
||||
clearTimeout(currentId)
|
||||
unsub()
|
||||
sub.remove()
|
||||
return e
|
||||
})
|
||||
|
||||
|
|
@ -65,7 +98,7 @@ function RecoveryModal() {
|
|||
<Design.AlertModal
|
||||
title="Recovery Mode"
|
||||
content={
|
||||
'Running with default plugins. Your enabled plugins are saved and will come back when you reload.\n\nUninstall plugins that might be causing issues, then reload the app to get back on track.'
|
||||
"You are now running with default plugins. Additional plugins can't be started in Recovery Mode.\n\nDisable or uninstall plugins that might be causing issues, then reload the app to exit Recovery Mode."
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { styles } from '@revenge-mod/components/_'
|
|||
import FormSwitch from '@revenge-mod/components/FormSwitch'
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import {
|
||||
isDefaultsOnlyBoot,
|
||||
isPluginEnabledInSavedStates,
|
||||
isPluginEssential,
|
||||
isPluginPendingUpdate,
|
||||
isPluginStartable,
|
||||
|
|
@ -12,6 +14,7 @@ import { memo } from 'react'
|
|||
import { Pressable } from 'react-native'
|
||||
import { handleDisablePlugin, handleEnablePlugin } from '../utils/actions'
|
||||
import { openPluginSettings } from '../utils/alerts'
|
||||
import { messageOf, showErrorToast } from '../utils/repos'
|
||||
import {
|
||||
showBrowsePluginActionSheet,
|
||||
showPluginOptionsActionSheet,
|
||||
|
|
@ -136,6 +139,7 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({
|
|||
meta: InternalPluginMeta
|
||||
}) {
|
||||
const enabled = usePluginEnabled(plugin)
|
||||
const savedEnabled = isPluginEnabledInSavedStates(plugin)
|
||||
|
||||
const {
|
||||
manifest: { name, description, version, author, icon },
|
||||
|
|
@ -208,14 +212,11 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({
|
|||
}}
|
||||
ref={switchRef}
|
||||
>
|
||||
<FormSwitch
|
||||
key={plugin.manifest.id}
|
||||
disabled={toggleDisabled}
|
||||
onValueChange={enabled => {
|
||||
if (enabled) handleEnablePlugin(plugin)
|
||||
else handleDisablePlugin(plugin)
|
||||
}}
|
||||
value={enabled}
|
||||
<InstalledPluginSwitch
|
||||
plugin={plugin}
|
||||
enabled={enabled}
|
||||
savedEnabled={savedEnabled}
|
||||
toggleDisabled={toggleDisabled}
|
||||
/>
|
||||
</Pressable>
|
||||
</>
|
||||
|
|
@ -224,6 +225,32 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({
|
|||
)
|
||||
})
|
||||
|
||||
export const InstalledPluginSwitch = memo(function InstalledPluginSwitch({
|
||||
plugin,
|
||||
enabled,
|
||||
savedEnabled,
|
||||
toggleDisabled,
|
||||
}: {
|
||||
plugin: AnyPlugin
|
||||
enabled: boolean
|
||||
savedEnabled: boolean
|
||||
toggleDisabled: boolean
|
||||
}) {
|
||||
return (
|
||||
<FormSwitch
|
||||
key={plugin.manifest.id}
|
||||
disabled={toggleDisabled}
|
||||
onValueChange={enabled => {
|
||||
;(enabled
|
||||
? handleEnablePlugin(plugin)
|
||||
: handleDisablePlugin(plugin)
|
||||
).catch(e => showErrorToast(messageOf(e)))
|
||||
}}
|
||||
value={isDefaultsOnlyBoot ? savedEnabled : enabled}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Card for a plugin that isn't installed yet, shown on the Browse screen.
|
||||
* No switch or settings button, just a more menu and a small Install button.
|
||||
|
|
|
|||
|
|
@ -53,8 +53,10 @@ export function PluginFlashList({
|
|||
}
|
||||
|
||||
export function InstalledPluginMasonryFlashList({
|
||||
ListHeaderComponent,
|
||||
plugins,
|
||||
}: {
|
||||
ListHeaderComponent?: React.ComponentType | null
|
||||
plugins: (readonly [AnyPlugin, InternalPluginMeta])[]
|
||||
}) {
|
||||
const numColumns = useNumColumns()
|
||||
|
|
@ -71,6 +73,7 @@ export function InstalledPluginMasonryFlashList({
|
|||
fadingEdgeLength={16}
|
||||
keyExtractor={([plugin]) => plugin.manifest.id}
|
||||
numColumns={numColumns}
|
||||
ListHeaderComponent={ListHeaderComponent}
|
||||
ListEmptyComponent={NoPlugins}
|
||||
renderItem={({ item: [plugin, meta] }) => (
|
||||
<InstalledPluginCard plugin={plugin} meta={meta} />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { getAssetIdByName } from '@revenge-mod/assets'
|
||||
import { FormSwitch, TableRowAssetIcon } from '@revenge-mod/components'
|
||||
import { TableRowAssetIcon } from '@revenge-mod/components'
|
||||
import {
|
||||
ActionSheetActionCreators,
|
||||
ToastActionCreators,
|
||||
|
|
@ -12,6 +12,8 @@ import {
|
|||
getPluginDependencies,
|
||||
getPluginDependents,
|
||||
InternalPluginFlags,
|
||||
isDefaultsOnlyBoot,
|
||||
isPluginEnabledInSavedStates,
|
||||
isPluginEssential,
|
||||
isPluginInternal,
|
||||
isPluginPendingUpdate,
|
||||
|
|
@ -27,14 +29,13 @@ import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
|
|||
import { useEffect, useState } from 'react'
|
||||
import { Pressable } from 'react-native'
|
||||
import { ClickOutsideProvider } from 'react-native-click-outside'
|
||||
import { handleDisablePlugin, handleEnablePlugin } from '../utils/actions'
|
||||
import {
|
||||
openPluginSettings,
|
||||
showPluginClearDataConfirmation,
|
||||
showPluginUninstallConfirmation,
|
||||
} from '../utils/alerts'
|
||||
import { messageOf, showErrorToast } from '../utils/repos'
|
||||
import { PluginInfo } from './PluginCard'
|
||||
import { InstalledPluginSwitch, PluginInfo } from './PluginCard'
|
||||
import { usePluginEnabled, usePluginStatus } from './PluginStateProvider'
|
||||
import {
|
||||
EnablePluginTooltipProvider,
|
||||
|
|
@ -77,13 +78,13 @@ export default function PluginOptionsActionSheet({
|
|||
icon={icon}
|
||||
actions={
|
||||
!essential && (
|
||||
<FormSwitch
|
||||
value={enabled}
|
||||
disabled={pendingUpdate}
|
||||
onValueChange={v => {
|
||||
if (v) handleEnablePlugin(plugin)
|
||||
else handleDisablePlugin(plugin)
|
||||
}}
|
||||
<InstalledPluginSwitch
|
||||
enabled={enabled}
|
||||
plugin={plugin}
|
||||
savedEnabled={isPluginEnabledInSavedStates(
|
||||
plugin,
|
||||
)}
|
||||
toggleDisabled={pendingUpdate}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -291,7 +292,8 @@ function PluginActions({
|
|||
size="lg"
|
||||
icon={running ? StopIcon : PlayIcon}
|
||||
label={running ? 'Stop' : 'Start'}
|
||||
disabled={!running && !startable}
|
||||
// Nothing can start in a defaults-only boot, stopping a default plugin is still fine
|
||||
disabled={!running && (!startable || isDefaultsOnlyBoot)}
|
||||
onPress={async () => {
|
||||
try {
|
||||
if (running) await stopPlugin(plugin)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { BundleUpdaterManager } from '@revenge-mod/discord/native'
|
||||
import { reloadApp } from '@revenge-mod/modules/native/app'
|
||||
import type { AnyPlugin } from '@revenge-mod/plugins/_'
|
||||
|
||||
const { AlertModal, AlertActionButton, Text } = Design
|
||||
|
|
@ -36,8 +36,7 @@ export default function PluginsRequireReloadAlert({
|
|||
variant="destructive"
|
||||
text="Reload"
|
||||
onPress={() => {
|
||||
// TODO: Replace with bridge call
|
||||
BundleUpdaterManager.reload()
|
||||
reloadApp()
|
||||
}}
|
||||
/>
|
||||
<AlertActionButton variant="secondary" text="Not now" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
|
||||
import { isPluginEnabled, pEmitter, pList } from '@revenge-mod/plugins/_'
|
||||
import { isDefaultsOnlyBoot } from '@revenge-mod/plugins/constants'
|
||||
import {
|
||||
isDefaultsOnlyBoot,
|
||||
isPluginEnabled,
|
||||
pEmitter,
|
||||
pList,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { useReRender } from '@revenge-mod/utils/react'
|
||||
import { useEffect } from 'react'
|
||||
import { RouteNames, Setting } from '../constants'
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@ import Page from '@revenge-mod/components/Page'
|
|||
import SearchInput from '@revenge-mod/components/SearchInput'
|
||||
import { ActionSheetActionCreators } from '@revenge-mod/discord/actions'
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { BundleUpdaterManager } from '@revenge-mod/discord/native'
|
||||
import { reloadApp } from '@revenge-mod/modules/native/app'
|
||||
import {
|
||||
getInternalPluginMeta,
|
||||
InternalPluginFlags,
|
||||
isDefaultsOnlyBoot,
|
||||
isPluginEnabled,
|
||||
isPluginEssential,
|
||||
isPluginInternal,
|
||||
|
|
@ -17,7 +18,6 @@ import {
|
|||
pEmitter,
|
||||
pList,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { isDefaultsOnlyBoot } from '@revenge-mod/plugins/constants'
|
||||
import { debounce } from '@revenge-mod/utils/callback'
|
||||
import {
|
||||
useCallback,
|
||||
|
|
@ -26,7 +26,7 @@ import {
|
|||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { View } from 'react-native'
|
||||
import { Image, View } from 'react-native'
|
||||
import { ClickOutsideProvider } from 'react-native-click-outside'
|
||||
import RevengeIcon from '~assets/RevengeIcon'
|
||||
import { InstalledPluginMasonryFlashList } from '../components/PluginList'
|
||||
|
|
@ -137,11 +137,12 @@ function HeaderButton() {
|
|||
)
|
||||
}
|
||||
|
||||
function BrowseFloatingActionButton() {
|
||||
function BrowseFloatingActionButton({ disabled }: { disabled?: boolean }) {
|
||||
const navigation = useNavigation<NavigationProp<any>>()
|
||||
|
||||
return (
|
||||
<FloatingActionButton
|
||||
disabled={disabled}
|
||||
icon={PlusLargeIcon}
|
||||
accessibilityLabel="Browse plugins"
|
||||
onPress={() =>
|
||||
|
|
@ -151,34 +152,34 @@ function BrowseFloatingActionButton() {
|
|||
)
|
||||
}
|
||||
|
||||
function DefaultsOnlyBanner() {
|
||||
const [hide, setHide] = useState(false)
|
||||
|
||||
if (!isDefaultsOnlyBoot || hide) return null
|
||||
|
||||
function RecoveryModeBanner() {
|
||||
return (
|
||||
<Card>
|
||||
<Stack spacing={16}>
|
||||
<Card style={{ marginHorizontal: 6, marginVertical: 6, boxShadow: '' }}>
|
||||
<Stack spacing={12}>
|
||||
<Stack direction="horizontal" spacing={8} align="center">
|
||||
<Image
|
||||
source={getAssetIdByName('ShieldIcon')!}
|
||||
style={{
|
||||
width: 18,
|
||||
height: 18,
|
||||
}}
|
||||
/>
|
||||
<Text variant="text-md/semibold">Recovery Mode</Text>
|
||||
</Stack>
|
||||
<Text variant="text-sm/medium">
|
||||
Running with default plugins. Your enabled plugins are saved
|
||||
and will come back when you reload.{'\n\n'}
|
||||
Uninstall plugins that might be causing issues, then reload
|
||||
the app to get back on track.
|
||||
You are now running with default plugins. Additional plugins
|
||||
can't be started in Recovery Mode.{'\n\n'}
|
||||
Disable or uninstall plugins that might be causing issues,
|
||||
then reload the app to exit Recovery Mode.
|
||||
</Text>
|
||||
<Stack spacing={8}>
|
||||
<Design.Button
|
||||
icon={getAssetIdByName('RetryIcon')!}
|
||||
size="sm"
|
||||
text="Reload with all plugins enabled"
|
||||
onPress={() => BundleUpdaterManager.reload()}
|
||||
/>
|
||||
{/* In case it's taking too much vertical space */}
|
||||
<Design.Button
|
||||
icon={getAssetIdByName('EyeSlashIcon')!}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
text="Hide this message"
|
||||
onPress={() => setHide(true)}
|
||||
text="Exit Recovery Mode"
|
||||
onPress={() => {
|
||||
reloadApp()
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
|
@ -263,7 +264,6 @@ function Screen() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<DefaultsOnlyBanner />
|
||||
<Stack direction="horizontal">
|
||||
<View style={styles.grow}>
|
||||
<SearchInput onChange={debouncedSetSearch} size="md" />
|
||||
|
|
@ -294,8 +294,13 @@ function Screen() {
|
|||
}
|
||||
/>
|
||||
</Stack>
|
||||
<InstalledPluginMasonryFlashList plugins={plugins} />
|
||||
<BrowseFloatingActionButton />
|
||||
<InstalledPluginMasonryFlashList
|
||||
ListHeaderComponent={
|
||||
isDefaultsOnlyBoot ? RecoveryModeBanner : null
|
||||
}
|
||||
plugins={plugins}
|
||||
/>
|
||||
<BrowseFloatingActionButton disabled={isDefaultsOnlyBoot} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export const Setting = {
|
|||
RevengeSourceRepository: 'RevengeSourceRepository',
|
||||
RevengeLicense: 'RevengeLicense',
|
||||
Reload: 'Reload',
|
||||
RevengeEnterRecoveryMode: 'RevengeEnterRecoveryMode',
|
||||
|
||||
RevengeVersion: 'RevengeVersion',
|
||||
ReactVersion: 'ReactVersion',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
|
||||
import { BundleUpdaterManager } from '@revenge-mod/discord/native'
|
||||
import { reloadApp } from '@revenge-mod/modules/native/app'
|
||||
import { Setting } from '../constants'
|
||||
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ const ReloadSetting: SettingsItem = {
|
|||
IconComponent: () => <TableRowAssetIcon name="RetryIcon" />,
|
||||
useTitle: () => 'Reload App',
|
||||
onPress: () => {
|
||||
BundleUpdaterManager.reload()
|
||||
reloadApp()
|
||||
},
|
||||
type: 'pressable',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,10 @@ export default function RevengeSettingScreen() {
|
|||
},
|
||||
{
|
||||
label: 'Actions',
|
||||
settings: [Setting.Reload],
|
||||
settings: [
|
||||
Setting.Reload,
|
||||
Setting.RevengeEnterRecoveryMode,
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue