diff --git a/main.go b/main.go index 7c4a3cb..fccad3e 100644 --- a/main.go +++ b/main.go @@ -32,8 +32,7 @@ import ( type entry = vault.Entry const ( - productName = "KeePassGO" - desktopSubtitle = "KeePass-compatible password management for desktop-first workflows" + productName = "KeePassGO" ) const maxAttachmentBytes = 10 << 20 @@ -124,6 +123,8 @@ type ui struct { showEntries widget.Clickable showTemplates widget.Clickable showRecycle widget.Clickable + showLocalLifecycle widget.Clickable + showRemoteLifecycle widget.Clickable masterKeyPasswordOnly widget.Clickable masterKeyKeyFileOnly widget.Clickable masterKeyComposite widget.Clickable @@ -150,6 +151,7 @@ type ui struct { copyIcon *widget.Icon clipboardWriter clipboard.Writer loadingMessage string + lifecycleMode string keyboardFocus focusID } @@ -225,6 +227,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession) *ui { state: appstate.State{}, masterKeyMode: vault.MasterKeyModePasswordOnly, selectedHistoryIndex: -1, + lifecycleMode: "local", } u.state.Session = sess u.phoneSplit.Value = 0.46 @@ -679,6 +682,12 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { for u.showRecycle.Clicked(gtx) { u.showRecycleBinSection() } + for u.showLocalLifecycle.Clicked(gtx) { + u.lifecycleMode = "local" + } + for u.showRemoteLifecycle.Clicked(gtx) { + u.lifecycleMode = "remote" + } for u.lockVault.Clicked(gtx) { u.runAction("lock vault", u.lockAction) } @@ -830,19 +839,9 @@ func (u *ui) header(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions { return layout.Flex{Alignment: layout.Middle}.Layout(gtx, layout.Flexed(1, 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(24), "Vault") - lbl.Text = productName - lbl.Color = accentColor - return lbl.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(13), desktopSubtitle) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - ) + lbl := material.Label(u.theme, unit.Sp(24), productName) + lbl.Color = accentColor + return lbl.Layout(gtx) }), layout.Rigid(func(gtx layout.Context) layout.Dimensions { btn := material.Button(u.theme, &u.lockVault, "Lock") diff --git a/main_test.go b/main_test.go index f71d9c9..5da2de9 100644 --- a/main_test.go +++ b/main_test.go @@ -1668,9 +1668,6 @@ func TestUIUsesKeePassGOProductCopy(t *testing.T) { if productName != "KeePassGO" { t.Fatalf("productName = %q, want %q", productName, "KeePassGO") } - if desktopSubtitle != "KeePass-compatible password management for desktop-first workflows" { - t.Fatalf("desktopSubtitle = %q, want updated product subtitle", desktopSubtitle) - } } func TestUICopyActionsWriteExpectedClipboardContentsAndSanitizedFeedback(t *testing.T) { diff --git a/ui_forms.go b/ui_forms.go index b751fb9..9b7a075 100644 --- a/ui_forms.go +++ b/ui_forms.go @@ -14,6 +14,18 @@ import ( func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { surface := u.sessionSurface() return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.showLocalLifecycle, "Local Vault") + }), + layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.showRemoteLifecycle, "Remote Vault") + }), + ) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(12), "MASTER KEY MODE") if surface.Locked { @@ -39,21 +51,28 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { ) }), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(labeledEditor(u.theme, "Master Password", &u.masterPassword, true)), + layout.Rigid(labeledEditorHelp(u.theme, "Master Password", "Used alone or together with a key file to unlock the vault.", &u.masterPassword, true)), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(labeledEditor(u.theme, "Key File", &u.keyFilePath, false)), + layout.Rigid(labeledEditorHelp(u.theme, "Key File", "Optional path to a KeePass-compatible key file.", &u.keyFilePath, false)), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(labeledEditor(u.theme, "Vault Path", &u.vaultPath, false)), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(labeledEditor(u.theme, "Save As Path", &u.saveAsPath, false)), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(labeledEditor(u.theme, "Remote Base URL", &u.remoteBaseURL, false)), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(labeledEditor(u.theme, "Remote Path", &u.remotePath, false)), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(labeledEditor(u.theme, "Remote Username", &u.remoteUsername, false)), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(labeledEditor(u.theme, "Remote Password", &u.remotePassword, true)), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if u.lifecycleMode == "remote" { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + 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(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(labeledEditorHelp(u.theme, "Remote Password", "Password or app token used to authenticate to the WebDAV server.", &u.remotePassword, true)), + ) + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(labeledEditorHelp(u.theme, "Vault Path", "Local path to an existing .kdbx file to open.", &u.vaultPath, false)), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(labeledEditorHelp(u.theme, "Save As Path", "Local target path used when creating a new vault or saving a copy.", &u.saveAsPath, false)), + ) + }), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, @@ -62,19 +81,21 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if u.lifecycleMode == "remote" { + return tonedButton(gtx, u.theme, &u.openRemote, "Open Remote") + } return tonedButton(gtx, u.theme, &u.openVault, "Open Vault") }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.saveVault, "Save") }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if u.lifecycleMode == "remote" { + return layout.Dimensions{} + } return tonedButton(gtx, u.theme, &u.saveAsVault, "Save As") }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.openRemote, "Open Remote") - }), - layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.changeMasterKey, "Change Master Key") }), @@ -261,6 +282,20 @@ func labeledEditor(th *material.Theme, label string, editor *widget.Editor, sens return labeledEditorWithFocus(th, label, editor, sensitive, false) } +func labeledEditorHelp(th *material.Theme, label, help string, editor *widget.Editor, sensitive bool) layout.Widget { + return func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(labeledEditor(th, label, editor, sensitive)), + layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(th, unit.Sp(11), help) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + ) + } +} + func labeledEditorWithFocus( th *material.Theme, label string,