Tighten navigation and admin UI

This commit is contained in:
Joe Julian
2026-04-01 16:24:11 -07:00
parent 14f4b31cf6
commit 1675811aa3
3 changed files with 402 additions and 78 deletions
+199 -16
View File
@@ -108,6 +108,7 @@ type recentRemoteRecord struct {
type uiPreferences struct {
GroupControlsHidden bool `json:"groupControlsHidden"`
LifecycleAdvancedHidden bool `json:"lifecycleAdvancedHidden"`
HistoryHidden bool `json:"historyHidden"`
}
type entriesSectionState struct {
@@ -222,6 +223,7 @@ type ui struct {
addCustomField widget.Clickable
toggleGroupControls widget.Clickable
toggleLifecycleAdvanced widget.Clickable
toggleHistory widget.Clickable
togglePasswordInline widget.Clickable
toggleSyncPassword widget.Clickable
showEntries widget.Clickable
@@ -304,6 +306,7 @@ type ui struct {
editingEntry bool
groupControlsHidden bool
lifecycleAdvancedHidden bool
historyHidden bool
recentVaults []string
recentRemotes []recentRemoteRecord
recentVaultGroups map[string][]string
@@ -423,6 +426,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
recentVaultGroups: map[string][]string{},
recentVaultUsedAt: map[string]time.Time{},
lifecycleAdvancedHidden: true,
historyHidden: true,
now: time.Now,
syncSourceMode: syncSourceLocal,
syncDirection: syncDirectionPull,
@@ -1105,6 +1109,7 @@ func (u *ui) loadUIPreferences() {
}
u.groupControlsHidden = prefs.GroupControlsHidden
u.lifecycleAdvancedHidden = prefs.LifecycleAdvancedHidden
u.historyHidden = prefs.HistoryHidden
}
func (u *ui) saveUIPreferences() {
@@ -1117,6 +1122,7 @@ func (u *ui) saveUIPreferences() {
content, err := json.MarshalIndent(uiPreferences{
GroupControlsHidden: u.groupControlsHidden,
LifecycleAdvancedHidden: u.lifecycleAdvancedHidden,
HistoryHidden: u.historyHidden,
}, "", " ")
if err != nil {
return
@@ -1959,6 +1965,10 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
u.groupControlsHidden = !u.groupControlsHidden
u.saveUIPreferences()
}
for u.toggleHistory.Clicked(gtx) {
u.historyHidden = !u.historyHidden
u.saveUIPreferences()
}
for u.renameGroup.Clicked(gtx) {
u.clearDeleteGroupConfirmation()
u.runAction("rename group", u.renameGroupAction)
@@ -2600,6 +2610,20 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
}
func (u *ui) navigationHeader(gtx layout.Context) layout.Dimensions {
if u.mode == "phone" {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return u.sectionBar(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.state.Section != appstate.SectionEntries {
return layout.Dimensions{}
}
return u.groupControlsDisclosure(gtx)
}),
)
}
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
return u.sectionBar(gtx)
@@ -2614,21 +2638,58 @@ func (u *ui) navigationHeader(gtx layout.Context) layout.Dimensions {
}
func (u *ui) sectionBar(gtx layout.Context) layout.Dimensions {
tabs := []struct {
click *widget.Clickable
label string
active bool
}{
{click: &u.showEntries, label: "Entries", active: u.state.Section == appstate.SectionEntries},
{click: &u.showRecycle, label: "Recycle Bin", active: u.state.Section == appstate.SectionRecycleBin},
{click: &u.showAPITokens, label: "API Tokens", active: u.state.Section == appstate.SectionAPITokens},
{click: &u.showAPIAudit, label: "API Audit", active: u.state.Section == appstate.SectionAPIAudit},
}
if u.mode == "phone" {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return sectionTabButton(gtx, u.theme, tabs[0].click, tabs[0].label, tabs[0].active)
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return sectionTabButton(gtx, u.theme, tabs[1].click, tabs[1].label, tabs[1].active)
}),
)
}),
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 {
return sectionTabButton(gtx, u.theme, tabs[2].click, tabs[2].label, tabs[2].active)
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return sectionTabButton(gtx, u.theme, tabs[3].click, tabs[3].label, tabs[3].active)
}),
)
}),
)
}
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return sectionTabButton(gtx, u.theme, &u.showEntries, "Entries", u.state.Section == appstate.SectionEntries)
return sectionTabButton(gtx, u.theme, tabs[0].click, tabs[0].label, tabs[0].active)
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return sectionTabButton(gtx, u.theme, &u.showRecycle, "Recycle Bin", u.state.Section == appstate.SectionRecycleBin)
return sectionTabButton(gtx, u.theme, tabs[1].click, tabs[1].label, tabs[1].active)
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return sectionTabButton(gtx, u.theme, &u.showAPITokens, "API Tokens", u.state.Section == appstate.SectionAPITokens)
return sectionTabButton(gtx, u.theme, tabs[2].click, tabs[2].label, tabs[2].active)
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return sectionTabButton(gtx, u.theme, &u.showAPIAudit, "API Audit", u.state.Section == appstate.SectionAPIAudit)
return sectionTabButton(gtx, u.theme, tabs[3].click, tabs[3].label, tabs[3].active)
}),
)
}
@@ -2780,7 +2841,12 @@ func (u *ui) detailPanel(gtx layout.Context) layout.Dimensions {
layout.Rigid(u.syncButtonGroup),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Security")
btn := material.IconButton(u.theme, &u.openSecuritySettings, u.settingsIcon, "Vault settings")
btn.Background = selectedColor
btn.Color = accentColor
btn.Size = unit.Dp(18)
btn.Inset = layout.UniformInset(unit.Dp(8))
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
@@ -2893,7 +2959,13 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions {
detailLine(u.theme, "URL", item.URL),
layout.Spacer{Height: sectionGap}.Layout,
detailLine(u.theme, "Tags", strings.Join(item.Tags, ", ")),
layout.Spacer{Height: unit.Dp(12)}.Layout,
layout.Spacer{Height: unit.Dp(10)}.Layout,
func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), "Quick actions")
lbl.Color = mutedColor
return lbl.Layout(gtx)
},
layout.Spacer{Height: unit.Dp(6)}.Layout,
func(gtx layout.Context) layout.Dimensions {
if u.mode == "phone" {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
@@ -2928,12 +3000,20 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions {
)
},
layout.Spacer{Height: unit.Dp(12)}.Layout,
func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), "Notes")
lbl.Color = mutedColor
return lbl.Layout(gtx)
},
layout.Spacer{Height: unit.Dp(4)}.Layout,
func(gtx layout.Context) layout.Dimensions {
lbl := material.Body1(u.theme, item.Notes)
lbl.Color = mutedColor
return lbl.Layout(gtx)
},
layout.Spacer{Height: unit.Dp(12)}.Layout,
u.attachmentSummaryPanel,
layout.Spacer{Height: unit.Dp(12)}.Layout,
u.historyPanel,
layout.Spacer{Height: unit.Dp(12)}.Layout,
func(gtx layout.Context) layout.Dimensions {
@@ -3013,12 +3093,42 @@ func (u *ui) historyPanel(gtx layout.Context) layout.Dimensions {
children := []layout.FlexChild{
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(14), "History")
lbl.Color = accentColor
return lbl.Layout(gtx)
return u.toggleHistory.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
if u.historyHidden {
icon = u.expandMoreIcon
}
if icon != nil {
return icon.Layout(gtx, accentColor)
}
lbl := material.Label(u.theme, unit.Sp(16), ">")
if !u.historyHidden {
lbl.Text = "v"
}
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(4)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(14), "History")
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
)
})
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
}
if u.historyHidden {
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), fmt.Sprintf("%d saved version(s).", len(history)))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}))
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
}
if len(history) == 0 {
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
@@ -3063,6 +3173,40 @@ func (u *ui) historyPanel(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
}
func (u *ui) attachmentSummaryPanel(gtx layout.Context) layout.Dimensions {
items := u.selectedAttachmentItems()
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), "Attachments")
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 len(items) == 0 {
lbl := material.Label(u.theme, unit.Sp(12), "No attachments on this entry.")
lbl.Color = mutedColor
return lbl.Layout(gtx)
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild {
children := make([]layout.FlexChild, 0, len(items)*2)
for i, item := range items {
label := fmt.Sprintf("%s (%d B)", item.Name, item.Size)
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), label)
lbl.Color = accentColor
return lbl.Layout(gtx)
}))
if i < len(items)-1 {
children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout))
}
}
return children
}()...)
}),
)
}
func (u *ui) historyRow(gtx layout.Context, click *widget.Clickable, index int, item entry) layout.Dimensions {
for click.Clicked(gtx) {
_ = u.selectHistoryVersion(index)
@@ -3128,10 +3272,11 @@ func (u *ui) pathBar(gtx layout.Context) layout.Dimensions {
u.syncCurrentPath()
displayPath := u.displayPath()
crumbs := append([]string{"/"}, append([]string{}, displayPath...)...)
pathSource := displayPath
if u.state.Section == appstate.SectionTemplates {
crumbs = append([]string{"Templates"}, append([]string{}, u.currentPath...)...)
pathSource = append([]string{}, u.currentPath...)
}
crumbs, indices := u.visibleBreadcrumbs(pathSource)
return layout.Flex{Alignment: layout.Middle}.Layout(gtx, func() []layout.FlexChild {
children := make([]layout.FlexChild, 0, len(crumbs)*2)
for i, name := range crumbs {
@@ -3139,7 +3284,8 @@ func (u *ui) pathBar(gtx layout.Context) layout.Dimensions {
label := name
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
for u.breadcrumbs[index].Clicked(gtx) {
if index == 0 {
target := indices[index]
if target == 0 {
root := u.hiddenVaultRoot()
if root == "" {
u.setCurrentPath(nil)
@@ -3147,7 +3293,7 @@ func (u *ui) pathBar(gtx layout.Context) layout.Dimensions {
u.setCurrentPath([]string{root})
}
} else {
nextPath := crumbs[1 : index+1]
nextPath := pathSource[:target]
root := u.hiddenVaultRoot()
if root != "" {
nextPath = append([]string{root}, nextPath...)
@@ -3159,7 +3305,12 @@ func (u *ui) pathBar(gtx layout.Context) layout.Dimensions {
btn := material.Button(u.theme, &u.breadcrumbs[index], label)
btn.Background, btn.Color = buttonFocusColors(u.isFocused(breadcrumbFocusID(index)))
btn.TextSize = unit.Sp(11)
btn.Inset = layout.Inset{Top: 5, Bottom: 5, Left: 9, Right: 9}
if u.mode == "phone" {
btn.TextSize = unit.Sp(10)
btn.Inset = layout.Inset{Top: 4, Bottom: 4, Left: 7, Right: 7}
} else {
btn.Inset = layout.Inset{Top: 5, Bottom: 5, Left: 9, Right: 9}
}
return btn.Layout(gtx)
}))
if i < len(crumbs)-1 {
@@ -3174,6 +3325,31 @@ func (u *ui) pathBar(gtx layout.Context) layout.Dimensions {
}()...)
}
func (u *ui) visibleBreadcrumbs(displayPath []string) ([]string, []int) {
if u.state.Section == appstate.SectionTemplates {
return append([]string{"Templates"}, append([]string{}, u.currentPath...)...), func() []int {
indices := make([]int, 0, len(u.currentPath)+1)
indices = append(indices, 0)
for i := range u.currentPath {
indices = append(indices, i+1)
}
return indices
}()
}
if u.mode != "phone" || len(displayPath) <= 2 {
crumbs := append([]string{"/"}, append([]string{}, displayPath...)...)
indices := make([]int, 0, len(crumbs))
indices = append(indices, 0)
for i := range displayPath {
indices = append(indices, i+1)
}
return crumbs, indices
}
crumbs := []string{"/", "…", displayPath[len(displayPath)-2], displayPath[len(displayPath)-1]}
indices := []int{0, len(displayPath) - 2, len(displayPath) - 1, len(displayPath)}
return crumbs, indices
}
func (u *ui) groupBar(gtx layout.Context) layout.Dimensions {
groups := append([]string{}, u.childGroups()...)
if len(u.groupClicks) < len(groups) {
@@ -3185,14 +3361,21 @@ func (u *ui) groupBar(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild {
children := []layout.FlexChild{
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), "Groups")
label := "Groups"
if len(u.displayPath()) > 0 {
label = "Subgroups"
}
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 {
const maxGroupListHeight = 160
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