Improve KeePassGO vault navigation UX

This commit is contained in:
Joe Julian
2026-03-29 15:18:14 -07:00
parent fa75908552
commit cd21cc26c9
4 changed files with 187 additions and 33 deletions
+99 -30
View File
@@ -143,6 +143,7 @@ type ui struct {
attachmentClicks []widget.Clickable
breadcrumbs []widget.Clickable
groupClicks []widget.Clickable
recentVaultClicks []widget.Clickable
state appstate.State
visible []entry
currentPath []string
@@ -164,6 +165,7 @@ type ui struct {
keyboardFocus focusID
defaultSaveAsPath string
editingEntry bool
recentVaults []string
}
var (
@@ -425,6 +427,7 @@ func (u *ui) createVaultAction() error {
return err
}
u.vaultPath.SetText(u.saveAsTargetPath())
u.noteRecentVault(u.saveAsTargetPath())
}
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.editingEntry = false
@@ -444,7 +447,9 @@ func (u *ui) openVaultAction() error {
if err := u.state.OpenVault(path, key); err != nil {
return err
}
u.noteRecentVault(path)
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.enterHiddenVaultRoot()
u.editingEntry = false
u.filter()
return nil
@@ -464,6 +469,7 @@ func (u *ui) saveAsAction() error {
return err
}
u.vaultPath.SetText(path)
u.noteRecentVault(path)
u.filter()
return nil
}
@@ -481,6 +487,7 @@ func (u *ui) openRemoteAction() error {
if err := u.state.OpenRemoteVault(client, strings.TrimSpace(u.remotePath.Text()), key); err != nil {
return err
}
u.enterHiddenVaultRoot()
u.editingEntry = false
u.filter()
return nil
@@ -535,6 +542,70 @@ func (u *ui) saveAsTargetPath() string {
return u.defaultSaveAsPath
}
func (u *ui) noteRecentVault(path string) {
path = strings.TrimSpace(path)
if path == "" {
return
}
next := []string{path}
for _, existing := range u.recentVaults {
if existing == path {
continue
}
next = append(next, existing)
if len(next) == 6 {
break
}
}
u.recentVaults = next
if len(u.recentVaultClicks) < len(u.recentVaults) {
u.recentVaultClicks = make([]widget.Clickable, len(u.recentVaults))
}
}
func (u *ui) hiddenVaultRoot() string {
if u.state.Section != appstate.SectionEntries {
return ""
}
model, err := u.state.Session.Current()
if err != nil {
return ""
}
if len(model.EntriesInPath(nil)) != 0 {
return ""
}
groups := model.ChildGroups(nil)
if len(groups) != 1 {
return ""
}
return groups[0]
}
func (u *ui) enterHiddenVaultRoot() {
root := u.hiddenVaultRoot()
if root == "" {
return
}
u.setCurrentPath([]string{root})
}
func (u *ui) displayPath() []string {
path := append([]string(nil), u.currentPath...)
root := u.hiddenVaultRoot()
if root == "" || len(path) == 0 || path[0] != root {
return path
}
return append([]string(nil), path[1:]...)
}
func (u *ui) displayEntryPath(path []string) []string {
root := u.hiddenVaultRoot()
if root == "" || len(path) == 0 || path[0] != root {
return append([]string(nil), path...)
}
return append([]string(nil), path[1:]...)
}
func (u *ui) runAction(label string, action func() error) {
u.loadingMessage = actionLoadingLabel(label)
if err := action(); err != nil {
@@ -756,10 +827,17 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
for u.pickKeyFile.Clicked(gtx) {
u.runAction("choose key file", func() error { return u.chooseExistingFileAction(&u.keyFilePath) })
}
for i := range u.recentVaultClicks {
for u.recentVaultClicks[i].Clicked(gtx) {
if i < len(u.recentVaults) {
u.vaultPath.SetText(u.recentVaults[i])
}
}
}
for u.addEntry.Clicked(gtx) {
u.state.BeginNewEntry()
u.loadSelectedEntryIntoEditor()
u.entryPath.SetText(strings.Join(u.state.CurrentPath, " / "))
u.entryPath.SetText(strings.Join(u.displayPath(), " / "))
u.editingEntry = true
}
for u.saveEntry.Clicked(gtx) {
@@ -903,7 +981,6 @@ func (u *ui) header(gtx layout.Context) layout.Dimensions {
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
)
}),
layout.Rigid(u.feedbackBanner),
)
})
}
@@ -929,7 +1006,6 @@ func (u *ui) header(gtx layout.Context) layout.Dimensions {
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
)
}),
layout.Rigid(u.feedbackBanner),
)
})
}
@@ -1250,7 +1326,7 @@ func (u *ui) detailPanel(gtx layout.Context) layout.Dimensions {
return lbl.Layout(gtx)
},
layout.Spacer{Height: titlePad}.Layout,
detailLine(u.theme, "Path", strings.Join(item.Path, " / ")),
detailLine(u.theme, "Path", strings.Join(u.displayEntryPath(item.Path), " / ")),
layout.Spacer{Height: sectionGap}.Layout,
detailLine(u.theme, "Username", item.Username),
layout.Spacer{Height: sectionGap}.Layout,
@@ -1489,7 +1565,8 @@ func (u *ui) pathBar(gtx layout.Context) layout.Dimensions {
}
u.syncCurrentPath()
crumbs := append([]string{"All Entries"}, append([]string{}, u.currentPath...)...)
displayPath := u.displayPath()
crumbs := append([]string{"/"}, append([]string{}, displayPath...)...)
if u.state.Section == appstate.SectionTemplates {
crumbs = append([]string{"Templates"}, append([]string{}, u.currentPath...)...)
}
@@ -1501,9 +1578,19 @@ func (u *ui) pathBar(gtx layout.Context) layout.Dimensions {
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
for u.breadcrumbs[index].Clicked(gtx) {
if index == 0 {
u.setCurrentPath(nil)
root := u.hiddenVaultRoot()
if root == "" {
u.setCurrentPath(nil)
} else {
u.setCurrentPath([]string{root})
}
} else {
u.setCurrentPath(crumbs[1 : index+1])
nextPath := crumbs[1 : index+1]
root := u.hiddenVaultRoot()
if root != "" {
nextPath = append([]string{root}, nextPath...)
}
u.setCurrentPath(nextPath)
}
u.filter()
}
@@ -1533,8 +1620,8 @@ func (u *ui) groupBar(gtx layout.Context) layout.Dimensions {
if len(groups) == 0 {
return layout.Dimensions{}
}
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, func() []layout.FlexChild {
children := make([]layout.FlexChild, 0, len(groups))
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild {
children := make([]layout.FlexChild, 0, len(groups)*2)
for i, group := range groups {
idx := i
name := group
@@ -1550,6 +1637,9 @@ func (u *ui) groupBar(gtx layout.Context) layout.Dimensions {
btn.TextSize = unit.Sp(12)
return btn.Layout(gtx)
}))
if i < len(groups)-1 {
children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
}
}
return children
}()...)
@@ -1571,27 +1661,6 @@ func detailLine(th *material.Theme, label, value string) layout.Widget {
}
}
func (u *ui) feedbackBanner(gtx layout.Context) layout.Dimensions {
message := u.state.StatusMessage
tone := color.NRGBA{R: 231, G: 239, B: 235, A: 255}
textColor := accentColor
if u.state.ErrorMessage != "" {
message = u.state.ErrorMessage
tone = color.NRGBA{R: 248, G: 226, B: 223, A: 255}
textColor = color.NRGBA{R: 140, G: 46, B: 34, A: 255}
}
if message == "" {
return layout.Dimensions{}
}
return layout.Background{}.Layout(gtx, fill(tone), func(gtx layout.Context) layout.Dimensions {
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(13), message)
lbl.Color = textColor
return lbl.Layout(gtx)
})
})
}
func (u *ui) passwordLine(label, value string) layout.Widget {
return func(gtx layout.Context) layout.Dimensions {
icon := u.eyeIcon