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

@@ -379,4 +379,77 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get vaultUpdated => 'Vault folder updated';
@override
String get syncSection => 'Sync (WebDAV)';
@override
String get syncServerUrl => 'Server URL';
@override
String get syncServerUrlHint =>
'https://dav.example.com/remote.php/dav/files/me';
@override
String get syncUsername => 'Username';
@override
String get syncPassword => 'Password';
@override
String get syncRemoteFolder => 'Remote folder';
@override
String get syncRemoteFolderHint => 'BadNote';
@override
String get syncSave => 'Save';
@override
String get syncSaved => 'Sync settings saved';
@override
String get syncTestConnection => 'Test connection';
@override
String get syncTestOk => 'Connection OK';
@override
String syncTestFailed(String error) {
return 'Connection failed: $error';
}
@override
String get syncNow => 'Sync now';
@override
String get syncRunning => 'Syncing…';
@override
String get syncNeverRun => 'Never synced';
@override
String syncLastRun(String when) {
return 'Last synced: $when';
}
@override
String syncResultSummary(int uploaded, int downloaded, int conflicts) {
return '$uploaded uploaded · $downloaded downloaded · $conflicts conflicts';
}
@override
String syncFailed(String error) {
return 'Sync failed: $error';
}
@override
String get syncAuto => 'Sync automatically on launch';
@override
String get syncCredentialsNote =>
'Credentials are stored locally in plain text. Use a dedicated app password.';
@override
String get syncNotConfigured => 'Enter a server URL to enable sync.';
}