Clarify group navigation and management

This commit is contained in:
Joe Julian
2026-04-01 17:10:01 -07:00
parent 18d57a2f8e
commit 6eee43c1df
3 changed files with 183 additions and 38 deletions
+121 -36
View File
@@ -219,6 +219,8 @@ type ui struct {
exportAttachment widget.Clickable
restoreHistory widget.Clickable
generatePassword widget.Clickable
goToRootGroup widget.Clickable
goToParentGroup widget.Clickable
createGroup widget.Clickable
moveGroup widget.Clickable
renameGroup widget.Clickable
@@ -1506,6 +1508,29 @@ func (u *ui) displayEntryPath(path []string) []string {
return append([]string(nil), path[1:]...)
}
func (u *ui) currentGroupDisplayName() string {
displayPath := u.displayPath()
if len(displayPath) == 0 {
return "Vault root (/)"
}
return strings.Join(displayPath, " / ")
}
func (u *ui) parentGroupDisplayName() string {
displayPath := u.displayPath()
if len(displayPath) <= 1 {
return "Vault root (/)"
}
return strings.Join(displayPath[:len(displayPath)-1], " / ")
}
func (u *ui) createGroupLabel() string {
if len(u.displayPath()) == 0 {
return "Create Top-Level Group"
}
return "Create Subgroup"
}
func pathHasPrefix(path, prefix []string) bool {
if len(prefix) > len(path) {
return false
@@ -3765,49 +3790,109 @@ func (u *ui) groupBar(gtx layout.Context) layout.Dimensions {
if len(u.groupClicks) < len(groups) {
u.groupClicks = make([]widget.Clickable, len(groups))
}
if len(groups) == 0 {
return layout.Dimensions{}
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild {
children := []layout.FlexChild{
displayPath := u.displayPath()
atRoot := len(displayPath) == 0
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 {
label := "Groups"
if len(u.displayPath()) > 0 {
label = "Subgroups"
lbl := material.Label(u.theme, unit.Sp(12), "BROWSE GROUPS")
lbl.Color = mutedColor
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), "Root anchors the vault, current group sets the listing, and child groups open deeper paths.")
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(detailLine(u.theme, "Root", "Vault root (/)")),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(detailLine(u.theme, "Current Group", u.currentGroupDisplayName())),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if atRoot {
return layout.Dimensions{}
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(detailLine(u.theme, "Parent Group", u.parentGroupDisplayName())),
)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if atRoot {
return layout.Dimensions{}
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.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 {
for u.goToRootGroup.Clicked(gtx) {
root := u.hiddenVaultRoot()
if root == "" {
u.setCurrentPath(nil)
} else {
u.setCurrentPath([]string{root})
}
u.filter()
}
return tonedButton(gtx, u.theme, &u.goToRootGroup, "Back to Root")
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
for u.goToParentGroup.Clicked(gtx) {
u.setCurrentPath(u.currentPath[:len(u.currentPath)-1])
u.filter()
}
return tonedButton(gtx, u.theme, &u.goToParentGroup, "Up One Group")
}),
)
}),
)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
label := "CHILD GROUPS"
if atRoot {
label = "TOP-LEVEL GROUPS"
}
lbl := material.Label(u.theme, unit.Sp(12), label)
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
}
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
maxGroupListHeight := 200
if u.mode == "phone" {
maxGroupListHeight = 112
}
maxY := gtx.Dp(unit.Dp(maxGroupListHeight))
if gtx.Constraints.Max.Y > maxY {
gtx.Constraints.Max.Y = maxY
}
if gtx.Constraints.Min.Y > gtx.Constraints.Max.Y {
gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
}
return material.List(u.theme, &u.groupList).Layout(gtx, len(groups), func(gtx layout.Context, i int) layout.Dimensions {
idx := i
name := groups[i]
return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
for u.groupClicks[idx].Clicked(gtx) {
u.state.EnterGroup(name)
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.filter()
}
return tonedButton(gtx, u.theme, &u.groupClicks[idx], name)
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if len(groups) == 0 {
lbl := material.Label(u.theme, unit.Sp(12), "No child groups here yet.")
lbl.Color = mutedColor
return lbl.Layout(gtx)
}
maxGroupListHeight := 200
if u.mode == "phone" {
maxGroupListHeight = 112
}
maxY := gtx.Dp(unit.Dp(maxGroupListHeight))
if gtx.Constraints.Max.Y > maxY {
gtx.Constraints.Max.Y = maxY
}
if gtx.Constraints.Min.Y > gtx.Constraints.Max.Y {
gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
}
return material.List(u.theme, &u.groupList).Layout(gtx, len(groups), func(gtx layout.Context, i int) layout.Dimensions {
idx := i
name := groups[i]
return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
for u.groupClicks[idx].Clicked(gtx) {
u.state.EnterGroup(name)
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.filter()
}
return tonedButton(gtx, u.theme, &u.groupClicks[idx], "Open "+name)
})
})
})
}))
return children
}()...)
}),
)
})
}
func detailLine(th *material.Theme, label, value string) layout.Widget {