import { Box, Button, Divider, Paper, Tab, Tabs } from "@mui/material"; import dayjs from "dayjs"; import customParseFormat from "dayjs/plugin/customParseFormat"; import type { Ref } from "react"; import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react"; import { createPortal } from "react-dom"; import { useTranslation } from "react-i18next"; import { BaseDialog, BaseLoadingOverlay, DialogRef } from "@/components/base"; import { deleteLocalBackup, deleteWebdavBackup, listLocalBackup, listWebDavBackup, exportLocalBackup, restoreLocalBackup, restoreWebDavBackup, } from "@/services/cmds"; import { BackupConfigViewer } from "./backup-config-viewer"; import { BackupFile, BackupTableViewer, DEFAULT_ROWS_PER_PAGE, } from "./backup-table-viewer"; import { LocalBackupActions } from "./local-backup-actions"; dayjs.extend(customParseFormat); 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"; export function BackupViewer({ ref }: { ref?: Ref }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const contentRef = useRef(null); const [dialogPaper, setDialogPaper] = useState(null); const [closeButtonPosition, setCloseButtonPosition] = useState<{ top: number; left: number; } | null>(null); const [isLoading, setIsLoading] = useState(false); const [backupFiles, setBackupFiles] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(0); const [source, setSource] = useState("local"); useImperativeHandle(ref, () => ({ open: () => { setOpen(true); }, close: () => setOpen(false), })); // Handle page change const handleChangePage = useCallback( (_: React.MouseEvent | null, page: number) => { setPage(page); }, [], ); const handleChangeSource = useCallback( (_event: React.SyntheticEvent, newSource: string) => { setSource(newSource as BackupSource); setPage(0); }, [], ); const buildBackupFile = useCallback((filename: string) => { const platform = filename.split("-")[0]; const fileBackupTimeStr = filename.match(FILENAME_PATTERN); if (fileBackupTimeStr === null) { return null; } const backupTime = dayjs(fileBackupTimeStr[0], DATE_FORMAT); const allowApply = true; return { filename, platform, backup_time: backupTime, allow_apply: allowApply, } as BackupFile; }, []); const getAllBackupFiles = useCallback(async (): Promise => { if (source === "local") { const files = await listLocalBackup(); return files .map((file) => buildBackupFile(file.filename)) .filter((item): item is BackupFile => item !== null) .sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1)); } const files = await listWebDavBackup(); return files .map((file) => { return buildBackupFile(file.filename); }) .filter((item): item is BackupFile => item !== null) .sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1)); }, [buildBackupFile, source]); const fetchAndSetBackupFiles = useCallback(async () => { try { setIsLoading(true); const files = await getAllBackupFiles(); setBackupFiles(files); setTotal(files.length); } catch (error) { setBackupFiles([]); setTotal(0); console.error(error); // Notice.error(t("Failed to fetch backup files")); } finally { setIsLoading(false); } }, [getAllBackupFiles]); useEffect(() => { if (open) { fetchAndSetBackupFiles(); const paper = contentRef.current?.closest(".MuiPaper-root"); setDialogPaper((paper as HTMLElement) ?? null); } else { setDialogPaper(null); } }, [open, fetchAndSetBackupFiles]); useEffect(() => { if (!open || dialogPaper) { return; } const frame = requestAnimationFrame(() => { const paper = contentRef.current?.closest(".MuiPaper-root"); setDialogPaper((paper as HTMLElement) ?? null); }); return () => cancelAnimationFrame(frame); }, [open, dialogPaper]); useEffect(() => { if (!dialogPaper) { setCloseButtonPosition(null); return; } if (typeof window === "undefined") { return; } const updatePosition = () => { const rect = dialogPaper.getBoundingClientRect(); setCloseButtonPosition({ top: rect.bottom - 16, left: rect.right - 24, }); }; updatePosition(); let resizeObserver: ResizeObserver | null = null; if (typeof ResizeObserver !== "undefined") { resizeObserver = new ResizeObserver(() => { updatePosition(); }); resizeObserver.observe(dialogPaper); } const scrollTargets: EventTarget[] = []; const addScrollListener = (target: EventTarget | null) => { if (!target) { return; } target.addEventListener("scroll", updatePosition, true); scrollTargets.push(target); }; addScrollListener(window); addScrollListener(dialogPaper); const dialogContent = dialogPaper.querySelector(".MuiDialogContent-root"); addScrollListener(dialogContent); window.addEventListener("resize", updatePosition); return () => { resizeObserver?.disconnect(); scrollTargets.forEach((target) => { target.removeEventListener("scroll", updatePosition, true); }); window.removeEventListener("resize", updatePosition); }; }, [dialogPaper]); const handleDelete = useCallback( async (filename: string) => { if (source === "local") { await deleteLocalBackup(filename); } else { await deleteWebdavBackup(filename); } }, [source], ); const handleRestore = useCallback( async (filename: string) => { if (source === "local") { await restoreLocalBackup(filename); } else { await restoreWebDavBackup(filename); } }, [source], ); const handleExport = useCallback( async (filename: string, destination: string) => { await exportLocalBackup(filename, destination); }, [], ); const dataSource = useMemo( () => backupFiles.slice( page * DEFAULT_ROWS_PER_PAGE, page * DEFAULT_ROWS_PER_PAGE + DEFAULT_ROWS_PER_PAGE, ), [backupFiles, page], ); return ( setOpen(false)} disableFooter > {source === "local" ? ( ) : ( )} {dialogPaper && closeButtonPosition && createPortal( theme.zIndex.modal + 1, }} > , dialogPaper, )} ); }