feat(sync): WebDAV vault sync
All checks were successful
CI / Windows build (push) Successful in 12m32s

Two-way sync of the vault folder to a user-configured WebDAV server,
so annotations (which travel with the file) sync with the file.

- WebDavSyncService.syncNow: per-file decision — local-only uploads,
  remote-only downloads, and when BOTH sides changed since the last
  sync it keeps the loser as <file>.conflict-<mtime> on both sides
  (last-write-wins by mtime) so no data is ever lost. Creates dirs as
  needed; deletes are conservative.
- The decision logic is pure and unit-tested against a fake WebDAV
  client; the real client is a thin http adapter (no dio dependency).
- Settings: WebDAV URL / user / password / remote folder, Test
  connection, Sync now (with status + last-synced), and an auto-sync
  toggle (default OFF).

Real server round-trips are device/server-validated. Credentials are in
SharedPreferences for now (TODO secure-storage). analyze clean, 432 tests.
This commit is contained in:
2026-06-25 01:35:13 +08:00
parent e939759458
commit 3cabc7e074
12 changed files with 2012 additions and 5 deletions

View File

@@ -150,5 +150,42 @@
"vaultFolderLabel": "Vault folder",
"vaultNoneSelected": "No folder selected",
"vaultChangeFolder": "Change vault folder",
"vaultUpdated": "Vault folder updated"
"vaultUpdated": "Vault folder updated",
"syncSection": "Sync (WebDAV)",
"syncServerUrl": "Server URL",
"syncServerUrlHint": "https://dav.example.com/remote.php/dav/files/me",
"syncUsername": "Username",
"syncPassword": "Password",
"syncRemoteFolder": "Remote folder",
"syncRemoteFolderHint": "BadNote",
"syncSave": "Save",
"syncSaved": "Sync settings saved",
"syncTestConnection": "Test connection",
"syncTestOk": "Connection OK",
"syncTestFailed": "Connection failed: {error}",
"@syncTestFailed": {
"placeholders": { "error": { "type": "String" } }
},
"syncNow": "Sync now",
"syncRunning": "Syncing…",
"syncNeverRun": "Never synced",
"syncLastRun": "Last synced: {when}",
"@syncLastRun": {
"placeholders": { "when": { "type": "String" } }
},
"syncResultSummary": "{uploaded} uploaded · {downloaded} downloaded · {conflicts} conflicts",
"@syncResultSummary": {
"placeholders": {
"uploaded": { "type": "int" },
"downloaded": { "type": "int" },
"conflicts": { "type": "int" }
}
},
"syncFailed": "Sync failed: {error}",
"@syncFailed": {
"placeholders": { "error": { "type": "String" } }
},
"syncAuto": "Sync automatically on launch",
"syncCredentialsNote": "Credentials are stored locally in plain text. Use a dedicated app password.",
"syncNotConfigured": "Enter a server URL to enable sync."
}