From 1fe3da811dd9a44b5ddead7a54a764dd8ae5eaae Mon Sep 17 00:00:00 2001 From: XianYuDaXian Date: Mon, 27 Apr 2026 12:35:42 +0800 Subject: [PATCH 01/16] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20WebDAV=20?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E4=B8=8E=E5=9B=BE=E7=89=87=E5=A4=8D=E5=88=B6?= =?UTF-8?q?=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 6 +- src/components/ImageContextMenu.tsx | 3 + src/components/SettingsModal.tsx | 160 ++++++++ src/lib/clipboard.ts | 92 ++++- src/lib/db.ts | 3 +- src/lib/snapshot.ts | 557 ++++++++++++++++++++++++++++ src/lib/webdavSync.ts | 262 +++++++++++++ src/store.ts | 140 +++---- src/types.ts | 25 ++ vite.config.ts | 4 + 10 files changed, 1153 insertions(+), 99 deletions(-) create mode 100644 src/lib/snapshot.ts create mode 100644 src/lib/webdavSync.ts diff --git a/src/App.tsx b/src/App.tsx index 55cef958..a4e60d1c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,6 +12,7 @@ import SettingsModal from './components/SettingsModal' import ConfirmDialog from './components/ConfirmDialog' import Toast from './components/Toast' import ImageContextMenu from './components/ImageContextMenu' +import { syncWebDavOnLaunch } from './lib/webdavSync' export default function App() { const setSettings = useStore((s) => s.setSettings) @@ -41,7 +42,10 @@ export default function App() { window.history.replaceState(null, '', nextUrl) } - initStore() + void (async () => { + await initStore() + await syncWebDavOnLaunch() + })() }, [setSettings]) useEffect(() => { diff --git a/src/components/ImageContextMenu.tsx b/src/components/ImageContextMenu.tsx index 0c7694f9..ab73e034 100644 --- a/src/components/ImageContextMenu.tsx +++ b/src/components/ImageContextMenu.tsx @@ -17,6 +17,9 @@ export default function ImageContextMenu() { // 忽略没有 src 或空的 img if (!imgTarget.src) return + // 当前页面不是安全上下文时,放行浏览器原生右键菜单,避免图片复制失效。 + if (!window.isSecureContext) return + e.preventDefault() setMenuInfo({ src: imgTarget.src, diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index fef21410..b223b7f3 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -3,6 +3,7 @@ import { normalizeBaseUrl } from '../lib/api' import { useStore, exportData, importData, clearAllData } from '../store' import { DEFAULT_IMAGES_MODEL, DEFAULT_RESPONSES_MODEL, DEFAULT_SETTINGS, type AppSettings } from '../types' import { useCloseOnEscape } from '../hooks/useCloseOnEscape' +import { syncWithWebDav, testWebDavDirectory } from '../lib/webdavSync' import Select from './Select' export default function SettingsModal() { @@ -28,14 +29,20 @@ export default function SettingsModal() { const commitSettings = (nextDraft: AppSettings) => { const apiMode = nextDraft.apiMode === 'responses' ? 'responses' : DEFAULT_SETTINGS.apiMode + const storageMode = nextDraft.storageMode === 'webdav' ? 'webdav' : DEFAULT_SETTINGS.storageMode const defaultModel = getDefaultModelForMode(apiMode) const normalizedDraft = { ...nextDraft, apiMode, + storageMode, baseUrl: normalizeBaseUrl(nextDraft.baseUrl.trim() || DEFAULT_SETTINGS.baseUrl), apiKey: nextDraft.apiKey, model: nextDraft.model.trim() || defaultModel, timeout: Number(nextDraft.timeout) || DEFAULT_SETTINGS.timeout, + webdav: { + ...DEFAULT_SETTINGS.webdav, + ...nextDraft.webdav, + }, } setDraft(normalizedDraft) setSettings(normalizedDraft) @@ -71,6 +78,28 @@ export default function SettingsModal() { e.target.value = '' } + const handleSyncNow = async () => { + try { + await syncWithWebDav() + } catch (err) { + useStore.getState().showToast( + `WebDAV 同步失败:${err instanceof Error ? err.message : String(err)}`, + 'error', + ) + } + } + + const handleTestDirectory = async () => { + try { + await testWebDavDirectory() + } catch (err) { + useStore.getState().showToast( + `WebDAV 目录测试失败:${err instanceof Error ? err.message : String(err)}`, + 'error', + ) + } + } + return (
+ + +
+ + +
+ + + +
+ + +
+
+ )} +