Unify open flow around local vaults

This commit is contained in:
Joe Julian
2026-04-06 20:24:41 -07:00
parent 99ff89de32
commit 57d6a304db
3 changed files with 240 additions and 199 deletions
+84 -8
View File
@@ -217,6 +217,13 @@ const (
syncDirectionPush syncDirection = "push" syncDirectionPush syncDirection = "push"
) )
type syncDialogPurpose string
const (
syncDialogPurposeAdvanced syncDialogPurpose = "advanced"
syncDialogPurposeRemoteSetup syncDialogPurpose = "remote-setup"
)
type ui struct { type ui struct {
mode string mode string
theme *material.Theme theme *material.Theme
@@ -286,6 +293,7 @@ type ui struct {
toggleSyncMenu widget.Clickable toggleSyncMenu widget.Clickable
toggleMainMenu widget.Clickable toggleMainMenu widget.Clickable
openAdvancedSync widget.Clickable openAdvancedSync widget.Clickable
useSavedAdvancedSyncRemote widget.Clickable
openSelectedVaultRemote widget.Clickable openSelectedVaultRemote widget.Clickable
saveCurrentRemoteBinding widget.Clickable saveCurrentRemoteBinding widget.Clickable
openSecuritySettings widget.Clickable openSecuritySettings widget.Clickable
@@ -446,6 +454,7 @@ type ui struct {
syncRemoteUsername widget.Editor syncRemoteUsername widget.Editor
syncRemotePassword widget.Editor syncRemotePassword widget.Editor
selectedSyncRemoteCredentialEntryID string selectedSyncRemoteCredentialEntryID string
syncDialogPurpose syncDialogPurpose
syncDialogOpen bool syncDialogOpen bool
syncMenuOpen bool syncMenuOpen bool
mainMenuOpen bool mainMenuOpen bool
@@ -1361,11 +1370,28 @@ func (u *ui) openAdvancedSyncDialog() {
u.syncDialogOpen = true u.syncDialogOpen = true
u.syncMenuOpen = false u.syncMenuOpen = false
u.showSyncPassword = false u.showSyncPassword = false
u.syncDialogPurpose = syncDialogPurposeAdvanced
u.syncSourceMode = u.syncDefaultSourceMode u.syncSourceMode = u.syncDefaultSourceMode
u.syncDirection = u.syncDefaultDirection u.syncDirection = u.syncDefaultDirection
if strings.TrimSpace(u.syncLocalPath.Text()) == "" { if strings.TrimSpace(u.syncLocalPath.Text()) == "" {
u.syncLocalPath.SetText(strings.TrimSpace(u.vaultPath.Text())) u.syncLocalPath.SetText(strings.TrimSpace(u.vaultPath.Text()))
} }
u.syncSavedRemoteBindingSelection()
u.prefillAdvancedSyncRemoteFromSavedBinding()
}
func (u *ui) openRemoteSyncSetupDialog() {
u.syncDialogOpen = true
u.syncMenuOpen = false
u.showSyncPassword = false
u.syncDialogPurpose = syncDialogPurposeRemoteSetup
u.syncSourceMode = syncSourceRemote
u.syncDirection = syncDirectionPush
if strings.TrimSpace(u.syncLocalPath.Text()) == "" {
u.syncLocalPath.SetText(strings.TrimSpace(u.vaultPath.Text()))
}
u.syncSavedRemoteBindingSelection()
u.prefillAdvancedSyncRemoteFromSavedBinding()
} }
func (u *ui) clearSyncLocalImport() { func (u *ui) clearSyncLocalImport() {
@@ -2012,12 +2038,15 @@ func (u *ui) restoreStartupLifecycleTarget() {
remoteRecord, hasRemote, remoteUsedAt := u.latestRecentRemote() remoteRecord, hasRemote, remoteUsedAt := u.latestRecentRemote()
switch { switch {
case hasRemote && (localPath == "" || remoteUsedAt.After(localUsedAt)): case hasRemote && strings.TrimSpace(remoteRecord.LocalVaultPath) != "" && (localPath == "" || remoteUsedAt.After(localUsedAt)):
u.lifecycleMode = "remote" u.lifecycleMode = "local"
u.applyRecentRemoteRecord(remoteRecord) u.vaultPath.SetText(strings.TrimSpace(remoteRecord.LocalVaultPath))
case localPath != "": case localPath != "":
u.lifecycleMode = "local" u.lifecycleMode = "local"
u.vaultPath.SetText(localPath) u.vaultPath.SetText(localPath)
case hasRemote:
u.lifecycleMode = "remote"
u.applyRecentRemoteRecord(remoteRecord)
} }
} }
@@ -2251,6 +2280,27 @@ func (u *ui) applyAdvancedSyncRemoteCredentialEntry(entry vault.Entry) {
u.syncRemotePassword.SetText(entry.Password) u.syncRemotePassword.SetText(entry.Password)
} }
func (u *ui) savedAdvancedSyncRemoteBinding() (appstate.ResolvedRemoteBinding, bool) {
if !u.hasOpenVault() {
return appstate.ResolvedRemoteBinding{}, false
}
_, resolved, ok, err := u.resolvedSelectedVaultRemoteBinding()
if err != nil || !ok {
return appstate.ResolvedRemoteBinding{}, false
}
return resolved, true
}
func (u *ui) prefillAdvancedSyncRemoteFromSavedBinding() {
resolved, ok := u.savedAdvancedSyncRemoteBinding()
if !ok {
return
}
u.syncRemoteBaseURL.SetText(resolved.Profile.BaseURL)
u.syncRemotePath.SetText(resolved.Profile.Path)
u.applyAdvancedSyncRemoteCredentialEntry(resolved.Credentials)
}
func (u *ui) selectVaultRemoteProfile(id string) { func (u *ui) selectVaultRemoteProfile(id string) {
id = strings.TrimSpace(id) id = strings.TrimSpace(id)
u.selectedVaultRemoteProfileID = id u.selectedVaultRemoteProfileID = id
@@ -2484,6 +2534,18 @@ func (u *ui) directRemoteSyncShortcutLabel() string {
return "Use Remote Sync" return "Use Remote Sync"
} }
func (u *ui) shouldShowRemoteSyncSetupShortcut() bool {
if !u.hasOpenVault() || u.isVaultLocked() || u.state.Section != appstate.SectionEntries {
return false
}
_, ok := u.selectedVaultRemoteBinding()
return !ok
}
func (u *ui) remoteSyncSetupShortcutLabel() string {
return "Set Up Remote Sync"
}
func remoteBindingSuffix(baseURL, path, username string) string { func remoteBindingSuffix(baseURL, path, username string) string {
sum := sha256.Sum256([]byte(strings.TrimSpace(baseURL) + "\n" + strings.TrimSpace(path) + "\n" + strings.TrimSpace(username))) sum := sha256.Sum256([]byte(strings.TrimSpace(baseURL) + "\n" + strings.TrimSpace(path) + "\n" + strings.TrimSpace(username)))
return hex.EncodeToString(sum[:8]) return hex.EncodeToString(sum[:8])
@@ -4105,6 +4167,9 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
} }
} }
} }
for u.useSavedAdvancedSyncRemote.Clicked(gtx) {
u.openRemoteSyncSetupDialog()
}
for u.openSelectedVaultRemote.Clicked(gtx) { for u.openSelectedVaultRemote.Clicked(gtx) {
if u.lifecycleBusy() { if u.lifecycleBusy() {
continue continue
@@ -6646,16 +6711,27 @@ func (u *ui) pathBar(gtx layout.Context) layout.Dimensions {
return children return children
}()...) }()...)
} }
if !u.shouldShowDirectRemoteSyncShortcut() { if !u.shouldShowDirectRemoteSyncShortcut() && !u.shouldShowRemoteSyncSetupShortcut() {
return crumbBar(gtx) return crumbBar(gtx)
} }
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children := []layout.FlexChild{
layout.Rigid(crumbBar), layout.Rigid(crumbBar),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { }
if u.shouldShowDirectRemoteSyncShortcut() {
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.directRemoteSyncShortcutLabel()) return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.directRemoteSyncShortcutLabel())
}), }))
) }
if u.shouldShowRemoteSyncSetupShortcut() {
if u.shouldShowDirectRemoteSyncShortcut() {
children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
}
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSetupShortcutLabel())
}))
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
} }
func (u *ui) visibleBreadcrumbs(displayPath []string) ([]string, []int) { func (u *ui) visibleBreadcrumbs(displayPath []string) ([]string, []int) {
+143 -7
View File
@@ -4934,14 +4934,11 @@ func TestUIStartupPreselectsNewestTargetAcrossLocalAndRemote(t *testing.T) {
second := newUIWithSession("desktop", &session.Manager{}, paths) second := newUIWithSession("desktop", &session.Manager{}, paths)
if got := second.lifecycleMode; got != "remote" { if got := second.lifecycleMode; got != "local" {
t.Fatalf("lifecycleMode = %q, want remote", got) t.Fatalf("lifecycleMode = %q, want local", got)
} }
if got := second.remoteBaseURL.Text(); got != "https://dav.example.com" { if got := second.vaultPath.Text(); got != "/tmp/local.kdbx" {
t.Fatalf("remoteBaseURL = %q, want https://dav.example.com", got) t.Fatalf("vaultPath = %q, want /tmp/local.kdbx", got)
}
if got := second.remotePath.Text(); got != "vaults/home.kdbx" {
t.Fatalf("remotePath = %q, want vaults/home.kdbx", got)
} }
if got := second.remoteUsername.Text(); got != "" { if got := second.remoteUsername.Text(); got != "" {
t.Fatalf("remoteUsername = %q, want empty for location-only recent remote", got) t.Fatalf("remoteUsername = %q, want empty for location-only recent remote", got)
@@ -5422,6 +5419,31 @@ func TestRestoreStartupLifecycleTargetSelectsMostRecentLocalVault(t *testing.T)
} }
} }
func TestRestoreStartupLifecycleTargetUsesLocalCacheFromRecentRemote(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
u.lifecycleMode = "remote"
u.vaultPath.SetText("")
u.recentVaults = []string{"/tmp/older.kdbx"}
u.recentVaultUsedAt["/tmp/older.kdbx"] = time.Date(2026, time.April, 5, 1, 2, 3, 0, time.UTC)
u.recentRemotes = []recentRemoteRecord{{
BaseURL: "https://dav.example.invalid/remote.php/dav",
Path: "files/family/keepass.kdbx",
LocalVaultPath: "/tmp/family-cache.kdbx",
UsedAt: time.Date(2026, time.April, 5, 2, 2, 3, 0, time.UTC).Format(time.RFC3339Nano),
}}
u.restoreStartupLifecycleTarget()
if got := u.lifecycleMode; got != "local" {
t.Fatalf("lifecycleMode after restore = %q, want local", got)
}
if got := u.vaultPath.Text(); got != "/tmp/family-cache.kdbx" {
t.Fatalf("vaultPath after restore = %q, want /tmp/family-cache.kdbx", got)
}
}
func TestShowLocalVaultChooser(t *testing.T) { func TestShowLocalVaultChooser(t *testing.T) {
t.Parallel() t.Parallel()
@@ -5795,6 +5817,120 @@ func TestUIDirectRemoteSyncShortcutLabelUsesSyncLanguage(t *testing.T) {
} }
} }
func TestUIShouldShowRemoteSyncSetupShortcutForOpenedLocalVaultWithoutSavedBinding(t *testing.T) {
t.Parallel()
u := newUIWithModel("desktop", vault.Model{
Entries: []vault.Entry{{
ID: "entry-1",
Title: "Vault Console",
Path: []string{"Crew", "Internet"},
}},
})
u.state.Section = appstate.SectionEntries
if !u.shouldShowRemoteSyncSetupShortcut() {
t.Fatal("shouldShowRemoteSyncSetupShortcut() = false, want true for opened local vault without saved binding")
}
}
func TestUIShouldHideRemoteSyncSetupShortcutWhenSavedBindingExists(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",
}},
})
u.state.Section = appstate.SectionEntries
if u.shouldShowRemoteSyncSetupShortcut() {
t.Fatal("shouldShowRemoteSyncSetupShortcut() = true, want false when saved binding already exists")
}
}
func TestUIRemoteSyncSetupShortcutLabelUsesClearLanguage(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
if got := u.remoteSyncSetupShortcutLabel(); got != "Set Up Remote Sync" {
t.Fatalf("remoteSyncSetupShortcutLabel() = %q, want Set Up Remote Sync", got)
}
}
func TestUIOpenRemoteSyncSetupDialogPrefillsCurrentVaultSetupFlow(t *testing.T) {
t.Parallel()
u := newUIWithModel("desktop", vault.Model{
Entries: []vault.Entry{{
ID: "remote-creds-1",
Title: "WebDAV Sign-In",
Username: "tjulian",
Password: "token-1",
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",
}},
})
u.vaultPath.SetText("/vaults/family.kdbx")
u.openRemoteSyncSetupDialog()
if !u.syncDialogOpen {
t.Fatal("syncDialogOpen = false, want true")
}
if got := u.syncDialogPurpose; got != syncDialogPurposeRemoteSetup {
t.Fatalf("syncDialogPurpose = %q, want remote setup", got)
}
if got := u.syncSourceMode; got != syncSourceRemote {
t.Fatalf("syncSourceMode = %q, want remote", got)
}
if got := u.syncDirection; got != syncDirectionPush {
t.Fatalf("syncDirection = %q, want push", got)
}
if got := u.syncLocalPath.Text(); got != "/vaults/family.kdbx" {
t.Fatalf("syncLocalPath = %q, want current vault path", got)
}
if got := u.syncRemoteBaseURL.Text(); got != "https://dav.example.invalid/remote.php/dav" {
t.Fatalf("syncRemoteBaseURL = %q, want saved remote base URL", got)
}
if got := u.syncRemotePath.Text(); got != "files/family/keepass.kdbx" {
t.Fatalf("syncRemotePath = %q, want saved remote path", got)
}
if got := u.syncRemoteUsername.Text(); got != "tjulian" {
t.Fatalf("syncRemoteUsername = %q, want tjulian", got)
}
if got := u.syncRemotePassword.Text(); got != "token-1" {
t.Fatalf("syncRemotePassword = %q, want token-1", got)
}
}
func TestUISelectedLocalVaultRemoteSyncSummaryMentionsSetup(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
if got := u.selectedLocalVaultRemoteSyncSummary("/vaults/family.kdbx"); got != "Open this vault to set up a WebDAV sync target for it." {
t.Fatalf("selectedLocalVaultRemoteSyncSummary() = %q, want setup guidance", got)
}
}
func TestUISaveCurrentRemoteBindingActionPersistsBindingIntoVault(t *testing.T) { func TestUISaveCurrentRemoteBindingActionPersistsBindingIntoVault(t *testing.T) {
t.Parallel() t.Parallel()
+13 -184
View File
@@ -21,7 +21,6 @@ import (
func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
busy := u.lifecycleBusy() busy := u.lifecycleBusy()
showLocalChooser := u.showLocalVaultChooser() showLocalChooser := u.showLocalVaultChooser()
showRemoteChooser := u.showRemoteConnectionChooser()
selectedLocalPath := strings.TrimSpace(u.vaultPath.Text()) selectedLocalPath := strings.TrimSpace(u.vaultPath.Text())
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
@@ -31,172 +30,13 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
message := "Choose a recent vault or enter a .kdbx path, then unlock it." message := "Choose a recent vault or enter a .kdbx path, then unlock it. Remote sync attaches to that local vault after it opens."
if u.lifecycleMode == "remote" {
message = u.remoteLifecycleMessage()
}
lbl := material.Label(u.theme, unit.Sp(14), message) lbl := material.Label(u.theme, unit.Sp(14), message)
lbl.Color = accentColor lbl.Color = accentColor
return lbl.Layout(gtx) return lbl.Layout(gtx)
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy {
return passiveSectionTab(gtx, u.theme, "Local Vault", u.lifecycleMode == "local")
}
return sectionTabButton(gtx, u.theme, &u.showLocalLifecycle, "Local Vault", u.lifecycleMode == "local")
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy {
return passiveSectionTab(gtx, u.theme, "Remote Vault", u.lifecycleMode == "remote")
}
return sectionTabButton(gtx, u.theme, &u.showRemoteLifecycle, "Remote Vault", u.lifecycleMode == "remote")
}),
)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(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(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 Base URL", "Base WebDAV endpoint, for example https://server/remote.php/webdav.", &u.remoteBaseURL, 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 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(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(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 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(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return compactCard(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), "Local Cache Bootstrap")
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), u.remoteLifecycleSetupSummary())
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
)
})
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(10)}.Layout(gtx)
}),
)
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showLocalChooser { if !showLocalChooser {
@@ -326,29 +166,6 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.lifecycleMode == "remote" {
label := u.remoteOpenButtonLabel()
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, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
label := "Open Vault" label := "Open Vault"
@@ -499,6 +316,11 @@ func (u *ui) selectedLocalVaultCard(gtx layout.Context, path string) layout.Dime
lbl.Color = mutedColor lbl.Color = mutedColor
return lbl.Layout(gtx) return lbl.Layout(gtx)
}), }),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(11), u.selectedLocalVaultRemoteSyncSummary(path))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.clearVaultSelection, "Open Different Vault") return tonedButton(gtx, u.theme, &u.clearVaultSelection, "Open Different Vault")
@@ -508,6 +330,13 @@ func (u *ui) selectedLocalVaultCard(gtx layout.Context, path string) layout.Dime
}) })
} }
func (u *ui) selectedLocalVaultRemoteSyncSummary(path string) string {
if record, ok := u.boundRecentRemoteForLocalVault(path); ok {
return "Saved remote sync target: " + friendlyRecentRemoteLabel(record)
}
return "Open this vault to set up a WebDAV sync target for it."
}
func (u *ui) lifecycleSecuritySettingsSummary() string { func (u *ui) lifecycleSecuritySettingsSummary() string {
return "Cipher and KDF now live in Vault Settings so opening and creating a vault stays focused on the file, key material, and sync choices." return "Cipher and KDF now live in Vault Settings so opening and creating a vault stays focused on the file, key material, and sync choices."
} }