Simplify recent vault open flow and Android local sync
ci / lint-test (push) Successful in 1m46s
ci / build (push) Successful in 3m44s

This commit is contained in:
Joe Julian
2026-04-05 16:37:43 -07:00
parent 37f1a0ef8f
commit eb6624cba5
10 changed files with 326 additions and 10 deletions
+13
View File
@@ -61,6 +61,7 @@ type SynchronizableSession interface {
type AdvancedSynchronizableSession interface {
CurrentSession
SynchronizeFromLocal(string) error
SynchronizeFromLocalBytes(string, []byte) error
SynchronizeToLocal(string) error
SynchronizeFromRemote(webdav.Client, string) error
SynchronizeToRemote(webdav.Client, string) error
@@ -722,6 +723,18 @@ func (s *State) SynchronizeFromLocal(path string) error {
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 {
session, ok := s.Session.(AdvancedSynchronizableSession)
if !ok {
+20
View File
@@ -1584,6 +1584,26 @@ func (s *lifecycleStubSession) OpenRemote(_ webdav.Client, path string, _ vault.
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 {
s.changedKey = key
return nil
+1 -1
View File
@@ -15,7 +15,7 @@ KeePassGO currently targets keyboard-first desktop use on Linux and Windows.
- list navigation
- search focus
- new-entry focus transitions
- 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).
- Controls that participate in keyboard navigation have intent-revealing accessibility labels through `accessibilityLabel` in [`ui_accessibility.go`](/workspace/keepassgo/ui_accessibility.go).
## Current screen-reader boundary
+5 -2
View File
@@ -5,7 +5,7 @@ go 1.26
replace gioui.org/cmd => ./third_party/gioui-cmd
require (
gioui.org v0.8.0
gioui.org v0.9.0
github.com/atotto/clipboard v0.1.4
github.com/tobischo/gokeepasslib/v3 v3.6.2
golang.org/x/exp/shiny v0.0.0-20260312153236-7ab1446f8b90
@@ -18,6 +18,8 @@ require (
4d63.com/gochecknoglobals v0.2.2 // indirect
gioui.org/cmd v0.8.0 // 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
github.com/4meepo/tagalign v1.4.2 // indirect
github.com/Abirdcfly/dupword v0.1.3 // indirect
github.com/Antonboom/errname v1.0.0 // indirect
@@ -74,6 +76,7 @@ require (
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/go-xmlfmt/xmlfmt v1.1.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/golang/protobuf v1.5.4 // indirect
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
@@ -192,7 +195,7 @@ require (
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/image v0.37.0 // indirect
golang.org/x/mod v0.33.0 // indirect
+10
View File
@@ -39,9 +39,15 @@ eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKw
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/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/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
gioui.org/x v0.9.0 h1:JUAP3okDXTEmN5WiDpaHbitVWajXKCXyyI5H8qt7KOQ=
gioui.org/x v0.9.0/go.mod h1:IWhEs8zCwiAUM1sfrdacHvcdUagoaKqcodF/N2D3pss=
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/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI=
github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE=
@@ -224,6 +230,8 @@ github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
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/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/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -671,6 +679,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
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/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/go.mod h1:jqkJFnLVkS8zgKKY4+MOPCZtuZGw3hONUjhapUSwZ8c=
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
+66 -1
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"image"
"image/color"
"io"
"os"
"os/exec"
"path/filepath"
@@ -26,6 +27,7 @@ import (
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/explorer"
"git.julianfamily.org/keepassgo/api"
"git.julianfamily.org/keepassgo/apiapproval"
"git.julianfamily.org/keepassgo/apiaudit"
@@ -200,6 +202,7 @@ const (
type ui struct {
mode string
theme *material.Theme
fileExplorer *explorer.Explorer
logoHorizontal paint.ImageOp
splashSquare paint.ImageOp
search widget.Editor
@@ -409,6 +412,8 @@ type ui struct {
lifecycleMode string
syncSourceMode syncSourceMode
syncDirection syncDirection
syncLocalImportName string
syncLocalImportContent []byte
syncLocalPath widget.Editor
syncRemoteBaseURL widget.Editor
syncRemotePath widget.Editor
@@ -1216,6 +1221,19 @@ 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 {
switch mode {
case syncSourceRemote:
@@ -1255,6 +1273,12 @@ func (u *ui) advancedSyncFromAction() error {
return err
}
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())
if path == "" {
return errors.New(errVaultPathRequired)
@@ -1269,6 +1293,37 @@ func (u *ui) advancedSyncFromAction() error {
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 {
switch u.syncSourceMode {
case syncSourceRemote:
@@ -1713,6 +1768,14 @@ func (u *ui) latestRecentVault() (string, 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) latestRecentRemote() (recentRemoteRecord, bool, time.Time) {
for _, record := range u.recentRemotes {
if strings.TrimSpace(record.BaseURL) == "" || strings.TrimSpace(record.Path) == "" {
@@ -3060,7 +3123,7 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
u.runAction("choose key file", func() error { return u.chooseExistingFileAction(&u.keyFilePath) })
}
for u.pickSyncLocalPath.Clicked(gtx) {
u.runAction("choose sync path", func() error { return u.chooseExistingFileAction(&u.syncLocalPath) })
u.startChooseSyncLocalSourceAction()
}
for i := range u.recentVaultClicks {
for u.recentVaultClicks[i].Clicked(gtx) {
@@ -5863,6 +5926,7 @@ func run(w *app.Window, mode string, paths statePaths, grpcAddr string) error {
var ops op.Ops
manager := &session.Manager{}
ui := newUIWithSession(mode, manager, paths)
ui.fileExplorer = explorer.NewExplorer(w)
ui.invalidate = 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 })
@@ -5877,6 +5941,7 @@ func run(w *app.Window, mode string, paths statePaths, grpcAddr string) error {
}
for {
e := w.Event()
ui.fileExplorer.ListenEvents(e)
switch e := e.(type) {
case app.DestroyEvent:
return e.Err
+103 -1
View File
@@ -135,7 +135,7 @@ func TestUISearchBehaviorIsConsistentAcrossDesktopAndPhoneLayouts(t *testing.T)
{ID: "tpl-2", Title: "SSH Login", URL: "ssh://infra.internal", Path: []string{"Templates", "Infra"}},
},
RecycleBin: []vault.Entry{
{ID: "deleted-1", Title: "Deleted Bellagio", URL: "https://bellagio.example.com", Path: []string{"Root", "Internet"}},
{ID: "deleted-1", Title: "Deleted Bellagio", URL: "https://bellagio.example.invalid", Path: []string{"Root", "Internet"}},
{ID: "deleted-2", Title: "Deleted HVAC", URL: "https://climate.example.com", Path: []string{"Root", "Home"}},
},
})
@@ -1754,6 +1754,67 @@ 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) {
t.Parallel()
@@ -4275,6 +4336,47 @@ 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 TestSelectingRecentRemoteSwitchesToRemoteMode(t *testing.T) {
t.Parallel()
+24
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
@@ -177,6 +178,18 @@ func (m *Manager) SynchronizeFromLocal(path string) error {
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 {
other, config, err := loadLocalSourceOrEmpty(path, m.key)
if err != nil {
@@ -908,6 +921,17 @@ func loadLocalSource(path string, key vault.MasterKey) (vault.Model, *vault.KDBX
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) {
model, config, err := loadLocalSource(path, key)
if err == nil {
+57
View File
@@ -953,6 +953,63 @@ 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) {
t.Parallel()
+27 -5
View File
@@ -20,6 +20,7 @@ import (
func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
busy := u.lifecycleBusy()
showLocalChooser := u.showLocalVaultChooser()
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), "OPEN A VAULT")
@@ -161,24 +162,45 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showLocalChooser {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(12), "RECENT VAULTS")
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy {
if !showLocalChooser {
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 u.recentVaultList(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
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.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showLocalChooser {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
selectedPath := strings.TrimSpace(u.vaultPath.Text())
switch {
@@ -221,7 +243,7 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
}),
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...")
return tonedButton(gtx, u.theme, &u.clearVaultSelection, "Open Different Vault")
}),
)
})