Compare commits
63 Commits
v0.1.0
..
4a01dd6ef3
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a01dd6ef3 | |||
| 88f246d9f3 | |||
| 56c5b0b91f | |||
| 9882a5776f | |||
| 8557df2ad4 | |||
| effd17a01a | |||
| f927949c75 | |||
| d7dc934055 | |||
| b79e741755 | |||
| 05f43aa7bb | |||
| eebb270158 | |||
| ec64f5d953 | |||
| 12980d5357 | |||
| a5397f37f8 | |||
| 520648e42c | |||
| 49e3beee55 | |||
| b4f7ca3002 | |||
| 8ad48f6124 | |||
| bf543170e4 | |||
| c94d039da8 | |||
| 4a3e971790 | |||
| a845b8fb77 | |||
| f2e4e707cf | |||
| 7691104d9e | |||
| ae91c9e726 | |||
| 4f54e9b6a8 | |||
| 7ad4759235 | |||
| 912118d9f9 | |||
| e0bd8b5c41 | |||
| 0100316fd1 | |||
| 2d07182059 | |||
| 9638740395 | |||
| 2e03db85a1 | |||
| 07c277f425 | |||
| ad22b03b53 | |||
| 2f13b0d42f | |||
| 2677f7db17 | |||
| b3447a42df | |||
| 4d35e7237d | |||
| 99a798afbd | |||
| c3521eea5b | |||
| d2fd4c55f5 | |||
| 7918afcf43 | |||
| dba8786e43 | |||
| 4fea8b360a | |||
| e364630b77 | |||
| b7e37820b8 | |||
| 7572710e31 | |||
| 8b7637e92e | |||
| 80737869ea | |||
| 4f2bb3d158 | |||
| 5cdfba1555 | |||
| 1ccc7d516c | |||
| 9bb838adb6 | |||
| f5f7747a0a | |||
| 103da5a55d | |||
| 3aa6c36847 | |||
| 1b1f11c2d0 | |||
| 4eda211666 | |||
| a4a5ad1579 | |||
| 380a335fcf | |||
| 574a14df9d | |||
| 3d99fb53e9 |
@@ -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 user’s 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 user’s 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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 user’s 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 user’s 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 user’s 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 user’s 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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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) \
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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,7 +4191,6 @@ 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 {
|
||||||
@@ -4426,10 +4203,9 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
|
|||||||
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Spacer{Height: spacing}.Layout(gtx)
|
return layout.Spacer{Height: spacing}.Layout(gtx)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
if !u.isVaultLocked() {
|
if !u.isVaultLocked() {
|
||||||
rows = append(rows, u.navigationHeader)
|
rows = append(rows, u.navigationHeader)
|
||||||
if u.state.Section == appstate.SectionEntries || 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,11 +5409,21 @@ 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 {
|
||||||
|
if len(groups) == 0 {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
maxY := gtx.Dp(unit.Dp(168))
|
||||||
|
if gtx.Constraints.Max.Y > maxY {
|
||||||
|
gtx.Constraints.Max.Y = maxY
|
||||||
|
}
|
||||||
|
if gtx.Constraints.Min.Y > gtx.Constraints.Max.Y {
|
||||||
|
gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
|
||||||
|
}
|
||||||
|
return material.List(u.theme, &u.groupList).Layout(gtx, len(groups), func(gtx layout.Context, i int) layout.Dimensions {
|
||||||
idx := i
|
idx := i
|
||||||
name := groups[i]
|
name := groups[i]
|
||||||
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
for u.groupClicks[idx].Clicked(gtx) {
|
for u.groupClicks[idx].Clicked(gtx) {
|
||||||
u.state.EnterGroup(name)
|
u.state.EnterGroup(name)
|
||||||
@@ -5643,9 +5432,9 @@ func (u *ui) groupBar(gtx layout.Context) layout.Dimensions {
|
|||||||
}
|
}
|
||||||
return tonedButton(gtx, u.theme, &u.groupClicks[idx], name)
|
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
@@ -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
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
+107
-263
@@ -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,173 +59,174 @@ 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 {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
if !showRemoteChooser {
|
lbl := material.Label(u.theme, unit.Sp(14), friendlyRecentRemoteLabel(record))
|
||||||
return layout.Dimensions{}
|
lbl.Color = accentColor
|
||||||
}
|
return lbl.Layout(gtx)
|
||||||
return labeledEditorHelp(u.theme, "Remote Base URL", "Base WebDAV endpoint, for example https://server/remote.php/webdav.", &u.remoteBaseURL, false)(gtx)
|
|
||||||
}),
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
if !showRemoteChooser {
|
lbl := material.Label(u.theme, unit.Sp(11), "Path: "+strings.TrimSpace(record.Path))
|
||||||
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.Color = mutedColor
|
lbl.Color = mutedColor
|
||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
if !showRemoteChooser {
|
lbl := material.Label(u.theme, unit.Sp(11), "Server: "+strings.TrimSpace(record.BaseURL))
|
||||||
return layout.Dimensions{}
|
lbl.Color = mutedColor
|
||||||
}
|
return lbl.Layout(gtx)
|
||||||
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx)
|
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
if !showRemoteChooser {
|
lbl := material.Label(u.theme, unit.Sp(11), "Auth: "+recentRemoteStoredAuthSummary(recentRemoteRecord{
|
||||||
return layout.Dimensions{}
|
Username: strings.TrimSpace(u.remoteUsername.Text()),
|
||||||
}
|
Password: u.remotePassword.Text(),
|
||||||
return labeledEditorHelp(u.theme, "Remote Username", "Username used to authenticate to the WebDAV server.", &u.remoteUsername, false)(gtx)
|
}))
|
||||||
|
lbl.Color = mutedColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
if !showRemoteChooser {
|
if len(lastGroup) == 0 {
|
||||||
return layout.Dimensions{}
|
return layout.Dimensions{}
|
||||||
}
|
}
|
||||||
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
|
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 {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
if !showRemoteChooser {
|
if busy {
|
||||||
return layout.Dimensions{}
|
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 {
|
||||||
|
if busy {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
return u.recentRemoteList(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(12), "AUTHENTICATION")
|
||||||
|
lbl.Color = mutedColor
|
||||||
|
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 {
|
||||||
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:
|
||||||
|
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{}
|
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,
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if busy {
|
if busy {
|
||||||
return passiveTonedButton(gtx, u.theme, label)
|
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,7 +1233,8 @@ func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
targetCard := func(gtx layout.Context) layout.Dimensions {
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
return compactCard(gtx, 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.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
@@ -1375,27 +1249,9 @@ func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions {
|
|||||||
lbl.Color = accentColor
|
lbl.Color = accentColor
|
||||||
return lbl.Layout(gtx)
|
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,
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if u.mode == "desktop" {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return targetCard(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)
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user