From 37297856c494a6bc3b815c810237a98abb6b29ec Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Mon, 6 Apr 2026 15:58:00 -0700 Subject: [PATCH] Add saved remote selectors to sync menu --- main.go | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++- main_test.go | 45 +++++++++++++++++ 2 files changed, 178 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 57e0ecd..6973a7a 100644 --- a/main.go +++ b/main.go @@ -275,6 +275,7 @@ type ui struct { toggleSyncMenu widget.Clickable toggleMainMenu widget.Clickable openAdvancedSync widget.Clickable + openSelectedVaultRemote widget.Clickable openSecuritySettings widget.Clickable openRemotePrefsHelp widget.Clickable closeAdvancedSync widget.Clickable @@ -378,6 +379,8 @@ type ui struct { groupClicks []widget.Clickable recentVaultClicks []widget.Clickable recentRemoteClicks []widget.Clickable + vaultRemoteProfileClicks []widget.Clickable + vaultRemoteCredentialClicks []widget.Clickable removeCustomFields []widget.Clickable state appstate.State visible []entry @@ -1936,6 +1939,41 @@ func (u *ui) availableRemoteCredentialEntries() []vault.Entry { return entries } +func (u *ui) selectVaultRemoteProfile(id string) { + id = strings.TrimSpace(id) + u.selectedVaultRemoteProfileID = id + for _, profile := range u.availableRemoteProfiles() { + if profile.ID != id { + continue + } + u.remoteBaseURL.SetText(profile.BaseURL) + u.remotePath.SetText(profile.Path) + return + } +} + +func (u *ui) selectVaultRemoteCredentialEntry(id string) { + u.selectedVaultRemoteCredentialEntryID = strings.TrimSpace(id) +} + +func (u *ui) selectedVaultRemoteProfile() (vault.RemoteProfile, bool) { + for _, profile := range u.availableRemoteProfiles() { + if profile.ID == strings.TrimSpace(u.selectedVaultRemoteProfileID) { + return profile, true + } + } + return vault.RemoteProfile{}, false +} + +func (u *ui) selectedVaultRemoteCredentialEntry() (vault.Entry, bool) { + for _, entry := range u.availableRemoteCredentialEntries() { + if entry.ID == strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID) { + return entry, true + } + } + return vault.Entry{}, false +} + func (u *ui) selectedVaultRemoteBinding() (appstate.RemoteBinding, bool) { if strings.TrimSpace(u.selectedVaultRemoteProfileID) == "" || strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID) == "" { return appstate.RemoteBinding{}, false @@ -3281,6 +3319,28 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { } } } + for i := range u.vaultRemoteProfileClicks { + for u.vaultRemoteProfileClicks[i].Clicked(gtx) { + profiles := u.availableRemoteProfiles() + if i < len(profiles) { + u.selectVaultRemoteProfile(profiles[i].ID) + } + } + } + for i := range u.vaultRemoteCredentialClicks { + for u.vaultRemoteCredentialClicks[i].Clicked(gtx) { + entries := u.availableRemoteCredentialEntries() + if i < len(entries) { + u.selectVaultRemoteCredentialEntry(entries[i].ID) + } + } + } + for u.openSelectedVaultRemote.Clicked(gtx) { + if u.lifecycleBusy() { + continue + } + u.startOpenRemoteAction() + } for u.clearVaultSelection.Clicked(gtx) { if u.lifecycleBusy() { continue @@ -4389,8 +4449,16 @@ func (u *ui) syncMenuToggle(gtx layout.Context) layout.Dimensions { } func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions { + profiles := u.availableRemoteProfiles() + credentials := u.availableRemoteCredentialEntries() + if len(u.vaultRemoteProfileClicks) < len(profiles) { + u.vaultRemoteProfileClicks = make([]widget.Clickable, len(profiles)) + } + if len(u.vaultRemoteCredentialClicks) < len(credentials) { + u.vaultRemoteCredentialClicks = make([]widget.Clickable, len(credentials)) + } return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + rows := []layout.FlexChild{ layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(11), "Need another source or direction?") lbl.Color = mutedColor @@ -4400,7 +4468,70 @@ func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.openAdvancedSync, "Open Advanced Sync") }), - ) + } + if u.hasOpenVault() && len(profiles) > 0 && len(credentials) > 0 { + rows = append(rows, + layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(11), "Use a saved remote profile from this vault") + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + ) + for i, profile := range profiles { + i := i + profile := profile + rows = append(rows, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + selected := strings.TrimSpace(u.selectedVaultRemoteProfileID) == profile.ID + return layout.Inset{Bottom: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return recentSelectionCard(gtx, selected, func(gtx layout.Context) layout.Dimensions { + return u.vaultRemoteProfileClicks[i].Layout(gtx, func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(13), profile.Name) + lbl.Color = accentColor + return lbl.Layout(gtx) + }) + }) + }) + }), + ) + } + rows = append(rows, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout)) + for i, entry := range credentials { + i := i + entry := entry + rows = append(rows, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + selected := strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID) == entry.ID + label := entry.Title + if strings.TrimSpace(entry.Username) != "" { + label += " ยท " + strings.TrimSpace(entry.Username) + } + return layout.Inset{Bottom: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return recentSelectionCard(gtx, selected, func(gtx layout.Context) layout.Dimensions { + return u.vaultRemoteCredentialClicks[i].Layout(gtx, func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(13), label) + lbl.Color = accentColor + return lbl.Layout(gtx) + }) + }) + }) + }), + ) + } + if _, ok := u.selectedVaultRemoteProfile(); ok { + if _, ok := u.selectedVaultRemoteCredentialEntry(); ok { + rows = append(rows, + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, "Open Saved Remote") + }), + ) + } + } + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, rows...) }) } diff --git a/main_test.go b/main_test.go index 32fde2c..f7a88db 100644 --- a/main_test.go +++ b/main_test.go @@ -4698,6 +4698,51 @@ func TestUIAvailableRemoteProfilesReturnsEmptyWhenLocked(t *testing.T) { } } +func TestUISelectVaultRemoteProfileUpdatesSelectionAndTargetFields(t *testing.T) { + t.Parallel() + + u := newUIWithModel("desktop", vault.Model{ + RemoteProfiles: []vault.RemoteProfile{{ + ID: "family-webdav", + Name: "Family Vault", + Backend: vault.RemoteBackendWebDAV, + BaseURL: "https://dav.example.invalid/remote.php/dav", + Path: "files/family/keepass.kdbx", + }}, + }) + + u.selectVaultRemoteProfile("family-webdav") + + if got := u.selectedVaultRemoteProfileID; got != "family-webdav" { + t.Fatalf("selectedVaultRemoteProfileID = %q, want family-webdav", got) + } + if got := u.remoteBaseURL.Text(); got != "https://dav.example.invalid/remote.php/dav" { + t.Fatalf("remoteBaseURL = %q, want resolved profile base URL", got) + } + if got := u.remotePath.Text(); got != "files/family/keepass.kdbx" { + t.Fatalf("remotePath = %q, want resolved profile path", got) + } +} + +func TestUISelectVaultRemoteCredentialEntryUpdatesSelection(t *testing.T) { + t.Parallel() + + u := newUIWithModel("desktop", vault.Model{ + Entries: []vault.Entry{{ + ID: "remote-creds-1", + Title: "WebDAV Sign-In", + Username: "tjulian", + Path: []string{"Crew", "Internet"}, + }}, + }) + + u.selectVaultRemoteCredentialEntry("remote-creds-1") + + if got := u.selectedVaultRemoteCredentialEntryID; got != "remote-creds-1" { + t.Fatalf("selectedVaultRemoteCredentialEntryID = %q, want remote-creds-1", got) + } +} + func TestSwitchToLifecycleSelectionResetsLockedLocalSession(t *testing.T) { t.Parallel()