Compare commits

...

5 Commits

Author SHA1 Message Date
Joe Julian 70f18e89bf Use fixed APK signing key in CI
ci / lint-test (push) Successful in 1m12s
ci / build (push) Successful in 2m36s
2026-04-06 07:26:48 -07:00
Joe Julian c361ec5ba3 Keep remote form open during manual entry
ci / lint-test (push) Successful in 1m11s
ci / build (push) Successful in 2m32s
2026-04-06 07:13:12 -07:00
Joe Julian 0c6d707325 Fix Android group browser scrolling
ci / lint-test (push) Successful in 1m16s
ci / build (push) Successful in 2m37s
2026-04-06 00:04:38 -07:00
Joe Julian 1c72a5009f Stamp app version into builds
ci / lint-test (push) Successful in 1m17s
ci / build (push) Successful in 2m33s
2026-04-05 23:56:58 -07:00
Joe Julian 9aeb98da58 Add About section
ci / lint-test (push) Successful in 1m12s
ci / build (push) Successful in 2m33s
2026-04-05 23:42:53 -07:00
10 changed files with 326 additions and 44 deletions
+8 -2
View File
@@ -107,6 +107,7 @@ jobs:
shell: bash
run: |
set -euo pipefail
app_version="$(git describe --tags --always --dirty)"
# Gio needs a Linux ARM64 cgo cross-toolchain for desktop builds.
# Keep the CI matrix to targets this runner can build reproducibly.
for target in \
@@ -126,14 +127,19 @@ jobs:
ext=".exe"
fi
out="${DIST_DIR}/keepassgo-${goos}-${goarch}${ext}"
GOOS="${goos}" GOARCH="${goarch}" CGO_ENABLED="${cgo_enabled}" go build -o "${out}" .
GOOS="${goos}" GOARCH="${goarch}" CGO_ENABLED="${cgo_enabled}" \
go build -ldflags "-X main.appVersion=${app_version}" -o "${out}" .
done
- name: Build APK
shell: bash
run: |
set -euo pipefail
make apk
signkey_path="$(mktemp)"
trap 'rm -f -- "$signkey_path"' EXIT
printf '%s' '${{ secrets.APK_SIGNKEY_B64 }}' | base64 -d > "$signkey_path"
export APP_VERSION="$(git describe --tags --always --dirty)"
make apk SIGNKEY="$signkey_path" SIGNPASS='${{ secrets.APK_SIGNPASS }}'
cp build/keepassgo.apk "${DIST_DIR}/keepassgo.apk"
- name: Upload CI artifacts
+1
View File
@@ -12,6 +12,7 @@ Environment:
- `ANDROID_NDK_ROOT` defaults to `/opt/android-ndk`.
- `JAVA_HOME` defaults to `/usr/lib/jvm/java-25-openjdk`.
- `APP_ID` overrides the Android application id.
- `APP_VERSION` overrides the version shown inside KeePassGO itself.
- `APK_OUT` overrides the output path.
- `APK_VERSION` overrides the packaged app version.
- `ANDROID_MIN_SDK` overrides the minimum supported Android SDK.
+14
View File
@@ -5,8 +5,20 @@ PATH := $(JAVA_HOME)/bin:$(ANDROID_SDK_ROOT)/cmdline-tools/latest/bin:$(ANDROID_
APP_ID ?= org.julianfamily.keepassgo
APK_OUT ?= build/keepassgo.apk
APK_VERSION ?= 0.1.0.1
APP_VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
GO_LDFLAGS ?= -X main.appVersion=$(APP_VERSION)
ANDROID_MIN_SDK ?= 28
ANDROID_TARGET_SDK ?= 35
SIGNKEY ?=
SIGNPASS ?=
GOGIO_SIGN_FLAGS :=
ifneq ($(strip $(SIGNKEY)),)
GOGIO_SIGN_FLAGS += -signkey $(SIGNKEY)
endif
ifneq ($(strip $(SIGNPASS)),)
GOGIO_SIGN_FLAGS += -signpass $(SIGNPASS)
endif
.PHONY: apk
apk: android/keepassgo-android.jar
@@ -24,6 +36,8 @@ apk: android/keepassgo-android.jar
go tool gogio -target android \
-buildmode exe \
-appid $(APP_ID) \
-ldflags "$(GO_LDFLAGS)" \
$(GOGIO_SIGN_FLAGS) \
-o $(APK_OUT) \
-version $(APK_VERSION) \
-minsdk $(ANDROID_MIN_SDK) \
+7
View File
@@ -41,6 +41,13 @@ Desktop build:
go build ./...
```
By default, build outputs stamp the app version from `git describe --tags --always --dirty`.
You can override the version shown in KeePassGO with:
```bash
go build -ldflags "-X main.appVersion=v0.0.1" ./...
```
## Arch Linux Package
An AUR-style package definition for the Linux desktop client lives under:
+2 -1
View File
@@ -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)
+4
View File
@@ -13,6 +13,7 @@ const (
DefaultAppID = "org.julianfamily.keepassgo"
DefaultAPKOut = "build/keepassgo.apk"
DefaultVersion = "0.1.0.1"
DefaultLdflags = "-X main.appVersion=dev"
DefaultMinSDK = "28"
DefaultTargetSDK = "35"
DefaultIconPath = "assets/keepassgo-icon.png"
@@ -25,6 +26,7 @@ type Config struct {
AppID string
APKOut string
Version string
Ldflags string
MinSDK string
TargetSDK string
IconPath string
@@ -38,6 +40,7 @@ func DefaultConfig() Config {
AppID: DefaultAppID,
APKOut: DefaultAPKOut,
Version: DefaultVersion,
Ldflags: DefaultLdflags,
MinSDK: DefaultMinSDK,
TargetSDK: DefaultTargetSDK,
IconPath: DefaultIconPath,
@@ -49,6 +52,7 @@ func (c Config) GogioArgs() []string {
"-target", "android",
"-buildmode", "exe",
"-appid", c.AppID,
"-ldflags", c.Ldflags,
"-o", c.APKOut,
"-version", c.Version,
"-minsdk", c.MinSDK,
+3
View File
@@ -15,6 +15,7 @@ func TestDefaultConfigGogioArgs(t *testing.T) {
"-target", "android",
"-buildmode", "exe",
"-appid", DefaultAppID,
"-ldflags", DefaultLdflags,
"-o", DefaultAPKOut,
"-version", DefaultVersion,
"-minsdk", DefaultMinSDK,
@@ -52,6 +53,7 @@ func TestValidateAcceptsCompleteAndroidToolchainLayout(t *testing.T) {
AppID: DefaultAppID,
APKOut: DefaultAPKOut,
Version: DefaultVersion,
Ldflags: DefaultLdflags,
MinSDK: DefaultMinSDK,
TargetSDK: DefaultTargetSDK,
IconPath: filepath.Join(root, "icon.png"),
@@ -77,6 +79,7 @@ func TestValidateRejectsMissingPrerequisites(t *testing.T) {
AppID: DefaultAppID,
APKOut: DefaultAPKOut,
Version: DefaultVersion,
Ldflags: DefaultLdflags,
MinSDK: DefaultMinSDK,
TargetSDK: DefaultTargetSDK,
}
+175 -39
View File
@@ -43,6 +43,15 @@ import (
"golang.org/x/exp/shiny/materialdesign/icons"
)
var appVersion = "dev"
func currentAppVersion() string {
if strings.TrimSpace(appVersion) == "" {
return "dev"
}
return strings.TrimSpace(appVersion)
}
type entry = vault.Entry
const (
@@ -334,6 +343,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
@@ -422,6 +432,7 @@ type ui struct {
syncDialogOpen bool
syncMenuOpen bool
mainMenuOpen bool
selectedRemoteConnection bool
securityDialogOpen bool
remotePrefsDialogOpen bool
showSyncPassword bool
@@ -685,6 +696,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 +827,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()
@@ -1759,7 +1780,7 @@ func (u *ui) hasSelectedLifecycleTarget() bool {
}
func (u *ui) hasSelectedRemoteTarget() bool {
return strings.TrimSpace(u.remoteBaseURL.Text()) != "" && strings.TrimSpace(u.remotePath.Text()) != ""
return u.selectedRemoteConnection
}
func (u *ui) latestRecentVault() (string, time.Time) {
@@ -1810,6 +1831,7 @@ func (u *ui) switchToLifecycleSelection(mode string) {
u.remoteUsername.SetText("")
u.remotePassword.SetText("")
u.rememberRemoteAuth.Value = false
u.selectedRemoteConnection = false
default:
u.vaultPath.SetText("")
u.remoteBaseURL.SetText("")
@@ -1817,6 +1839,7 @@ func (u *ui) switchToLifecycleSelection(mode string) {
u.remoteUsername.SetText("")
u.remotePassword.SetText("")
u.rememberRemoteAuth.Value = false
u.selectedRemoteConnection = false
}
u.requestMasterPassFocus = u.hasSelectedLifecycleTarget()
u.filter()
@@ -1865,6 +1888,7 @@ func (u *ui) applyRecentRemoteRecord(record recentRemoteRecord) {
u.remotePassword.SetText(record.Password)
u.remotePassword.Mask = '•'
u.rememberRemoteAuth.Value = strings.TrimSpace(record.Username) != "" || record.Password != ""
u.selectedRemoteConnection = true
}
func (u *ui) remotePreferencesCurrentSummary() string {
@@ -2752,6 +2776,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 +2819,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 +3038,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
@@ -3019,6 +3054,7 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
continue
}
u.lifecycleMode = "remote"
u.selectedRemoteConnection = false
u.requestMasterPassFocus = true
}
for u.toggleLifecycleAdvanced.Clicked(gtx) {
@@ -3216,6 +3252,7 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
u.switchToLifecycleSelection("remote")
continue
}
u.selectedRemoteConnection = false
u.remoteBaseURL.SetText("")
u.remotePath.SetText("")
u.remoteUsername.SetText("")
@@ -4191,6 +4228,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 +4239,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), "Version")
lbl.Color = mutedColor
return lbl.Layout(gtx)
},
func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(14), currentAppVersion())
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 +4413,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 +4459,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 +4474,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 +4501,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 +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.mode == "phone" {
gtx.Constraints.Min.X = gtx.Constraints.Max.X
}
@@ -4425,6 +4550,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 +4578,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 +4596,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 +4913,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{
@@ -5484,32 +5630,22 @@ func (u *ui) groupBar(gtx layout.Context) layout.Dimensions {
if atRoot {
u.phoneGroupBrowserExpanded = true
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if len(groups) == 0 {
return layout.Dimensions{}
}
maxY := gtx.Dp(unit.Dp(168))
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)
})
children := make([]layout.FlexChild, 0, len(groups))
for i := range groups {
idx := i
name := groups[i]
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
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)
})
}),
)
}))
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
+109 -1
View File
@@ -775,6 +775,37 @@ func TestUIPhoneGroupBrowserToggleDoesNotChangeCurrentGroupToolsState(t *testing
}
}
func TestUIPhoneGroupBarDoesNotClampScrollableContentHeight(t *testing.T) {
t.Parallel()
u := newUIWithModel("phone", vault.Model{
Groups: [][]string{
{"Crew"},
{"Crew", "One"},
{"Crew", "Two"},
{"Crew", "Three"},
{"Crew", "Four"},
{"Crew", "Five"},
{"Crew", "Six"},
{"Crew", "Seven"},
{"Crew", "Eight"},
},
})
u.setCurrentPath([]string{"Crew"})
ops := new(op.Ops)
gtx := layout.Context{
Ops: ops,
Constraints: layout.Exact(image.Pt(1080, 2400)),
}
dims := u.groupBar(gtx)
minOldCap := gtx.Dp(unit.Dp(220))
if dims.Size.Y <= minOldCap {
t.Fatalf("groupBar() phone height = %d, want > %d to avoid nested-scroll clamp", dims.Size.Y, minOldCap)
}
}
func TestUIPhoneStartsWithGroupToolsCollapsed(t *testing.T) {
t.Parallel()
@@ -4390,8 +4421,16 @@ func TestShowRemoteConnectionChooser(t *testing.T) {
u.remoteBaseURL.SetText("https://dav.crew.example.invalid")
u.remotePath.SetText("vaults/bellagio.kdbx")
if got := u.showRemoteConnectionChooser(); !got {
t.Fatal("showRemoteConnectionChooser() = false, want true while manually entering a remote connection")
}
u.applyRecentRemoteRecord(recentRemoteRecord{
BaseURL: "https://dav.crew.example.invalid",
Path: "vaults/bellagio.kdbx",
})
if got := u.showRemoteConnectionChooser(); got {
t.Fatal("showRemoteConnectionChooser() = true, want false when a remote connection is selected")
t.Fatal("showRemoteConnectionChooser() = true, want false after selecting a saved remote connection")
}
u.lifecycleMode = "local"
@@ -4400,6 +4439,26 @@ func TestShowRemoteConnectionChooser(t *testing.T) {
}
}
func TestApplyingRecentRemoteRecordMarksSelectedRemoteConnection(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
if u.hasSelectedRemoteTarget() {
t.Fatal("hasSelectedRemoteTarget() = true, want false before selecting a saved remote connection")
}
u.applyRecentRemoteRecord(recentRemoteRecord{
BaseURL: "https://dav.crew.example.invalid",
Path: "vaults/bellagio.kdbx",
Username: "dannyocean",
Password: "topsecret",
})
if !u.hasSelectedRemoteTarget() {
t.Fatal("hasSelectedRemoteTarget() = false, want true after selecting a saved remote connection")
}
}
func TestSwitchToLifecycleSelectionResetsLockedLocalSession(t *testing.T) {
t.Parallel()
@@ -5485,6 +5544,55 @@ 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 TestCurrentAppVersion(t *testing.T) {
t.Parallel()
previous := appVersion
t.Cleanup(func() {
appVersion = previous
})
appVersion = ""
if got := currentAppVersion(); got != "dev" {
t.Fatalf("currentAppVersion() with empty version = %q, want dev", got)
}
appVersion = " v0.0.1 "
if got := currentAppVersion(); got != "v0.0.1" {
t.Fatalf("currentAppVersion() with linker version = %q, want v0.0.1", got)
}
}
func TestUIAPIPolicyTargetActionsUseCurrentContext(t *testing.T) {
+3 -1
View File
@@ -45,7 +45,9 @@ build() {
cd "$(_repo_dir)"
export CGO_ENABLED=1
export GOFLAGS="-trimpath"
go build -o keepassgo .
local app_version
app_version="$(git describe --tags --always --dirty)"
go build -ldflags "-X main.appVersion=${app_version}" -o keepassgo .
}
package() {