From c19bdd48e25eb34d39ce3a33514ac26c23d8d4ef Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Sun, 5 Apr 2026 21:34:59 -0700 Subject: [PATCH] Simplify desktop remote open flow --- main.go | 10 ++- main_test.go | 23 +++++ ui_forms.go | 231 +++++++++++++++++++++++++++++++++++---------------- 3 files changed, 191 insertions(+), 73 deletions(-) diff --git a/main.go b/main.go index 4d0e392..cfe6962 100644 --- a/main.go +++ b/main.go @@ -1752,12 +1752,16 @@ func (u *ui) restoreStartupLifecycleTarget() { func (u *ui) hasSelectedLifecycleTarget() bool { switch strings.TrimSpace(u.lifecycleMode) { case "remote": - return strings.TrimSpace(u.remoteBaseURL.Text()) != "" && strings.TrimSpace(u.remotePath.Text()) != "" + return u.hasSelectedRemoteTarget() default: return strings.TrimSpace(u.vaultPath.Text()) != "" } } +func (u *ui) hasSelectedRemoteTarget() bool { + return strings.TrimSpace(u.remoteBaseURL.Text()) != "" && strings.TrimSpace(u.remotePath.Text()) != "" +} + func (u *ui) latestRecentVault() (string, time.Time) { for _, path := range u.recentVaults { if strings.TrimSpace(path) == "" { @@ -1776,6 +1780,10 @@ func (u *ui) showLocalVaultChooser() bool { return u.lifecycleMode != "local" || !u.hasSelectedVaultPath() } +func (u *ui) showRemoteConnectionChooser() bool { + return u.lifecycleMode != "remote" || !u.hasSelectedRemoteTarget() +} + func (u *ui) switchToLifecycleSelection(mode string) { u.state.Session = &session.Manager{} u.state.CurrentPath = nil diff --git a/main_test.go b/main_test.go index 46c00cb..18cccfb 100644 --- a/main_test.go +++ b/main_test.go @@ -4377,6 +4377,29 @@ func TestShowLocalVaultChooser(t *testing.T) { } } +func TestShowRemoteConnectionChooser(t *testing.T) { + t.Parallel() + + u := newUIWithSession("desktop", &session.Manager{}) + u.lifecycleMode = "remote" + u.remoteBaseURL.SetText("") + u.remotePath.SetText("") + if got := u.showRemoteConnectionChooser(); !got { + t.Fatal("showRemoteConnectionChooser() = false, want true when no remote connection is selected") + } + + u.remoteBaseURL.SetText("https://dav.crew.example.invalid") + u.remotePath.SetText("vaults/bellagio.kdbx") + if got := u.showRemoteConnectionChooser(); got { + t.Fatal("showRemoteConnectionChooser() = true, want false when a remote connection is selected") + } + + u.lifecycleMode = "local" + if got := u.showRemoteConnectionChooser(); !got { + t.Fatal("showRemoteConnectionChooser() = false, want true outside remote lifecycle mode") + } +} + func TestSwitchToLifecycleSelectionResetsLockedLocalSession(t *testing.T) { t.Parallel() diff --git a/ui_forms.go b/ui_forms.go index 5035f08..a31f9f1 100644 --- a/ui_forms.go +++ b/ui_forms.go @@ -21,6 +21,7 @@ import ( func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { busy := u.lifecycleBusy() showLocalChooser := u.showLocalVaultChooser() + showRemoteChooser := u.showRemoteConnectionChooser() selectedLocalPath := strings.TrimSpace(u.vaultPath.Text()) return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { @@ -61,104 +62,121 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { if u.lifecycleMode == "remote" { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !showRemoteChooser { + return layout.Dimensions{} + } lbl := material.Label(u.theme, unit.Sp(12), "LOCATION") lbl.Color = mutedColor return lbl.Layout(gtx) }), - layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), - layout.Rigid(labeledEditorHelp(u.theme, "Remote Base URL", "Base WebDAV endpoint, for example https://server/remote.php/webdav.", &u.remoteBaseURL, false)), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(labeledEditorHelp(u.theme, "Remote Path", "Path to the remote .kdbx file under the WebDAV base URL.", &u.remotePath, false)), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if strings.TrimSpace(u.remoteBaseURL.Text()) == "" || strings.TrimSpace(u.remotePath.Text()) == "" { + if !showRemoteChooser { return layout.Dimensions{} } - record := u.currentRemoteRecord() - lastGroup := u.recentRemoteGroup(record.BaseURL, record.Path) - return compactCard(gtx, 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(12), "SELECTED CONNECTION") - lbl.Color = mutedColor - 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(14), friendlyRecentRemoteLabel(record)) - 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(11), "Path: "+strings.TrimSpace(record.Path)) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(11), "Server: "+strings.TrimSpace(record.BaseURL)) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(11), "Auth: "+recentRemoteStoredAuthSummary(recentRemoteRecord{ - Username: strings.TrimSpace(u.remoteUsername.Text()), - Password: u.remotePassword.Text(), - })) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if len(lastGroup) == 0 { - return layout.Dimensions{} - } - lbl := material.Label(u.theme, unit.Sp(11), "Last group: "+strings.Join(u.displayEntryPath(lastGroup), " / ")) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if busy { - return layout.Dimensions{} - } - return tonedButton(gtx, u.theme, &u.clearRemoteSelection, "Change...") - }), - ) - }) - }) + return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx) }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if busy { + if !showRemoteChooser { return layout.Dimensions{} } - return u.recentRemoteList(gtx) + return labeledEditorHelp(u.theme, "Remote Base URL", "Base WebDAV endpoint, for example https://server/remote.php/webdav.", &u.remoteBaseURL, false)(gtx) }), - layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !showRemoteChooser { + return layout.Dimensions{} + } + return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !showRemoteChooser { + return layout.Dimensions{} + } + return labeledEditorHelp(u.theme, "Remote Path", "Path to the remote .kdbx file under the WebDAV base URL.", &u.remotePath, false)(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !showRemoteChooser { + return layout.Dimensions{} + } + return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if showRemoteChooser || !u.hasSelectedRemoteTarget() { + return layout.Dimensions{} + } + return layout.Dimensions{} + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if showRemoteChooser && !busy { + return u.recentRemoteList(gtx) + } + return layout.Dimensions{} + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !showRemoteChooser { + return layout.Dimensions{} + } + return layout.Spacer{Height: unit.Dp(10)}.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !showRemoteChooser { + return layout.Dimensions{} + } lbl := material.Label(u.theme, unit.Sp(12), "AUTHENTICATION") lbl.Color = mutedColor return lbl.Layout(gtx) }), - layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), - layout.Rigid(labeledEditorHelp(u.theme, "Remote Username", "Username used to authenticate to the WebDAV server.", &u.remoteUsername, false)), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !showRemoteChooser { + return layout.Dimensions{} + } + return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !showRemoteChooser { + return layout.Dimensions{} + } + return labeledEditorHelp(u.theme, "Remote Username", "Username used to authenticate to the WebDAV server.", &u.remoteUsername, false)(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !showRemoteChooser { + return layout.Dimensions{} + } + return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !showRemoteChooser { + return layout.Dimensions{} + } return labeledEditorHelp(u.theme, "Remote Password", "Password or app token used to authenticate to the WebDAV server.", &u.remotePassword, true)(gtx) }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !showRemoteChooser { + return layout.Dimensions{} + } + return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !showRemoteChooser { + return layout.Dimensions{} + } box := material.CheckBox(u.theme, &u.rememberRemoteAuth, "Remember sign-in on this device") box.Color = accentColor return box.Layout(gtx) }), layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !showRemoteChooser { + return layout.Dimensions{} + } return layout.Inset{Top: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.openRemotePrefsHelp, "Settings & Help") }) }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !showRemoteChooser { + return layout.Dimensions{} + } + return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx) + }), ) } return layout.Flex{Axis: layout.Vertical}.Layout(gtx, @@ -280,10 +298,26 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions { if u.lifecycleMode == "remote" { label := u.remoteOpenButtonLabel() - if busy { - return passiveTonedButton(gtx, u.theme, label) - } - return tonedButton(gtx, u.theme, &u.openRemote, label) + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if busy { + return passiveTonedButton(gtx, u.theme, label) + } + return tonedButton(gtx, u.theme, &u.openRemote, label) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if busy || !u.hasSelectedRemoteTarget() { + return layout.Dimensions{} + } + return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if busy || !u.hasSelectedRemoteTarget() { + return layout.Dimensions{} + } + return u.selectedRemoteConnectionCard(gtx) + }), + ) } return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { @@ -326,6 +360,59 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { ) } +func (u *ui) selectedRemoteConnectionCard(gtx layout.Context) layout.Dimensions { + record := u.currentRemoteRecord() + lastGroup := u.recentRemoteGroup(record.BaseURL, record.Path) + return compactCard(gtx, 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(12), "SELECTED CONNECTION") + lbl.Color = mutedColor + 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(14), friendlyRecentRemoteLabel(record)) + 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(11), "Path: "+strings.TrimSpace(record.Path)) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(11), "Server: "+strings.TrimSpace(record.BaseURL)) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(11), "Auth: "+recentRemoteStoredAuthSummary(recentRemoteRecord{ + Username: strings.TrimSpace(u.remoteUsername.Text()), + Password: u.remotePassword.Text(), + })) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if len(lastGroup) == 0 { + return layout.Dimensions{} + } + lbl := material.Label(u.theme, unit.Sp(11), "Last group: "+strings.Join(u.displayEntryPath(lastGroup), " / ")) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.clearRemoteSelection, "Open Different Connection") + }), + ) + }) + }) +} + func (u *ui) selectedLocalVaultCard(gtx layout.Context, path string) layout.Dimensions { lastGroup := u.recentVaultGroup(path) return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {