diff --git a/src/components/setting/mods/backup-config-viewer.tsx b/src/components/setting/mods/backup-config-viewer.tsx index 1349fc9c3..ff7c0c8e0 100644 --- a/src/components/setting/mods/backup-config-viewer.tsx +++ b/src/components/setting/mods/backup-config-viewer.tsx @@ -16,11 +16,16 @@ import { useTranslation } from "react-i18next"; import { useVerge } from "@/hooks/use-verge"; import { saveWebdavConfig, createWebdavBackup } from "@/services/cmds"; import { showNotice } from "@/services/notice-service"; +import { + buildWebdavSignature, + getWebdavStatus, + setWebdavStatus, +} from "@/services/webdav-status"; import { isValidUrl } from "@/utils/helper"; interface BackupConfigViewerProps { onBackupSuccess: () => Promise; - onSaveSuccess: () => Promise; + onSaveSuccess: (signature?: string) => Promise; onRefresh: () => Promise; onInit: () => Promise; setLoading: (loading: boolean) => void; @@ -35,7 +40,7 @@ export const BackupConfigViewer = memo( setLoading, }: BackupConfigViewerProps) => { const { t } = useTranslation(); - const { verge } = useVerge(); + const { verge, mutateVerge } = useVerge(); const { webdav_url, webdav_username, webdav_password } = verge || {}; const [showPassword, setShowPassword] = useState(false); const usernameRef = useRef(null); @@ -58,6 +63,10 @@ export const BackupConfigViewer = memo( webdav_username !== username || webdav_password !== password; + const webdavSignature = buildWebdavSignature(verge); + const webdavStatus = getWebdavStatus(webdavSignature); + const shouldAutoInit = webdavStatus !== "failed"; + const handleClickShowPassword = () => { setShowPassword((prev) => !prev); }; @@ -66,8 +75,11 @@ export const BackupConfigViewer = memo( if (!webdav_url || !webdav_username || !webdav_password) { return; } + if (!shouldAutoInit) { + return; + } void onInit(); - }, [webdav_url, webdav_username, webdav_password, onInit]); + }, [webdav_url, webdav_username, webdav_password, onInit, shouldAutoInit]); const checkForm = () => { const username = usernameRef.current?.value; @@ -97,18 +109,32 @@ export const BackupConfigViewer = memo( const save = useLockFn(async (data: IWebDavConfig) => { checkForm(); + const signature = buildWebdavSignature({ + webdav_url: data.url, + webdav_username: data.username, + webdav_password: data.password, + }); + const trimmedUrl = data.url.trim(); + const trimmedUsername = data.username.trim(); + try { setLoading(true); - await saveWebdavConfig( - data.url.trim(), - data.username.trim(), - data.password, - ).then(() => { - showNotice.success( - "settings.modals.backup.messages.webdavConfigSaved", - ); - onSaveSuccess(); - }); + await saveWebdavConfig(trimmedUrl, trimmedUsername, data.password); + await mutateVerge( + (current) => + current + ? { + ...current, + webdav_url: trimmedUrl, + webdav_username: trimmedUsername, + webdav_password: data.password, + } + : current, + false, + ); + setWebdavStatus(signature, "unknown"); + showNotice.success("settings.modals.backup.messages.webdavConfigSaved"); + await onSaveSuccess(signature); } catch (error) { showNotice.error( "settings.modals.backup.messages.webdavConfigSaveFailed", @@ -122,16 +148,24 @@ export const BackupConfigViewer = memo( const handleBackup = useLockFn(async () => { checkForm(); + const signature = buildWebdavSignature({ + webdav_url: url, + webdav_username: username, + webdav_password: password, + }); + try { setLoading(true); await createWebdavBackup().then(async () => { showNotice.success("settings.modals.backup.messages.backupCreated"); await onBackupSuccess(); }); + setWebdavStatus(signature, "ready"); } catch (error) { showNotice.error("settings.modals.backup.messages.backupFailed", { error, }); + setWebdavStatus(signature, "failed"); } finally { setLoading(false); } diff --git a/src/components/setting/mods/backup-history-viewer.tsx b/src/components/setting/mods/backup-history-viewer.tsx index f4501d5c8..71dbab5c5 100644 --- a/src/components/setting/mods/backup-history-viewer.tsx +++ b/src/components/setting/mods/backup-history-viewer.tsx @@ -36,6 +36,11 @@ import { restoreWebDavBackup, } from "@/services/cmds"; import { showNotice } from "@/services/notice-service"; +import { + buildWebdavSignature, + getWebdavStatus, + setWebdavStatus, +} from "@/services/webdav-status"; dayjs.extend(customParseFormat); dayjs.extend(relativeTime); @@ -85,6 +90,8 @@ export const BackupHistoryViewer = ({ const isWebDavConfigured = Boolean( verge?.webdav_url && verge?.webdav_username && verge?.webdav_password, ); + const webdavSignature = buildWebdavSignature(verge); + const webdavStatus = getWebdavStatus(webdavSignature); const shouldSkipWebDav = !isLocal && !isWebDavConfigured; const pageSize = 8; const isBusy = loading || isRestoring || isRestarting; @@ -128,33 +135,49 @@ export const BackupHistoryViewer = ({ [t], ); - const fetchRows = useCallback(async () => { - if (!open) return; - if (shouldSkipWebDav) { - setRows([]); - return; - } - setLoading(true); - try { - const list = isLocal ? await listLocalBackup() : await listWebDavBackup(); - setRows( - list - .map((item) => buildRow(item)) - .filter((item): item is BackupRow => item !== null) - .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); - setRows([]); - showNotice.error(error); - } finally { - setLoading(false); - } - }, [buildRow, isLocal, open, shouldSkipWebDav]); + const fetchRows = useCallback( + async (options?: { force?: boolean }) => { + if (!open) return; + if (shouldSkipWebDav) { + setRows([]); + return; + } + if (!isLocal && webdavStatus === "failed" && !options?.force) { + setRows([]); + return; + } + + setLoading(true); + try { + const list = isLocal + ? await listLocalBackup() + : await listWebDavBackup(); + if (!isLocal) { + setWebdavStatus(webdavSignature, "ready"); + } + setRows( + list + .map((item) => buildRow(item)) + .filter((item): item is BackupRow => item !== null) + .sort((a, b) => + a.sort_value === b.sort_value + ? b.filename.localeCompare(a.filename) + : b.sort_value - a.sort_value, + ), + ); + } catch (error) { + if (!isLocal) { + setWebdavStatus(webdavSignature, "failed"); + } + console.error(error); + setRows([]); + showNotice.error(error); + } finally { + setLoading(false); + } + }, + [buildRow, isLocal, open, shouldSkipWebDav, webdavSignature, webdavStatus], + ); useEffect(() => { void fetchRows(); @@ -169,7 +192,7 @@ export const BackupHistoryViewer = ({ ); const summary = useMemo(() => { - if (shouldSkipWebDav) { + if (shouldSkipWebDav || (!isLocal && webdavStatus === "failed")) { return t("settings.modals.backup.manual.webdav"); } if (!total) return t("settings.modals.backup.history.empty"); @@ -179,7 +202,7 @@ export const BackupHistoryViewer = ({ count: total, recent, }); - }, [rows, shouldSkipWebDav, t, total]); + }, [isLocal, rows, shouldSkipWebDav, t, total, webdavStatus]); const handleDelete = useLockFn(async (filename: string) => { if (isRestarting) return; @@ -241,7 +264,7 @@ export const BackupHistoryViewer = ({ const handleRefresh = () => { if (isRestarting) return; - void fetchRows(); + void fetchRows({ force: true }); }; return ( diff --git a/src/components/setting/mods/backup-viewer.tsx b/src/components/setting/mods/backup-viewer.tsx index 89802aaa5..858db5499 100644 --- a/src/components/setting/mods/backup-viewer.tsx +++ b/src/components/setting/mods/backup-viewer.tsx @@ -14,12 +14,17 @@ import { useCallback, useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; import { BaseDialog, DialogRef } from "@/components/base"; +import { useVerge } from "@/hooks/use-verge"; import { createLocalBackup, createWebdavBackup, importLocalBackup, } from "@/services/cmds"; import { showNotice } from "@/services/notice-service"; +import { + buildWebdavSignature, + setWebdavStatus, +} from "@/services/webdav-status"; import { AutoBackupSettings } from "./auto-backup-settings"; import { BackupHistoryViewer } from "./backup-history-viewer"; @@ -29,6 +34,7 @@ type BackupSource = "local" | "webdav"; export function BackupViewer({ ref }: { ref?: Ref }) { const { t } = useTranslation(); + const { verge } = useVerge(); const [open, setOpen] = useState(false); const [busyAction, setBusyAction] = useState(null); const [localImporting, setLocalImporting] = useState(false); @@ -36,6 +42,7 @@ export function BackupViewer({ ref }: { ref?: Ref }) { const [historySource, setHistorySource] = useState("local"); const [historyPage, setHistoryPage] = useState(0); const [webdavDialogOpen, setWebdavDialogOpen] = useState(false); + const webdavSignature = buildWebdavSignature(verge); useImperativeHandle(ref, () => ({ open: () => setOpen(true), @@ -59,6 +66,7 @@ export function BackupViewer({ ref }: { ref?: Ref }) { } else { await createWebdavBackup(); showNotice.success("settings.modals.backup.messages.backupCreated"); + setWebdavStatus(webdavSignature, "ready"); } } catch (error) { console.error(error); @@ -68,6 +76,9 @@ export function BackupViewer({ ref }: { ref?: Ref }) { : "settings.modals.backup.messages.backupFailed", target === "local" ? undefined : { error }, ); + if (target === "webdav") { + setWebdavStatus(webdavSignature, "failed"); + } } finally { setBusyAction(null); } diff --git a/src/components/setting/mods/backup-webdav-dialog.tsx b/src/components/setting/mods/backup-webdav-dialog.tsx index 610350cc6..58c6deee8 100644 --- a/src/components/setting/mods/backup-webdav-dialog.tsx +++ b/src/components/setting/mods/backup-webdav-dialog.tsx @@ -3,8 +3,13 @@ import { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; import { BaseDialog, BaseLoadingOverlay } from "@/components/base"; +import { useVerge } from "@/hooks/use-verge"; import { listWebDavBackup } from "@/services/cmds"; import { showNotice } from "@/services/notice-service"; +import { + buildWebdavSignature, + setWebdavStatus, +} from "@/services/webdav-status"; import { BackupConfigViewer } from "./backup-config-viewer"; @@ -22,7 +27,9 @@ export const BackupWebdavDialog = ({ setBusy, }: BackupWebdavDialogProps) => { const { t } = useTranslation(); + const { verge } = useVerge(); const [loading, setLoading] = useState(false); + const webdavSignature = buildWebdavSignature(verge); const handleLoading = useCallback( (value: boolean) => { @@ -33,16 +40,19 @@ export const BackupWebdavDialog = ({ ); const refreshWebdav = useCallback( - async (options?: { silent?: boolean }) => { + async (options?: { silent?: boolean; signature?: string }) => { + const signature = options?.signature ?? webdavSignature; handleLoading(true); try { await listWebDavBackup(); + setWebdavStatus(signature, "ready"); if (!options?.silent) { showNotice.success( "settings.modals.backup.messages.webdavRefreshSuccess", ); } } catch (error) { + setWebdavStatus(signature, "failed"); showNotice.error( "settings.modals.backup.messages.webdavRefreshFailed", { error }, @@ -51,11 +61,11 @@ export const BackupWebdavDialog = ({ handleLoading(false); } }, - [handleLoading], + [handleLoading, webdavSignature], ); const refreshSilently = useCallback( - () => refreshWebdav({ silent: true }), + (signature?: string) => refreshWebdav({ silent: true, signature }), [refreshWebdav], ); diff --git a/src/services/webdav-status.ts b/src/services/webdav-status.ts new file mode 100644 index 000000000..a33c1bbef --- /dev/null +++ b/src/services/webdav-status.ts @@ -0,0 +1,55 @@ +export type WebdavStatus = "unknown" | "ready" | "failed"; + +interface WebdavStatusCache { + signature: string; + status: WebdavStatus; + updatedAt: number; +} + +const WEBDAV_STATUS_KEY = "webdav_status_cache"; + +export const buildWebdavSignature = ( + verge?: Pick< + IVergeConfig, + "webdav_url" | "webdav_username" | "webdav_password" + > | null, +) => { + const url = verge?.webdav_url?.trim() ?? ""; + const username = verge?.webdav_username?.trim() ?? ""; + const password = verge?.webdav_password ?? ""; + + if (!url && !username && !password) return ""; + + return JSON.stringify([url, username, password]); +}; + +const canUseStorage = () => typeof localStorage !== "undefined"; + +export const getWebdavStatus = (signature: string): WebdavStatus => { + if (!signature || !canUseStorage()) return "unknown"; + + const raw = localStorage.getItem(WEBDAV_STATUS_KEY); + if (!raw) return "unknown"; + + try { + const data = JSON.parse(raw) as Partial; + if (!data || data.signature !== signature) return "unknown"; + return data.status === "ready" || data.status === "failed" + ? data.status + : "unknown"; + } catch { + return "unknown"; + } +}; + +export const setWebdavStatus = (signature: string, status: WebdavStatus) => { + if (!signature || !canUseStorage()) return; + + const payload: WebdavStatusCache = { + signature, + status, + updatedAt: Date.now(), + }; + + localStorage.setItem(WEBDAV_STATUS_KEY, JSON.stringify(payload)); +};