diff --git a/lib/plugins/src/constants.ts b/lib/plugins/src/constants.ts
index b7c746a..7b71f77 100644
--- a/lib/plugins/src/constants.ts
+++ b/lib/plugins/src/constants.ts
@@ -13,11 +13,18 @@ export const PluginStatus = {
Stopping: 1 << 6,
}
-const { storageRootPath } = callNativeMethodSync(
+const { storageRootPath, defaultsOnly } = 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/lib/plugins/src/types.ts b/lib/plugins/src/types.ts
index 68f6f40..31759ec 100644
--- a/lib/plugins/src/types.ts
+++ b/lib/plugins/src/types.ts
@@ -321,6 +321,9 @@ export interface PluginSettingsComponent<
declare module '@revenge-mod/modules/native' {
export interface NativeMethods {
- 'revenge.plugins.getConstants': [[], { storageRootPath: string }]
+ 'revenge.plugins.getConstants': [
+ [],
+ { storageRootPath: string; defaultsOnly: boolean },
+ ]
}
}
diff --git a/src/plugins/start/recovery/index.tsx b/src/plugins/start/recovery/index.tsx
new file mode 100644
index 0000000..cc9bf39
--- /dev/null
+++ b/src/plugins/start/recovery/index.tsx
@@ -0,0 +1,90 @@
+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 { callNativeMethod } from '@revenge-mod/modules/native'
+import {
+ InternalPluginFlags,
+ PluginFlags,
+ registerInternalPlugin,
+} from '@revenge-mod/plugins/_'
+import { isDefaultsOnlyBoot } from '@revenge-mod/plugins/constants'
+import { asap } from '@revenge-mod/utils/callback'
+import { FullVersion } from '~constants'
+import { RouteNames } from '../settings.plugins/constants'
+
+registerInternalPlugin(
+ {
+ id: 'revenge.recovery',
+ name: 'Recovery',
+ description: 'Provides troubleshooting options.',
+ author: 'Revenge',
+ icon: 'WrenchIcon',
+ },
+ {
+ start() {
+ // 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)
+
+ const unsub = onFluxEventDispatched('APP_STATE_UPDATE', e => {
+ clearTimeout(id)
+ unsub()
+ return e
+ })
+
+ if (isDefaultsOnlyBoot) {
+ asap(() => {
+ AlertActionCreators.openAlert(
+ 'revenge-recovery',
+ ,
+ )
+ })
+ }
+ },
+ },
+ PluginFlags.Enabled,
+ InternalPluginFlags.Internal | InternalPluginFlags.Essential,
+)
+
+function RecoveryModal() {
+ const navigation = RootNavigationRef.getRootNavigationRef()
+
+ return (
+
+ {
+ // React Navigation will bubble our requests to the main navigator if we're already in settings
+ navigation.navigate('settings', {
+ screen: RouteNames.RevengePlugins,
+ })
+ }}
+ />
+
+ >
+ }
+ />
+ )
+}
diff --git a/src/plugins/start/settings.plugins/definitions/RevengePluginsSetting.tsx b/src/plugins/start/settings.plugins/definitions/RevengePluginsSetting.tsx
index 96576ae..4dc4c4f 100644
--- a/src/plugins/start/settings.plugins/definitions/RevengePluginsSetting.tsx
+++ b/src/plugins/start/settings.plugins/definitions/RevengePluginsSetting.tsx
@@ -1,5 +1,6 @@
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { isPluginEnabled, pEmitter, pList } from '@revenge-mod/plugins/_'
+import { isDefaultsOnlyBoot } from '@revenge-mod/plugins/constants'
import { useReRender } from '@revenge-mod/utils/react'
import { useEffect } from 'react'
import { RouteNames, Setting } from '../constants'
@@ -11,7 +12,9 @@ const RevengePluginsSetting: SettingsItem = {
type: 'route',
IconComponent: () => ,
useTitle: () => 'Plugins',
- useTrailing: () => `${useEnabledPluginCount()} enabled`,
+ useTrailing: () =>
+ `${useEnabledPluginCount()} enabled` +
+ (isDefaultsOnlyBoot ? ' (recovery)' : ''),
screen: {
route: RouteNames[Setting.RevengePlugins],
getComponent: () => RevengePluginsSettingScreen.default,
diff --git a/src/plugins/start/settings.plugins/screens/RevengePluginsSettingScreen.tsx b/src/plugins/start/settings.plugins/screens/RevengePluginsSettingScreen.tsx
index 8beb392..cdfce26 100644
--- a/src/plugins/start/settings.plugins/screens/RevengePluginsSettingScreen.tsx
+++ b/src/plugins/start/settings.plugins/screens/RevengePluginsSettingScreen.tsx
@@ -5,6 +5,7 @@ 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 {
getInternalPluginMeta,
InternalPluginFlags,
@@ -16,6 +17,7 @@ import {
pEmitter,
pList,
} from '@revenge-mod/plugins/_'
+import { isDefaultsOnlyBoot } from '@revenge-mod/plugins/constants'
import { debounce } from '@revenge-mod/utils/callback'
import {
useCallback,
@@ -38,7 +40,8 @@ import type { NavigationProp, RouteProp } from '@react-navigation/core'
import type { ReactNavigationParamList } from '@revenge-mod/externals/react-navigation'
import type { FilterAndSortActionSheetProps } from '../components/FilterAndSortActionSheet'
-const { Stack, IconButton, FloatingActionButton, LayerScope } = Design
+const { Stack, IconButton, FloatingActionButton, LayerScope, Card, Text } =
+ Design
const FiltersHorizontalIcon = getAssetIdByName('FiltersHorizontalIcon', 'png')!
const SettingsIcon = getAssetIdByName('SettingsIcon')!
@@ -148,6 +151,41 @@ function BrowseFloatingActionButton() {
)
}
+function DefaultsOnlyBanner() {
+ const [hide, setHide] = useState(false)
+
+ if (!isDefaultsOnlyBoot || hide) return null
+
+ return (
+
+
+
+ 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.
+
+
+ BundleUpdaterManager.reload()}
+ />
+ {/* In case it's taking too much vertical space */}
+ setHide(true)}
+ />
+
+
+
+ )
+}
+
function snapshotPlugins() {
return [...pList.values()].map(
plugin => [plugin, getInternalPluginMeta(plugin)] as const,
@@ -225,6 +263,7 @@ function Screen() {
return (
<>
+