Files
clash-verge-rev/src/components/setting/mods/backup-viewer.tsx
T
Tunglies 4e62db44f8 refactor(settings): route settings-domain commands through the mutate funnel
Ports port saving, webdav/local backup operations, core change/restart/
upgrade, service install/uninstall/repair flows, the system-proxy toggle
queue, the DNS override toggle, and app lifecycle commands onto the single
funnel. Bespoke flows keep their own error handling via errorNotice:false;
simple flows gain the funnel's default error toast and revalidation.
2026-10-01 04:20:26 +08:00

277 lines
9.1 KiB
TypeScript

import {
Button,
List,
ListItem,
ListItemText,
Stack,
Typography,
} from '@mui/material'
import { open as openDialog } from '@tauri-apps/plugin-dialog'
import { useLockFn } from 'ahooks'
import type { ReactNode, Ref } from 'react'
import { useCallback, useImperativeHandle, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BaseDialog, DialogRef } from '@/components/base'
import { useVerge } from '@/hooks/use-verge'
import {
createLocalBackup,
createWebdavBackup,
importLocalBackup,
} from '@/services/cmds'
import { mutate } from '@/services/mutate'
import { errorDetail, showNotice } from '@/services/notice-service'
import { buildWebdavSignature, setWebdavStatus } from '@/services/webdav-status'
import { AutoBackupSettings } from './auto-backup-settings'
import { BackupHistoryViewer } from './backup-history-viewer'
import { BackupWebdavDialog } from './backup-webdav-dialog'
type BackupSource = 'local' | 'webdav'
export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
const { t } = useTranslation()
const { verge } = useVerge()
const [open, setOpen] = useState(false)
const [busyAction, setBusyAction] = useState<BackupSource | null>(null)
const [localImporting, setLocalImporting] = useState(false)
const [historyOpen, setHistoryOpen] = useState(false)
const [historySource, setHistorySource] = useState<BackupSource>('local')
const [historyPage, setHistoryPage] = useState(0)
const [webdavDialogOpen, setWebdavDialogOpen] = useState(false)
const webdavSignature = buildWebdavSignature(verge)
useImperativeHandle(ref, () => ({
open: () => setOpen(true),
close: () => setOpen(false),
}))
const openHistory = (target: BackupSource) => {
setHistorySource(target)
setHistoryPage(0)
setHistoryOpen(true)
}
const handleBackup = useLockFn(async (target: BackupSource) => {
try {
setBusyAction(target)
if (target === 'local') {
await mutate(() => createLocalBackup(), {
id: 'create-local-backup',
errorNotice: false,
successNotice: 'settings.modals.backup.messages.localBackupCreated',
})
} else {
await mutate(() => createWebdavBackup(), {
id: 'create-webdav-backup',
errorNotice: false,
successNotice: 'settings.modals.backup.messages.backupCreated',
onFulfilled: () => setWebdavStatus(webdavSignature, 'ready'),
})
}
} catch (error) {
console.error(error)
showNotice.error(
target === 'local'
? 'settings.modals.backup.messages.localBackupFailed'
: 'settings.modals.backup.messages.backupFailed',
target === 'local' ? undefined : { error: errorDetail(error) },
)
if (target === 'webdav') {
setWebdavStatus(webdavSignature, 'failed')
}
} finally {
setBusyAction(null)
}
})
const handleImport = useLockFn(async () => {
const selected = await openDialog({
multiple: false,
filters: [{ name: 'Backup File', extensions: ['zip'] }],
})
if (!selected || Array.isArray(selected)) return
try {
setLocalImporting(true)
await mutate(() => importLocalBackup(selected), {
id: 'import-local-backup',
errorNotice: false,
successNotice: 'settings.modals.backup.messages.localBackupImported',
})
openHistory('local')
} catch (error) {
console.error(error)
showNotice.error(
'settings.modals.backup.messages.localBackupImportFailed',
{ error: errorDetail(error) },
)
} finally {
setLocalImporting(false)
}
})
const setWebdavBusy = useCallback(
(loading: boolean) => {
setBusyAction(loading ? 'webdav' : null)
},
[setBusyAction],
)
const isLocalBusy = busyAction === 'local' || localImporting
return (
<BaseDialog
open={open}
title={t('settings.modals.backup.title')}
contentSx={{ width: { xs: 360, sm: 520 } }}
disableOk
cancelBtn={t('shared.actions.close')}
onCancel={() => setOpen(false)}
onClose={() => setOpen(false)}
>
<Stack spacing={2}>
<Stack
spacing={1}
sx={{
border: (theme) => `1px solid ${theme.palette.divider}`,
borderRadius: 2,
p: 2,
}}
>
<Typography variant="subtitle1">
{t('settings.modals.backup.auto.title')}
</Typography>
<List disablePadding sx={{ '.MuiListItem-root': { px: 0 } }}>
<AutoBackupSettings />
</List>
</Stack>
<Stack
spacing={1}
sx={{
border: (theme) => `1px solid ${theme.palette.divider}`,
borderRadius: 2,
p: 2,
}}
>
<Typography variant="subtitle1">
{t('settings.modals.backup.manual.title')}
</Typography>
<List disablePadding sx={{ '.MuiListItem-root': { px: 0 } }}>
{(
[
{
key: 'local' as BackupSource,
title: t('settings.modals.backup.tabs.local'),
description: t('settings.modals.backup.manual.local'),
actions: [
<Button
key="backup"
variant="contained"
size="small"
loading={busyAction === 'local'}
disabled={localImporting}
onClick={() => handleBackup('local')}
>
{t('settings.modals.backup.actions.backup')}
</Button>,
<Button
key="history"
variant="outlined"
size="small"
disabled={isLocalBusy}
onClick={() => openHistory('local')}
>
{t('settings.modals.backup.actions.viewHistory')}
</Button>,
<Button
key="import"
variant="text"
size="small"
loading={localImporting}
disabled={busyAction === 'local'}
onClick={() => handleImport()}
>
{t('settings.modals.backup.actions.importBackup')}
</Button>,
],
},
{
key: 'webdav' as BackupSource,
title: t('settings.modals.backup.tabs.webdav'),
description: t('settings.modals.backup.manual.webdav'),
actions: [
<Button
key="backup"
variant="contained"
size="small"
loading={busyAction === 'webdav'}
onClick={() => handleBackup('webdav')}
>
{t('settings.modals.backup.actions.backup')}
</Button>,
<Button
key="history"
variant="outlined"
size="small"
onClick={() => openHistory('webdav')}
>
{t('settings.modals.backup.actions.viewHistory')}
</Button>,
<Button
key="configure"
variant="text"
size="small"
onClick={() => setWebdavDialogOpen(true)}
>
{t('settings.modals.backup.manual.configureWebdav')}
</Button>,
],
},
] satisfies Array<{
key: BackupSource
title: string
description: string
actions: ReactNode[]
}>
).map((item, idx) => (
<ListItem key={item.key} disableGutters divider={idx === 0}>
<Stack spacing={1} sx={{ width: '100%' }}>
<ListItemText
primary={item.title}
slotProps={{ secondary: { component: 'span' } }}
secondary={item.description}
/>
<Stack
direction="row"
spacing={1}
useFlexGap
sx={{ flexWrap: 'wrap', alignItems: 'center' }}
>
{item.actions}
</Stack>
</Stack>
</ListItem>
))}
</List>
</Stack>
</Stack>
<BackupHistoryViewer
open={historyOpen}
source={historySource}
page={historyPage}
onSourceChange={setHistorySource}
onPageChange={setHistoryPage}
onClose={() => setHistoryOpen(false)}
/>
<BackupWebdavDialog
open={webdavDialogOpen}
onClose={() => setWebdavDialogOpen(false)}
onBackupSuccess={() => openHistory('webdav')}
setBusy={setWebdavBusy}
/>
</BaseDialog>
)
}