From d2fd4c55f5fb95ea3d2af8f1ba02fcb7dc2f3a2e Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Wed, 1 Apr 2026 17:11:31 -0700 Subject: [PATCH] Tighten mobile locked vault hierarchy --- main.go | 128 ++++++++++++++++++++++++++++++++++++++++++++++++--- main_test.go | 71 ++++++++++++++++++++++++++++ ui_forms.go | 18 ++++++-- 3 files changed, 206 insertions(+), 11 deletions(-) diff --git a/main.go b/main.go index ebdf2bd..fbfe78b 100644 --- a/main.go +++ b/main.go @@ -96,6 +96,12 @@ type emptyState struct { Body string } +type vaultSummary struct { + Title string + Detail string + Context string +} + type sessionStatus interface { HasVault() bool IsLocked() bool @@ -2000,6 +2006,47 @@ func (u *ui) loadingDetailMessage() string { return "Target: " + path } +func (u *ui) currentVaultSummary() vaultSummary { + status, ok := u.state.Session.(sessionStatus) + if !ok || !status.HasVault() { + return vaultSummary{} + } + if status.IsRemote() { + baseURL := strings.TrimSpace(u.remoteBaseURL.Text()) + path := strings.TrimSpace(u.remotePath.Text()) + summary := vaultSummary{ + Title: friendlyRecentRemoteLabel(recentRemoteRecord{BaseURL: baseURL, Path: path}), + Detail: baseURL, + } + if strings.TrimSpace(summary.Title) == "" { + summary.Title = "Remote vault" + } + summary.Context = u.vaultResumeContext(u.recentRemoteGroup(baseURL, path)) + return summary + } + path := strings.TrimSpace(u.vaultPath.Text()) + summary := vaultSummary{ + Title: friendlyRecentVaultLabel(path), + Detail: path, + } + if strings.TrimSpace(summary.Title) == "" { + summary.Title = "Local vault" + } + summary.Context = u.vaultResumeContext(u.recentVaultGroup(path)) + return summary +} + +func (u *ui) vaultResumeContext(path []string) string { + if len(path) == 0 { + return "" + } + displayPath := append([]string(nil), path...) + if len(displayPath) == 0 { + return "" + } + return "Resume in: " + strings.Join(displayPath, " / ") +} + func (u *ui) sessionSurface() uiSurface { if u.state.Session == nil { return uiSurface{} @@ -3027,7 +3074,28 @@ func (u *ui) header(gtx layout.Context) layout.Dimensions { return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Flex{Alignment: layout.Middle}.Layout(gtx, layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { - return u.brandMark(gtx, 132, 42) + summary := u.currentVaultSummary() + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(11), "VAULT") + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(1)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(15), summary.Title) + lbl.Color = accentColor + return lbl.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if strings.TrimSpace(summary.Detail) == "" || summary.Detail == summary.Title { + return layout.Dimensions{} + } + lbl := material.Label(u.theme, unit.Sp(11), summary.Detail) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + ) }), layout.Rigid(u.headerActions), ) @@ -3132,7 +3200,7 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions { spacing := unit.Dp(12) if u.mode == "phone" { panel = compactCard - spacing = unit.Dp(8) + spacing = unit.Dp(6) } u.ensureNavClickables() return panel(gtx, func(gtx layout.Context) layout.Dimensions { @@ -3542,6 +3610,7 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions { _ = panel return layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild { if u.isVaultLocked() { + summary := u.currentVaultSummary() return []layout.FlexChild{ layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(18), "Unlock Vault") @@ -3554,6 +3623,47 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions { lbl.Color = mutedColor return lbl.Layout(gtx) }), + layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if strings.TrimSpace(summary.Title) == "" { + 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(11), "UNLOCK TARGET") + 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(15), summary.Title) + lbl.Color = accentColor + return lbl.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if strings.TrimSpace(summary.Detail) == "" || summary.Detail == summary.Title { + return layout.Dimensions{} + } + return layout.Inset{Top: unit.Dp(2)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(12), summary.Detail) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if strings.TrimSpace(summary.Context) == "" { + return layout.Dimensions{} + } + return layout.Inset{Top: unit.Dp(2)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(12), summary.Context) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }) + }), + ) + }) + }), layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), layout.Rigid(u.unlockPanel), } @@ -4124,7 +4234,7 @@ func (u *ui) pathBar(gtx layout.Context) layout.Dimensions { btn.Background, btn.Color = buttonFocusColors(u.isFocused(breadcrumbFocusID(index))) btn.TextSize = unit.Sp(11) if u.mode == "phone" { - btn.TextSize = unit.Sp(10) + btn.TextSize = unit.Sp(9) btn.Inset = layout.Inset{Top: 3, Bottom: 3, Left: 6, Right: 6} } else { btn.Inset = layout.Inset{Top: 5, Bottom: 5, Left: 9, Right: 9} @@ -4135,7 +4245,11 @@ func (u *ui) pathBar(gtx layout.Context) layout.Dimensions { children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(12), "/") lbl.Color = mutedColor - return layout.UniformInset(unit.Dp(6)).Layout(gtx, lbl.Layout) + inset := unit.Dp(6) + if u.mode == "phone" { + inset = unit.Dp(4) + } + return layout.UniformInset(inset).Layout(gtx, lbl.Layout) })) } } @@ -4255,7 +4369,7 @@ func (u *ui) groupBar(gtx layout.Context) layout.Dimensions { } maxGroupListHeight := 200 if u.mode == "phone" { - maxGroupListHeight = 112 + maxGroupListHeight = 96 } maxY := gtx.Dp(unit.Dp(maxGroupListHeight)) if gtx.Constraints.Max.Y > maxY { @@ -4563,8 +4677,8 @@ func sectionTabButton(gtx layout.Context, th *material.Theme, click *widget.Clic btn.TextSize = unit.Sp(11) btn.Inset = layout.Inset{Top: 5, Bottom: 5, Left: 9, Right: 9} if gtx.Constraints.Max.X <= gtx.Dp(unit.Dp(460)) { - btn.TextSize = unit.Sp(10) - btn.Inset = layout.Inset{Top: 4, Bottom: 4, Left: 8, Right: 8} + btn.TextSize = unit.Sp(9) + btn.Inset = layout.Inset{Top: 3, Bottom: 3, Left: 7, Right: 7} } if gtx.Constraints.Max.X <= gtx.Dp(unit.Dp(220)) { btn.TextSize = unit.Sp(9) diff --git a/main_test.go b/main_test.go index 1cd904a..b8ae25c 100644 --- a/main_test.go +++ b/main_test.go @@ -55,6 +55,32 @@ func waitForBackgroundResult(t *testing.T, u *ui) backgroundActionResult { } } +type summarySession struct { + model vault.Model + hasVault bool + locked bool + remote bool +} + +func (s summarySession) Current() (vault.Model, error) { + if s.locked { + return vault.Model{}, session.ErrLocked + } + return s.model, nil +} + +func (s summarySession) Save(vault.Model) error { return nil } +func (s summarySession) SaveAs(string, vault.MasterKey) error { return nil } +func (s summarySession) Create(vault.MasterKey) error { return nil } +func (s summarySession) Open(string, vault.MasterKey) error { return nil } +func (s summarySession) OpenRemote(*webdav.Client, string, vault.MasterKey) error { return nil } +func (s summarySession) ChangeMasterKey(vault.MasterKey) error { return nil } +func (s summarySession) Lock() error { return nil } +func (s summarySession) Unlock(vault.MasterKey) error { return nil } +func (s summarySession) HasVault() bool { return s.hasVault } +func (s summarySession) IsLocked() bool { return s.locked } +func (s summarySession) IsRemote() bool { return s.remote } + func TestUIFiltersUsingVaultModelPathsAndSearch(t *testing.T) { t.Parallel() @@ -135,6 +161,51 @@ func TestUISearchBehaviorIsConsistentAcrossDesktopAndPhoneLayouts(t *testing.T) } } +func TestUICurrentVaultSummary(t *testing.T) { + t.Parallel() + + t.Run("local", func(t *testing.T) { + t.Parallel() + + u := newUIWithSession("phone", summarySession{hasVault: true}) + u.vaultPath.SetText("/vaults/family.kdbx") + u.recentVaultGroups["/vaults/family.kdbx"] = []string{"Root", "Internet"} + + got := u.currentVaultSummary() + want := vaultSummary{ + Title: "family.kdbx", + Detail: "/vaults/family.kdbx", + Context: "Resume in: Root / Internet", + } + if got != want { + t.Fatalf("currentVaultSummary() = %#v, want %#v", got, want) + } + }) + + t.Run("remote", func(t *testing.T) { + t.Parallel() + + u := newUIWithSession("phone", summarySession{hasVault: true, remote: true}) + u.remoteBaseURL.SetText("https://dav.example.com") + u.remotePath.SetText("vaults/home.kdbx") + u.recentRemotes = []recentRemoteRecord{{ + BaseURL: "https://dav.example.com", + Path: "vaults/home.kdbx", + LastGroup: []string{"Root", "Shared"}, + }} + + got := u.currentVaultSummary() + want := vaultSummary{ + Title: "home.kdbx ยท dav.example.com", + Detail: "https://dav.example.com", + Context: "Resume in: Root / Shared", + } + if got != want { + t.Fatalf("currentVaultSummary() = %#v, want %#v", got, want) + } + }) +} + func TestUIClearingSearchResetsToCurrentSectionListing(t *testing.T) { t.Parallel() diff --git a/ui_forms.go b/ui_forms.go index caf10c9..5dc449c 100644 --- a/ui_forms.go +++ b/ui_forms.go @@ -784,8 +784,8 @@ func (u *ui) groupControlsSection(gtx layout.Context) layout.Dimensions { func (u *ui) groupControlsDisclosure(gtx layout.Context) layout.Dimensions { return u.toggleGroupControls.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.UniformInset(unit.Dp(6)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + content := func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(2)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Flex{Alignment: layout.Middle}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { icon := u.expandLessIcon @@ -804,13 +804,23 @@ func (u *ui) groupControlsDisclosure(gtx layout.Context) layout.Dimensions { }), layout.Rigid(layout.Spacer{Width: unit.Dp(4)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(12), "Group management") + label := "Group management" + size := unit.Sp(12) + if u.mode == "phone" { + label = "Tools" + size = unit.Sp(11) + } + lbl := material.Label(u.theme, size, label) lbl.Color = mutedColor return lbl.Layout(gtx) }), ) }) - }) + } + if u.mode == "phone" { + return content(gtx) + } + return compactCard(gtx, content) }) }