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

@@ -787,6 +787,132 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Vault folder updated'**
String get vaultUpdated;
/// No description provided for @syncSection.
///
/// In en, this message translates to:
/// **'Sync (WebDAV)'**
String get syncSection;
/// No description provided for @syncServerUrl.
///
/// In en, this message translates to:
/// **'Server URL'**
String get syncServerUrl;
/// No description provided for @syncServerUrlHint.
///
/// In en, this message translates to:
/// **'https://dav.example.com/remote.php/dav/files/me'**
String get syncServerUrlHint;
/// No description provided for @syncUsername.
///
/// In en, this message translates to:
/// **'Username'**
String get syncUsername;
/// No description provided for @syncPassword.
///
/// In en, this message translates to:
/// **'Password'**
String get syncPassword;
/// No description provided for @syncRemoteFolder.
///
/// In en, this message translates to:
/// **'Remote folder'**
String get syncRemoteFolder;
/// No description provided for @syncRemoteFolderHint.
///
/// In en, this message translates to:
/// **'BadNote'**
String get syncRemoteFolderHint;
/// No description provided for @syncSave.
///
/// In en, this message translates to:
/// **'Save'**
String get syncSave;
/// No description provided for @syncSaved.
///
/// In en, this message translates to:
/// **'Sync settings saved'**
String get syncSaved;
/// No description provided for @syncTestConnection.
///
/// In en, this message translates to:
/// **'Test connection'**
String get syncTestConnection;
/// No description provided for @syncTestOk.
///
/// In en, this message translates to:
/// **'Connection OK'**
String get syncTestOk;
/// No description provided for @syncTestFailed.
///
/// In en, this message translates to:
/// **'Connection failed: {error}'**
String syncTestFailed(String error);
/// No description provided for @syncNow.
///
/// In en, this message translates to:
/// **'Sync now'**
String get syncNow;
/// No description provided for @syncRunning.
///
/// In en, this message translates to:
/// **'Syncing…'**
String get syncRunning;
/// No description provided for @syncNeverRun.
///
/// In en, this message translates to:
/// **'Never synced'**
String get syncNeverRun;
/// No description provided for @syncLastRun.
///
/// In en, this message translates to:
/// **'Last synced: {when}'**
String syncLastRun(String when);
/// No description provided for @syncResultSummary.
///
/// In en, this message translates to:
/// **'{uploaded} uploaded · {downloaded} downloaded · {conflicts} conflicts'**
String syncResultSummary(int uploaded, int downloaded, int conflicts);
/// No description provided for @syncFailed.
///
/// In en, this message translates to:
/// **'Sync failed: {error}'**
String syncFailed(String error);
/// No description provided for @syncAuto.
///
/// In en, this message translates to:
/// **'Sync automatically on launch'**
String get syncAuto;
/// No description provided for @syncCredentialsNote.
///
/// In en, this message translates to:
/// **'Credentials are stored locally in plain text. Use a dedicated app password.'**
String get syncCredentialsNote;
/// No description provided for @syncNotConfigured.
///
/// In en, this message translates to:
/// **'Enter a server URL to enable sync.'**
String get syncNotConfigured;
}
class _AppLocalizationsDelegate