diff --git a/main.go b/main.go index de1647a..a630f6e 100644 --- a/main.go +++ b/main.go @@ -2195,6 +2195,31 @@ func (u *ui) selectedVaultRemoteBinding() (appstate.RemoteBinding, bool) { }, true } +func (u *ui) shouldShowSavedRemoteBindingSelectors() bool { + profiles := u.availableRemoteProfiles() + entries := u.availableRemoteCredentialEntries() + if len(profiles) == 0 || len(entries) == 0 { + return false + } + return len(profiles) > 1 || len(entries) > 1 +} + +func (u *ui) savedRemoteBindingSummary() (profileLabel, credentialLabel string, ok bool) { + profile, ok := u.selectedVaultRemoteProfile() + if !ok { + return "", "", false + } + entry, ok := u.selectedVaultRemoteCredentialEntry() + if !ok { + return "", "", false + } + credentialLabel = entry.Title + if strings.TrimSpace(entry.Username) != "" { + credentialLabel += " · " + strings.TrimSpace(entry.Username) + } + return profile.Name, credentialLabel, true +} + func remoteBindingSuffix(baseURL, path, username string) string { sum := sha256.Sum256([]byte(strings.TrimSpace(baseURL) + "\n" + strings.TrimSpace(path) + "\n" + strings.TrimSpace(username))) return hex.EncodeToString(sum[:8]) @@ -4858,46 +4883,74 @@ func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions { }), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), ) - for i, profile := range profiles { - i := i - profile := profile + if !u.shouldShowSavedRemoteBindingSelectors() { 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) + profileLabel, credentialLabel, ok := u.savedRemoteBindingSummary() + if !ok { + return layout.Dimensions{} } - 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) - }) + return layout.Background{}.Layout(gtx, fill(color.NRGBA{R: 242, G: 245, B: 240, A: 255}), func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(13), profileLabel) + lbl.Color = accentColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(12), "Credential: "+credentialLabel) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + ) }) }) }), ) + } else { + 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 { diff --git a/main_test.go b/main_test.go index 9b41952..1795626 100644 --- a/main_test.go +++ b/main_test.go @@ -5169,6 +5169,80 @@ func TestUISelectVaultRemoteCredentialEntryUpdatesSelection(t *testing.T) { } } +func TestUIShouldShowSavedRemoteBindingSelectorsWhenMultipleChoices(t *testing.T) { + t.Parallel() + + u := newUIWithModel("desktop", vault.Model{ + Entries: []vault.Entry{ + {ID: "remote-creds-1", Title: "Alpha Sign-In", Username: "auser", Path: []string{"Crew", "Internet"}}, + {ID: "remote-creds-2", Title: "Bravo Sign-In", Username: "buser", Path: []string{"Crew", "Internet"}}, + }, + RemoteProfiles: []vault.RemoteProfile{ + {ID: "profile-1", Name: "Bellagio Vault", Backend: vault.RemoteBackendWebDAV, BaseURL: "https://dav1.example.invalid", Path: "files/bellagio.kdbx"}, + {ID: "profile-2", Name: "Vault Console", Backend: vault.RemoteBackendWebDAV, BaseURL: "https://dav2.example.invalid", Path: "files/console.kdbx"}, + }, + }) + + if !u.shouldShowSavedRemoteBindingSelectors() { + t.Fatal("shouldShowSavedRemoteBindingSelectors() = false, want true with multiple profiles and credentials") + } +} + +func TestUIShouldHideSavedRemoteBindingSelectorsForSingleChoice(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"}, + }}, + 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", + }}, + }) + + if u.shouldShowSavedRemoteBindingSelectors() { + t.Fatal("shouldShowSavedRemoteBindingSelectors() = true, want false with a single saved binding choice") + } +} + +func TestUISavedRemoteBindingSummaryUsesImplicitSingleChoice(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"}, + }}, + 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", + }}, + }) + + profileLabel, credentialLabel, ok := u.savedRemoteBindingSummary() + if !ok { + t.Fatal("savedRemoteBindingSummary() ok = false, want true") + } + if profileLabel != "Family Vault" { + t.Fatalf("profileLabel = %q, want Family Vault", profileLabel) + } + if credentialLabel != "WebDAV Sign-In · tjulian" { + t.Fatalf("credentialLabel = %q, want WebDAV Sign-In · tjulian", credentialLabel) + } +} + func TestUISaveCurrentRemoteBindingActionPersistsBindingIntoVault(t *testing.T) { t.Parallel()