Compare commits
6 Commits
eb6624cba5
...
v0.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| c19bdd48e2 | |||
| e5f42924f8 | |||
| 13eb9028c7 | |||
| 5fa79bdf08 | |||
| 1a11944423 | |||
| b0842566c0 |
@@ -0,0 +1,116 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
@@ -14,6 +14,8 @@ 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`,
|
||||||
@@ -126,6 +128,20 @@ 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.
|
||||||
|
|||||||
@@ -41,6 +41,24 @@ Desktop build:
|
|||||||
go build ./...
|
go build ./...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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`.
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ go 1.26
|
|||||||
replace gioui.org/cmd => ./third_party/gioui-cmd
|
replace gioui.org/cmd => ./third_party/gioui-cmd
|
||||||
|
|
||||||
require (
|
require (
|
||||||
gioui.org v0.9.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
|
||||||
@@ -18,7 +19,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
|
||||||
gioui.org/x v0.9.0 // indirect
|
|
||||||
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 // 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
|
||||||
|
|||||||
@@ -39,13 +39,11 @@ eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKw
|
|||||||
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA=
|
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA=
|
||||||
gioui.org v0.8.0 h1:QV5p5JvsmSmGiIXVYOKn6d9YDliTfjtLlVf5J+BZ9Pg=
|
gioui.org v0.8.0 h1:QV5p5JvsmSmGiIXVYOKn6d9YDliTfjtLlVf5J+BZ9Pg=
|
||||||
gioui.org v0.8.0/go.mod h1:vEMmpxMOd/iwJhXvGVIzWEbxMWhnMQ9aByOGQdlQ8rc=
|
gioui.org v0.8.0/go.mod h1:vEMmpxMOd/iwJhXvGVIzWEbxMWhnMQ9aByOGQdlQ8rc=
|
||||||
gioui.org v0.9.0 h1:4u7XZwnb5kzQW91Nz/vR0wKD6LdW9CaVF96r3rfy4kc=
|
|
||||||
gioui.org v0.9.0/go.mod h1:CjNig0wAhLt9WZxOPAusgFD8x8IRvqt26LdDBa3Jvao=
|
|
||||||
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.9.0 h1:JUAP3okDXTEmN5WiDpaHbitVWajXKCXyyI5H8qt7KOQ=
|
gioui.org/x v0.8.0 h1:RhIlQNOFKKn8D8FeaKKaXCo7vB3x+fq4VcD10HW/YpA=
|
||||||
gioui.org/x v0.9.0/go.mod h1:IWhEs8zCwiAUM1sfrdacHvcdUagoaKqcodF/N2D3pss=
|
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 h1:bGG/g4ypjrCJoSvFrP5hafr9PPB5aw8SjcOWWila7ZI=
|
||||||
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0/go.mod h1:+axXBRUTIDlCeE73IKeD/os7LoEnTKdkp8/gQOFjqyo=
|
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=
|
||||||
@@ -677,8 +675,6 @@ 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-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
|
||||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||||
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=
|
||||||
|
|||||||
@@ -1752,12 +1752,16 @@ func (u *ui) restoreStartupLifecycleTarget() {
|
|||||||
func (u *ui) hasSelectedLifecycleTarget() bool {
|
func (u *ui) hasSelectedLifecycleTarget() bool {
|
||||||
switch strings.TrimSpace(u.lifecycleMode) {
|
switch strings.TrimSpace(u.lifecycleMode) {
|
||||||
case "remote":
|
case "remote":
|
||||||
return strings.TrimSpace(u.remoteBaseURL.Text()) != "" && strings.TrimSpace(u.remotePath.Text()) != ""
|
return u.hasSelectedRemoteTarget()
|
||||||
default:
|
default:
|
||||||
return strings.TrimSpace(u.vaultPath.Text()) != ""
|
return strings.TrimSpace(u.vaultPath.Text()) != ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ui) hasSelectedRemoteTarget() bool {
|
||||||
|
return strings.TrimSpace(u.remoteBaseURL.Text()) != "" && strings.TrimSpace(u.remotePath.Text()) != ""
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ui) latestRecentVault() (string, time.Time) {
|
func (u *ui) latestRecentVault() (string, time.Time) {
|
||||||
for _, path := range u.recentVaults {
|
for _, path := range u.recentVaults {
|
||||||
if strings.TrimSpace(path) == "" {
|
if strings.TrimSpace(path) == "" {
|
||||||
@@ -1776,6 +1780,48 @@ func (u *ui) showLocalVaultChooser() bool {
|
|||||||
return u.lifecycleMode != "local" || !u.hasSelectedVaultPath()
|
return u.lifecycleMode != "local" || !u.hasSelectedVaultPath()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ui) showRemoteConnectionChooser() bool {
|
||||||
|
return u.lifecycleMode != "remote" || !u.hasSelectedRemoteTarget()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) switchToLifecycleSelection(mode string) {
|
||||||
|
u.state.Session = &session.Manager{}
|
||||||
|
u.state.CurrentPath = nil
|
||||||
|
u.state.SelectedEntryID = ""
|
||||||
|
u.state.Section = appstate.SectionEntries
|
||||||
|
u.state.Dirty = false
|
||||||
|
u.state.ErrorMessage = ""
|
||||||
|
u.state.StatusMessage = ""
|
||||||
|
u.loadingMessage = ""
|
||||||
|
u.loadingActionLabel = ""
|
||||||
|
u.lastLifecycleAction = ""
|
||||||
|
u.lifecycleMode = mode
|
||||||
|
u.editingEntry = false
|
||||||
|
u.currentPath = nil
|
||||||
|
u.syncedPath = nil
|
||||||
|
u.clearMasterPassword()
|
||||||
|
u.keyFilePath.SetText("")
|
||||||
|
u.search.SetText("")
|
||||||
|
switch mode {
|
||||||
|
case "remote":
|
||||||
|
u.vaultPath.SetText("")
|
||||||
|
u.remoteBaseURL.SetText("")
|
||||||
|
u.remotePath.SetText("")
|
||||||
|
u.remoteUsername.SetText("")
|
||||||
|
u.remotePassword.SetText("")
|
||||||
|
u.rememberRemoteAuth.Value = false
|
||||||
|
default:
|
||||||
|
u.vaultPath.SetText("")
|
||||||
|
u.remoteBaseURL.SetText("")
|
||||||
|
u.remotePath.SetText("")
|
||||||
|
u.remoteUsername.SetText("")
|
||||||
|
u.remotePassword.SetText("")
|
||||||
|
u.rememberRemoteAuth.Value = 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) == "" {
|
||||||
@@ -3153,6 +3199,10 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
|||||||
if u.lifecycleBusy() {
|
if u.lifecycleBusy() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if u.shouldUseLockedSinglePane() {
|
||||||
|
u.switchToLifecycleSelection("local")
|
||||||
|
continue
|
||||||
|
}
|
||||||
u.vaultPath.SetText("")
|
u.vaultPath.SetText("")
|
||||||
u.state.ErrorMessage = ""
|
u.state.ErrorMessage = ""
|
||||||
u.state.StatusMessage = ""
|
u.state.StatusMessage = ""
|
||||||
@@ -3162,6 +3212,10 @@ 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.remoteBaseURL.SetText("")
|
u.remoteBaseURL.SetText("")
|
||||||
u.remotePath.SetText("")
|
u.remotePath.SetText("")
|
||||||
u.remoteUsername.SetText("")
|
u.remoteUsername.SetText("")
|
||||||
@@ -4692,7 +4746,6 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions {
|
|||||||
_ = panel
|
_ = panel
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild {
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild {
|
||||||
if u.isVaultLocked() {
|
if u.isVaultLocked() {
|
||||||
summary := u.currentVaultSummary()
|
|
||||||
return []layout.FlexChild{
|
return []layout.FlexChild{
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
lbl := material.Label(u.theme, unit.Sp(18), "Unlock Vault")
|
lbl := material.Label(u.theme, unit.Sp(18), "Unlock Vault")
|
||||||
@@ -4705,47 +4758,6 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions {
|
|||||||
lbl.Color = mutedColor
|
lbl.Color = mutedColor
|
||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if strings.TrimSpace(summary.Title) == "" {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
lbl := material.Label(u.theme, unit.Sp(11), "UNLOCK TARGET")
|
|
||||||
lbl.Color = mutedColor
|
|
||||||
return lbl.Layout(gtx)
|
|
||||||
}),
|
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
lbl := material.Label(u.theme, unit.Sp(15), summary.Title)
|
|
||||||
lbl.Color = accentColor
|
|
||||||
return lbl.Layout(gtx)
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if strings.TrimSpace(summary.Detail) == "" || summary.Detail == summary.Title {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return layout.Inset{Top: unit.Dp(2)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
||||||
lbl := material.Label(u.theme, unit.Sp(12), summary.Detail)
|
|
||||||
lbl.Color = mutedColor
|
|
||||||
return lbl.Layout(gtx)
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if strings.TrimSpace(summary.Context) == "" {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return layout.Inset{Top: unit.Dp(2)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
||||||
lbl := material.Label(u.theme, unit.Sp(12), summary.Context)
|
|
||||||
lbl.Color = mutedColor
|
|
||||||
return lbl.Layout(gtx)
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||||
layout.Rigid(u.unlockPanel),
|
layout.Rigid(u.unlockPanel),
|
||||||
}
|
}
|
||||||
|
|||||||
+131
@@ -4377,6 +4377,137 @@ func TestShowLocalVaultChooser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShowRemoteConnectionChooser(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
u := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
u.lifecycleMode = "remote"
|
||||||
|
u.remoteBaseURL.SetText("")
|
||||||
|
u.remotePath.SetText("")
|
||||||
|
if got := u.showRemoteConnectionChooser(); !got {
|
||||||
|
t.Fatal("showRemoteConnectionChooser() = false, want true when no remote connection is selected")
|
||||||
|
}
|
||||||
|
|
||||||
|
u.remoteBaseURL.SetText("https://dav.crew.example.invalid")
|
||||||
|
u.remotePath.SetText("vaults/bellagio.kdbx")
|
||||||
|
if got := u.showRemoteConnectionChooser(); got {
|
||||||
|
t.Fatal("showRemoteConnectionChooser() = true, want false when a remote connection is selected")
|
||||||
|
}
|
||||||
|
|
||||||
|
u.lifecycleMode = "local"
|
||||||
|
if got := u.showRemoteConnectionChooser(); !got {
|
||||||
|
t.Fatal("showRemoteConnectionChooser() = false, want true outside remote lifecycle mode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
pkgbase = keepassgo-git
|
||||||
|
pkgdesc = KeePass-compatible password manager written in Go
|
||||||
|
pkgver = r160.5fa79bd
|
||||||
|
pkgrel = 1
|
||||||
|
url = https://git.julianfamily.org/joejulian/keepassgo
|
||||||
|
arch = x86_64
|
||||||
|
arch = aarch64
|
||||||
|
license = custom
|
||||||
|
makedepends = git
|
||||||
|
makedepends = go
|
||||||
|
makedepends = pkgconf
|
||||||
|
depends = glibc
|
||||||
|
depends = hicolor-icon-theme
|
||||||
|
depends = libx11
|
||||||
|
depends = libxcursor
|
||||||
|
depends = libxfixes
|
||||||
|
depends = libxkbcommon
|
||||||
|
depends = libxkbcommon-x11
|
||||||
|
depends = mesa
|
||||||
|
depends = wayland
|
||||||
|
provides = keepassgo
|
||||||
|
conflicts = keepassgo
|
||||||
|
source = git+https://git.julianfamily.org/joejulian/keepassgo.git
|
||||||
|
sha256sums = SKIP
|
||||||
|
|
||||||
|
pkgname = keepassgo-git
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
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"
|
||||||
|
go build -o keepassgo .
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cd "$(_repo_dir)"
|
||||||
|
|
||||||
|
install -Dm755 keepassgo "${pkgdir}/usr/bin/keepassgo"
|
||||||
|
install -Dm644 assets/keepassgo-icon.png \
|
||||||
|
"${pkgdir}/usr/share/icons/hicolor/512x512/apps/keepassgo.png"
|
||||||
|
install -Dm644 assets/keepassgo-icon.svg \
|
||||||
|
"${pkgdir}/usr/share/icons/hicolor/scalable/apps/keepassgo.svg"
|
||||||
|
install -Dm644 packaging/archlinux/keepassgo-git/keepassgo.desktop \
|
||||||
|
"${pkgdir}/usr/share/applications/keepassgo.desktop"
|
||||||
|
install -Dm644 README.md \
|
||||||
|
"${pkgdir}/usr/share/licenses/${pkgname}/README.md"
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=KeePassGO
|
||||||
|
Comment=KeePass-compatible password manager
|
||||||
|
Exec=keepassgo
|
||||||
|
Icon=keepassgo
|
||||||
|
Terminal=false
|
||||||
|
Categories=Utility;Security;
|
||||||
|
Keywords=keepass;password;vault;kdbx;
|
||||||
|
StartupNotify=true
|
||||||
+235
-101
@@ -21,6 +21,8 @@ import (
|
|||||||
func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
||||||
busy := u.lifecycleBusy()
|
busy := u.lifecycleBusy()
|
||||||
showLocalChooser := u.showLocalVaultChooser()
|
showLocalChooser := u.showLocalVaultChooser()
|
||||||
|
showRemoteChooser := u.showRemoteConnectionChooser()
|
||||||
|
selectedLocalPath := strings.TrimSpace(u.vaultPath.Text())
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
lbl := material.Label(u.theme, unit.Sp(12), "OPEN A VAULT")
|
lbl := material.Label(u.theme, unit.Sp(12), "OPEN A VAULT")
|
||||||
@@ -60,104 +62,121 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
|||||||
if u.lifecycleMode == "remote" {
|
if u.lifecycleMode == "remote" {
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
if !showRemoteChooser {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
lbl := material.Label(u.theme, unit.Sp(12), "LOCATION")
|
lbl := material.Label(u.theme, unit.Sp(12), "LOCATION")
|
||||||
lbl.Color = mutedColor
|
lbl.Color = mutedColor
|
||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
|
||||||
layout.Rigid(labeledEditorHelp(u.theme, "Remote Base URL", "Base WebDAV endpoint, for example https://server/remote.php/webdav.", &u.remoteBaseURL, false)),
|
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
|
||||||
layout.Rigid(labeledEditorHelp(u.theme, "Remote Path", "Path to the remote .kdbx file under the WebDAV base URL.", &u.remotePath, false)),
|
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
if strings.TrimSpace(u.remoteBaseURL.Text()) == "" || strings.TrimSpace(u.remotePath.Text()) == "" {
|
if !showRemoteChooser {
|
||||||
return layout.Dimensions{}
|
return layout.Dimensions{}
|
||||||
}
|
}
|
||||||
record := u.currentRemoteRecord()
|
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx)
|
||||||
lastGroup := u.recentRemoteGroup(record.BaseURL, record.Path)
|
|
||||||
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
||||||
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
lbl := material.Label(u.theme, unit.Sp(12), "SELECTED CONNECTION")
|
|
||||||
lbl.Color = mutedColor
|
|
||||||
return lbl.Layout(gtx)
|
|
||||||
}),
|
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
lbl := material.Label(u.theme, unit.Sp(14), friendlyRecentRemoteLabel(record))
|
|
||||||
lbl.Color = accentColor
|
|
||||||
return lbl.Layout(gtx)
|
|
||||||
}),
|
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
lbl := material.Label(u.theme, unit.Sp(11), "Path: "+strings.TrimSpace(record.Path))
|
|
||||||
lbl.Color = mutedColor
|
|
||||||
return lbl.Layout(gtx)
|
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
lbl := material.Label(u.theme, unit.Sp(11), "Server: "+strings.TrimSpace(record.BaseURL))
|
if !showRemoteChooser {
|
||||||
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{}
|
return layout.Dimensions{}
|
||||||
}
|
}
|
||||||
lbl := material.Label(u.theme, unit.Sp(11), "Last group: "+strings.Join(u.displayEntryPath(lastGroup), " / "))
|
return labeledEditorHelp(u.theme, "Remote Base URL", "Base WebDAV endpoint, for example https://server/remote.php/webdav.", &u.remoteBaseURL, false)(gtx)
|
||||||
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 busy {
|
if !showRemoteChooser {
|
||||||
return layout.Dimensions{}
|
return layout.Dimensions{}
|
||||||
}
|
}
|
||||||
return tonedButton(gtx, u.theme, &u.clearRemoteSelection, "Change...")
|
return layout.Spacer{Height: unit.Dp(6)}.Layout(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 busy {
|
if !showRemoteChooser {
|
||||||
return layout.Dimensions{}
|
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 u.recentRemoteList(gtx)
|
||||||
|
}
|
||||||
|
return layout.Dimensions{}
|
||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
if !showRemoteChooser {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
return layout.Spacer{Height: unit.Dp(10)}.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
if !showRemoteChooser {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
lbl := material.Label(u.theme, unit.Sp(12), "AUTHENTICATION")
|
lbl := material.Label(u.theme, unit.Sp(12), "AUTHENTICATION")
|
||||||
lbl.Color = mutedColor
|
lbl.Color = mutedColor
|
||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
|
||||||
layout.Rigid(labeledEditorHelp(u.theme, "Remote Username", "Username used to authenticate to the WebDAV server.", &u.remoteUsername, false)),
|
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
if !showRemoteChooser {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
if !showRemoteChooser {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
return labeledEditorHelp(u.theme, "Remote Username", "Username used to authenticate to the WebDAV server.", &u.remoteUsername, false)(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
if !showRemoteChooser {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
if !showRemoteChooser {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
return labeledEditorHelp(u.theme, "Remote Password", "Password or app token used to authenticate to the WebDAV server.", &u.remotePassword, true)(gtx)
|
return labeledEditorHelp(u.theme, "Remote Password", "Password or app token used to authenticate to the WebDAV server.", &u.remotePassword, true)(gtx)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
if !showRemoteChooser {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
if !showRemoteChooser {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
box := material.CheckBox(u.theme, &u.rememberRemoteAuth, "Remember sign-in on this device")
|
box := material.CheckBox(u.theme, &u.rememberRemoteAuth, "Remember sign-in on this device")
|
||||||
box.Color = accentColor
|
box.Color = accentColor
|
||||||
return box.Layout(gtx)
|
return box.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
if !showRemoteChooser {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
return layout.Inset{Top: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
return layout.Inset{Top: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
return tonedButton(gtx, u.theme, &u.openRemotePrefsHelp, "Settings & Help")
|
return tonedButton(gtx, u.theme, &u.openRemotePrefsHelp, "Settings & Help")
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
if !showRemoteChooser {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
@@ -202,53 +221,14 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
|||||||
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx)
|
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
selectedPath := strings.TrimSpace(u.vaultPath.Text())
|
|
||||||
switch {
|
switch {
|
||||||
case busy:
|
case busy:
|
||||||
return labeledEditorHelp(u.theme, "Vault Path", localVaultPathHelp(), &u.vaultPath, false)(gtx)
|
return labeledEditorHelp(u.theme, "Vault Path", localVaultPathHelp(), &u.vaultPath, false)(gtx)
|
||||||
case selectedPath == "":
|
case selectedLocalPath == "":
|
||||||
return localPathSelector(u.theme, &u.vaultPath, &u.pickVaultPath)(gtx)
|
return localPathSelector(u.theme, &u.vaultPath, &u.pickVaultPath)(gtx)
|
||||||
default:
|
default:
|
||||||
lastGroup := u.recentVaultGroup(selectedPath)
|
|
||||||
return 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, "Open Different Vault")
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
@@ -318,10 +298,26 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
|||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
if u.lifecycleMode == "remote" {
|
if u.lifecycleMode == "remote" {
|
||||||
label := u.remoteOpenButtonLabel()
|
label := u.remoteOpenButtonLabel()
|
||||||
|
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 {
|
||||||
@@ -347,11 +343,118 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
|||||||
}
|
}
|
||||||
return sectionTabButton(gtx, u.theme, &u.createVault, "Create New Vault", false)
|
return sectionTabButton(gtx, u.theme, &u.createVault, "Create New Vault", false)
|
||||||
}),
|
}),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
if busy || selectedLocalPath == "" {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
if busy || selectedLocalPath == "" {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
return u.selectedLocalVaultCard(gtx, selectedLocalPath)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ui) selectedRemoteConnectionCard(gtx layout.Context) layout.Dimensions {
|
||||||
|
record := u.currentRemoteRecord()
|
||||||
|
lastGroup := u.recentRemoteGroup(record.BaseURL, record.Path)
|
||||||
|
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(12), "SELECTED CONNECTION")
|
||||||
|
lbl.Color = mutedColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(14), friendlyRecentRemoteLabel(record))
|
||||||
|
lbl.Color = accentColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(11), "Path: "+strings.TrimSpace(record.Path))
|
||||||
|
lbl.Color = mutedColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(11), "Server: "+strings.TrimSpace(record.BaseURL))
|
||||||
|
lbl.Color = mutedColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(11), "Auth: "+recentRemoteStoredAuthSummary(recentRemoteRecord{
|
||||||
|
Username: strings.TrimSpace(u.remoteUsername.Text()),
|
||||||
|
Password: u.remotePassword.Text(),
|
||||||
|
}))
|
||||||
|
lbl.Color = mutedColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
if len(lastGroup) == 0 {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(11), "Last group: "+strings.Join(u.displayEntryPath(lastGroup), " / "))
|
||||||
|
lbl.Color = mutedColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.clearRemoteSelection, "Open Different Connection")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) selectedLocalVaultCard(gtx layout.Context, path string) layout.Dimensions {
|
||||||
|
lastGroup := u.recentVaultGroup(path)
|
||||||
|
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(12), "SELECTED VAULT")
|
||||||
|
lbl.Color = mutedColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(16), friendlyRecentVaultLabel(path))
|
||||||
|
lbl.Color = accentColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
dir := compactPathDirectorySummary(path)
|
||||||
|
if dir == "" {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(11), dir)
|
||||||
|
lbl.Color = mutedColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
if len(lastGroup) == 0 {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(11), "Last group: "+strings.Join(u.displayEntryPath(lastGroup), " / "))
|
||||||
|
lbl.Color = mutedColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.clearVaultSelection, "Open Different Vault")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ui) lifecycleSecuritySettingsSummary() string {
|
func (u *ui) lifecycleSecuritySettingsSummary() string {
|
||||||
return "Cipher and KDF now live in Vault Settings so opening and creating a vault stays focused on the file, key material, and sync choices."
|
return "Cipher and KDF now live in Vault Settings so opening and creating a vault stays focused on the file, key material, and sync choices."
|
||||||
}
|
}
|
||||||
@@ -1234,12 +1337,14 @@ func selectorEditorHelp(th *material.Theme, label, help string, editor *widget.E
|
|||||||
func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions {
|
func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions {
|
||||||
targetLabel := "Locked vault"
|
targetLabel := "Locked vault"
|
||||||
targetValue := "Unlock the active vault to continue."
|
targetValue := "Unlock the active vault to continue."
|
||||||
|
changeLabel := "Open Different Vault"
|
||||||
if u.state.Session != nil {
|
if u.state.Session != nil {
|
||||||
if strings.TrimSpace(u.remoteBaseURL.Text()) != "" || strings.TrimSpace(u.remotePath.Text()) != "" {
|
if strings.TrimSpace(u.remoteBaseURL.Text()) != "" || strings.TrimSpace(u.remotePath.Text()) != "" {
|
||||||
baseURL := strings.TrimSpace(u.remoteBaseURL.Text())
|
baseURL := strings.TrimSpace(u.remoteBaseURL.Text())
|
||||||
path := strings.TrimSpace(u.remotePath.Text())
|
path := strings.TrimSpace(u.remotePath.Text())
|
||||||
targetLabel = "Remote vault"
|
targetLabel = "Remote vault"
|
||||||
targetValue = friendlyRecentRemoteLabel(recentRemoteRecord{BaseURL: baseURL, Path: path})
|
targetValue = friendlyRecentRemoteLabel(recentRemoteRecord{BaseURL: baseURL, Path: path})
|
||||||
|
changeLabel = "Open Different Connection"
|
||||||
if strings.TrimSpace(targetValue) == "" {
|
if strings.TrimSpace(targetValue) == "" {
|
||||||
targetValue = "Remote WebDAV vault"
|
targetValue = "Remote WebDAV vault"
|
||||||
}
|
}
|
||||||
@@ -1255,8 +1360,7 @@ func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
targetCard := func(gtx layout.Context) layout.Dimensions {
|
||||||
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,
|
||||||
@@ -1271,9 +1375,27 @@ 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 {
|
||||||
@@ -1285,6 +1407,18 @@ func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions {
|
|||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
return tonedButton(gtx, u.theme, &u.unlockVault, "Unlock")
|
return tonedButton(gtx, u.theme, &u.unlockVault, "Unlock")
|
||||||
}),
|
}),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
if u.mode != "desktop" {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
if u.mode != "desktop" {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
return targetCard(gtx)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user