Files
clash-verge-rev/src/components/setting/mods/backup-viewer.tsx
T
Sline bd8eccdcea feat(backup): add local backup import flow (#5669)
* feat(backup): add local backup import flow

* refactor(backup): robustify history listing and propagate import error details

* docs: Changelog.md
2025-12-19 17:46:31 +08:00

263 lines
8.5 KiB
TypeScript

import { LoadingButton } from "@mui/lab";
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 {
createLocalBackup,
createWebdavBackup,
importLocalBackup,
} from "@/services/cmds";
import { showNotice } from "@/services/notice-service";
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 [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);
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 createLocalBackup();
showNotice.success(
"settings.modals.backup.messages.localBackupCreated",
);
} else {
await createWebdavBackup();
showNotice.success("settings.modals.backup.messages.backupCreated");
}
} catch (error) {
console.error(error);
showNotice.error(
target === "local"
? "settings.modals.backup.messages.localBackupFailed"
: "settings.modals.backup.messages.backupFailed",
target === "local" ? undefined : { error },
);
} 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 importLocalBackup(selected);
showNotice.success("settings.modals.backup.messages.localBackupImported");
openHistory("local");
} catch (error) {
console.error(error);
showNotice.error(
"settings.modals.backup.messages.localBackupImportFailed",
{ 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: [
<LoadingButton
key="backup"
variant="contained"
size="small"
loading={busyAction === "local"}
disabled={localImporting}
onClick={() => handleBackup("local")}
>
{t("settings.modals.backup.actions.backup")}
</LoadingButton>,
<Button
key="history"
variant="outlined"
size="small"
disabled={isLocalBusy}
onClick={() => openHistory("local")}
>
{t("settings.modals.backup.actions.viewHistory")}
</Button>,
<LoadingButton
key="import"
variant="text"
size="small"
loading={localImporting}
disabled={busyAction === "local"}
onClick={() => handleImport()}
>
{t("settings.modals.backup.actions.importBackup")}
</LoadingButton>,
],
},
{
key: "webdav" as BackupSource,
title: t("settings.modals.backup.tabs.webdav"),
description: t("settings.modals.backup.manual.webdav"),
actions: [
<LoadingButton
key="backup"
variant="contained"
size="small"
loading={busyAction === "webdav"}
onClick={() => handleBackup("webdav")}
>
{t("settings.modals.backup.actions.backup")}
</LoadingButton>,
<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
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>
);
}