From 72e67039cf577719e7e3aa4bf52764bd191bab76 Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Wed, 1 Apr 2026 16:24:11 -0700 Subject: [PATCH] Tighten navigation and admin UI --- main.go | 215 ++++++++++++++++++++++++++++++++++++++++++++++++---- ui_api.go | 112 ++++++++++++++++++--------- ui_forms.go | 153 ++++++++++++++++++++++++++++++------- 3 files changed, 402 insertions(+), 78 deletions(-) diff --git a/main.go b/main.go index e2f90a9..6b08728 100644 --- a/main.go +++ b/main.go @@ -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 diff --git a/ui_api.go b/ui_api.go index 5bde171..d50dd48 100644 --- a/ui_api.go +++ b/ui_api.go @@ -293,16 +293,16 @@ func (u *ui) apiAuditEvents() []apiaudit.Event { return filtered } -func formatAPIPolicyRule(rule apitokens.PolicyRule) string { - scope := strings.Join(rule.Resource.Path, " / ") +func policyRuleParts(rule apitokens.PolicyRule) (string, string, string) { + effect := strings.ToUpper(string(rule.Effect)) + operation := string(rule.Operation) + resource := "/" if rule.Resource.Kind == apitokens.ResourceEntry { - scope = "entry " + rule.Resource.EntryID + resource = "Entry: " + rule.Resource.EntryID + } else if len(rule.Resource.Path) > 0 { + resource = strings.Join(rule.Resource.Path, " / ") } - return strings.TrimSpace(strings.Join([]string{ - strings.ToUpper(string(rule.Effect)), - string(rule.Operation), - scope, - }, " ")) + return effect, operation, resource } func uiHasPolicyRule(rules []apitokens.PolicyRule, target apitokens.PolicyRule) bool { @@ -346,6 +346,12 @@ func (u *ui) apiTokenRow(gtx layout.Context, click *widget.Clickable, idx int, t if token.ExpiresAt != nil { text = "Expires " + token.ExpiresAt.Local().Format(time.RFC3339) } + if token.Disabled { + text = "Disabled · " + text + } + if token.RevokedAt != nil { + text = "Revoked · " + text + } lbl := material.Label(u.theme, unit.Sp(12), text) lbl.Color = mutedColor return lbl.Layout(gtx) @@ -392,6 +398,11 @@ func (u *ui) apiAuditRow(gtx layout.Context, click *widget.Clickable, idx int, e lbl.Color = accentColor return lbl.Layout(gtx) }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(12), string(event.Operation)) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(12), event.At.Local().Format(time.RFC3339)) lbl.Color = mutedColor @@ -441,9 +452,15 @@ func (u *ui) apiAuditListPanel(gtx layout.Context) layout.Dimensions { return lbl.Layout(gtx) }), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(12), "Filter by typing a token name, decision, operation, or resource in Search vault.") + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { if len(events) == 0 { - lbl := material.Label(u.theme, unit.Sp(14), "No audit events yet.") + lbl := material.Label(u.theme, unit.Sp(14), "No audit events yet. Approval prompts, denials, token changes, and filled requests will appear here.") lbl.Color = mutedColor return lbl.Layout(gtx) } @@ -520,25 +537,33 @@ func (u *ui) apiTokenDetailPanel(gtx layout.Context) layout.Dimensions { rows = append(rows, layout.Spacer{Height: unit.Dp(10)}.Layout, func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.saveAPIToken, "Save Token") + return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.saveAPIToken, "Save Token") + }), + layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.rotateAPIToken, "Rotate") + }), + layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.disableAPIToken, "Disable") + }), + ) }), - layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.rotateAPIToken, "Rotate") - }), - layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.disableAPIToken, "Disable") - }), - layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.revokeAPIToken, "Revoke") - }), - layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.deleteAPIToken, "Delete") + return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.revokeAPIToken, "Revoke") + }), + layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.deleteAPIToken, "Delete") + }), + ) }), ) }, @@ -553,21 +578,38 @@ func (u *ui) apiTokenDetailPanel(gtx layout.Context) layout.Dimensions { if ok && len(token.Policies) > 0 { for i, rule := range token.Policies { index := i - ruleText := formatAPIPolicyRule(rule) + effect, operation, resource := policyRuleParts(rule) rows = append(rows, func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Alignment: layout.Middle}.Layout(gtx, - layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(13), ruleText) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), + 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 { - return tonedButton(gtx, u.theme, &removeClicks[index], "Remove") + return layout.Flex{Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(12), effect) + lbl.Color = accentColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(12), operation) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &removeClicks[index], "Remove") + }), + ) + }), + 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), resource) + lbl.Color = mutedColor + return lbl.Layout(gtx) }), ) - }, + }) + }, layout.Spacer{Height: unit.Dp(6)}.Layout, ) } diff --git a/ui_forms.go b/ui_forms.go index 7560111..d080577 100644 --- a/ui_forms.go +++ b/ui_forms.go @@ -36,9 +36,22 @@ func (u *ui) lifecycleControls(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 { + lbl := material.Label(u.theme, unit.Sp(12), "LOCATION") + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), layout.Rigid(labeledEditorHelp(u.theme, "Remote Base URL", "Base WebDAV endpoint, for example https://server/remote.php/webdav.", &u.remoteBaseURL, false)), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(labeledEditorHelp(u.theme, "Remote Path", "Path to the remote .kdbx file under the WebDAV base URL.", &u.remotePath, false)), + layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(12), "AUTHENTICATION") + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(labeledEditorHelp(u.theme, "Remote Username", "Username used to authenticate to the WebDAV server.", &u.remoteUsername, false)), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), @@ -57,6 +70,35 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { } return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(selectorEditorHelp(u.theme, "Vault Path", "Choose the existing .kdbx file to open.", &u.vaultPath, &u.pickVaultPath, "Choose File", false)), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if strings.TrimSpace(u.vaultPath.Text()) == "" { + return layout.Dimensions{} + } + return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(10)).Layout(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(12), "SELECTED VAULT") + 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(14), friendlyRecentVaultLabel(u.vaultPath.Text())) + lbl.Color = accentColor + 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(11), u.vaultPath.Text()) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + ) + }) + }) + }), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(u.recentVaultList), ) @@ -146,7 +188,25 @@ func (u *ui) recentVaultList(gtx layout.Context) layout.Dimensions { label = friendly } children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.recentVaultClicks[index], label) + return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { + return u.recentVaultClicks[index].Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(10)).Layout(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(14), label) + lbl.Color = accentColor + 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(11), path) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + ) + }) + }) + }) })) if i < len(u.recentVaults)-1 { children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout)) @@ -179,7 +239,25 @@ func (u *ui) recentRemoteList(gtx layout.Context) layout.Dimensions { index := i label := friendlyRecentRemoteLabel(record) children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.recentRemoteClicks[index], label) + return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { + return u.recentRemoteClicks[index].Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(10)).Layout(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(14), label) + lbl.Color = accentColor + 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(11), strings.TrimSpace(record.BaseURL)) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + ) + }) + }) + }) })) if i < len(u.recentRemotes)-1 { children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout)) @@ -288,7 +366,7 @@ func (u *ui) customFieldEditorPanel(gtx layout.Context) layout.Dimensions { if len(u.customFieldKeys) == 1 && (strings.TrimSpace(u.customFieldKeys[index].Text()) != "" || strings.TrimSpace(u.customFieldValues[index].Text()) != "") { return layout.Dimensions{} } - return tonedButton(gtx, u.theme, &u.removeCustomFields[index], "-") + return tonedButton(gtx, u.theme, &u.removeCustomFields[index], "Remove") }), ) })) @@ -304,7 +382,7 @@ func (u *ui) customFieldEditorPanel(gtx layout.Context) layout.Dimensions { for u.addCustomField.Clicked(gtx) { u.appendCustomFieldRow("", "") } - return tonedButton(gtx, u.theme, &u.addCustomField, "+") + return tonedButton(gtx, u.theme, &u.addCustomField, "Add Custom Field") }), ) } @@ -416,36 +494,44 @@ 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 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 - if u.groupControlsHidden { - icon = u.expandMoreIcon - } - if icon == nil { - lbl := material.Label(u.theme, unit.Sp(16), ">") - if !u.groupControlsHidden { - lbl.Text = "v" + return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(6)).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.groupControlsHidden { + icon = u.expandMoreIcon } - lbl.Color = accentColor + if icon == nil { + lbl := material.Label(u.theme, unit.Sp(16), ">") + if !u.groupControlsHidden { + lbl.Text = "v" + } + lbl.Color = accentColor + return lbl.Layout(gtx) + } + return icon.Layout(gtx, accentColor) + }), + 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 tools") + lbl.Color = mutedColor return lbl.Layout(gtx) - } - return icon.Layout(gtx, accentColor) - }), - 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 Tools") - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - ) + }), + ) + }) }) }) } func (u *ui) entryEditorPanel(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(12), "BASICS") + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(labeledEditorWithFocus(u.theme, "Title", &u.entryTitle, false, u.isFocused(detailFocusID(detailFieldTitle)))), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(labeledEditorWithFocus(u.theme, "Username", &u.entryUsername, false, u.isFocused(detailFocusID(detailFieldUsername)))), @@ -465,10 +551,23 @@ func (u *ui) entryEditorPanel(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 { + lbl := material.Label(u.theme, unit.Sp(12), "NOTES") + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(labeledMultilineEditorWithFocus(u.theme, "Notes", &u.entryNotes, false, u.isFocused(detailFocusID(detailFieldNotes)), unit.Dp(120))), + layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(u.customFieldEditorPanel), + layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(12), "HISTORY") + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(labeledEditorWithFocus(u.theme, "History Index", &u.historyIndex, false, u.isFocused(detailFocusID(detailFieldHistoryIndex)))), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), @@ -508,7 +607,7 @@ func (u *ui) entryEditorPanel(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.copyURL, "Copy URL") }), ) }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), + layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(12), "ATTACHMENTS") lbl.Color = mutedColor