Compare commits

...

9 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
Joe Julian c19bdd48e2 Simplify desktop remote open flow
ci / lint-test (push) Successful in 1m14s
ci / build (push) Successful in 2m42s
2026-04-05 21:34:59 -07:00
Joe Julian e5f42924f8 Simplify desktop unlock flow
ci / lint-test (push) Successful in 1m16s
ci / build (push) Successful in 2m39s
2026-04-05 21:24:36 -07:00
Joe Julian 13eb9028c7 Add Arch package for Linux client
ci / lint-test (push) Successful in 1m21s
ci / build (push) Successful in 2m45s
2026-04-05 20:31:17 -07:00
Joe Julian 5fa79bdf08 Move unlock controls ahead of vault summary
ci / lint-test (push) Successful in 1m14s
ci / build (push) Successful in 2m39s
2026-04-05 18:35:42 -07:00
13 changed files with 890 additions and 214 deletions
+8 -2
View File
@@ -107,6 +107,7 @@ jobs:
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
app_version="$(git describe --tags --always --dirty)"
# Gio needs a Linux ARM64 cgo cross-toolchain for desktop builds. # Gio needs a Linux ARM64 cgo cross-toolchain for desktop builds.
# Keep the CI matrix to targets this runner can build reproducibly. # Keep the CI matrix to targets this runner can build reproducibly.
for target in \ for target in \
@@ -126,14 +127,19 @@ jobs:
ext=".exe" ext=".exe"
fi fi
out="${DIST_DIR}/keepassgo-${goos}-${goarch}${ext}" 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 done
- name: Build APK - name: Build APK
shell: bash shell: bash
run: | run: |
set -euo pipefail 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" cp build/keepassgo.apk "${DIST_DIR}/keepassgo.apk"
- name: Upload CI artifacts - name: Upload CI artifacts
+1
View File
@@ -12,6 +12,7 @@ Environment:
- `ANDROID_NDK_ROOT` defaults to `/opt/android-ndk`. - `ANDROID_NDK_ROOT` defaults to `/opt/android-ndk`.
- `JAVA_HOME` defaults to `/usr/lib/jvm/java-25-openjdk`. - `JAVA_HOME` defaults to `/usr/lib/jvm/java-25-openjdk`.
- `APP_ID` overrides the Android application id. - `APP_ID` overrides the Android application id.
- `APP_VERSION` overrides the version shown inside KeePassGO itself.
- `APK_OUT` overrides the output path. - `APK_OUT` overrides the output path.
- `APK_VERSION` overrides the packaged app version. - `APK_VERSION` overrides the packaged app version.
- `ANDROID_MIN_SDK` overrides the minimum supported Android SDK. - `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 APP_ID ?= org.julianfamily.keepassgo
APK_OUT ?= build/keepassgo.apk APK_OUT ?= build/keepassgo.apk
APK_VERSION ?= 0.1.0.1 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_MIN_SDK ?= 28
ANDROID_TARGET_SDK ?= 35 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 .PHONY: apk
apk: android/keepassgo-android.jar apk: android/keepassgo-android.jar
@@ -24,6 +36,8 @@ apk: android/keepassgo-android.jar
go tool gogio -target android \ go tool gogio -target android \
-buildmode exe \ -buildmode exe \
-appid $(APP_ID) \ -appid $(APP_ID) \
-ldflags "$(GO_LDFLAGS)" \
$(GOGIO_SIGN_FLAGS) \
-o $(APK_OUT) \ -o $(APK_OUT) \
-version $(APK_VERSION) \ -version $(APK_VERSION) \
-minsdk $(ANDROID_MIN_SDK) \ -minsdk $(ANDROID_MIN_SDK) \
+25
View File
@@ -41,6 +41,31 @@ Desktop build:
go 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:
- `packaging/archlinux/keepassgo-git/`
From that directory you can build and install it with:
```bash
makepkg -si
```
The package installs:
- `/usr/bin/keepassgo`
- a desktop entry at `/usr/share/applications/keepassgo.desktop`
- application icons under the hicolor theme
## Android Packaging ## Android Packaging
KeePassGO uses Gio, so Android packaging is done with `gogio`. KeePassGO uses Gio, so Android packaging is done with `gogio`.
+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)
+4
View File
@@ -13,6 +13,7 @@ const (
DefaultAppID = "org.julianfamily.keepassgo" DefaultAppID = "org.julianfamily.keepassgo"
DefaultAPKOut = "build/keepassgo.apk" DefaultAPKOut = "build/keepassgo.apk"
DefaultVersion = "0.1.0.1" DefaultVersion = "0.1.0.1"
DefaultLdflags = "-X main.appVersion=dev"
DefaultMinSDK = "28" DefaultMinSDK = "28"
DefaultTargetSDK = "35" DefaultTargetSDK = "35"
DefaultIconPath = "assets/keepassgo-icon.png" DefaultIconPath = "assets/keepassgo-icon.png"
@@ -25,6 +26,7 @@ type Config struct {
AppID string AppID string
APKOut string APKOut string
Version string Version string
Ldflags string
MinSDK string MinSDK string
TargetSDK string TargetSDK string
IconPath string IconPath string
@@ -38,6 +40,7 @@ func DefaultConfig() Config {
AppID: DefaultAppID, AppID: DefaultAppID,
APKOut: DefaultAPKOut, APKOut: DefaultAPKOut,
Version: DefaultVersion, Version: DefaultVersion,
Ldflags: DefaultLdflags,
MinSDK: DefaultMinSDK, MinSDK: DefaultMinSDK,
TargetSDK: DefaultTargetSDK, TargetSDK: DefaultTargetSDK,
IconPath: DefaultIconPath, IconPath: DefaultIconPath,
@@ -49,6 +52,7 @@ func (c Config) GogioArgs() []string {
"-target", "android", "-target", "android",
"-buildmode", "exe", "-buildmode", "exe",
"-appid", c.AppID, "-appid", c.AppID,
"-ldflags", c.Ldflags,
"-o", c.APKOut, "-o", c.APKOut,
"-version", c.Version, "-version", c.Version,
"-minsdk", c.MinSDK, "-minsdk", c.MinSDK,
+3
View File
@@ -15,6 +15,7 @@ func TestDefaultConfigGogioArgs(t *testing.T) {
"-target", "android", "-target", "android",
"-buildmode", "exe", "-buildmode", "exe",
"-appid", DefaultAppID, "-appid", DefaultAppID,
"-ldflags", DefaultLdflags,
"-o", DefaultAPKOut, "-o", DefaultAPKOut,
"-version", DefaultVersion, "-version", DefaultVersion,
"-minsdk", DefaultMinSDK, "-minsdk", DefaultMinSDK,
@@ -52,6 +53,7 @@ func TestValidateAcceptsCompleteAndroidToolchainLayout(t *testing.T) {
AppID: DefaultAppID, AppID: DefaultAppID,
APKOut: DefaultAPKOut, APKOut: DefaultAPKOut,
Version: DefaultVersion, Version: DefaultVersion,
Ldflags: DefaultLdflags,
MinSDK: DefaultMinSDK, MinSDK: DefaultMinSDK,
TargetSDK: DefaultTargetSDK, TargetSDK: DefaultTargetSDK,
IconPath: filepath.Join(root, "icon.png"), IconPath: filepath.Join(root, "icon.png"),
@@ -77,6 +79,7 @@ func TestValidateRejectsMissingPrerequisites(t *testing.T) {
AppID: DefaultAppID, AppID: DefaultAppID,
APKOut: DefaultAPKOut, APKOut: DefaultAPKOut,
Version: DefaultVersion, Version: DefaultVersion,
Ldflags: DefaultLdflags,
MinSDK: DefaultMinSDK, MinSDK: DefaultMinSDK,
TargetSDK: DefaultTargetSDK, TargetSDK: DefaultTargetSDK,
} }
+229 -81
View File
@@ -43,6 +43,15 @@ import (
"golang.org/x/exp/shiny/materialdesign/icons" "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 type entry = vault.Entry
const ( const (
@@ -334,6 +343,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
@@ -422,6 +432,7 @@ type ui struct {
syncDialogOpen bool syncDialogOpen bool
syncMenuOpen bool syncMenuOpen bool
mainMenuOpen bool mainMenuOpen bool
selectedRemoteConnection bool
securityDialogOpen bool securityDialogOpen bool
remotePrefsDialogOpen bool remotePrefsDialogOpen bool
showSyncPassword bool showSyncPassword bool
@@ -685,6 +696,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 +827,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()
@@ -1752,12 +1773,16 @@ func (u *ui) restoreStartupLifecycleTarget() {
func (u *ui) hasSelectedLifecycleTarget() bool { func (u *ui) hasSelectedLifecycleTarget() bool {
switch strings.TrimSpace(u.lifecycleMode) { switch strings.TrimSpace(u.lifecycleMode) {
case "remote": case "remote":
return strings.TrimSpace(u.remoteBaseURL.Text()) != "" && strings.TrimSpace(u.remotePath.Text()) != "" return u.hasSelectedRemoteTarget()
default: default:
return strings.TrimSpace(u.vaultPath.Text()) != "" return strings.TrimSpace(u.vaultPath.Text()) != ""
} }
} }
func (u *ui) hasSelectedRemoteTarget() bool {
return u.selectedRemoteConnection
}
func (u *ui) latestRecentVault() (string, time.Time) { func (u *ui) latestRecentVault() (string, time.Time) {
for _, path := range u.recentVaults { for _, path := range u.recentVaults {
if strings.TrimSpace(path) == "" { if strings.TrimSpace(path) == "" {
@@ -1776,6 +1801,50 @@ func (u *ui) showLocalVaultChooser() bool {
return u.lifecycleMode != "local" || !u.hasSelectedVaultPath() return u.lifecycleMode != "local" || !u.hasSelectedVaultPath()
} }
func (u *ui) showRemoteConnectionChooser() bool {
return u.lifecycleMode != "remote" || !u.hasSelectedRemoteTarget()
}
func (u *ui) switchToLifecycleSelection(mode string) {
u.state.Session = &session.Manager{}
u.state.CurrentPath = nil
u.state.SelectedEntryID = ""
u.state.Section = appstate.SectionEntries
u.state.Dirty = false
u.state.ErrorMessage = ""
u.state.StatusMessage = ""
u.loadingMessage = ""
u.loadingActionLabel = ""
u.lastLifecycleAction = ""
u.lifecycleMode = mode
u.editingEntry = false
u.currentPath = nil
u.syncedPath = nil
u.clearMasterPassword()
u.keyFilePath.SetText("")
u.search.SetText("")
switch mode {
case "remote":
u.vaultPath.SetText("")
u.remoteBaseURL.SetText("")
u.remotePath.SetText("")
u.remoteUsername.SetText("")
u.remotePassword.SetText("")
u.rememberRemoteAuth.Value = false
u.selectedRemoteConnection = false
default:
u.vaultPath.SetText("")
u.remoteBaseURL.SetText("")
u.remotePath.SetText("")
u.remoteUsername.SetText("")
u.remotePassword.SetText("")
u.rememberRemoteAuth.Value = false
u.selectedRemoteConnection = false
}
u.requestMasterPassFocus = u.hasSelectedLifecycleTarget()
u.filter()
}
func (u *ui) latestRecentRemote() (recentRemoteRecord, bool, time.Time) { func (u *ui) latestRecentRemote() (recentRemoteRecord, bool, time.Time) {
for _, record := range u.recentRemotes { for _, record := range u.recentRemotes {
if strings.TrimSpace(record.BaseURL) == "" || strings.TrimSpace(record.Path) == "" { if strings.TrimSpace(record.BaseURL) == "" || strings.TrimSpace(record.Path) == "" {
@@ -1819,6 +1888,7 @@ func (u *ui) applyRecentRemoteRecord(record recentRemoteRecord) {
u.remotePassword.SetText(record.Password) u.remotePassword.SetText(record.Password)
u.remotePassword.Mask = '•' u.remotePassword.Mask = '•'
u.rememberRemoteAuth.Value = strings.TrimSpace(record.Username) != "" || record.Password != "" u.rememberRemoteAuth.Value = strings.TrimSpace(record.Username) != "" || record.Password != ""
u.selectedRemoteConnection = true
} }
func (u *ui) remotePreferencesCurrentSummary() string { func (u *ui) remotePreferencesCurrentSummary() string {
@@ -2706,6 +2776,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",
@@ -2744,6 +2819,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:
@@ -2961,6 +3038,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
@@ -2973,6 +3054,7 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
continue continue
} }
u.lifecycleMode = "remote" u.lifecycleMode = "remote"
u.selectedRemoteConnection = false
u.requestMasterPassFocus = true u.requestMasterPassFocus = true
} }
for u.toggleLifecycleAdvanced.Clicked(gtx) { for u.toggleLifecycleAdvanced.Clicked(gtx) {
@@ -3153,6 +3235,10 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
if u.lifecycleBusy() { if u.lifecycleBusy() {
continue continue
} }
if u.shouldUseLockedSinglePane() {
u.switchToLifecycleSelection("local")
continue
}
u.vaultPath.SetText("") u.vaultPath.SetText("")
u.state.ErrorMessage = "" u.state.ErrorMessage = ""
u.state.StatusMessage = "" u.state.StatusMessage = ""
@@ -3162,6 +3248,11 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
if u.lifecycleBusy() { if u.lifecycleBusy() {
continue continue
} }
if u.shouldUseLockedSinglePane() {
u.switchToLifecycleSelection("remote")
continue
}
u.selectedRemoteConnection = false
u.remoteBaseURL.SetText("") u.remoteBaseURL.SetText("")
u.remotePath.SetText("") u.remotePath.SetText("")
u.remoteUsername.SetText("") u.remoteUsername.SetText("")
@@ -4137,6 +4228,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")
}), }),
@@ -4144,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 { func (u *ui) syncButtonGroup(gtx layout.Context) layout.Dimensions {
label := "Sync" label := "Sync"
spacing := unit.Dp(4) spacing := unit.Dp(4)
@@ -4254,21 +4413,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)
}) })
@@ -4298,6 +4459,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{}
} }
@@ -4311,6 +4474,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())
@@ -4337,6 +4501,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{}
@@ -4359,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.mode == "phone" { if u.mode == "phone" {
gtx.Constraints.Min.X = gtx.Constraints.Max.X gtx.Constraints.Min.X = gtx.Constraints.Max.X
} }
@@ -4371,6 +4550,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{}
} }
@@ -4396,6 +4578,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())
} }
@@ -4411,13 +4596,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 {
@@ -4692,7 +4887,6 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions {
_ = panel _ = panel
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild {
if u.isVaultLocked() { if u.isVaultLocked() {
summary := u.currentVaultSummary()
return []layout.FlexChild{ return []layout.FlexChild{
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(18), "Unlock Vault") lbl := material.Label(u.theme, unit.Sp(18), "Unlock Vault")
@@ -4705,47 +4899,6 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions {
lbl.Color = mutedColor lbl.Color = mutedColor
return lbl.Layout(gtx) return lbl.Layout(gtx)
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if strings.TrimSpace(summary.Title) == "" {
return layout.Dimensions{}
}
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 {
lbl := material.Label(u.theme, unit.Sp(11), "UNLOCK TARGET")
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(15), summary.Title)
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if strings.TrimSpace(summary.Detail) == "" || summary.Detail == summary.Title {
return layout.Dimensions{}
}
return layout.Inset{Top: unit.Dp(2)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), summary.Detail)
lbl.Color = mutedColor
return lbl.Layout(gtx)
})
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if strings.TrimSpace(summary.Context) == "" {
return layout.Dimensions{}
}
return layout.Inset{Top: unit.Dp(2)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), summary.Context)
lbl.Color = mutedColor
return lbl.Layout(gtx)
})
}),
)
})
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
layout.Rigid(u.unlockPanel), layout.Rigid(u.unlockPanel),
} }
@@ -4760,6 +4913,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{
@@ -5472,32 +5630,22 @@ func (u *ui) groupBar(gtx layout.Context) layout.Dimensions {
if atRoot { if atRoot {
u.phoneGroupBrowserExpanded = true u.phoneGroupBrowserExpanded = true
} }
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children := make([]layout.FlexChild, 0, len(groups))
layout.Rigid(func(gtx layout.Context) layout.Dimensions { for i := range groups {
if len(groups) == 0 { idx := i
return layout.Dimensions{} name := groups[i]
} children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
maxY := gtx.Dp(unit.Dp(168)) return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
if gtx.Constraints.Max.Y > maxY { for u.groupClicks[idx].Clicked(gtx) {
gtx.Constraints.Max.Y = maxY u.state.EnterGroup(name)
} u.currentPath = append([]string(nil), u.state.CurrentPath...)
if gtx.Constraints.Min.Y > gtx.Constraints.Max.Y { u.filter()
gtx.Constraints.Min.Y = gtx.Constraints.Max.Y }
} return tonedButton(gtx, u.theme, &u.groupClicks[idx], name)
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)
})
}) })
}), }))
) }
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
} }
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
+239
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) { func TestUIPhoneStartsWithGroupToolsCollapsed(t *testing.T) {
t.Parallel() t.Parallel()
@@ -4377,6 +4408,165 @@ func TestShowLocalVaultChooser(t *testing.T) {
} }
} }
func TestShowRemoteConnectionChooser(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
u.lifecycleMode = "remote"
u.remoteBaseURL.SetText("")
u.remotePath.SetText("")
if got := u.showRemoteConnectionChooser(); !got {
t.Fatal("showRemoteConnectionChooser() = false, want true when no remote connection is selected")
}
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 after selecting a saved remote connection")
}
u.lifecycleMode = "local"
if got := u.showRemoteConnectionChooser(); !got {
t.Fatal("showRemoteConnectionChooser() = false, want true outside remote lifecycle mode")
}
}
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()
u := newUIWithSession("desktop", summarySession{hasVault: true, locked: true})
u.lifecycleMode = "local"
u.vaultPath.SetText("/vaults/bellagio.kdbx")
u.remoteBaseURL.SetText("https://dav.crew.example.invalid")
u.remotePath.SetText("vaults/remote.kdbx")
u.remoteUsername.SetText("dannyocean")
u.remotePassword.SetText("topsecret")
u.rememberRemoteAuth.Value = true
u.masterPassword.SetText("correct horse battery staple")
u.keyFilePath.SetText("/vaults/keyfile.keyx")
u.search.SetText("crew")
u.state.CurrentPath = []string{"Crew"}
u.state.SelectedEntryID = "entry-1"
u.state.Section = appstate.SectionTemplates
u.state.Dirty = true
u.switchToLifecycleSelection("local")
if !u.shouldShowLifecycleSetup() {
t.Fatal("shouldShowLifecycleSetup() = false, want true after switching away from locked local vault")
}
if got := u.lifecycleMode; got != "local" {
t.Fatalf("lifecycleMode = %q, want local", got)
}
if got := u.vaultPath.Text(); got != "" {
t.Fatalf("vaultPath = %q, want empty", got)
}
if got := u.remoteBaseURL.Text(); got != "" {
t.Fatalf("remoteBaseURL = %q, want empty", got)
}
if got := u.remotePath.Text(); got != "" {
t.Fatalf("remotePath = %q, want empty", got)
}
if got := u.remoteUsername.Text(); got != "" {
t.Fatalf("remoteUsername = %q, want empty", got)
}
if got := u.remotePassword.Text(); got != "" {
t.Fatalf("remotePassword = %q, want empty", got)
}
if u.rememberRemoteAuth.Value {
t.Fatal("rememberRemoteAuth = true, want false")
}
if got := u.masterPassword.Text(); got != "" {
t.Fatalf("masterPassword = %q, want empty", got)
}
if got := u.keyFilePath.Text(); got != "" {
t.Fatalf("keyFilePath = %q, want empty", got)
}
if got := u.search.Text(); got != "" {
t.Fatalf("search = %q, want empty", got)
}
if got := u.state.Section; got != appstate.SectionEntries {
t.Fatalf("state.Section = %q, want %q", got, appstate.SectionEntries)
}
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)
}
if u.state.Dirty {
t.Fatal("state.Dirty = true, want false")
}
}
func TestSwitchToLifecycleSelectionResetsLockedRemoteSession(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", summarySession{hasVault: true, locked: true, remote: true})
u.lifecycleMode = "local"
u.vaultPath.SetText("/vaults/bellagio.kdbx")
u.remoteBaseURL.SetText("https://dav.crew.example.invalid")
u.remotePath.SetText("vaults/remote.kdbx")
u.remoteUsername.SetText("rustyryan")
u.remotePassword.SetText("topsecret")
u.rememberRemoteAuth.Value = true
u.switchToLifecycleSelection("remote")
if !u.shouldShowLifecycleSetup() {
t.Fatal("shouldShowLifecycleSetup() = false, want true after switching away from locked remote vault")
}
if got := u.lifecycleMode; got != "remote" {
t.Fatalf("lifecycleMode = %q, want remote", got)
}
if got := u.vaultPath.Text(); got != "" {
t.Fatalf("vaultPath = %q, want empty", got)
}
if got := u.remoteBaseURL.Text(); got != "" {
t.Fatalf("remoteBaseURL = %q, want empty", got)
}
if got := u.remotePath.Text(); got != "" {
t.Fatalf("remotePath = %q, want empty", got)
}
if got := u.remoteUsername.Text(); got != "" {
t.Fatalf("remoteUsername = %q, want empty", got)
}
if got := u.remotePassword.Text(); got != "" {
t.Fatalf("remotePassword = %q, want empty", got)
}
if u.rememberRemoteAuth.Value {
t.Fatal("rememberRemoteAuth = true, want false")
}
}
func TestSelectingRecentRemoteSwitchesToRemoteMode(t *testing.T) { func TestSelectingRecentRemoteSwitchesToRemoteMode(t *testing.T) {
t.Parallel() t.Parallel()
@@ -5354,6 +5544,55 @@ 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 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) { func TestUIAPIPolicyTargetActionsUseCurrentContext(t *testing.T) {
@@ -0,0 +1,26 @@
pkgbase = keepassgo-git
pkgdesc = KeePass-compatible password manager written in Go
pkgver = r160.5fa79bd
pkgrel = 1
url = https://git.julianfamily.org/joejulian/keepassgo
arch = x86_64
arch = aarch64
license = custom
makedepends = git
makedepends = go
makedepends = pkgconf
depends = glibc
depends = hicolor-icon-theme
depends = libx11
depends = libxcursor
depends = libxfixes
depends = libxkbcommon
depends = libxkbcommon-x11
depends = mesa
depends = wayland
provides = keepassgo
conflicts = keepassgo
source = git+https://git.julianfamily.org/joejulian/keepassgo.git
sha256sums = SKIP
pkgname = keepassgo-git
@@ -0,0 +1,65 @@
pkgname=keepassgo-git
pkgver=r0.0000000
pkgrel=1
pkgdesc='KeePass-compatible password manager written in Go'
arch=('x86_64' 'aarch64')
url='https://git.julianfamily.org/joejulian/keepassgo'
license=('custom')
depends=(
'glibc'
'hicolor-icon-theme'
'libx11'
'libxcursor'
'libxfixes'
'libxkbcommon'
'libxkbcommon-x11'
'mesa'
'wayland'
)
makedepends=(
'git'
'go'
'pkgconf'
)
provides=('keepassgo')
conflicts=('keepassgo')
source=('git+https://git.julianfamily.org/joejulian/keepassgo.git')
sha256sums=('SKIP')
_repo_dir() {
if [[ -d "${srcdir}/keepassgo/.git" ]]; then
printf '%s\n' "${srcdir}/keepassgo"
return
fi
cd "${startdir}/../../.." || exit 1
pwd
}
pkgver() {
cd "$(_repo_dir)"
printf 'r%s.%s' "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}
build() {
cd "$(_repo_dir)"
export CGO_ENABLED=1
export GOFLAGS="-trimpath"
local app_version
app_version="$(git describe --tags --always --dirty)"
go build -ldflags "-X main.appVersion=${app_version}" -o keepassgo .
}
package() {
cd "$(_repo_dir)"
install -Dm755 keepassgo "${pkgdir}/usr/bin/keepassgo"
install -Dm644 assets/keepassgo-icon.png \
"${pkgdir}/usr/share/icons/hicolor/512x512/apps/keepassgo.png"
install -Dm644 assets/keepassgo-icon.svg \
"${pkgdir}/usr/share/icons/hicolor/scalable/apps/keepassgo.svg"
install -Dm644 packaging/archlinux/keepassgo-git/keepassgo.desktop \
"${pkgdir}/usr/share/applications/keepassgo.desktop"
install -Dm644 README.md \
"${pkgdir}/usr/share/licenses/${pkgname}/README.md"
}
@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=KeePassGO
Comment=KeePass-compatible password manager
Exec=keepassgo
Icon=keepassgo
Terminal=false
Categories=Utility;Security;
Keywords=keepass;password;vault;kdbx;
StartupNotify=true
+264 -130
View File
@@ -21,6 +21,8 @@ import (
func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
busy := u.lifecycleBusy() busy := u.lifecycleBusy()
showLocalChooser := u.showLocalVaultChooser() showLocalChooser := u.showLocalVaultChooser()
showRemoteChooser := u.showRemoteConnectionChooser()
selectedLocalPath := strings.TrimSpace(u.vaultPath.Text())
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), "OPEN A VAULT") lbl := material.Label(u.theme, unit.Sp(12), "OPEN A VAULT")
@@ -60,104 +62,121 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
if u.lifecycleMode == "remote" { if u.lifecycleMode == "remote" {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(12), "LOCATION") lbl := material.Label(u.theme, unit.Sp(12), "LOCATION")
lbl.Color = mutedColor lbl.Color = mutedColor
return lbl.Layout(gtx) 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(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if strings.TrimSpace(u.remoteBaseURL.Text()) == "" || strings.TrimSpace(u.remotePath.Text()) == "" { if !showRemoteChooser {
return layout.Dimensions{} return layout.Dimensions{}
} }
record := u.currentRemoteRecord() return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx)
lastGroup := u.recentRemoteGroup(record.BaseURL, record.Path)
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 CONNECTION")
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), friendlyRecentRemoteLabel(record))
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: "+strings.TrimSpace(record.Path))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(11), "Server: "+strings.TrimSpace(record.BaseURL))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(11), "Auth: "+recentRemoteStoredAuthSummary(recentRemoteRecord{
Username: strings.TrimSpace(u.remoteUsername.Text()),
Password: u.remotePassword.Text(),
}))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if len(lastGroup) == 0 {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(11), "Last group: "+strings.Join(u.displayEntryPath(lastGroup), " / "))
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 busy {
return layout.Dimensions{}
}
return tonedButton(gtx, u.theme, &u.clearRemoteSelection, "Change...")
}),
)
})
})
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy { if !showRemoteChooser {
return layout.Dimensions{} return layout.Dimensions{}
} }
return u.recentRemoteList(gtx) return labeledEditorHelp(u.theme, "Remote Base URL", "Base WebDAV endpoint, for example https://server/remote.php/webdav.", &u.remoteBaseURL, false)(gtx)
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return labeledEditorHelp(u.theme, "Remote Path", "Path to the remote .kdbx file under the WebDAV base URL.", &u.remotePath, false)(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if showRemoteChooser || !u.hasSelectedRemoteTarget() {
return layout.Dimensions{}
}
return layout.Dimensions{}
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if showRemoteChooser && !busy {
return u.recentRemoteList(gtx)
}
return layout.Dimensions{}
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(10)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(12), "AUTHENTICATION") lbl := material.Label(u.theme, unit.Sp(12), "AUTHENTICATION")
lbl.Color = mutedColor lbl.Color = mutedColor
return lbl.Layout(gtx) return lbl.Layout(gtx)
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.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),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return labeledEditorHelp(u.theme, "Remote Username", "Username used to authenticate to the WebDAV server.", &u.remoteUsername, false)(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return labeledEditorHelp(u.theme, "Remote Password", "Password or app token used to authenticate to the WebDAV server.", &u.remotePassword, true)(gtx) return labeledEditorHelp(u.theme, "Remote Password", "Password or app token used to authenticate to the WebDAV server.", &u.remotePassword, true)(gtx)
}), }),
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 {
if !showRemoteChooser {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
box := material.CheckBox(u.theme, &u.rememberRemoteAuth, "Remember sign-in on this device") box := material.CheckBox(u.theme, &u.rememberRemoteAuth, "Remember sign-in on this device")
box.Color = accentColor box.Color = accentColor
return box.Layout(gtx) return box.Layout(gtx)
}), }),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return layout.Inset{Top: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Inset{Top: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.openRemotePrefsHelp, "Settings & Help") return tonedButton(gtx, u.theme, &u.openRemotePrefsHelp, "Settings & Help")
}) })
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
}),
) )
} }
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
@@ -202,52 +221,13 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx) return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx)
}), }),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
selectedPath := strings.TrimSpace(u.vaultPath.Text())
switch { switch {
case busy: case busy:
return labeledEditorHelp(u.theme, "Vault Path", localVaultPathHelp(), &u.vaultPath, false)(gtx) return labeledEditorHelp(u.theme, "Vault Path", localVaultPathHelp(), &u.vaultPath, false)(gtx)
case selectedPath == "": case selectedLocalPath == "":
return localPathSelector(u.theme, &u.vaultPath, &u.pickVaultPath)(gtx) return localPathSelector(u.theme, &u.vaultPath, &u.pickVaultPath)(gtx)
default: default:
lastGroup := u.recentVaultGroup(selectedPath) 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(16), friendlyRecentVaultLabel(selectedPath))
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
dir := compactPathDirectorySummary(selectedPath)
if dir == "" {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(11), dir)
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if len(lastGroup) == 0 {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(11), "Last group: "+strings.Join(u.displayEntryPath(lastGroup), " / "))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.clearVaultSelection, "Open Different Vault")
}),
)
})
})
} }
}), }),
) )
@@ -318,10 +298,26 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.lifecycleMode == "remote" { if u.lifecycleMode == "remote" {
label := u.remoteOpenButtonLabel() label := u.remoteOpenButtonLabel()
if busy { return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
return passiveTonedButton(gtx, u.theme, label) layout.Rigid(func(gtx layout.Context) layout.Dimensions {
} if busy {
return tonedButton(gtx, u.theme, &u.openRemote, label) return passiveTonedButton(gtx, u.theme, label)
}
return tonedButton(gtx, u.theme, &u.openRemote, label)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy || !u.hasSelectedRemoteTarget() {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy || !u.hasSelectedRemoteTarget() {
return layout.Dimensions{}
}
return u.selectedRemoteConnectionCard(gtx)
}),
)
} }
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
@@ -347,11 +343,118 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
} }
return sectionTabButton(gtx, u.theme, &u.createVault, "Create New Vault", false) return sectionTabButton(gtx, u.theme, &u.createVault, "Create New Vault", false)
}), }),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy || selectedLocalPath == "" {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy || selectedLocalPath == "" {
return layout.Dimensions{}
}
return u.selectedLocalVaultCard(gtx, selectedLocalPath)
}),
) )
}), }),
) )
} }
func (u *ui) selectedRemoteConnectionCard(gtx layout.Context) layout.Dimensions {
record := u.currentRemoteRecord()
lastGroup := u.recentRemoteGroup(record.BaseURL, record.Path)
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 CONNECTION")
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), friendlyRecentRemoteLabel(record))
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: "+strings.TrimSpace(record.Path))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(11), "Server: "+strings.TrimSpace(record.BaseURL))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(11), "Auth: "+recentRemoteStoredAuthSummary(recentRemoteRecord{
Username: strings.TrimSpace(u.remoteUsername.Text()),
Password: u.remotePassword.Text(),
}))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if len(lastGroup) == 0 {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(11), "Last group: "+strings.Join(u.displayEntryPath(lastGroup), " / "))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.clearRemoteSelection, "Open Different Connection")
}),
)
})
})
}
func (u *ui) selectedLocalVaultCard(gtx layout.Context, path string) layout.Dimensions {
lastGroup := u.recentVaultGroup(path)
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(16), friendlyRecentVaultLabel(path))
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
dir := compactPathDirectorySummary(path)
if dir == "" {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(11), dir)
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if len(lastGroup) == 0 {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(11), "Last group: "+strings.Join(u.displayEntryPath(lastGroup), " / "))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.clearVaultSelection, "Open Different Vault")
}),
)
})
})
}
func (u *ui) lifecycleSecuritySettingsSummary() string { func (u *ui) lifecycleSecuritySettingsSummary() string {
return "Cipher and KDF now live in Vault Settings so opening and creating a vault stays focused on the file, key material, and sync choices." return "Cipher and KDF now live in Vault Settings so opening and creating a vault stays focused on the file, key material, and sync choices."
} }
@@ -1234,12 +1337,14 @@ func selectorEditorHelp(th *material.Theme, label, help string, editor *widget.E
func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions { func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions {
targetLabel := "Locked vault" targetLabel := "Locked vault"
targetValue := "Unlock the active vault to continue." targetValue := "Unlock the active vault to continue."
changeLabel := "Open Different Vault"
if u.state.Session != nil { if u.state.Session != nil {
if strings.TrimSpace(u.remoteBaseURL.Text()) != "" || strings.TrimSpace(u.remotePath.Text()) != "" { if strings.TrimSpace(u.remoteBaseURL.Text()) != "" || strings.TrimSpace(u.remotePath.Text()) != "" {
baseURL := strings.TrimSpace(u.remoteBaseURL.Text()) baseURL := strings.TrimSpace(u.remoteBaseURL.Text())
path := strings.TrimSpace(u.remotePath.Text()) path := strings.TrimSpace(u.remotePath.Text())
targetLabel = "Remote vault" targetLabel = "Remote vault"
targetValue = friendlyRecentRemoteLabel(recentRemoteRecord{BaseURL: baseURL, Path: path}) targetValue = friendlyRecentRemoteLabel(recentRemoteRecord{BaseURL: baseURL, Path: path})
changeLabel = "Open Different Connection"
if strings.TrimSpace(targetValue) == "" { if strings.TrimSpace(targetValue) == "" {
targetValue = "Remote WebDAV vault" targetValue = "Remote WebDAV vault"
} }
@@ -1255,25 +1360,42 @@ func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions {
} }
} }
} }
targetCard := 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(u.theme, unit.Sp(12), strings.ToUpper(targetLabel))
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.Body1(u.theme, targetValue)
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !u.shouldUseLockedSinglePane() {
return layout.Dimensions{}
}
return layout.Inset{Top: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
if targetLabel == "Remote vault" {
return tonedButton(gtx, u.theme, &u.clearRemoteSelection, changeLabel)
}
return tonedButton(gtx, u.theme, &u.clearVaultSelection, changeLabel)
})
}),
)
})
})
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { if u.mode == "desktop" {
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Dimensions{}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, }
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return targetCard(gtx)
lbl := material.Label(u.theme, unit.Sp(12), strings.ToUpper(targetLabel))
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.Body1(u.theme, targetValue)
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
)
})
})
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
@@ -1285,6 +1407,18 @@ func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions {
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.unlockVault, "Unlock") return tonedButton(gtx, u.theme, &u.unlockVault, "Unlock")
}), }),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.mode != "desktop" {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.mode != "desktop" {
return layout.Dimensions{}
}
return targetCard(gtx)
}),
) )
} }