Compare commits

...

4 Commits

Author SHA1 Message Date
Joe Julian c19bdd48e2 Simplify desktop remote open flow
ci / lint-test (push) Successful in 1m14s
ci / build (push) Successful in 2m42s
2026-04-05 21:34:59 -07:00
Joe Julian e5f42924f8 Simplify desktop unlock flow
ci / lint-test (push) Successful in 1m16s
ci / build (push) Successful in 2m39s
2026-04-05 21:24:36 -07:00
Joe Julian 13eb9028c7 Add Arch package for Linux client
ci / lint-test (push) Successful in 1m21s
ci / build (push) Successful in 2m45s
2026-04-05 20:31:17 -07:00
Joe Julian 5fa79bdf08 Move unlock controls ahead of vault summary
ci / lint-test (push) Successful in 1m14s
ci / build (push) Successful in 2m39s
2026-04-05 18:35:42 -07:00
7 changed files with 567 additions and 173 deletions
+18
View File
@@ -41,6 +41,24 @@ Desktop 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
KeePassGO uses Gio, so Android packaging is done with `gogio`.
+55 -43
View File
@@ -1752,12 +1752,16 @@ func (u *ui) restoreStartupLifecycleTarget() {
func (u *ui) hasSelectedLifecycleTarget() bool {
switch strings.TrimSpace(u.lifecycleMode) {
case "remote":
return strings.TrimSpace(u.remoteBaseURL.Text()) != "" && strings.TrimSpace(u.remotePath.Text()) != ""
return u.hasSelectedRemoteTarget()
default:
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) {
for _, path := range u.recentVaults {
if strings.TrimSpace(path) == "" {
@@ -1776,6 +1780,48 @@ 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
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) {
for _, record := range u.recentRemotes {
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() {
continue
}
if u.shouldUseLockedSinglePane() {
u.switchToLifecycleSelection("local")
continue
}
u.vaultPath.SetText("")
u.state.ErrorMessage = ""
u.state.StatusMessage = ""
@@ -3162,6 +3212,10 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
if u.lifecycleBusy() {
continue
}
if u.shouldUseLockedSinglePane() {
u.switchToLifecycleSelection("remote")
continue
}
u.remoteBaseURL.SetText("")
u.remotePath.SetText("")
u.remoteUsername.SetText("")
@@ -4692,7 +4746,6 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions {
_ = panel
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild {
if u.isVaultLocked() {
summary := u.currentVaultSummary()
return []layout.FlexChild{
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
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
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(u.unlockPanel),
}
+131
View File
@@ -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) {
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
+264 -130
View File
@@ -21,6 +21,8 @@ import (
func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
busy := u.lifecycleBusy()
showLocalChooser := u.showLocalVaultChooser()
showRemoteChooser := u.showRemoteConnectionChooser()
selectedLocalPath := strings.TrimSpace(u.vaultPath.Text())
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")
@@ -60,104 +62,121 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
if u.lifecycleMode == "remote" {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(12), "LOCATION")
lbl.Color = mutedColor
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 {
if strings.TrimSpace(u.remoteBaseURL.Text()) == "" || strings.TrimSpace(u.remotePath.Text()) == "" {
if !showRemoteChooser {
return 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(4)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy {
return layout.Dimensions{}
}
return tonedButton(gtx, u.theme, &u.clearRemoteSelection, "Change...")
}),
)
})
})
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy {
if !showRemoteChooser {
return layout.Dimensions{}
}
return u.recentRemoteList(gtx)
return labeledEditorHelp(u.theme, "Remote Base URL", "Base WebDAV endpoint, for example https://server/remote.php/webdav.", &u.remoteBaseURL, false)(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return labeledEditorHelp(u.theme, "Remote Path", "Path to the remote .kdbx file under the WebDAV base URL.", &u.remotePath, false)(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if showRemoteChooser || !u.hasSelectedRemoteTarget() {
return layout.Dimensions{}
}
return layout.Dimensions{}
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if showRemoteChooser && !busy {
return u.recentRemoteList(gtx)
}
return layout.Dimensions{}
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(10)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(12), "AUTHENTICATION")
lbl.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 {
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)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
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.Color = accentColor
return box.Layout(gtx)
}),
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 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,
@@ -202,52 +221,13 @@ func (u *ui) lifecycleControls(gtx layout.Context) 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 {
case busy:
return labeledEditorHelp(u.theme, "Vault Path", localVaultPathHelp(), &u.vaultPath, false)(gtx)
case selectedPath == "":
case selectedLocalPath == "":
return localPathSelector(u.theme, &u.vaultPath, &u.pickVaultPath)(gtx)
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{}
}
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")
}),
)
})
})
return layout.Dimensions{}
}
}),
)
@@ -318,10 +298,26 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.lifecycleMode == "remote" {
label := u.remoteOpenButtonLabel()
if busy {
return passiveTonedButton(gtx, u.theme, label)
}
return tonedButton(gtx, u.theme, &u.openRemote, label)
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy {
return passiveTonedButton(gtx, u.theme, label)
}
return tonedButton(gtx, u.theme, &u.openRemote, label)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy || !u.hasSelectedRemoteTarget() {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy || !u.hasSelectedRemoteTarget() {
return layout.Dimensions{}
}
return u.selectedRemoteConnectionCard(gtx)
}),
)
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
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)
}),
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 {
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 {
targetLabel := "Locked vault"
targetValue := "Unlock the active vault to continue."
changeLabel := "Open Different Vault"
if u.state.Session != nil {
if strings.TrimSpace(u.remoteBaseURL.Text()) != "" || strings.TrimSpace(u.remotePath.Text()) != "" {
baseURL := strings.TrimSpace(u.remoteBaseURL.Text())
path := strings.TrimSpace(u.remotePath.Text())
targetLabel = "Remote vault"
targetValue = friendlyRecentRemoteLabel(recentRemoteRecord{BaseURL: baseURL, Path: path})
changeLabel = "Open Different Connection"
if strings.TrimSpace(targetValue) == "" {
targetValue = "Remote WebDAV vault"
}
@@ -1255,25 +1360,42 @@ func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions {
}
}
}
targetCard := func(gtx layout.Context) layout.Dimensions {
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), strings.ToUpper(targetLabel))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Body1(u.theme, targetValue)
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !u.shouldUseLockedSinglePane() {
return layout.Dimensions{}
}
return layout.Inset{Top: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
if targetLabel == "Remote vault" {
return tonedButton(gtx, u.theme, &u.clearRemoteSelection, changeLabel)
}
return tonedButton(gtx, u.theme, &u.clearVaultSelection, changeLabel)
})
}),
)
})
})
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), strings.ToUpper(targetLabel))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Body1(u.theme, targetValue)
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
)
})
})
if u.mode == "desktop" {
return layout.Dimensions{}
}
return targetCard(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
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 {
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)
}),
)
}