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,
})
}