From 944dbb6838e938d7bdca2bb46ee80c948565786b Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Sat, 1 Aug 2026 01:51:29 +0700 Subject: [PATCH] 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()` --- lib/discord/src/types/index.ts | 9 +- lib/modules/package.json | 5 +- lib/modules/src/native/app.ts | 11 ++ lib/plugins/src/_internal/index.ts | 103 +++++++++++++----- lib/plugins/src/apis/modules.ts | 3 + lib/plugins/src/constants.ts | 9 +- scripts/types.ts | 1 + .../RevengeEnterRecoveryModeSetting.tsx | 26 +++++ src/plugins/start/recovery/index.tsx | 67 +++++++++--- .../components/PluginCard.tsx | 43 ++++++-- .../components/PluginList.tsx | 3 + .../components/PluginOptionsActionSheet.tsx | 24 ++-- .../components/PluginsRequireReloadAlert.tsx | 5 +- .../definitions/RevengePluginsSetting.tsx | 8 +- .../screens/RevengePluginsSettingScreen.tsx | 61 ++++++----- src/plugins/start/settings/constants.ts | 1 + .../settings/definitions/ReloadSetting.tsx | 4 +- .../settings/screens/RevengeSettingScreen.tsx | 5 +- 18 files changed, 270 insertions(+), 118 deletions(-) create mode 100644 lib/modules/src/native/app.ts create mode 100644 src/plugins/start/recovery/definitions/RevengeEnterRecoveryModeSetting.tsx diff --git a/lib/discord/src/types/index.ts b/lib/discord/src/types/index.ts index 49c8c7e..feeaa50 100644 --- a/lib/discord/src/types/index.ts +++ b/lib/discord/src/types/index.ts @@ -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' diff --git a/lib/modules/package.json b/lib/modules/package.json index 5a74d85..f3e51fd 100644 --- a/lib/modules/package.json +++ b/lib/modules/package.json @@ -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" } } -} +} \ No newline at end of file diff --git a/lib/modules/src/native/app.ts b/lib/modules/src/native/app.ts new file mode 100644 index 0000000..6a302da --- /dev/null +++ b/lib/modules/src/native/app.ts @@ -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] + } +} diff --git a/lib/plugins/src/_internal/index.ts b/lib/plugins/src/_internal/index.ts index 2642c16..c6f50de 100644 --- a/lib/plugins/src/_internal/index.ts +++ b/lib/plugins/src/_internal/index.ts @@ -232,10 +232,29 @@ const pMetadata = new WeakMap() /// 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) { 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 + } } diff --git a/lib/plugins/src/apis/modules.ts b/lib/plugins/src/apis/modules.ts index 9d9763c..21fe0a7 100644 --- a/lib/plugins/src/apis/modules.ts +++ b/lib/plugins/src/apis/modules.ts @@ -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, }), } diff --git a/lib/plugins/src/constants.ts b/lib/plugins/src/constants.ts index 7b71f77..b7c746a 100644 --- a/lib/plugins/src/constants.ts +++ b/lib/plugins/src/constants.ts @@ -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. */ diff --git a/scripts/types.ts b/scripts/types.ts index a1948e4..f50966d 100644 --- a/scripts/types.ts +++ b/scripts/types.ts @@ -276,6 +276,7 @@ function getLibraries() { ['metro/subscriptions', 'metro'], ['metro/utils', 'metro'], 'native', + 'native/app', 'native/fs', 'types', ]), diff --git a/src/plugins/start/recovery/definitions/RevengeEnterRecoveryModeSetting.tsx b/src/plugins/start/recovery/definitions/RevengeEnterRecoveryModeSetting.tsx new file mode 100644 index 0000000..966135e --- /dev/null +++ b/src/plugins/start/recovery/definitions/RevengeEnterRecoveryModeSetting.tsx @@ -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: () => , + onPress: () => { + requestNextBootDefaultsOnly() + reloadApp() + }, +} + +export default RevengeEnterRecoveryModeSetting diff --git a/src/plugins/start/recovery/index.tsx b/src/plugins/start/recovery/index.tsx index cc9bf39..98a837e 100644 --- a/src/plugins/start/recovery/index.tsx +++ b/src/plugins/start/recovery/index.tsx @@ -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() { diff --git a/src/plugins/start/settings.plugins/components/PluginCard.tsx b/src/plugins/start/settings.plugins/components/PluginCard.tsx index b5ecd60..c593dd3 100644 --- a/src/plugins/start/settings.plugins/components/PluginCard.tsx +++ b/src/plugins/start/settings.plugins/components/PluginCard.tsx @@ -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} > - { - if (enabled) handleEnablePlugin(plugin) - else handleDisablePlugin(plugin) - }} - value={enabled} + @@ -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 ( + { + ;(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. diff --git a/src/plugins/start/settings.plugins/components/PluginList.tsx b/src/plugins/start/settings.plugins/components/PluginList.tsx index 9252cd0..47b0990 100644 --- a/src/plugins/start/settings.plugins/components/PluginList.tsx +++ b/src/plugins/start/settings.plugins/components/PluginList.tsx @@ -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] }) => ( diff --git a/src/plugins/start/settings.plugins/components/PluginOptionsActionSheet.tsx b/src/plugins/start/settings.plugins/components/PluginOptionsActionSheet.tsx index 19941b1..87998c1 100644 --- a/src/plugins/start/settings.plugins/components/PluginOptionsActionSheet.tsx +++ b/src/plugins/start/settings.plugins/components/PluginOptionsActionSheet.tsx @@ -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 && ( - { - if (v) handleEnablePlugin(plugin) - else handleDisablePlugin(plugin) - }} + ) } @@ -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) diff --git a/src/plugins/start/settings.plugins/components/PluginsRequireReloadAlert.tsx b/src/plugins/start/settings.plugins/components/PluginsRequireReloadAlert.tsx index 0c7209a..5e0723d 100644 --- a/src/plugins/start/settings.plugins/components/PluginsRequireReloadAlert.tsx +++ b/src/plugins/start/settings.plugins/components/PluginsRequireReloadAlert.tsx @@ -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() }} /> diff --git a/src/plugins/start/settings.plugins/definitions/RevengePluginsSetting.tsx b/src/plugins/start/settings.plugins/definitions/RevengePluginsSetting.tsx index 4dc4c4f..88a8492 100644 --- a/src/plugins/start/settings.plugins/definitions/RevengePluginsSetting.tsx +++ b/src/plugins/start/settings.plugins/definitions/RevengePluginsSetting.tsx @@ -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' diff --git a/src/plugins/start/settings.plugins/screens/RevengePluginsSettingScreen.tsx b/src/plugins/start/settings.plugins/screens/RevengePluginsSettingScreen.tsx index cdfce26..b21ab18 100644 --- a/src/plugins/start/settings.plugins/screens/RevengePluginsSettingScreen.tsx +++ b/src/plugins/start/settings.plugins/screens/RevengePluginsSettingScreen.tsx @@ -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>() return ( @@ -151,34 +152,34 @@ function BrowseFloatingActionButton() { ) } -function DefaultsOnlyBanner() { - const [hide, setHide] = useState(false) - - if (!isDefaultsOnlyBoot || hide) return null - +function RecoveryModeBanner() { return ( - - + + + + + Recovery Mode + - 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. BundleUpdaterManager.reload()} - /> - {/* In case it's taking too much vertical space */} - setHide(true)} + text="Exit Recovery Mode" + onPress={() => { + reloadApp() + }} /> @@ -263,7 +264,6 @@ function Screen() { return ( <> - @@ -294,8 +294,13 @@ function Screen() { } /> - - + + ) } diff --git a/src/plugins/start/settings/constants.ts b/src/plugins/start/settings/constants.ts index 88d578a..1114169 100644 --- a/src/plugins/start/settings/constants.ts +++ b/src/plugins/start/settings/constants.ts @@ -9,6 +9,7 @@ export const Setting = { RevengeSourceRepository: 'RevengeSourceRepository', RevengeLicense: 'RevengeLicense', Reload: 'Reload', + RevengeEnterRecoveryMode: 'RevengeEnterRecoveryMode', RevengeVersion: 'RevengeVersion', ReactVersion: 'ReactVersion', diff --git a/src/plugins/start/settings/definitions/ReloadSetting.tsx b/src/plugins/start/settings/definitions/ReloadSetting.tsx index 8447c04..592a958 100644 --- a/src/plugins/start/settings/definitions/ReloadSetting.tsx +++ b/src/plugins/start/settings/definitions/ReloadSetting.tsx @@ -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: () => , useTitle: () => 'Reload App', onPress: () => { - BundleUpdaterManager.reload() + reloadApp() }, type: 'pressable', } diff --git a/src/plugins/start/settings/screens/RevengeSettingScreen.tsx b/src/plugins/start/settings/screens/RevengeSettingScreen.tsx index a627c74..c598398 100644 --- a/src/plugins/start/settings/screens/RevengeSettingScreen.tsx +++ b/src/plugins/start/settings/screens/RevengeSettingScreen.tsx @@ -27,7 +27,10 @@ export default function RevengeSettingScreen() { }, { label: 'Actions', - settings: [Setting.Reload], + settings: [ + Setting.Reload, + Setting.RevengeEnterRecoveryMode, + ], }, ], }}