Add About section
This commit is contained in:
+2
-1
@@ -26,6 +26,7 @@ const (
|
||||
SectionRecycleBin Section = "recycle-bin"
|
||||
SectionAPITokens Section = "api-tokens"
|
||||
SectionAPIAudit Section = "api-audit"
|
||||
SectionAbout Section = "about"
|
||||
)
|
||||
|
||||
type CurrentSession interface {
|
||||
@@ -376,7 +377,7 @@ func (s *State) entriesForSection(model vault.Model) []vault.Entry {
|
||||
return slices.Clone(model.Templates)
|
||||
case SectionRecycleBin:
|
||||
return slices.Clone(model.RecycleBin)
|
||||
case SectionAPITokens, SectionAPIAudit:
|
||||
case SectionAPITokens, SectionAPIAudit, SectionAbout:
|
||||
return nil
|
||||
default:
|
||||
return slices.Clone(model.Entries)
|
||||
|
||||
@@ -334,6 +334,7 @@ type ui struct {
|
||||
showRecycle widget.Clickable
|
||||
showAPITokens widget.Clickable
|
||||
showAPIAudit widget.Clickable
|
||||
showAbout widget.Clickable
|
||||
showLocalLifecycle widget.Clickable
|
||||
showRemoteLifecycle widget.Clickable
|
||||
showSyncLocal widget.Clickable
|
||||
@@ -685,6 +686,8 @@ func (u *ui) searchPlaceholder() string {
|
||||
return "Search audit log"
|
||||
case appstate.SectionRecycleBin:
|
||||
return "Search recycle bin"
|
||||
case appstate.SectionAbout:
|
||||
return "Search disabled on About"
|
||||
default:
|
||||
return "Search vault"
|
||||
}
|
||||
@@ -814,6 +817,14 @@ func (u *ui) showAPIAuditSection() {
|
||||
u.filter()
|
||||
}
|
||||
|
||||
func (u *ui) showAboutSection() {
|
||||
u.resetPasswordPeek()
|
||||
u.rememberEntriesSectionState()
|
||||
u.state.ShowSection(appstate.SectionAbout)
|
||||
u.mainMenuOpen = false
|
||||
u.filter()
|
||||
}
|
||||
|
||||
func (u *ui) returnToMainEntries() {
|
||||
u.clearDeleteGroupConfirmation()
|
||||
u.showEntriesSection()
|
||||
@@ -2752,6 +2763,11 @@ func (u *ui) listEmptyState() emptyState {
|
||||
Title: "No API audit events yet",
|
||||
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:
|
||||
return emptyState{
|
||||
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."
|
||||
case appstate.SectionAPIAudit:
|
||||
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:
|
||||
return "Select a template or start a reusable entry."
|
||||
case appstate.SectionRecycleBin:
|
||||
@@ -3007,6 +3025,10 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
||||
u.clearDeleteGroupConfirmation()
|
||||
u.showAPIAuditSection()
|
||||
}
|
||||
for u.showAbout.Clicked(gtx) {
|
||||
u.clearDeleteGroupConfirmation()
|
||||
u.showAboutSection()
|
||||
}
|
||||
for u.showLocalLifecycle.Clicked(gtx) {
|
||||
if u.lifecycleBusy() {
|
||||
continue
|
||||
@@ -4191,6 +4213,10 @@ func (u *ui) mainMenu(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.showAPIAudit, "API Audit")
|
||||
}),
|
||||
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 {
|
||||
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 {
|
||||
label := "Sync"
|
||||
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 {
|
||||
visibleEntries, entryClicks := u.visibleEntrySnapshot()
|
||||
rows := make([]layout.Widget, 0, 16+len(visibleEntries))
|
||||
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
return u.outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions {
|
||||
editor := material.Editor(u.theme, &u.search, u.searchPlaceholder())
|
||||
editor.Color = u.theme.Palette.Fg
|
||||
editor.HintColor = mutedColor
|
||||
return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout)
|
||||
if u.state.Section != appstate.SectionAbout {
|
||||
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
return u.outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions {
|
||||
editor := material.Editor(u.theme, &u.search, u.searchPlaceholder())
|
||||
editor.Color = u.theme.Palette.Fg
|
||||
editor.HintColor = mutedColor
|
||||
return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout)
|
||||
})
|
||||
})
|
||||
})
|
||||
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Spacer{Height: spacing}.Layout(gtx)
|
||||
})
|
||||
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Spacer{Height: spacing}.Layout(gtx)
|
||||
})
|
||||
}
|
||||
if !u.isVaultLocked() {
|
||||
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 {
|
||||
return layout.Spacer{Height: spacing}.Layout(gtx)
|
||||
})
|
||||
@@ -4352,6 +4444,8 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
|
||||
return btn.Layout(gtx)
|
||||
case appstate.SectionAPITokens:
|
||||
return tonedButton(gtx, u.theme, &u.issueAPIToken, "Issue API Token")
|
||||
case appstate.SectionAbout:
|
||||
return emptyStatePanel(gtx, u.theme, u.listEmptyState())
|
||||
default:
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
@@ -4365,6 +4459,7 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
|
||||
rows = append(rows, u.apiTokenListPanel)
|
||||
case u.state.Section == appstate.SectionAPIAudit:
|
||||
rows = append(rows, u.apiAuditListPanel)
|
||||
case u.state.Section == appstate.SectionAbout:
|
||||
case len(visibleEntries) == 0:
|
||||
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
||||
return emptyStatePanel(gtx, u.theme, u.listEmptyState())
|
||||
@@ -4391,6 +4486,18 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
|
||||
return u.navigationHeader(gtx)
|
||||
}),
|
||||
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 {
|
||||
if u.isVaultLocked() || (u.state.Section != appstate.SectionEntries && u.state.Section != appstate.SectionRecycleBin) {
|
||||
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(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.state.Section == appstate.SectionAbout {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
if u.mode == "phone" {
|
||||
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(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.state.Section == appstate.SectionAbout {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
if u.isVaultLocked() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
@@ -4450,6 +4563,9 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
|
||||
if u.state.Section == appstate.SectionAPIAudit {
|
||||
return u.apiAuditListPanel(gtx)
|
||||
}
|
||||
if u.state.Section == appstate.SectionAbout {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
if len(u.visible) == 0 {
|
||||
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 {
|
||||
if u.mode == "phone" {
|
||||
if u.state.Section != appstate.SectionEntries {
|
||||
if u.state.Section != appstate.SectionEntries && u.state.Section != appstate.SectionAbout {
|
||||
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 layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
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)
|
||||
}),
|
||||
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),
|
||||
}
|
||||
}
|
||||
if u.state.Section == appstate.SectionAbout {
|
||||
return []layout.FlexChild{
|
||||
layout.Flexed(1, u.aboutDetailPanel),
|
||||
}
|
||||
}
|
||||
item, ok := u.selectedEntry()
|
||||
if !ok && !u.editingEntry {
|
||||
return []layout.FlexChild{
|
||||
|
||||
@@ -5485,6 +5485,36 @@ func TestUISearchPlaceholderIsContextual(t *testing.T) {
|
||||
if got := u.searchPlaceholder(); 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) {
|
||||
|
||||
Reference in New Issue
Block a user