Add About section
ci / lint-test (push) Successful in 1m12s
ci / build (push) Successful in 2m33s

This commit is contained in:
Joe Julian
2026-04-05 23:42:53 -07:00
parent c19bdd48e2
commit 9aeb98da58
3 changed files with 176 additions and 14 deletions
+2 -1
View File
@@ -26,6 +26,7 @@ const (
SectionRecycleBin Section = "recycle-bin" SectionRecycleBin Section = "recycle-bin"
SectionAPITokens Section = "api-tokens" SectionAPITokens Section = "api-tokens"
SectionAPIAudit Section = "api-audit" SectionAPIAudit Section = "api-audit"
SectionAbout Section = "about"
) )
type CurrentSession interface { type CurrentSession interface {
@@ -376,7 +377,7 @@ func (s *State) entriesForSection(model vault.Model) []vault.Entry {
return slices.Clone(model.Templates) return slices.Clone(model.Templates)
case SectionRecycleBin: case SectionRecycleBin:
return slices.Clone(model.RecycleBin) return slices.Clone(model.RecycleBin)
case SectionAPITokens, SectionAPIAudit: case SectionAPITokens, SectionAPIAudit, SectionAbout:
return nil return nil
default: default:
return slices.Clone(model.Entries) return slices.Clone(model.Entries)
+144 -13
View File
@@ -334,6 +334,7 @@ type ui struct {
showRecycle widget.Clickable showRecycle widget.Clickable
showAPITokens widget.Clickable showAPITokens widget.Clickable
showAPIAudit widget.Clickable showAPIAudit widget.Clickable
showAbout widget.Clickable
showLocalLifecycle widget.Clickable showLocalLifecycle widget.Clickable
showRemoteLifecycle widget.Clickable showRemoteLifecycle widget.Clickable
showSyncLocal widget.Clickable showSyncLocal widget.Clickable
@@ -685,6 +686,8 @@ func (u *ui) searchPlaceholder() string {
return "Search audit log" return "Search audit log"
case appstate.SectionRecycleBin: case appstate.SectionRecycleBin:
return "Search recycle bin" return "Search recycle bin"
case appstate.SectionAbout:
return "Search disabled on About"
default: default:
return "Search vault" return "Search vault"
} }
@@ -814,6 +817,14 @@ func (u *ui) showAPIAuditSection() {
u.filter() u.filter()
} }
func (u *ui) showAboutSection() {
u.resetPasswordPeek()
u.rememberEntriesSectionState()
u.state.ShowSection(appstate.SectionAbout)
u.mainMenuOpen = false
u.filter()
}
func (u *ui) returnToMainEntries() { func (u *ui) returnToMainEntries() {
u.clearDeleteGroupConfirmation() u.clearDeleteGroupConfirmation()
u.showEntriesSection() u.showEntriesSection()
@@ -2752,6 +2763,11 @@ func (u *ui) listEmptyState() emptyState {
Title: "No API audit events yet", Title: "No API audit events yet",
Body: "Connect a trusted client, respond to approval prompts, or issue a token to start recording activity.", Body: "Connect a trusted client, respond to approval prompts, or issue a token to start recording activity.",
} }
case appstate.SectionAbout:
return emptyState{
Title: "About KeePassGO",
Body: "Product details, compatibility notes, and platform targets appear in the detail pane.",
}
case appstate.SectionTemplates: case appstate.SectionTemplates:
return emptyState{ return emptyState{
Title: "Templates unavailable", Title: "Templates unavailable",
@@ -2790,6 +2806,8 @@ func (u *ui) detailPlaceholderMessage() string {
return "Select an API token, issue a new one, or search to narrow the list." return "Select an API token, issue a new one, or search to narrow the list."
case appstate.SectionAPIAudit: case appstate.SectionAPIAudit:
return "Select an audit event to inspect it, or use Search audit log or the quick filters above." return "Select an audit event to inspect it, or use Search audit log or the quick filters above."
case appstate.SectionAbout:
return "Review the product overview, platform support, and compatibility goals."
case appstate.SectionTemplates: case appstate.SectionTemplates:
return "Select a template or start a reusable entry." return "Select a template or start a reusable entry."
case appstate.SectionRecycleBin: case appstate.SectionRecycleBin:
@@ -3007,6 +3025,10 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
u.clearDeleteGroupConfirmation() u.clearDeleteGroupConfirmation()
u.showAPIAuditSection() u.showAPIAuditSection()
} }
for u.showAbout.Clicked(gtx) {
u.clearDeleteGroupConfirmation()
u.showAboutSection()
}
for u.showLocalLifecycle.Clicked(gtx) { for u.showLocalLifecycle.Clicked(gtx) {
if u.lifecycleBusy() { if u.lifecycleBusy() {
continue continue
@@ -4191,6 +4213,10 @@ func (u *ui) mainMenu(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.showAPIAudit, "API Audit") return tonedButton(gtx, u.theme, &u.showAPIAudit, "API Audit")
}), }),
layout.Rigid(layout.Spacer{Height: 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.showAbout, "About")
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Settings") return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Settings")
}), }),
@@ -4198,6 +4224,70 @@ func (u *ui) mainMenu(gtx layout.Context) layout.Dimensions {
}) })
} }
func (u *ui) aboutDetailPanel(gtx layout.Context) layout.Dimensions {
rows := []layout.Widget{
func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(22), "KeePassGO")
lbl.Color = accentColor
return lbl.Layout(gtx)
},
func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(15), "A KeePass-compatible password manager built in Go.")
lbl.Color = mutedColor
return lbl.Layout(gtx)
},
layout.Spacer{Height: unit.Dp(14)}.Layout,
aboutFact(u.theme, "Compatibility", "KeePass and KDBX interoperability", "Designed to coexist with desktop KeePass and KeePass2Android workflows."),
layout.Spacer{Height: unit.Dp(10)}.Layout,
aboutFact(u.theme, "Platforms", "Windows and Linux first, Android supported", "Desktop remains the primary product surface while Android stays compatible."),
layout.Spacer{Height: unit.Dp(10)}.Layout,
aboutFact(u.theme, "Sync", "Local files and direct WebDAV", "Remote-file workflows are first-class and avoid browser-stack dependencies."),
layout.Spacer{Height: unit.Dp(10)}.Layout,
aboutFact(u.theme, "Programmatic Access", "Secure local gRPC API", "Built for trusted clients such as browser extensions and automation."),
layout.Spacer{Height: unit.Dp(14)}.Layout,
func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), "Runtime")
lbl.Color = mutedColor
return lbl.Layout(gtx)
},
func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(14), fmt.Sprintf("%s on %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH))
lbl.Color = u.theme.Palette.Fg
return lbl.Layout(gtx)
},
}
return material.List(u.theme, &u.detailList).Layout(gtx, len(rows), func(gtx layout.Context, i int) layout.Dimensions {
return rows[i](gtx)
})
}
func aboutFact(theme *material.Theme, title, primary, secondary string) layout.Widget {
return func(gtx layout.Context) 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(theme, unit.Sp(12), strings.ToUpper(title))
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(theme, unit.Sp(16), primary)
lbl.Color = theme.Palette.Fg
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(theme, unit.Sp(13), secondary)
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
)
})
})
}
}
func (u *ui) syncButtonGroup(gtx layout.Context) layout.Dimensions { func (u *ui) syncButtonGroup(gtx layout.Context) layout.Dimensions {
label := "Sync" label := "Sync"
spacing := unit.Dp(4) spacing := unit.Dp(4)
@@ -4308,21 +4398,23 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
return panel(gtx, func(gtx layout.Context) layout.Dimensions { return panel(gtx, func(gtx layout.Context) layout.Dimensions {
visibleEntries, entryClicks := u.visibleEntrySnapshot() visibleEntries, entryClicks := u.visibleEntrySnapshot()
rows := make([]layout.Widget, 0, 16+len(visibleEntries)) rows := make([]layout.Widget, 0, 16+len(visibleEntries))
rows = append(rows, func(gtx layout.Context) layout.Dimensions { if u.state.Section != appstate.SectionAbout {
gtx.Constraints.Min.X = gtx.Constraints.Max.X rows = append(rows, func(gtx layout.Context) layout.Dimensions {
return u.outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions { gtx.Constraints.Min.X = gtx.Constraints.Max.X
editor := material.Editor(u.theme, &u.search, u.searchPlaceholder()) return u.outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions {
editor.Color = u.theme.Palette.Fg editor := material.Editor(u.theme, &u.search, u.searchPlaceholder())
editor.HintColor = mutedColor editor.Color = u.theme.Palette.Fg
return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout) editor.HintColor = mutedColor
return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout)
})
}) })
}) rows = append(rows, func(gtx layout.Context) layout.Dimensions {
rows = append(rows, func(gtx layout.Context) layout.Dimensions { return layout.Spacer{Height: spacing}.Layout(gtx)
return layout.Spacer{Height: spacing}.Layout(gtx) })
}) }
if !u.isVaultLocked() { if !u.isVaultLocked() {
rows = append(rows, u.navigationHeader) rows = append(rows, u.navigationHeader)
if u.state.Section == appstate.SectionEntries { if u.state.Section == appstate.SectionEntries || u.state.Section == appstate.SectionAbout {
rows = append(rows, func(gtx layout.Context) layout.Dimensions { rows = append(rows, func(gtx layout.Context) layout.Dimensions {
return layout.Spacer{Height: spacing}.Layout(gtx) return layout.Spacer{Height: spacing}.Layout(gtx)
}) })
@@ -4352,6 +4444,8 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
return btn.Layout(gtx) return btn.Layout(gtx)
case appstate.SectionAPITokens: case appstate.SectionAPITokens:
return tonedButton(gtx, u.theme, &u.issueAPIToken, "Issue API Token") return tonedButton(gtx, u.theme, &u.issueAPIToken, "Issue API Token")
case appstate.SectionAbout:
return emptyStatePanel(gtx, u.theme, u.listEmptyState())
default: default:
return layout.Dimensions{} return layout.Dimensions{}
} }
@@ -4365,6 +4459,7 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
rows = append(rows, u.apiTokenListPanel) rows = append(rows, u.apiTokenListPanel)
case u.state.Section == appstate.SectionAPIAudit: case u.state.Section == appstate.SectionAPIAudit:
rows = append(rows, u.apiAuditListPanel) rows = append(rows, u.apiAuditListPanel)
case u.state.Section == appstate.SectionAbout:
case len(visibleEntries) == 0: case len(visibleEntries) == 0:
rows = append(rows, func(gtx layout.Context) layout.Dimensions { rows = append(rows, func(gtx layout.Context) layout.Dimensions {
return emptyStatePanel(gtx, u.theme, u.listEmptyState()) return emptyStatePanel(gtx, u.theme, u.listEmptyState())
@@ -4391,6 +4486,18 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
return u.navigationHeader(gtx) return u.navigationHeader(gtx)
}), }),
layout.Rigid(layout.Spacer{Height: spacing}.Layout), layout.Rigid(layout.Spacer{Height: spacing}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.state.Section == appstate.SectionAbout {
return emptyStatePanel(gtx, u.theme, u.listEmptyState())
}
return layout.Dimensions{}
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.state.Section == appstate.SectionAbout {
return layout.Dimensions{}
}
return layout.Spacer{Height: spacing}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.isVaultLocked() || (u.state.Section != appstate.SectionEntries && u.state.Section != appstate.SectionRecycleBin) { if u.isVaultLocked() || (u.state.Section != appstate.SectionEntries && u.state.Section != appstate.SectionRecycleBin) {
return layout.Dimensions{} return layout.Dimensions{}
@@ -4413,6 +4520,9 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
}), }),
layout.Rigid(layout.Spacer{Height: spacing}.Layout), layout.Rigid(layout.Spacer{Height: spacing}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.state.Section == appstate.SectionAbout {
return layout.Dimensions{}
}
if u.mode == "phone" { if u.mode == "phone" {
gtx.Constraints.Min.X = gtx.Constraints.Max.X gtx.Constraints.Min.X = gtx.Constraints.Max.X
} }
@@ -4425,6 +4535,9 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
}), }),
layout.Rigid(layout.Spacer{Height: spacing}.Layout), layout.Rigid(layout.Spacer{Height: spacing}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.state.Section == appstate.SectionAbout {
return layout.Dimensions{}
}
if u.isVaultLocked() { if u.isVaultLocked() {
return layout.Dimensions{} return layout.Dimensions{}
} }
@@ -4450,6 +4563,9 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
if u.state.Section == appstate.SectionAPIAudit { if u.state.Section == appstate.SectionAPIAudit {
return u.apiAuditListPanel(gtx) return u.apiAuditListPanel(gtx)
} }
if u.state.Section == appstate.SectionAbout {
return layout.Dimensions{}
}
if len(u.visible) == 0 { if len(u.visible) == 0 {
return emptyStatePanel(gtx, u.theme, u.listEmptyState()) return emptyStatePanel(gtx, u.theme, u.listEmptyState())
} }
@@ -4465,13 +4581,23 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
func (u *ui) navigationHeader(gtx layout.Context) layout.Dimensions { func (u *ui) navigationHeader(gtx layout.Context) layout.Dimensions {
if u.mode == "phone" { if u.mode == "phone" {
if u.state.Section != appstate.SectionEntries { if u.state.Section != appstate.SectionEntries && u.state.Section != appstate.SectionAbout {
return layout.Dimensions{} return layout.Dimensions{}
} }
if u.state.Section == appstate.SectionAbout {
lbl := material.Label(u.theme, unit.Sp(18), "About")
lbl.Color = accentColor
return lbl.Layout(gtx)
}
return u.groupControlsDisclosure(gtx) return u.groupControlsDisclosure(gtx)
} }
return layout.Flex{Alignment: layout.Middle}.Layout(gtx, return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
if u.state.Section == appstate.SectionAbout {
lbl := material.Label(u.theme, unit.Sp(18), "About")
lbl.Color = accentColor
return lbl.Layout(gtx)
}
return u.sectionBar(gtx) return u.sectionBar(gtx)
}), }),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
@@ -4772,6 +4898,11 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions {
layout.Flexed(1, u.apiAuditDetailPanel), layout.Flexed(1, u.apiAuditDetailPanel),
} }
} }
if u.state.Section == appstate.SectionAbout {
return []layout.FlexChild{
layout.Flexed(1, u.aboutDetailPanel),
}
}
item, ok := u.selectedEntry() item, ok := u.selectedEntry()
if !ok && !u.editingEntry { if !ok && !u.editingEntry {
return []layout.FlexChild{ return []layout.FlexChild{
+30
View File
@@ -5485,6 +5485,36 @@ func TestUISearchPlaceholderIsContextual(t *testing.T) {
if got := u.searchPlaceholder(); got != "Search audit log" { if got := u.searchPlaceholder(); got != "Search audit log" {
t.Fatalf("api audit searchPlaceholder() = %q, want %q", got, "Search audit log") t.Fatalf("api audit searchPlaceholder() = %q, want %q", got, "Search audit log")
} }
u.showAboutSection()
if got := u.searchPlaceholder(); got != "Search disabled on About" {
t.Fatalf("about searchPlaceholder() = %q, want %q", got, "Search disabled on About")
}
}
func TestShowAboutSection(t *testing.T) {
t.Parallel()
u := newUIWithModel("desktop", vault.Model{
Entries: []vault.Entry{{ID: "entry-1", Title: "Bellagio", Path: []string{"Crew"}}},
})
u.mainMenuOpen = true
u.state.CurrentPath = []string{"Crew"}
u.state.SelectedEntryID = "entry-1"
u.showAboutSection()
if got := u.state.Section; got != appstate.SectionAbout {
t.Fatalf("state.Section = %q, want %q", got, appstate.SectionAbout)
}
if u.mainMenuOpen {
t.Fatal("mainMenuOpen = true, want false")
}
if len(u.state.CurrentPath) != 0 {
t.Fatalf("state.CurrentPath = %v, want empty", u.state.CurrentPath)
}
if got := u.state.SelectedEntryID; got != "" {
t.Fatalf("state.SelectedEntryID = %q, want empty", got)
}
} }
func TestUIAPIPolicyTargetActionsUseCurrentContext(t *testing.T) { func TestUIAPIPolicyTargetActionsUseCurrentContext(t *testing.T) {