mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-28 17:22:15 +08:00
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
This commit is contained in:
@@ -57,7 +57,9 @@ interface BackupHistoryViewerProps {
|
||||
interface BackupRow {
|
||||
filename: string;
|
||||
platform: string;
|
||||
backup_time: dayjs.Dayjs;
|
||||
backup_time: dayjs.Dayjs | null;
|
||||
display_time: string;
|
||||
sort_value: number;
|
||||
}
|
||||
|
||||
const confirmAsync = async (message: string) => {
|
||||
@@ -86,16 +88,44 @@ export const BackupHistoryViewer = ({
|
||||
const pageSize = 8;
|
||||
const isBusy = loading || isRestarting;
|
||||
|
||||
const buildRow = useCallback((filename: string): BackupRow | null => {
|
||||
const platform = filename.split("-")[0];
|
||||
const match = filename.match(FILENAME_PATTERN);
|
||||
if (!match) return null;
|
||||
return {
|
||||
filename,
|
||||
platform,
|
||||
backup_time: dayjs(match[0], DATE_FORMAT),
|
||||
};
|
||||
}, []);
|
||||
const buildRow = useCallback(
|
||||
(item: ILocalBackupFile | IWebDavFile): BackupRow | null => {
|
||||
const { filename, last_modified } = item;
|
||||
if (!filename.toLowerCase().endsWith(".zip")) return null;
|
||||
|
||||
const platform =
|
||||
(filename.includes("-") && filename.split("-")[0]) ||
|
||||
t("settings.modals.backup.history.unknownPlatform", {
|
||||
defaultValue: "unknown",
|
||||
});
|
||||
const match = filename.match(FILENAME_PATTERN);
|
||||
const parsedFromName = match ? dayjs(match[0], DATE_FORMAT, true) : null;
|
||||
const parsedFromModified =
|
||||
last_modified && dayjs(last_modified).isValid()
|
||||
? dayjs(last_modified)
|
||||
: null;
|
||||
const backupTime = parsedFromName?.isValid()
|
||||
? parsedFromName
|
||||
: parsedFromModified;
|
||||
|
||||
return {
|
||||
filename,
|
||||
platform,
|
||||
backup_time: backupTime ?? null,
|
||||
display_time:
|
||||
backupTime?.format("YYYY-MM-DD HH:mm") ??
|
||||
parsedFromModified?.format("YYYY-MM-DD HH:mm") ??
|
||||
t("settings.modals.backup.history.unknownTime", {
|
||||
defaultValue: "Unknown time",
|
||||
}),
|
||||
sort_value:
|
||||
backupTime?.valueOf() ??
|
||||
parsedFromModified?.valueOf() ??
|
||||
Number.NEGATIVE_INFINITY,
|
||||
};
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
const fetchRows = useCallback(async () => {
|
||||
if (!open) return;
|
||||
@@ -108,9 +138,13 @@ export const BackupHistoryViewer = ({
|
||||
const list = isLocal ? await listLocalBackup() : await listWebDavBackup();
|
||||
setRows(
|
||||
list
|
||||
.map((item) => buildRow(item.filename))
|
||||
.map((item) => buildRow(item))
|
||||
.filter((item): item is BackupRow => item !== null)
|
||||
.sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1)),
|
||||
.sort((a, b) =>
|
||||
a.sort_value === b.sort_value
|
||||
? b.filename.localeCompare(a.filename)
|
||||
: b.sort_value - a.sort_value,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -138,7 +172,8 @@ export const BackupHistoryViewer = ({
|
||||
return t("settings.modals.backup.manual.webdav");
|
||||
}
|
||||
if (!total) return t("settings.modals.backup.history.empty");
|
||||
const recent = rows[0]?.backup_time.fromNow();
|
||||
const recent =
|
||||
rows[0]?.backup_time?.fromNow() ?? rows[0]?.display_time ?? "";
|
||||
return t("settings.modals.backup.history.summary", {
|
||||
count: total,
|
||||
recent,
|
||||
@@ -283,7 +318,7 @@ export const BackupHistoryViewer = ({
|
||||
spacing={1.5}
|
||||
>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{`${row.platform} · ${row.backup_time.format("YYYY-MM-DD HH:mm")}`}
|
||||
{`${row.platform} · ${row.display_time}`}
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
|
||||
@@ -7,13 +7,18 @@ import {
|
||||
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 } from "@/services/cmds";
|
||||
import {
|
||||
createLocalBackup,
|
||||
createWebdavBackup,
|
||||
importLocalBackup,
|
||||
} from "@/services/cmds";
|
||||
import { showNotice } from "@/services/notice-service";
|
||||
|
||||
import { AutoBackupSettings } from "./auto-backup-settings";
|
||||
@@ -26,6 +31,7 @@ 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);
|
||||
@@ -67,6 +73,28 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -74,6 +102,8 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
[setBusyAction],
|
||||
);
|
||||
|
||||
const isLocalBusy = busyAction === "local" || localImporting;
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
@@ -125,6 +155,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
variant="contained"
|
||||
size="small"
|
||||
loading={busyAction === "local"}
|
||||
disabled={localImporting}
|
||||
onClick={() => handleBackup("local")}
|
||||
>
|
||||
{t("settings.modals.backup.actions.backup")}
|
||||
@@ -133,10 +164,21 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
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>,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user