diff --git a/main.go b/main.go index 04fd9ef..3822e27 100644 --- a/main.go +++ b/main.go @@ -380,6 +380,7 @@ type ui struct { cancelLifecycleProgress widget.Clickable retryLifecycleOpen widget.Clickable approvalPermanent widget.Bool + syncSetupAutomatic widget.Bool apiPolicyAllow widget.Bool apiPolicyGroupScopeW widget.Bool apiTokenDisabled widget.Bool @@ -1387,11 +1388,15 @@ func (u *ui) openRemoteSyncSetupDialog() { u.syncDialogPurpose = syncDialogPurposeRemoteSetup u.syncSourceMode = syncSourceRemote u.syncDirection = syncDirectionPush + u.syncSetupAutomatic.Value = true if strings.TrimSpace(u.syncLocalPath.Text()) == "" { u.syncLocalPath.SetText(strings.TrimSpace(u.vaultPath.Text())) } u.syncSavedRemoteBindingSelection() u.prefillAdvancedSyncRemoteFromSavedBinding() + if _, ok := u.selectedVaultRemoteBinding(); ok && u.selectedVaultRemoteSyncMode == appstate.SyncModeManual { + u.syncSetupAutomatic.Value = false + } } func (u *ui) clearSyncLocalImport() { @@ -1576,7 +1581,7 @@ func (u *ui) persistSyncDialogRemoteBinding(baseURL, remotePath string) error { Username: strings.TrimSpace(u.syncRemoteUsername.Text()), Password: u.syncRemotePassword.Text(), CredentialPath: append([]string(nil), u.currentPath...), - SyncMode: u.newRemoteBindingSyncMode(), + SyncMode: u.syncSetupMode(), } binding, err := u.state.ConfigureRemoteBinding(input) if err != nil { @@ -2380,6 +2385,13 @@ func (u *ui) shouldShowSyncSourceChoices() bool { return u.syncDialogPurpose != syncDialogPurposeRemoteSetup } +func (u *ui) syncSetupMode() appstate.SyncMode { + if u.syncSetupAutomatic.Value { + return appstate.SyncModeAutomaticOnOpenSave + } + return appstate.SyncModeManual +} + func (u *ui) selectVaultRemoteProfile(id string) { id = strings.TrimSpace(id) u.selectedVaultRemoteProfileID = id @@ -5079,6 +5091,16 @@ func (u *ui) syncDialogContent(gtx layout.Context) layout.Dimensions { return u.syncPasswordField(gtx) }), } + if u.syncDialogPurpose == syncDialogPurposeRemoteSetup { + children = append(children, + layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + check := material.CheckBox(u.theme, &u.syncSetupAutomatic, "Sync automatically on open and save") + check.Color = accentColor + return check.Layout(gtx) + }), + ) + } if len(matchingCredentials) > 0 { children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), diff --git a/main_test.go b/main_test.go index e9fda9d..5705c22 100644 --- a/main_test.go +++ b/main_test.go @@ -5942,6 +5942,7 @@ func TestUISyncDialogUsesRemoteSetupCopy(t *testing.T) { u := newUIWithSession("desktop", &session.Manager{}) u.syncDialogPurpose = syncDialogPurposeRemoteSetup + u.syncSetupAutomatic.Value = true if got := u.syncDialogTitle(); got != "Set Up Remote Sync" { t.Fatalf("syncDialogTitle() = %q, want Set Up Remote Sync", got) @@ -5961,6 +5962,9 @@ func TestUISyncDialogUsesRemoteSetupCopy(t *testing.T) { if got := syncDialogSummaryText(syncDialogPurposeRemoteSetup, syncSourceRemote, syncDirectionPush); got != "Push this local vault to a WebDAV target and save that target for future sync." { t.Fatalf("syncDialogSummaryText(remote setup) = %q, want setup-specific summary", got) } + if got := u.syncSetupMode(); got != appstate.SyncModeAutomaticOnOpenSave { + t.Fatalf("syncSetupMode() = %q, want automatic_on_open_save", got) + } } func TestUISyncDialogUsesAdvancedCopy(t *testing.T) { @@ -6056,6 +6060,9 @@ func TestUIRemoteSyncSetupPersistsBindingAfterSuccessfulPush(t *testing.T) { if got := u.recentRemotes[0].LocalVaultPath; got != currentPath { t.Fatalf("recentRemotes[0].LocalVaultPath = %q, want %q", got, currentPath) } + if got := u.selectedVaultRemoteSyncMode; got != appstate.SyncModeAutomaticOnOpenSave { + t.Fatalf("selectedVaultRemoteSyncMode = %q, want automatic_on_open_save", got) + } if got := u.state.StatusMessage; got != "Remote sync is set up for this vault." { t.Fatalf("StatusMessage = %q, want setup success message", got) } @@ -6090,6 +6097,60 @@ func TestUIRemoteSyncSetupPersistsBindingAfterSuccessfulPush(t *testing.T) { } } +func TestUIRemoteSyncSetupCanPersistManualSyncMode(t *testing.T) { + t.Parallel() + + key := vault.MasterKey{Password: "correct horse battery staple"} + currentPath := filepath.Join(t.TempDir(), "current.kdbx") + writeKDBXMainTestFile(t, currentPath, vault.Model{ + Entries: []vault.Entry{{ID: "entry-current", Title: "Vault Console", Path: []string{"Root", "Internet"}}}, + }, key) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusNotFound) + case http.MethodPut: + w.Header().Set("ETag", "\"v1\"") + w.WriteHeader(http.StatusNoContent) + default: + t.Fatalf("unexpected method %s", r.Method) + } + })) + defer server.Close() + + dir := t.TempDir() + u := newUIWithSession("desktop", &session.Manager{}, statePaths{ + DefaultSaveAsPath: filepath.Join(dir, "vault.kdbx"), + RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"), + RecentRemotesPath: filepath.Join(dir, "recent-remotes.json"), + UIPreferencesPath: filepath.Join(dir, "ui-prefs.json"), + }) + u.masterPassword.SetText(key.Password) + u.vaultPath.SetText(currentPath) + if err := u.openVaultAction(); err != nil { + t.Fatalf("openVaultAction() error = %v", err) + } + + u.openRemoteSyncSetupDialog() + u.syncSetupAutomatic.Value = false + u.syncRemoteBaseURL.SetText(server.URL) + u.syncRemotePath.SetText("vaults/manual.kdbx") + u.syncRemoteUsername.SetText("tjulian") + u.syncRemotePassword.SetText("token-1") + + if err := u.advancedSyncAction(); err != nil { + t.Fatalf("advancedSyncAction() error = %v", err) + } + + if got := u.selectedVaultRemoteSyncMode; got != appstate.SyncModeManual { + t.Fatalf("selectedVaultRemoteSyncMode = %q, want manual", got) + } + if got := u.recentRemotes[0].SyncMode; got != string(appstate.SyncModeManual) { + t.Fatalf("recentRemotes[0].SyncMode = %q, want manual", got) + } +} + func TestUISaveCurrentRemoteBindingActionPersistsBindingIntoVault(t *testing.T) { t.Parallel()