import { getAssetIdByName } from '@revenge-mod/assets' import { TableRowAssetIcon } from '@revenge-mod/components' import { ActionSheetActionCreators, ToastActionCreators, } from '@revenge-mod/discord/actions' import { Design } from '@revenge-mod/discord/design' import { Clipboard } from '@revenge-mod/externals/react-native-clipboard' import { formatPluginError, getInternalPluginMeta, getPluginDependencies, getPluginDependents, InternalPluginFlags, isDefaultsOnlyBoot, isPluginEnabledInSavedStates, isPluginEssential, isPluginInternal, isPluginPendingUpdate, isPluginStartable, PluginFlags, runPluginLate, stopPlugin, } from '@revenge-mod/plugins/_' import { listRepos } from '@revenge-mod/plugins/_/repositories' import { PluginStatus } from '@revenge-mod/plugins/constants' import { formatVersion } from '@revenge-mod/plugins/utils' 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 { openPluginSettings, showPluginClearDataConfirmation, showPluginUninstallConfirmation, } from '../utils/alerts' import { messageOf, showErrorToast } from '../utils/repos' import { InstalledPluginSwitch, PluginInfo } from './PluginCard' import { usePluginEnabled, usePluginStatus } from './PluginStateProvider' import PluginTooltipsProvider, { PluginTooltip, usePluginTooltip, } from './TooltipProvider' import type { AnyPlugin } from '@revenge-mod/plugins/_' export interface PluginOptionsActionSheetProps { plugin: AnyPlugin sheetKey: string } const { ActionSheet, IconButton, TableRowGroup, TableRow, Stack } = Design const FileWarningIcon = getAssetIdByName('FileWarningIcon', 'png')! const PlayIcon = getAssetIdByName('PlayIcon', 'png')! const SettingsIcon = getAssetIdByName('SettingsIcon', 'png')! const StopIcon = getAssetIdByName('StopIcon', 'png')! const TrashIcon = getAssetIdByName('TrashIcon', 'png')! export default function PluginOptionsActionSheet({ plugin, sheetKey, }: PluginOptionsActionSheetProps) { return ( ) } function PluginOptions({ plugin, sheetKey }: PluginOptionsActionSheetProps) { const enabled = usePluginEnabled(plugin) const meta = getInternalPluginMeta(plugin) const essential = isPluginEssential(meta) const pendingUpdate = isPluginPendingUpdate(plugin) const { name, author, description, icon, version } = plugin.manifest const [switchRef, showPendingUpdateTooltip] = usePluginTooltip( PluginTooltip.PendingUpdate, ) return ( { if (pendingUpdate) showPendingUpdateTooltip() }} ref={switchRef} > ) } /> { ActionSheetActionCreators.hideActionSheet(sheetKey) }} /> ) } function StatusSection({ plugin }: { plugin: AnyPlugin }) { const status = usePluginStatus(plugin) const meta = getInternalPluginMeta(plugin) const errors = [...plugin.errors, ...meta.nativeErrors] return ( } label="Status (TODO)" subLabel={bitFieldToString(PluginStatus, status)} /> {errors.length > 0 && ( } subLabel={`${errors.length} errors. Tap to copy.`} onPress={() => { Clipboard.setString( errors.map(formatPluginError).join('\n\n'), ) showCopiedToClipboardToast() }} /> )} ) } function AdvancedSection({ plugin }: { plugin: AnyPlugin }) { const meta = getInternalPluginMeta(plugin) const dependents = getPluginDependents(plugin, true) const dependencies = getPluginDependencies(plugin, false) const repositoryText = usePluginRepositoryText(plugin) const { id, name } = plugin.manifest return ( } label="Flags" subLabel={bitFieldToString(PluginFlags, meta.flags)} /> {meta.iflags > 0 && ( } label="Internal Flags" subLabel={bitFieldToString( InternalPluginFlags, meta.iflags, )} /> )} {dependencies.length > 0 && ( } label="Dependencies (TODO)" subLabel={`${name} depends on ${dependencies.length} other plugins`} /> )} {dependents.length > 0 && ( } label="Dependents (TODO)" subLabel={`${dependents.length} other plugins depend on ${name}`} /> )} ) } function bitFieldToString(map: Record, bitField: number) { return ( Object.entries(map) .filter(([, value]) => bitField & value) .map(([key]) => key) .join(', ') || '-' ) } export function IdRow({ id }: { id: string }) { return ( } label="ID" subLabel={id} onPress={() => { Clipboard.setString(id) showCopiedToClipboardToast() }} /> ) } export function RepositoryRow({ text, copyable, }: { text: string copyable?: boolean }) { return ( } label="Repository" subLabel={text} onPress={ copyable ? () => { Clipboard.setString(text) showCopiedToClipboardToast() } : undefined } /> ) } function usePluginRepositoryText(plugin: AnyPlugin) { const meta = getInternalPluginMeta(plugin) const internal = isPluginInternal(meta) const repoUrl = meta.source?.repo ?? null const hasUrl = !internal && repoUrl const [repoName, setRepoName] = useState(null) useEffect(() => { if (!hasUrl) return listRepos().then( repos => { const repo = repos.find(r => r.url === repoUrl) if (repo?.name) setRepoName(repo.name) }, () => {}, ) }, [repoUrl, hasUrl]) return hasUrl ? repoName ? `${repoName} (${repoUrl})` : repoUrl : internal ? 'Built-in' : 'Sideloaded' } function PluginActions({ plugin, closeSheet, }: { plugin: AnyPlugin closeSheet: () => void }) { const [settingsRef, showEnableTooltip] = usePluginTooltip( PluginTooltip.Enable, ) const meta = getInternalPluginMeta(plugin) const startable = isPluginStartable(plugin) const enabled = usePluginEnabled(plugin) // Any lifecycle progress counts as running, stop waits for in-flight lifecycles const running = Boolean(usePluginStatus(plugin)) return ( {enabled && !isPluginEssential(meta) && ( { try { if (running) await stopPlugin(plugin) else await runPluginLate(plugin) } catch (e) { showErrorToast(messageOf(e)) } }} /> )} { showPluginClearDataConfirmation(plugin, closeSheet) }} /> {!isPluginInternal(meta) && ( { showPluginUninstallConfirmation(plugin, closeSheet) }} /> )} {plugin.SettingsComponent && ( { if (!startable) showEnableTooltip() }} > { openPluginSettings(plugin) closeSheet() }} /> )} ) } const CopyIcon = lookupGeneratedIconComponent('CopyIcon')! export function showCopiedToClipboardToast() { ToastActionCreators.open({ key: 'REVENGE_PLUGIN_SETTINGS_COPIED', content: 'Copied to clipboard', IconComponent: CopyIcon, }) }