Simplify desktop remote open flow

This commit is contained in:
Joe Julian
2026-04-05 21:34:59 -07:00
parent e5f42924f8
commit 225802252d
3 changed files with 191 additions and 73 deletions
+9 -1
View File
@@ -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
+23
View File
@@ -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()
+159 -72
View File
@@ -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 {