import { forwardRef, useImperativeHandle, useState, useRef, SVGProps, useCallback, useMemo, memo, } from "react"; import { useTranslation } from "react-i18next"; import { useLockFn } from "ahooks"; import { Typography } from "@mui/material"; import { useForm, UseFormRegister } from "react-hook-form"; import { useVerge } from "@/hooks/use-verge"; import { BaseDialog, DialogRef, Notice } from "@/components/base"; import { isValidUrl } from "@/utils/helper"; import getSystem from "@/utils/get-system"; import { BaseLoadingOverlay } from "@/components/base"; import dayjs, { Dayjs } from "dayjs"; import customParseFormat from "dayjs/plugin/customParseFormat"; dayjs.extend(customParseFormat); import { TextField, Button, Grid, Box, Paper, Stack, IconButton, InputAdornment, Divider, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, } from "@mui/material"; import Visibility from "@mui/icons-material/Visibility"; import VisibilityOff from "@mui/icons-material/VisibilityOff"; import DeleteIcon from "@mui/icons-material/Delete"; import RestoreIcon from "@mui/icons-material/Restore"; import { createWebdavBackup, listWebDavBackup, saveWebdavConfig, } from "@/services/cmds"; type BackupFile = IWebDavFile & { platform: string; backup_time: Dayjs; allow_apply: boolean; }; interface BackupTableProps { datasource: BackupFile[]; page: number; rowsPerPage: number; onPageChange: (event: any, newPage: number) => void; onRowsPerPageChange: (event: React.ChangeEvent) => void; totalCount: number; } interface WebDAVConfigFormProps { onSubmit: (e: React.FormEvent) => void; initialValues: Partial; urlRef: React.RefObject; usernameRef: React.RefObject; passwordRef: React.RefObject; showPassword: boolean; onShowPasswordClick: () => void; webdavChanged: boolean; webdavUrl: string | undefined | null; webdavUsername: string | undefined | null; webdavPassword: string | undefined | null; handleBackup: () => Promise; register: UseFormRegister; } // 将魔法数字和配置提取为常量 const DEFAULT_ROWS_PER_PAGE = 5; const DATE_FORMAT = "YYYY-MM-DD_HH-mm-ss"; const FILENAME_PATTERN = /\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/; export const BackupViewer = forwardRef((props, ref) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); const { verge, mutateVerge } = useVerge(); const { webdav_url, webdav_username, webdav_password } = verge || {}; const [showPassword, setShowPassword] = useState(false); const usernameRef = useRef(null); const passwordRef = useRef(null); const [backupState, setBackupState] = useState({ files: [] as BackupFile[], page: 0, rowsPerPage: DEFAULT_ROWS_PER_PAGE, isLoading: false, }); const OS = getSystem(); const urlRef = useRef(null); const { register, handleSubmit, watch } = useForm({ defaultValues: { url: webdav_url, username: webdav_username, password: webdav_password, }, }); const url = watch("url"); const username = watch("username"); const password = watch("password"); const webdavChanged = webdav_url !== url || webdav_username !== username || webdav_password !== password; useImperativeHandle(ref, () => ({ open: () => { setOpen(true); if (webdav_url && webdav_username && webdav_password) { fetchAndSetBackupFiles(); } }, close: () => setOpen(false), })); // Handle page change const handleChangePage = useCallback( (_: React.MouseEvent | null, page: number) => { setBackupState((prev) => ({ ...prev, page })); }, [] ); // Handle rows per page change const handleChangeRowsPerPage = useCallback( (event: React.ChangeEvent) => { setBackupState((prev) => ({ ...prev, rowsPerPage: parseInt(event.target.value, 10), page: 0, })); }, [] ); const fetchAndSetBackupFiles = async () => { try { setBackupState((prev) => ({ ...prev, isLoading: true })); const files = await getAllBackupFiles(); setBackupState((prev) => ({ ...prev, files })); } catch (error) { console.error("Failed to fetch backup files:", error); Notice.error(t("Failed to fetch backup files")); } finally { setBackupState((prev) => ({ ...prev, isLoading: false })); } }; const checkForm = () => { const username = usernameRef.current?.value; const password = passwordRef.current?.value; const url = urlRef.current?.value; if (!url) { Notice.error(t("WebDAV URL Required")); urlRef.current?.focus(); return; } else if (!isValidUrl(url)) { Notice.error(t("Invalid WebDAV URL")); urlRef.current?.focus(); return; } if (!username) { Notice.error(t("Username Required")); usernameRef.current?.focus(); return; } if (!password) { Notice.error(t("Password Required")); passwordRef.current?.focus(); return; } }; const submit = async (data: IWebDavConfig) => { checkForm(); setBackupState((prev) => ({ ...prev, isLoading: true })); await saveWebdavConfig(data.url, data.username, data.password) .then(() => { mutateVerge( { webdav_url: data.url, webdav_username: data.username, webdav_password: data.password, }, false ); Notice.success(t("WebDAV Config Saved")); }) .catch((e) => { Notice.error(t("WebDAV Config Save Failed", { error: e }), 3000); }) .finally(() => { setBackupState((prev) => ({ ...prev, isLoading: false })); fetchAndSetBackupFiles(); }); }; const handleClickShowPassword = useCallback(() => { setShowPassword((prev) => !prev); }, []); const handleBackup = useLockFn(async () => { try { checkForm(); setBackupState((prev) => ({ ...prev, isLoading: true })); await createWebdavBackup(); Notice.success(t("Backup Created")); await fetchAndSetBackupFiles(); } catch (error) { console.error("Backup failed:", error); Notice.error(t("Backup Failed", { error })); } finally { setBackupState((prev) => ({ ...prev, isLoading: false })); } }); const getAllBackupFiles = async () => { const files = await listWebDavBackup(); return files .map((file) => { const platform = file.filename.split("-")[0]; const fileBackupTimeStr = file.filename.match(FILENAME_PATTERN)!; const backupTime = dayjs(fileBackupTimeStr[0], DATE_FORMAT); const allowApply = OS === platform; return { ...file, platform, backup_time: backupTime, allow_apply: allowApply, } as BackupFile; }) .sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1)); }; const datasource = useMemo(() => { return backupState.files.slice( backupState.page * backupState.rowsPerPage, backupState.page * backupState.rowsPerPage + backupState.rowsPerPage ); }, [backupState.files, backupState.page, backupState.rowsPerPage]); const onFormSubmit = (e: React.FormEvent) => { e.preventDefault(); handleSubmit(submit)(e); }; return ( setOpen(false)} onCancel={() => setOpen(false)} > handleChangePage(null, page)} onRowsPerPageChange={handleChangeRowsPerPage} totalCount={backupState.files.length} /> ); }); const BackupTable = memo( ({ datasource, page, rowsPerPage, onPageChange, onRowsPerPageChange, totalCount, }: BackupTableProps) => { const { t } = useTranslation(); return ( {t("Filename")} {t("Backup Time")} {t("Actions")} {datasource.length > 0 ? ( datasource?.map((file, index) => ( {file.platform === "windows" ? ( ) : file.platform === "linux" ? ( ) : ( )} {file.filename} {file.backup_time.fromNow()} )) ) : ( {t("No Backups")} )}
); } ); const WebDAVConfigForm = memo( ({ onSubmit, initialValues, urlRef, usernameRef, passwordRef, showPassword, onShowPasswordClick, webdavChanged, webdavUrl, webdavUsername, webdavPassword, handleBackup, register, }: WebDAVConfigFormProps) => { const { t } = useTranslation(); return (
{showPassword ? : } ), }} /> {webdavChanged || webdavUrl === null || webdavUsername == null || webdavPassword == null ? ( ) : ( )}
); } ); export function LinuxIcon(props: SVGProps) { return ( ); } export function WindowsIcon(props: SVGProps) { return ( ); } export function MacIcon(props: SVGProps) { return ( ); }