Compare commits

..

63 Commits

Author SHA1 Message Date
Joe Julian 4a01dd6ef3 Use Gio clipboard commands on Android
ci / lint-test (push) Successful in 1m42s
ci / build (push) Successful in 3m10s
2026-04-04 09:41:28 -07:00
Joe Julian 88f246d9f3 Trigger CI after Traefik TLS fix 2026-04-03 21:41:20 -07:00
Joe Julian 56c5b0b91f Use system CA for CI artifact uploads 2026-04-03 20:37:27 -07:00
Joe Julian 9882a5776f Enable cgo for Linux desktop CI builds 2026-04-03 20:31:34 -07:00
Joe Julian 8557df2ad4 Limit CI desktop builds to supported targets 2026-04-03 20:27:13 -07:00
Joe Julian effd17a01a Use dedicated Android CI runner 2026-04-03 18:04:53 -07:00
Joe Julian f927949c75 Match Gitea workflow runner labels 2026-04-03 17:23:38 -07:00
Joe Julian d7dc934055 Add Gitea CI and release pipeline 2026-04-03 16:38:56 -07:00
Joe Julian b79e741755 Route Android back through Gio settings flow 2026-04-03 11:21:17 -07:00
Joe Julian 05f43aa7bb Add API policy pickers and contextual search labels 2026-04-03 10:55:17 -07:00
Joe Julian eebb270158 Remove redundant recycle bin notice 2026-04-03 10:45:47 -07:00
Joe Julian ec64f5d953 Fix group tools disclosure icon 2026-04-03 10:43:07 -07:00
Joe Julian 12980d5357 Harden phone entry click state 2026-04-03 10:38:14 -07:00
Joe Julian a5397f37f8 Limit group view to direct entries 2026-04-03 08:51:19 -07:00
Joe Julian 520648e42c Keep parent breadcrumb on phone 2026-04-03 08:35:09 -07:00
Joe Julian 49e3beee55 Snapshot phone entry list during redraw 2026-04-03 08:28:58 -07:00
Joe Julian b4f7ca3002 Fix phone header spacer 2026-04-03 07:25:25 -07:00
Joe Julian 8ad48f6124 Autosave Android settings and refine header 2026-04-03 07:23:31 -07:00
Joe Julian bf543170e4 Right-align phone action row 2026-04-03 07:19:26 -07:00
Joe Julian c94d039da8 Align phone header actions right 2026-04-03 07:18:10 -07:00
Joe Julian 4a3e971790 Tighten phone group and detail layout 2026-04-03 07:10:25 -07:00
Joe Julian a845b8fb77 Simplify phone vault screen and settings navigation 2026-04-02 21:50:36 -07:00
Joe Julian f2e4e707cf Make phone entry pane fully scrollable 2026-04-02 21:38:51 -07:00
Joe Julian 7691104d9e Trim verbose group navigation copy 2026-04-02 21:32:07 -07:00
Joe Julian ae91c9e726 Collapse phone subgroup browser after navigation 2026-04-02 09:41:33 -07:00
Joe Julian 4f54e9b6a8 Fix Android reopen crash and simplify group navigation 2026-04-02 07:28:55 -07:00
Joe Julian 7ad4759235 Fix Android local open flow 2026-04-01 21:42:19 -07:00
Joe Julian 912118d9f9 Move remote preference help out of open flow 2026-04-01 19:22:56 -07:00
Joe Julian e0bd8b5c41 Integrate autofill privacy settings controls 2026-04-01 18:46:10 -07:00
Joe Julian 0100316fd1 Add accessibility settings preferences 2026-04-01 18:46:10 -07:00
Joe Julian 2d07182059 Move sync defaults into vault settings 2026-04-01 18:46:10 -07:00
Joe Julian 9638740395 Stabilize settings persistence tests 2026-04-01 18:46:10 -07:00
Joe Julian 2e03db85a1 Add autofill privacy settings controls 2026-04-01 18:46:10 -07:00
Joe Julian 07c277f425 Add accessibility settings preferences 2026-04-01 18:46:10 -07:00
Joe Julian ad22b03b53 Add notification feedback settings 2026-04-01 18:46:10 -07:00
Joe Julian 2f13b0d42f Add persisted UI settings preferences 2026-04-01 18:46:10 -07:00
Joe Julian 2677f7db17 Move remote preference help out of open flow 2026-04-01 17:37:06 -07:00
Joe Julian b3447a42df Align audit UI tests with empty-state model 2026-04-01 17:21:16 -07:00
Joe Julian 4d35e7237d Refine audit UI and vault settings placement 2026-04-01 17:21:16 -07:00
Joe Julian 99a798afbd Improve local vault open usability 2026-04-01 17:17:37 -07:00
Joe Julian c3521eea5b Refine entry detail and edit panels 2026-04-01 17:15:58 -07:00
Joe Julian d2fd4c55f5 Tighten mobile locked vault hierarchy 2026-04-01 17:15:20 -07:00
Joe Julian 7918afcf43 Improve autofill status feedback 2026-04-01 17:13:43 -07:00
Joe Julian dba8786e43 Refine API token management UI 2026-04-01 17:12:29 -07:00
Joe Julian 4fea8b360a Improve entries navigation readability 2026-04-01 17:12:17 -07:00
Joe Julian e364630b77 Refine remote open lifecycle flow 2026-04-01 17:10:26 -07:00
Joe Julian b7e37820b8 Clarify group navigation and management 2026-04-01 17:10:04 -07:00
Joe Julian 7572710e31 Refine recycle bin and sync UX 2026-04-01 17:09:51 -07:00
Joe Julian 8b7637e92e Clarify lifecycle loading and target selection 2026-04-01 17:00:24 -07:00
Joe Julian 80737869ea Avoid blocking UI during vault open and unlock 2026-04-01 16:54:16 -07:00
Joe Julian 4f2bb3d158 Improve recent targets and workflow context 2026-04-01 16:43:15 -07:00
Joe Julian 5cdfba1555 Refine group, editor, and admin workflows 2026-04-01 16:34:24 -07:00
Joe Julian 1ccc7d516c Clarify start, locked, and recycle flows 2026-04-01 16:29:53 -07:00
Joe Julian 9bb838adb6 Tighten navigation and admin UI 2026-04-01 16:24:11 -07:00
Joe Julian f5f7747a0a Refine settings and empty-state affordances 2026-04-01 15:00:46 -07:00
Joe Julian 103da5a55d Simplify lifecycle and section UI 2026-04-01 14:58:56 -07:00
Joe Julian 3aa6c36847 Add UI review follow-ups to TODO 2026-04-01 14:22:53 -07:00
Joe Julian 1b1f11c2d0 Honor alternate autofill targets from entry fields 2026-04-01 10:36:08 -07:00
Joe Julian 4eda211666 Fix entry and group navigation workflows 2026-04-01 10:24:57 -07:00
Joe Julian a4a5ad1579 Support Android app targets in autofill service 2026-04-01 05:56:52 -07:00
Joe Julian 380a335fcf Disambiguate same-host Android fill matches 2026-04-01 05:21:15 -07:00
Joe Julian 574a14df9d Add Android accessibility-based Chrome form fill 2026-04-01 01:24:28 -07:00
Joe Julian 3d99fb53e9 Add Android autofill service packaging 2026-03-31 23:13:10 -07:00
22 changed files with 225 additions and 1345 deletions
-116
View File
@@ -1,116 +0,0 @@
---
name: keepassgo-apk-test
description: Build and run the KeePassGO Android APK for emulator testing. Use when work requires `make apk`, APK install/launch, emulator validation, or checking known KeePassGO Android regressions such as black screens, clipboard, open flow, or local sync behavior.
---
# KeePassGO APK Test
Use this skill together with the installed `android-emulator-debug` skill. That skill covers generic emulator and `adb` mechanics. This skill adds the KeePassGO-specific build, install, and validation requirements that have already been established in this repo.
## Use This Skill When
- Building `build/keepassgo.apk`.
- Installing or launching KeePassGO in the Android emulator.
- Verifying Android regressions such as black screen, clipboard, open flow, file picker, or Advanced Sync behavior.
- Checking whether a Gio or Android toolchain change broke runtime behavior.
## Known Working Environment
- Preferred emulator AVD: `KeepassGoAPI35`
- Package: `org.julianfamily.keepassgo`
- Activity: `org.gioui.GioActivity`
- Local APK build defaults:
`ANDROID_SDK_ROOT=/opt/android-sdk`
`ANDROID_NDK_ROOT=/opt/android-ndk`
`JAVA_HOME=/usr/lib/jvm/java-25-openjdk`
- CI APK build uses:
`ANDROID_SDK_ROOT=/opt/android-sdk`
`ANDROID_NDK_ROOT=/opt/android-sdk/ndk`
`JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64`
## Known Regression
- `gioui.org v0.9.0` caused a black screen in the `KeepassGoAPI35` emulator.
- `gioui.org v0.8.0` rendered correctly in the same environment.
- If Android rendering regresses after a Gio change, treat Gio as a primary suspect and verify against this known-bad versus known-good history before guessing about app logic.
## Safety Rules
- Do not clobber the users real KeePassGO state while testing.
- For host-side validation, use isolated state such as:
`KEEPASSGO_STATE_DIR="$(mktemp -d)" go test ./...`
- Prefer sanitized demo or test vaults when seeding emulator tests.
- If a real vault is absolutely required to reproduce a problem, do not modify it and do not overwrite the users existing recent-vault state.
## Build Workflow
1. Verify the JDK/SDK paths match the known working environment.
2. Build with `make apk`.
3. If `make apk` fails, inspect the effective `JAVA_HOME`, `ANDROID_SDK_ROOT`, and `ANDROID_NDK_ROOT` before changing code.
4. If the problem is Android-only, avoid desktop-only conclusions from `go test ./...`.
Typical local build:
```sh
JAVA_HOME=/usr/lib/jvm/java-25-openjdk make apk
```
## Emulator Workflow
1. Reuse an existing emulator session if one is already running.
2. Prefer the `KeepassGoAPI35` AVD.
3. Install with `adb install -r build/keepassgo.apk`.
4. Launch with:
```sh
adb shell monkey -p org.julianfamily.keepassgo -c android.intent.category.LAUNCHER 1
```
5. Confirm focus before drawing conclusions:
```sh
adb shell dumpsys window | rg 'mCurrentFocus|mFocusedApp'
```
6. Capture evidence before editing code:
screenshot
focused window
relevant `logcat`
## Validation Checklist
- APK builds successfully with `make apk`.
- App launches to `org.julianfamily.keepassgo/org.gioui.GioActivity`.
- Screenshot shows the expected screen, not just a black frame.
- `logcat` shows no app crash or Android runtime fatal error.
- If the change is about user interaction, perform the tap-through in the emulator instead of stopping at install success.
## Issue-Specific Checks
### Black Screen
- Confirm the app is focused.
- Capture a screenshot and `logcat` before changing anything.
- Compare the current Gio version against the known `v0.9.0` regression history.
- If needed, test whether a minimal Gio example renders in the same emulator before blaming KeePassGO layout code.
### Clipboard
- Verify behavior in the emulator, not just unit tests.
- Android clipboard writes must use the Gio-native command path, not desktop clipboard backends.
### File Picker Or Local Sync
- Verify the picker flow on Android itself.
- Prefer document-stream or content-URI based behavior over raw filesystem paths when Android permissions are involved.
## Report Back
When closing out work, state:
- the exact build command used
- whether the emulator session was reused or newly started
- whether install succeeded
- whether the app actually rendered
- what was verified manually in the emulator
- what remains unverified
+2 -8
View File
@@ -107,7 +107,6 @@ 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 \
@@ -127,19 +126,14 @@ 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}" \ GOOS="${goos}" GOARCH="${goarch}" CGO_ENABLED="${cgo_enabled}" go build -o "${out}" .
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
signkey_path="$(mktemp)" make apk
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
-16
View File
@@ -14,8 +14,6 @@ These instructions apply to all future work in this repository.
## Skills ## Skills
- Use the installed Go skills whenever they materially apply to the current slice of work. - Use the installed Go skills whenever they materially apply to the current slice of work.
- Use the installed `android-emulator-debug` skill for Android emulator lifecycle, `adb`, screenshots, and log capture work.
- Use the repo-local `keepassgo-apk-test` skill for KeePassGO-specific APK build and emulator test runs.
- The available Go skills for this repository are: - The available Go skills for this repository are:
`go-code-review`, `go-code-review`,
`go-concurrency`, `go-concurrency`,
@@ -128,20 +126,6 @@ These features are product requirements, not “nice to have” ideas.
- Keep `golangci-lint` passing. - Keep `golangci-lint` passing.
- Keep `go test ./...` passing. - Keep `go test ./...` passing.
- Track `gogio` as a Go tool and keep a reproducible `make apk` path for Android packaging. - Track `gogio` as a Go tool and keep a reproducible `make apk` path for Android packaging.
- Keep the Android build requirements aligned with the known working setup:
`ANDROID_SDK_ROOT=/opt/android-sdk`,
local `ANDROID_NDK_ROOT=/opt/android-ndk`,
CI `ANDROID_NDK_ROOT=/opt/android-sdk/ndk`,
local `JAVA_HOME=/usr/lib/jvm/java-25-openjdk`,
CI `JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64`.
- Remember the known Android runtime regression:
`gioui.org v0.9.0` produced a black screen on the `KeepassGoAPI35` emulator, while `gioui.org v0.8.0` rendered correctly. Treat Gio upgrades on Android as regression-sensitive and verify them on-device or in the emulator.
- When validating an APK in the emulator, prefer the known KeePassGO setup:
AVD `KeepassGoAPI35`,
package `org.julianfamily.keepassgo`,
activity `org.gioui.GioActivity`.
- Do not run emulator/manual APK tests against the users real persisted app state.
Use an isolated `KEEPASSGO_STATE_DIR` for host-side validation, and when emulator testing requires seeded vault data, use sanitized test/demo vaults rather than the users real vault files whenever possible.
- When running tests or other automated validation that may touch persisted UI state, set `KEEPASSGO_STATE_DIR` to an isolated temporary directory so recent-vault history and other local state do not pollute the users real config. - When running tests or other automated validation that may touch persisted UI state, set `KEEPASSGO_STATE_DIR` to an isolated temporary directory so recent-vault history and other local state do not pollute the users real config.
- Prefer commands shaped like `KEEPASSGO_STATE_DIR=\"$(mktemp -d)\" go test ./...` for ad hoc local validation unless a test already manages its own isolated state directory. - Prefer commands shaped like `KEEPASSGO_STATE_DIR=\"$(mktemp -d)\" go test ./...` for ad hoc local validation unless a test already manages its own isolated state directory.
- Do not assume the agent can decrypt SOPS-encrypted secrets in this repository. - Do not assume the agent can decrypt SOPS-encrypted secrets in this repository.
-1
View File
@@ -12,7 +12,6 @@ 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,20 +5,8 @@ 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
@@ -36,8 +24,6 @@ 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,31 +41,6 @@ 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`.
+1 -15
View File
@@ -26,7 +26,6 @@ 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 {
@@ -62,7 +61,6 @@ type SynchronizableSession interface {
type AdvancedSynchronizableSession interface { type AdvancedSynchronizableSession interface {
CurrentSession CurrentSession
SynchronizeFromLocal(string) error SynchronizeFromLocal(string) error
SynchronizeFromLocalBytes(string, []byte) error
SynchronizeToLocal(string) error SynchronizeToLocal(string) error
SynchronizeFromRemote(webdav.Client, string) error SynchronizeFromRemote(webdav.Client, string) error
SynchronizeToRemote(webdav.Client, string) error SynchronizeToRemote(webdav.Client, string) error
@@ -377,7 +375,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, SectionAbout: case SectionAPITokens, SectionAPIAudit:
return nil return nil
default: default:
return slices.Clone(model.Entries) return slices.Clone(model.Entries)
@@ -724,18 +722,6 @@ func (s *State) SynchronizeFromLocal(path string) error {
return nil return nil
} }
func (s *State) SynchronizeFromLocalBytes(name string, content []byte) error {
session, ok := s.Session.(AdvancedSynchronizableSession)
if !ok {
return fmt.Errorf("session is not advanced-synchronizable")
}
if err := session.SynchronizeFromLocalBytes(name, content); err != nil {
return err
}
s.Dirty = false
return nil
}
func (s *State) SynchronizeToLocal(path string) error { func (s *State) SynchronizeToLocal(path string) error {
session, ok := s.Session.(AdvancedSynchronizableSession) session, ok := s.Session.(AdvancedSynchronizableSession)
if !ok { if !ok {
-20
View File
@@ -1584,26 +1584,6 @@ func (s *lifecycleStubSession) OpenRemote(_ webdav.Client, path string, _ vault.
return nil return nil
} }
func (s *lifecycleStubSession) SynchronizeFromLocal(string) error {
return nil
}
func (s *lifecycleStubSession) SynchronizeFromLocalBytes(string, []byte) error {
return nil
}
func (s *lifecycleStubSession) SynchronizeToLocal(string) error {
return nil
}
func (s *lifecycleStubSession) SynchronizeFromRemote(webdav.Client, string) error {
return nil
}
func (s *lifecycleStubSession) SynchronizeToRemote(webdav.Client, string) error {
return nil
}
func (s *lifecycleStubSession) ChangeMasterKey(key vault.MasterKey) error { func (s *lifecycleStubSession) ChangeMasterKey(key vault.MasterKey) error {
s.changedKey = key s.changedKey = key
return nil return nil
+1 -1
View File
@@ -51,7 +51,7 @@ func TestBuildFiltersAndNormalizesEntries(t *testing.T) {
t.Fatalf("first host = %q, want 10.0.2.2", got.Entries[0].Host) t.Fatalf("first host = %q, want 10.0.2.2", got.Entries[0].Host)
} }
if got.Entries[1].Host != "surveillance.crew.example.invalid" { if got.Entries[1].Host != "surveillance.crew.example.invalid" {
t.Fatalf("second host = %q, want surveillance.crew.example.invalid", got.Entries[1].Host) t.Fatalf("second host = %q, want lights.julianfamily.org", got.Entries[1].Host)
} }
if len(got.Entries[1].Targets) != 3 { if len(got.Entries[1].Targets) != 3 {
t.Fatalf("len(second targets) = %d, want 3", len(got.Entries[1].Targets)) t.Fatalf("len(second targets) = %d, want 3", len(got.Entries[1].Targets))
-4
View File
@@ -13,7 +13,6 @@ 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"
@@ -26,7 +25,6 @@ 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
@@ -40,7 +38,6 @@ 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,
@@ -52,7 +49,6 @@ 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,7 +15,6 @@ 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,
@@ -53,7 +52,6 @@ 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"),
@@ -79,7 +77,6 @@ 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,
} }
+1 -1
View File
@@ -15,7 +15,7 @@ KeePassGO currently targets keyboard-first desktop use on Linux and Windows.
- list navigation - list navigation
- search focus - search focus
- new-entry focus transitions - new-entry focus transitions
- Controls that participate in keyboard navigation have intent-revealing accessibility labels through `accessibilityLabel` in [`ui_accessibility.go`](/workspace/keepassgo/ui_accessibility.go). - Controls that participate in keyboard navigation have intent-revealing accessibility labels through `accessibilityLabel` in [`ui_accessibility.go`](/home/rustyryan/dev/go/src/git.julianfamily.org/keepassgo/ui_accessibility.go).
## Current screen-reader boundary ## Current screen-reader boundary
+1 -4
View File
@@ -6,7 +6,6 @@ replace gioui.org/cmd => ./third_party/gioui-cmd
require ( require (
gioui.org v0.8.0 gioui.org v0.8.0
gioui.org/x v0.8.0
github.com/atotto/clipboard v0.1.4 github.com/atotto/clipboard v0.1.4
github.com/tobischo/gokeepasslib/v3 v3.6.2 github.com/tobischo/gokeepasslib/v3 v3.6.2
golang.org/x/exp/shiny v0.0.0-20260312153236-7ab1446f8b90 golang.org/x/exp/shiny v0.0.0-20260312153236-7ab1446f8b90
@@ -19,7 +18,6 @@ require (
4d63.com/gochecknoglobals v0.2.2 // indirect 4d63.com/gochecknoglobals v0.2.2 // indirect
gioui.org/cmd v0.8.0 // indirect gioui.org/cmd v0.8.0 // indirect
gioui.org/shader v1.0.8 // indirect gioui.org/shader v1.0.8 // indirect
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 // indirect
github.com/4meepo/tagalign v1.4.2 // indirect github.com/4meepo/tagalign v1.4.2 // indirect
github.com/Abirdcfly/dupword v0.1.3 // indirect github.com/Abirdcfly/dupword v0.1.3 // indirect
github.com/Antonboom/errname v1.0.0 // indirect github.com/Antonboom/errname v1.0.0 // indirect
@@ -76,7 +74,6 @@ require (
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/glob v0.2.3 // indirect
github.com/godbus/dbus/v5 v5.0.6 // indirect
github.com/gofrs/flock v0.12.1 // indirect github.com/gofrs/flock v0.12.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
@@ -195,7 +192,7 @@ require (
go.uber.org/multierr v1.6.0 // indirect go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.24.0 // indirect go.uber.org/zap v1.24.0 // indirect
golang.org/x/crypto v0.48.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/image v0.37.0 // indirect golang.org/x/image v0.37.0 // indirect
golang.org/x/mod v0.33.0 // indirect golang.org/x/mod v0.33.0 // indirect
+2 -8
View File
@@ -42,10 +42,6 @@ gioui.org v0.8.0/go.mod h1:vEMmpxMOd/iwJhXvGVIzWEbxMWhnMQ9aByOGQdlQ8rc=
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA= gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
gioui.org/x v0.8.0 h1:RhIlQNOFKKn8D8FeaKKaXCo7vB3x+fq4VcD10HW/YpA=
gioui.org/x v0.8.0/go.mod h1:aXtQb+kyqoUOjDl5/uMqAopjzVzMkeHBbMQOGT5KnSE=
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 h1:bGG/g4ypjrCJoSvFrP5hafr9PPB5aw8SjcOWWila7ZI=
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0/go.mod h1:+axXBRUTIDlCeE73IKeD/os7LoEnTKdkp8/gQOFjqyo=
github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E= github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E=
github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI= github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI=
github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE= github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE=
@@ -228,8 +224,6 @@ github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -675,8 +669,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
golang.org/x/exp/shiny v0.0.0-20260312153236-7ab1446f8b90 h1:kyPrwnEYXdME284bE7xgS9BPxhG7MCa5hw1/TpaTJVs= golang.org/x/exp/shiny v0.0.0-20260312153236-7ab1446f8b90 h1:kyPrwnEYXdME284bE7xgS9BPxhG7MCa5hw1/TpaTJVs=
golang.org/x/exp/shiny v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:jqkJFnLVkS8zgKKY4+MOPCZtuZGw3hONUjhapUSwZ8c= golang.org/x/exp/shiny v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:jqkJFnLVkS8zgKKY4+MOPCZtuZGw3hONUjhapUSwZ8c=
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
+82 -295
View File
@@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
"io"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -27,7 +26,6 @@ import (
"gioui.org/unit" "gioui.org/unit"
"gioui.org/widget" "gioui.org/widget"
"gioui.org/widget/material" "gioui.org/widget/material"
"gioui.org/x/explorer"
"git.julianfamily.org/keepassgo/api" "git.julianfamily.org/keepassgo/api"
"git.julianfamily.org/keepassgo/apiapproval" "git.julianfamily.org/keepassgo/apiapproval"
"git.julianfamily.org/keepassgo/apiaudit" "git.julianfamily.org/keepassgo/apiaudit"
@@ -43,15 +41,6 @@ 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 (
@@ -211,7 +200,6 @@ const (
type ui struct { type ui struct {
mode string mode string
theme *material.Theme theme *material.Theme
fileExplorer *explorer.Explorer
logoHorizontal paint.ImageOp logoHorizontal paint.ImageOp
splashSquare paint.ImageOp splashSquare paint.ImageOp
search widget.Editor search widget.Editor
@@ -343,7 +331,6 @@ 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,8 +409,6 @@ type ui struct {
lifecycleMode string lifecycleMode string
syncSourceMode syncSourceMode syncSourceMode syncSourceMode
syncDirection syncDirection syncDirection syncDirection
syncLocalImportName string
syncLocalImportContent []byte
syncLocalPath widget.Editor syncLocalPath widget.Editor
syncRemoteBaseURL widget.Editor syncRemoteBaseURL widget.Editor
syncRemotePath widget.Editor syncRemotePath widget.Editor
@@ -432,7 +417,6 @@ 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
@@ -696,8 +680,6 @@ 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"
} }
@@ -827,14 +809,6 @@ 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()
@@ -1242,19 +1216,6 @@ func (u *ui) openAdvancedSyncDialog() {
} }
} }
func (u *ui) clearSyncLocalImport() {
u.syncLocalImportName = ""
u.syncLocalImportContent = nil
}
func (u *ui) selectedSyncLocalImport() (string, []byte, bool) {
name := strings.TrimSpace(u.syncLocalImportName)
if name == "" || name != strings.TrimSpace(u.syncLocalPath.Text()) || len(u.syncLocalImportContent) == 0 {
return "", nil, false
}
return name, append([]byte(nil), u.syncLocalImportContent...), true
}
func sanitizeSyncSourceMode(mode syncSourceMode) syncSourceMode { func sanitizeSyncSourceMode(mode syncSourceMode) syncSourceMode {
switch mode { switch mode {
case syncSourceRemote: case syncSourceRemote:
@@ -1294,12 +1255,6 @@ func (u *ui) advancedSyncFromAction() error {
return err return err
} }
default: default:
if name, content, ok := u.selectedSyncLocalImport(); ok {
if err := u.state.SynchronizeFromLocalBytes(name, content); err != nil {
return err
}
break
}
path := strings.TrimSpace(u.syncLocalPath.Text()) path := strings.TrimSpace(u.syncLocalPath.Text())
if path == "" { if path == "" {
return errors.New(errVaultPathRequired) return errors.New(errVaultPathRequired)
@@ -1314,37 +1269,6 @@ func (u *ui) advancedSyncFromAction() error {
return nil return nil
} }
func (u *ui) startChooseSyncLocalSourceAction() {
if runtime.GOOS != "android" || u.fileExplorer == nil {
u.runAction("choose sync path", func() error {
u.clearSyncLocalImport()
return u.chooseExistingFileAction(&u.syncLocalPath)
})
return
}
u.runBackgroundAction("choose sync file", func() (func() error, error) {
file, err := u.fileExplorer.ChooseFile(".kdbx")
if err != nil {
if errors.Is(err, explorer.ErrUserDecline) {
return func() error { return nil }, nil
}
return nil, err
}
defer file.Close()
content, err := io.ReadAll(file)
if err != nil {
return nil, err
}
label := "Selected Android vault"
return func() error {
u.syncLocalImportName = label
u.syncLocalImportContent = append([]byte(nil), content...)
u.syncLocalPath.SetText(label)
return nil
}, nil
})
}
func (u *ui) advancedSyncToAction() error { func (u *ui) advancedSyncToAction() error {
switch u.syncSourceMode { switch u.syncSourceMode {
case syncSourceRemote: case syncSourceRemote:
@@ -1773,16 +1697,12 @@ 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 u.hasSelectedRemoteTarget() return strings.TrimSpace(u.remoteBaseURL.Text()) != "" && strings.TrimSpace(u.remotePath.Text()) != ""
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) == "" {
@@ -1793,58 +1713,6 @@ func (u *ui) latestRecentVault() (string, time.Time) {
return "", time.Time{} return "", time.Time{}
} }
func (u *ui) hasSelectedVaultPath() bool {
return strings.TrimSpace(u.vaultPath.Text()) != ""
}
func (u *ui) showLocalVaultChooser() bool {
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) == "" {
@@ -1888,7 +1756,6 @@ 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 {
@@ -2776,11 +2643,6 @@ 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",
@@ -2819,8 +2681,6 @@ 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:
@@ -3038,10 +2898,6 @@ 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
@@ -3054,7 +2910,6 @@ 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) {
@@ -3205,7 +3060,7 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
u.runAction("choose key file", func() error { return u.chooseExistingFileAction(&u.keyFilePath) }) u.runAction("choose key file", func() error { return u.chooseExistingFileAction(&u.keyFilePath) })
} }
for u.pickSyncLocalPath.Clicked(gtx) { for u.pickSyncLocalPath.Clicked(gtx) {
u.startChooseSyncLocalSourceAction() u.runAction("choose sync path", func() error { return u.chooseExistingFileAction(&u.syncLocalPath) })
} }
for i := range u.recentVaultClicks { for i := range u.recentVaultClicks {
for u.recentVaultClicks[i].Clicked(gtx) { for u.recentVaultClicks[i].Clicked(gtx) {
@@ -3235,10 +3090,6 @@ 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 = ""
@@ -3248,11 +3099,6 @@ 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("")
@@ -4228,10 +4074,6 @@ 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")
}), }),
@@ -4239,70 +4081,6 @@ 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)
@@ -4413,23 +4191,21 @@ 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))
if u.state.Section != appstate.SectionAbout { rows = append(rows, func(gtx layout.Context) layout.Dimensions {
rows = append(rows, func(gtx layout.Context) layout.Dimensions { gtx.Constraints.Min.X = gtx.Constraints.Max.X
gtx.Constraints.Min.X = gtx.Constraints.Max.X return u.outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions {
return u.outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions { editor := material.Editor(u.theme, &u.search, u.searchPlaceholder())
editor := material.Editor(u.theme, &u.search, u.searchPlaceholder()) editor.Color = u.theme.Palette.Fg
editor.Color = u.theme.Palette.Fg editor.HintColor = mutedColor
editor.HintColor = mutedColor return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout)
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() { if !u.isVaultLocked() {
rows = append(rows, u.navigationHeader) rows = append(rows, u.navigationHeader)
if u.state.Section == appstate.SectionEntries || u.state.Section == appstate.SectionAbout { if u.state.Section == appstate.SectionEntries {
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)
}) })
@@ -4459,8 +4235,6 @@ 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{}
} }
@@ -4474,7 +4248,6 @@ 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())
@@ -4501,18 +4274,6 @@ 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{}
@@ -4535,9 +4296,6 @@ 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
} }
@@ -4550,9 +4308,6 @@ 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{}
} }
@@ -4578,9 +4333,6 @@ 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())
} }
@@ -4596,23 +4348,13 @@ 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 && u.state.Section != appstate.SectionAbout { if u.state.Section != appstate.SectionEntries {
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 {
@@ -4887,6 +4629,7 @@ 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")
@@ -4899,6 +4642,47 @@ 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),
} }
@@ -4913,11 +4697,6 @@ 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{
@@ -5630,22 +5409,32 @@ func (u *ui) groupBar(gtx layout.Context) layout.Dimensions {
if atRoot { if atRoot {
u.phoneGroupBrowserExpanded = true u.phoneGroupBrowserExpanded = true
} }
children := make([]layout.FlexChild, 0, len(groups)) return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
for i := range groups { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
idx := i if len(groups) == 0 {
name := groups[i] return layout.Dimensions{}
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 { maxY := gtx.Dp(unit.Dp(168))
for u.groupClicks[idx].Clicked(gtx) { if gtx.Constraints.Max.Y > maxY {
u.state.EnterGroup(name) gtx.Constraints.Max.Y = maxY
u.currentPath = append([]string(nil), u.state.CurrentPath...) }
u.filter() if gtx.Constraints.Min.Y > gtx.Constraints.Max.Y {
} 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 {
@@ -6074,7 +5863,6 @@ func run(w *app.Window, mode string, paths statePaths, grpcAddr string) error {
var ops op.Ops var ops op.Ops
manager := &session.Manager{} manager := &session.Manager{}
ui := newUIWithSession(mode, manager, paths) ui := newUIWithSession(mode, manager, paths)
ui.fileExplorer = explorer.NewExplorer(w)
ui.invalidate = w.Invalidate ui.invalidate = w.Invalidate
ui.clipboardWriter = newPlatformClipboardWriter(runtime.GOOS, w.Invalidate) ui.clipboardWriter = newPlatformClipboardWriter(runtime.GOOS, w.Invalidate)
host, err := api.StartHost(grpcAddr, manager, passwords.DefaultProfiles(), ui.clipboardWriter, func() bool { return ui.state.Dirty }) host, err := api.StartHost(grpcAddr, manager, passwords.DefaultProfiles(), ui.clipboardWriter, func() bool { return ui.state.Dirty })
@@ -6089,7 +5877,6 @@ func run(w *app.Window, mode string, paths statePaths, grpcAddr string) error {
} }
for { for {
e := w.Event() e := w.Event()
ui.fileExplorer.ListenEvents(e)
switch e := e.(type) { switch e := e.(type) {
case app.DestroyEvent: case app.DestroyEvent:
return e.Err return e.Err
+1 -342
View File
@@ -135,7 +135,7 @@ func TestUISearchBehaviorIsConsistentAcrossDesktopAndPhoneLayouts(t *testing.T)
{ID: "tpl-2", Title: "SSH Login", URL: "ssh://infra.internal", Path: []string{"Templates", "Infra"}}, {ID: "tpl-2", Title: "SSH Login", URL: "ssh://infra.internal", Path: []string{"Templates", "Infra"}},
}, },
RecycleBin: []vault.Entry{ RecycleBin: []vault.Entry{
{ID: "deleted-1", Title: "Deleted Bellagio", URL: "https://bellagio.example.invalid", Path: []string{"Root", "Internet"}}, {ID: "deleted-1", Title: "Deleted Bellagio", URL: "https://bellagio.example.com", Path: []string{"Root", "Internet"}},
{ID: "deleted-2", Title: "Deleted HVAC", URL: "https://climate.example.com", Path: []string{"Root", "Home"}}, {ID: "deleted-2", Title: "Deleted HVAC", URL: "https://climate.example.com", Path: []string{"Root", "Home"}},
}, },
}) })
@@ -775,37 +775,6 @@ 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()
@@ -1785,67 +1754,6 @@ func TestUIAdvancedSynchronizeFromLocalMergesIntoCurrentVault(t *testing.T) {
} }
} }
func TestUIAdvancedSynchronizeFromImportedLocalVaultMergesIntoCurrentVault(t *testing.T) {
t.Parallel()
key := vault.MasterKey{Password: "correct horse battery staple"}
currentPath := filepath.Join(t.TempDir(), "current.kdbx")
writeKDBXMainTestFile(t, currentPath, vault.Model{
Entries: []vault.Entry{{
ID: "entry-current",
Title: "Vault Console",
Username: "dannyocean",
Password: "token-current",
URL: "https://vault.crew.example.invalid",
Path: []string{"Root", "Internet"},
}},
}, key)
var other bytes.Buffer
if err := vault.SaveKDBX(&other, vault.Model{
Entries: []vault.Entry{{
ID: "entry-other",
Title: "Bellagio",
Username: "rustyryan",
Password: "token-other",
URL: "https://bellagio.example.invalid",
Path: []string{"Root", "Internet"},
}},
}, key.Password); err != nil {
t.Fatalf("SaveKDBX(other) error = %v", err)
}
u := newUIWithSession("desktop", &session.Manager{})
u.masterPassword.SetText(key.Password)
u.vaultPath.SetText(currentPath)
if err := u.openVaultAction(); err != nil {
t.Fatalf("openVaultAction() error = %v", err)
}
u.openAdvancedSyncDialog()
u.syncDirection = syncDirectionPull
u.syncSourceMode = syncSourceLocal
u.syncLocalImportName = "Selected Android vault"
u.syncLocalImportContent = other.Bytes()
u.syncLocalPath.SetText("Selected Android vault")
if err := u.advancedSyncAction(); err != nil {
t.Fatalf("advancedSyncAction() error = %v", err)
}
var reopened session.Manager
if err := reopened.Open(currentPath, key); err != nil {
t.Fatalf("reopen Open(current) error = %v", err)
}
model, err := reopened.Current()
if err != nil {
t.Fatalf("reopened Current() error = %v", err)
}
if got := len(model.EntriesInPath([]string{"Root", "Internet"})); got != 2 {
t.Fatalf("len(EntriesInPath(Root/Internet)) = %d, want 2", got)
}
}
func TestUIStartOpenVaultActionAppliesResultOnMainThread(t *testing.T) { func TestUIStartOpenVaultActionAppliesResultOnMainThread(t *testing.T) {
t.Parallel() t.Parallel()
@@ -4367,206 +4275,6 @@ func TestSelectingRecentVaultSwitchesToLocalMode(t *testing.T) {
} }
} }
func TestRestoreStartupLifecycleTargetSelectsMostRecentLocalVault(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
u.lifecycleMode = "remote"
u.vaultPath.SetText("")
u.recentVaults = []string{"/tmp/example.kdbx"}
u.recentVaultUsedAt["/tmp/example.kdbx"] = time.Date(2026, time.April, 5, 1, 2, 3, 0, time.UTC)
u.recentRemotes = nil
u.restoreStartupLifecycleTarget()
if got := u.lifecycleMode; got != "local" {
t.Fatalf("lifecycleMode after restore = %q, want local", got)
}
if got := u.vaultPath.Text(); got != "/tmp/example.kdbx" {
t.Fatalf("vaultPath after restore = %q, want /tmp/example.kdbx", got)
}
}
func TestShowLocalVaultChooser(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
u.lifecycleMode = "local"
u.vaultPath.SetText("")
if got := u.showLocalVaultChooser(); !got {
t.Fatal("showLocalVaultChooser() = false, want true when no local vault is selected")
}
u.vaultPath.SetText("/tmp/example.kdbx")
if got := u.showLocalVaultChooser(); got {
t.Fatal("showLocalVaultChooser() = true, want false when a local vault is selected")
}
u.lifecycleMode = "remote"
if got := u.showLocalVaultChooser(); !got {
t.Fatal("showLocalVaultChooser() = false, want true outside local lifecycle mode")
}
}
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()
@@ -5544,55 +5252,6 @@ 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) {
@@ -1,26 +0,0 @@
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
@@ -1,65 +0,0 @@
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"
}
@@ -1,10 +0,0 @@
[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
-24
View File
@@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@@ -178,18 +177,6 @@ func (m *Manager) SynchronizeFromLocal(path string) error {
return m.persistMergedToCurrentSource(merged) return m.persistMergedToCurrentSource(merged)
} }
func (m *Manager) SynchronizeFromLocalBytes(name string, content []byte) error {
other, _, err := loadLocalSourceBytes(name, content, m.key)
if err != nil {
return err
}
merged, err := m.mergedWithPeer(other)
if err != nil {
return err
}
return m.persistMergedToCurrentSource(merged)
}
func (m *Manager) SynchronizeToLocal(path string) error { func (m *Manager) SynchronizeToLocal(path string) error {
other, config, err := loadLocalSourceOrEmpty(path, m.key) other, config, err := loadLocalSourceOrEmpty(path, m.key)
if err != nil { if err != nil {
@@ -921,17 +908,6 @@ func loadLocalSource(path string, key vault.MasterKey) (vault.Model, *vault.KDBX
return model, config, nil return model, config, nil
} }
func loadLocalSourceBytes(name string, content []byte, key vault.MasterKey) (vault.Model, *vault.KDBXConfig, error) {
if len(content) == 0 {
return vault.Model{}, nil, fmt.Errorf("open %s for synchronize: %w", name, io.EOF)
}
model, config, err := vault.LoadKDBXWithConfig(bytes.NewReader(content), key)
if err != nil {
return vault.Model{}, nil, fmt.Errorf("decode %s for synchronize: %w", name, err)
}
return model, config, nil
}
func loadLocalSourceOrEmpty(path string, key vault.MasterKey) (vault.Model, *vault.KDBXConfig, error) { func loadLocalSourceOrEmpty(path string, key vault.MasterKey) (vault.Model, *vault.KDBXConfig, error) {
model, config, err := loadLocalSource(path, key) model, config, err := loadLocalSource(path, key)
if err == nil { if err == nil {
-57
View File
@@ -953,63 +953,6 @@ func TestSynchronizeFromLocalMergesOtherVaultIntoCurrentSource(t *testing.T) {
} }
} }
func TestSynchronizeFromLocalBytesMergesOtherVaultIntoCurrentSource(t *testing.T) {
t.Parallel()
key := vault.MasterKey{Password: "correct horse battery staple"}
currentPath := filepath.Join(t.TempDir(), "current.kdbx")
currentModel := vault.Model{
Entries: []vault.Entry{{
ID: "entry-current",
Title: "Vault Console",
Username: "dannyocean",
Password: "token-current",
URL: "https://vault.crew.example.invalid",
Path: []string{"Root", "Internet"},
}},
}
otherModel := vault.Model{
Entries: []vault.Entry{{
ID: "entry-other",
Title: "Bellagio",
Username: "rustyryan",
Password: "token-other",
URL: "https://bellagio.example.invalid",
Path: []string{"Root", "Internet"},
}},
}
writeKDBXTestFile(t, currentPath, currentModel, key)
var other bytes.Buffer
if err := vault.SaveKDBX(&other, otherModel, key.Password); err != nil {
t.Fatalf("SaveKDBX(other) error = %v", err)
}
var sess Manager
if err := sess.Open(currentPath, key); err != nil {
t.Fatalf("Open(current) error = %v", err)
}
if err := sess.SynchronizeFromLocalBytes("picked-other.kdbx", other.Bytes()); err != nil {
t.Fatalf("SynchronizeFromLocalBytes() error = %v", err)
}
var reopened Manager
if err := reopened.Open(currentPath, key); err != nil {
t.Fatalf("reopen Open(current) error = %v", err)
}
current, err := reopened.Current()
if err != nil {
t.Fatalf("reopened Current() error = %v", err)
}
got := current.EntriesInPath([]string{"Root", "Internet"})
if len(got) != 2 {
t.Fatalf("len(EntriesInPath(Root/Internet)) = %d, want 2", len(got))
}
}
func TestSynchronizeToLocalWritesMergedVaultToTarget(t *testing.T) { func TestSynchronizeToLocalWritesMergedVaultToTarget(t *testing.T) {
t.Parallel() t.Parallel()
+134 -290
View File
@@ -20,9 +20,6 @@ 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()
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")
@@ -62,172 +59,173 @@ 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 !showRemoteChooser { if strings.TrimSpace(u.remoteBaseURL.Text()) == "" || strings.TrimSpace(u.remotePath.Text()) == "" {
return layout.Dimensions{} return layout.Dimensions{}
} }
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx) 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(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 !showRemoteChooser { if busy {
return layout.Dimensions{} return layout.Dimensions{}
} }
return labeledEditorHelp(u.theme, "Remote Base URL", "Base WebDAV endpoint, for example https://server/remote.php/webdav.", &u.remoteBaseURL, false)(gtx) return u.recentRemoteList(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(func(gtx layout.Context) layout.Dimensions { layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
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,
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showLocalChooser {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(12), "RECENT VAULTS") lbl := material.Label(u.theme, unit.Sp(12), "RECENT VAULTS")
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(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showLocalChooser { if busy {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showLocalChooser || busy {
return layout.Dimensions{} return layout.Dimensions{}
} }
return u.recentVaultList(gtx) return u.recentVaultList(gtx)
}), }),
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 !showLocalChooser {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showLocalChooser {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(12), "VAULT FILE") lbl := material.Label(u.theme, unit.Sp(12), "VAULT FILE")
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(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showLocalChooser { selectedPath := strings.TrimSpace(u.vaultPath.Text())
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
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 selectedLocalPath == "": case selectedPath == "":
return localPathSelector(u.theme, &u.vaultPath, &u.pickVaultPath)(gtx) return localPathSelector(u.theme, &u.vaultPath, &u.pickVaultPath)(gtx)
default: default:
return layout.Dimensions{} lastGroup := u.recentVaultGroup(selectedPath)
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, "Change...")
}),
)
})
})
} }
}), }),
) )
@@ -298,26 +296,10 @@ 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()
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, if busy {
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return passiveTonedButton(gtx, u.theme, label)
if busy { }
return passiveTonedButton(gtx, u.theme, label) return tonedButton(gtx, u.theme, &u.openRemote, 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 {
@@ -343,118 +325,11 @@ 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."
} }
@@ -1337,14 +1212,12 @@ 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"
} }
@@ -1360,42 +1233,25 @@ 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 {
if u.mode == "desktop" { return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Dimensions{} return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
} return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
return targetCard(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(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 {
@@ -1407,18 +1263,6 @@ 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)
}),
) )
} }