fix: align backup dialogs with in-app modal behavior

This commit is contained in:
wonfen
2026-05-20 10:24:11 +08:00
parent 6f36613103
commit 15de400f2f
11 changed files with 115 additions and 145 deletions
@@ -49,6 +49,11 @@ const DATE_FORMAT = 'YYYY-MM-DD_HH-mm-ss'
const FILENAME_PATTERN = /\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/
type BackupSource = 'local' | 'webdav'
type PendingConfirmation = {
action: 'delete' | 'restore'
filename: string
source: BackupSource
} | null
interface BackupHistoryViewerProps {
open: boolean
@@ -67,11 +72,6 @@ interface BackupRow {
sort_value: number
}
const confirmAsync = async (message: string) => {
const fn = window.confirm as (msg?: string) => boolean
return fn(message)
}
export const BackupHistoryViewer = ({
open,
source,
@@ -86,6 +86,9 @@ export const BackupHistoryViewer = ({
const [loading, setLoading] = useState(false)
const [isRestoring, setIsRestoring] = useState(false)
const [isRestarting, setIsRestarting] = useState(false)
const [isConfirming, setIsConfirming] = useState(false)
const [pendingConfirmation, setPendingConfirmation] =
useState<PendingConfirmation>(null)
const isLocal = source === 'local'
const isWebDavConfigured = Boolean(
verge?.webdav_url && verge?.webdav_username && verge?.webdav_password,
@@ -94,7 +97,7 @@ export const BackupHistoryViewer = ({
const webdavStatus = getWebdavStatus(webdavSignature)
const shouldSkipWebDav = !isLocal && !isWebDavConfigured
const pageSize = 8
const isBusy = loading || isRestoring || isRestarting
const isBusy = loading || isRestoring || isRestarting || isConfirming
const buildRow = useCallback(
(item: ILocalBackupFile | IWebDavFile): BackupRow | null => {
@@ -204,45 +207,54 @@ export const BackupHistoryViewer = ({
})
}, [isLocal, rows, shouldSkipWebDav, t, total, webdavStatus])
const handleDelete = useLockFn(async (filename: string) => {
const handleDelete = (filename: string) => {
if (isRestarting) return
if (
!(await confirmAsync(t('settings.modals.backup.messages.confirmDelete')))
)
return
if (isLocal) {
await deleteLocalBackup(filename)
} else {
await deleteWebdavBackup(filename)
}
await fetchRows()
})
setPendingConfirmation({ action: 'delete', filename, source })
}
const handleRestore = useLockFn(async (filename: string) => {
const handleRestore = (filename: string) => {
if (isRestoring || isRestarting) return
if (
!(await confirmAsync(t('settings.modals.backup.messages.confirmRestore')))
)
return
setIsRestoring(true)
setPendingConfirmation({ action: 'restore', filename, source })
}
const handleConfirmAction = useLockFn(async () => {
if (!pendingConfirmation) return
const { action, filename, source: actionSource } = pendingConfirmation
const actionIsLocal = actionSource === 'local'
setIsConfirming(true)
if (action === 'restore') {
setIsRestoring(true)
}
try {
if (isLocal) {
await restoreLocalBackup(filename)
if (action === 'delete') {
if (actionIsLocal) {
await deleteLocalBackup(filename)
} else {
await deleteWebdavBackup(filename)
}
setPendingConfirmation(null)
await fetchRows()
} else {
await restoreWebDavBackup(filename)
if (actionIsLocal) {
await restoreLocalBackup(filename)
} else {
await restoreWebDavBackup(filename)
}
setPendingConfirmation(null)
showNotice.success('settings.modals.backup.messages.restoreSuccess')
setIsRestarting(true)
window.setTimeout(() => {
void restartApp().catch((err: unknown) => {
setIsRestarting(false)
showNotice.error(err)
})
}, 1000)
}
showNotice.success('settings.modals.backup.messages.restoreSuccess')
setIsRestarting(true)
window.setTimeout(() => {
void restartApp().catch((err: unknown) => {
setIsRestarting(false)
showNotice.error(err)
})
}, 1000)
} catch (error) {
console.error(error)
showNotice.error(error)
} finally {
setIsConfirming(false)
setIsRestoring(false)
}
})
@@ -267,6 +279,20 @@ export const BackupHistoryViewer = ({
void fetchRows({ force: true })
}
const closeConfirmDialog = () => {
if (isConfirming) return
setPendingConfirmation(null)
}
const confirmTitle =
pendingConfirmation?.action === 'delete'
? t('settings.modals.backup.actions.deleteBackup')
: t('settings.modals.backup.actions.restoreBackup')
const confirmMessage =
pendingConfirmation?.action === 'delete'
? t('settings.modals.backup.messages.confirmDelete')
: t('settings.modals.backup.messages.confirmRestore')
return (
<BaseDialog
open={open}
@@ -423,6 +449,30 @@ export const BackupHistoryViewer = ({
)}
</Stack>
</Box>
<BaseDialog
open={pendingConfirmation !== null}
title={confirmTitle}
okBtn={t('shared.actions.confirm')}
cancelBtn={t('shared.actions.cancel')}
contentSx={{ width: { xs: 320, sm: 420 } }}
loading={isConfirming}
onCancel={closeConfirmDialog}
onClose={closeConfirmDialog}
onOk={handleConfirmAction}
>
<Typography variant="body2" sx={{ wordBreak: 'break-word' }}>
{confirmMessage}
</Typography>
{pendingConfirmation?.filename && (
<Typography
variant="caption"
color="text.secondary"
sx={{ display: 'block', mt: 1, wordBreak: 'break-all' }}
>
{pendingConfirmation.filename}
</Typography>
)}
</BaseDialog>
</BaseDialog>
)
}
@@ -1,4 +1,3 @@
import { LoadingButton } from '@mui/lab'
import {
Button,
List,
@@ -156,7 +155,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
title: t('settings.modals.backup.tabs.local'),
description: t('settings.modals.backup.manual.local'),
actions: [
<LoadingButton
<Button
key="backup"
variant="contained"
size="small"
@@ -165,7 +164,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
onClick={() => handleBackup('local')}
>
{t('settings.modals.backup.actions.backup')}
</LoadingButton>,
</Button>,
<Button
key="history"
variant="outlined"
@@ -175,7 +174,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
>
{t('settings.modals.backup.actions.viewHistory')}
</Button>,
<LoadingButton
<Button
key="import"
variant="text"
size="small"
@@ -184,7 +183,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
onClick={() => handleImport()}
>
{t('settings.modals.backup.actions.importBackup')}
</LoadingButton>,
</Button>,
],
},
{
@@ -192,7 +191,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
title: t('settings.modals.backup.tabs.webdav'),
description: t('settings.modals.backup.manual.webdav'),
actions: [
<LoadingButton
<Button
key="backup"
variant="contained"
size="small"
@@ -200,7 +199,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
onClick={() => handleBackup('webdav')}
>
{t('settings.modals.backup.actions.backup')}
</LoadingButton>,
</Button>,
<Button
key="history"
variant="outlined"
@@ -2,9 +2,9 @@ import {
RestartAltRounded,
SwitchAccessShortcutRounded,
} from '@mui/icons-material'
import { LoadingButton } from '@mui/lab'
import {
Box,
Button,
Chip,
CircularProgress,
List,
@@ -120,7 +120,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
{t('settings.sections.clash.form.fields.clashCore')}
<Box>
<LoadingButton
<Button
variant="contained"
size="small"
startIcon={<SwitchAccessShortcutRounded />}
@@ -131,8 +131,8 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
onClick={onUpgrade}
>
{t('shared.actions.upgrade')}
</LoadingButton>
<LoadingButton
</Button>
<Button
variant="contained"
size="small"
startIcon={<RestartAltRounded />}
@@ -142,7 +142,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
onClick={onRestart}
>
{t('shared.actions.restart')}
</LoadingButton>
</Button>
</Box>
</Box>
}