diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 9547eb34..e3dd7f59 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -57,7 +57,7 @@ url = "2.5"
auto-launch = "0.5"
once_cell = "1.21.3"
base64 = "0.22"
-rusqlite = { version = "0.31", features = ["bundled", "backup"] }
+rusqlite = { version = "0.31", features = ["bundled", "backup", "hooks"] }
indexmap = { version = "2", features = ["serde"] }
rust_decimal = "1.33"
uuid = { version = "1.11", features = ["v4"] }
diff --git a/src-tauri/src/commands/webdav_sync.rs b/src-tauri/src/commands/webdav_sync.rs
index e1bbd70d..31879d13 100644
--- a/src-tauri/src/commands/webdav_sync.rs
+++ b/src-tauri/src/commands/webdav_sync.rs
@@ -1,8 +1,6 @@
#![allow(non_snake_case)]
-use serde_json::{Value, json};
-use std::future::Future;
-use std::sync::OnceLock;
+use serde_json::{json, Value};
use tauri::State;
use crate::commands::sync_support::{
@@ -13,8 +11,9 @@ use crate::services::webdav_sync as webdav_sync_service;
use crate::settings::{self, WebDavSyncSettings};
use crate::store::AppState;
-fn persist_sync_error(settings: &mut WebDavSyncSettings, error: &AppError) {
+fn persist_sync_error(settings: &mut WebDavSyncSettings, error: &AppError, source: &str) {
settings.status.last_error = Some(error.to_string());
+ settings.status.last_error_source = Some(source.to_string());
let _ = settings::update_webdav_sync_status(settings.status.clone());
}
@@ -57,20 +56,16 @@ fn resolve_password_for_request(
incoming
}
+#[cfg(test)]
fn webdav_sync_mutex() -> &'static tokio::sync::Mutex<()> {
- static LOCK: OnceLock> = OnceLock::new();
- LOCK.get_or_init(|| tokio::sync::Mutex::new(()))
+ webdav_sync_service::sync_mutex()
}
async fn run_with_webdav_lock(operation: Fut) -> Result
where
- Fut: Future
)}
+ {showAutoSyncError && (
+
+
+ {t("settings.webdavSync.autoSyncLastErrorTitle")}
+
+
{lastError}
+
+ {t("settings.webdavSync.autoSyncLastErrorHint")}
+
+
+ )}
{/* Config buttons + save status */}
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 69acd81d..38dd8e96 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -283,6 +283,8 @@
"passwordPlaceholder": "App password",
"remoteRoot": "Remote Root Directory",
"profile": "Sync Profile Name",
+ "autoSync": "Auto Sync",
+ "autoSyncHint": "When enabled, each database change triggers an automatic WebDAV upload.",
"test": "Test Connection",
"testing": "Testing...",
"testSuccess": "Connection successful",
@@ -294,11 +296,14 @@
"uploading": "Uploading...",
"uploadSuccess": "Uploaded to WebDAV",
"uploadFailed": "Upload failed: {{error}}",
+ "autoSyncFailedToast": "Auto sync failed: {{error}}",
"download": "Download from Cloud",
"downloading": "Downloading...",
"downloadSuccess": "Downloaded and restored from WebDAV",
"downloadFailed": "Download failed: {{error}}",
"lastSync": "Last sync: {{time}}",
+ "autoSyncLastErrorTitle": "Last auto sync failed",
+ "autoSyncLastErrorHint": "Please check network or WebDAV settings. Auto sync will retry on future changes.",
"missingUrl": "Please enter the WebDAV server URL",
"presets": {
"label": "Provider",
diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json
index 4b37c501..1e185b6e 100644
--- a/src/i18n/locales/ja.json
+++ b/src/i18n/locales/ja.json
@@ -283,6 +283,8 @@
"passwordPlaceholder": "アプリパスワード",
"remoteRoot": "リモートルートディレクトリ",
"profile": "同期プロファイル名",
+ "autoSync": "自動同期",
+ "autoSyncHint": "有効にすると、データベース変更のたびに WebDAV へ自動アップロードします。",
"test": "接続テスト",
"testing": "テスト中...",
"testSuccess": "接続成功",
@@ -294,11 +296,14 @@
"uploading": "アップロード中...",
"uploadSuccess": "WebDAV にアップロードしました",
"uploadFailed": "アップロードに失敗しました:{{error}}",
+ "autoSyncFailedToast": "自動同期に失敗しました:{{error}}",
"download": "クラウドからダウンロード",
"downloading": "ダウンロード中...",
"downloadSuccess": "WebDAV からダウンロード・復元しました",
"downloadFailed": "ダウンロードに失敗しました:{{error}}",
"lastSync": "前回の同期:{{time}}",
+ "autoSyncLastErrorTitle": "前回の自動同期に失敗しました",
+ "autoSyncLastErrorHint": "ネットワークまたは WebDAV 設定を確認してください。次回の変更時に自動再試行されます。",
"missingUrl": "WebDAV サーバー URL を入力してください",
"presets": {
"label": "サービス",
diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json
index f18795ab..842856f4 100644
--- a/src/i18n/locales/zh.json
+++ b/src/i18n/locales/zh.json
@@ -283,6 +283,8 @@
"passwordPlaceholder": "应用密码(坚果云请使用「第三方应用密码」)",
"remoteRoot": "远程根目录",
"profile": "同步配置名",
+ "autoSync": "自动同步",
+ "autoSyncHint": "开启后每次数据库变更都会自动上传到 WebDAV。",
"test": "测试连接",
"testing": "测试中...",
"testSuccess": "连接成功",
@@ -294,11 +296,14 @@
"uploading": "上传中...",
"uploadSuccess": "已上传到 WebDAV",
"uploadFailed": "上传失败:{{error}}",
+ "autoSyncFailedToast": "自动同步失败:{{error}}",
"download": "从云端下载",
"downloading": "下载中...",
"downloadSuccess": "已从 WebDAV 下载并恢复",
"downloadFailed": "下载失败:{{error}}",
"lastSync": "上次同步:{{time}}",
+ "autoSyncLastErrorTitle": "上次自动同步失败",
+ "autoSyncLastErrorHint": "请检查网络或 WebDAV 配置,系统会在后续变更时继续自动重试。",
"missingUrl": "请填写 WebDAV 服务器地址",
"presets": {
"label": "服务商",
diff --git a/src/lib/schemas/settings.ts b/src/lib/schemas/settings.ts
index d226ef34..87436c0d 100644
--- a/src/lib/schemas/settings.ts
+++ b/src/lib/schemas/settings.ts
@@ -33,6 +33,7 @@ export const settingsSchema = z.object({
webdavSync: z
.object({
enabled: z.boolean().optional(),
+ autoSync: z.boolean().optional(),
baseUrl: z.string().trim().optional().or(z.literal("")),
username: z.string().trim().optional().or(z.literal("")),
password: z.string().optional(),
@@ -42,6 +43,7 @@ export const settingsSchema = z.object({
.object({
lastSyncAt: z.number().nullable().optional(),
lastError: z.string().nullable().optional(),
+ lastErrorSource: z.string().nullable().optional(),
lastRemoteEtag: z.string().nullable().optional(),
lastLocalManifestHash: z.string().nullable().optional(),
lastRemoteManifestHash: z.string().nullable().optional(),
diff --git a/src/types.ts b/src/types.ts
index d42b86ee..ecbd17a7 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -167,6 +167,7 @@ export interface VisibleApps {
export interface WebDavSyncStatus {
lastSyncAt?: number | null;
lastError?: string | null;
+ lastErrorSource?: string | null;
lastRemoteEtag?: string | null;
lastLocalManifestHash?: string | null;
lastRemoteManifestHash?: string | null;
@@ -175,6 +176,7 @@ export interface WebDavSyncStatus {
// WebDAV v2 同步配置
export interface WebDavSyncSettings {
enabled?: boolean;
+ autoSync?: boolean;
baseUrl?: string;
username?: string;
password?: string;
diff --git a/tests/components/WebdavSyncSection.test.tsx b/tests/components/WebdavSyncSection.test.tsx
index 27a487a7..ddffd51e 100644
--- a/tests/components/WebdavSyncSection.test.tsx
+++ b/tests/components/WebdavSyncSection.test.tsx
@@ -34,6 +34,17 @@ vi.mock("@/components/ui/input", () => ({
Input: (props: any) => ,
}));
+vi.mock("@/components/ui/switch", () => ({
+ Switch: ({ checked, onCheckedChange, ...props }: any) => (
+