feat(sync): WebDAV vault sync
All checks were successful
CI / Windows build (push) Successful in 12m32s
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:
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -376,4 +376,76 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get vaultUpdated => '笔记库文件夹已更新';
|
||||
|
||||
@override
|
||||
String get syncSection => '同步(WebDAV)';
|
||||
|
||||
@override
|
||||
String get syncServerUrl => '服务器地址';
|
||||
|
||||
@override
|
||||
String get syncServerUrlHint =>
|
||||
'https://dav.example.com/remote.php/dav/files/me';
|
||||
|
||||
@override
|
||||
String get syncUsername => '用户名';
|
||||
|
||||
@override
|
||||
String get syncPassword => '密码';
|
||||
|
||||
@override
|
||||
String get syncRemoteFolder => '远程文件夹';
|
||||
|
||||
@override
|
||||
String get syncRemoteFolderHint => 'BadNote';
|
||||
|
||||
@override
|
||||
String get syncSave => '保存';
|
||||
|
||||
@override
|
||||
String get syncSaved => '同步设置已保存';
|
||||
|
||||
@override
|
||||
String get syncTestConnection => '测试连接';
|
||||
|
||||
@override
|
||||
String get syncTestOk => '连接成功';
|
||||
|
||||
@override
|
||||
String syncTestFailed(String error) {
|
||||
return '连接失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get syncNow => '立即同步';
|
||||
|
||||
@override
|
||||
String get syncRunning => '同步中…';
|
||||
|
||||
@override
|
||||
String get syncNeverRun => '尚未同步';
|
||||
|
||||
@override
|
||||
String syncLastRun(String when) {
|
||||
return '上次同步:$when';
|
||||
}
|
||||
|
||||
@override
|
||||
String syncResultSummary(int uploaded, int downloaded, int conflicts) {
|
||||
return '上传 $uploaded · 下载 $downloaded · 冲突 $conflicts';
|
||||
}
|
||||
|
||||
@override
|
||||
String syncFailed(String error) {
|
||||
return '同步失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get syncAuto => '启动时自动同步';
|
||||
|
||||
@override
|
||||
String get syncCredentialsNote => '凭据以明文保存在本地,建议使用专用的应用密码。';
|
||||
|
||||
@override
|
||||
String get syncNotConfigured => '请输入服务器地址以启用同步。';
|
||||
}
|
||||
|
||||
@@ -120,5 +120,42 @@
|
||||
"vaultFolderLabel": "笔记库文件夹",
|
||||
"vaultNoneSelected": "尚未选择文件夹",
|
||||
"vaultChangeFolder": "更改笔记库文件夹",
|
||||
"vaultUpdated": "笔记库文件夹已更新"
|
||||
"vaultUpdated": "笔记库文件夹已更新",
|
||||
"syncSection": "同步(WebDAV)",
|
||||
"syncServerUrl": "服务器地址",
|
||||
"syncServerUrlHint": "https://dav.example.com/remote.php/dav/files/me",
|
||||
"syncUsername": "用户名",
|
||||
"syncPassword": "密码",
|
||||
"syncRemoteFolder": "远程文件夹",
|
||||
"syncRemoteFolderHint": "BadNote",
|
||||
"syncSave": "保存",
|
||||
"syncSaved": "同步设置已保存",
|
||||
"syncTestConnection": "测试连接",
|
||||
"syncTestOk": "连接成功",
|
||||
"syncTestFailed": "连接失败:{error}",
|
||||
"@syncTestFailed": {
|
||||
"placeholders": { "error": { "type": "String" } }
|
||||
},
|
||||
"syncNow": "立即同步",
|
||||
"syncRunning": "同步中…",
|
||||
"syncNeverRun": "尚未同步",
|
||||
"syncLastRun": "上次同步:{when}",
|
||||
"@syncLastRun": {
|
||||
"placeholders": { "when": { "type": "String" } }
|
||||
},
|
||||
"syncResultSummary": "上传 {uploaded} · 下载 {downloaded} · 冲突 {conflicts}",
|
||||
"@syncResultSummary": {
|
||||
"placeholders": {
|
||||
"uploaded": { "type": "int" },
|
||||
"downloaded": { "type": "int" },
|
||||
"conflicts": { "type": "int" }
|
||||
}
|
||||
},
|
||||
"syncFailed": "同步失败:{error}",
|
||||
"@syncFailed": {
|
||||
"placeholders": { "error": { "type": "String" } }
|
||||
},
|
||||
"syncAuto": "启动时自动同步",
|
||||
"syncCredentialsNote": "凭据以明文保存在本地,建议使用专用的应用密码。",
|
||||
"syncNotConfigured": "请输入服务器地址以启用同步。"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user