diff --git a/main.go b/main.go index 15979ff..b0b5694 100644 --- a/main.go +++ b/main.go @@ -953,6 +953,10 @@ func (u *ui) shouldShowLifecycleSetup() bool { return !u.hasOpenVault() } +func (u *ui) shouldUseLockedSinglePane() bool { + return u.isVaultLocked() && !u.shouldShowLifecycleSetup() +} + func (u *ui) chooseExistingFileAction(target *widget.Editor) error { path, err := pickExistingFile() if err != nil { @@ -1226,6 +1230,9 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { if u.shouldShowLifecycleSetup() { return layout.Dimensions{} } + if u.shouldUseLockedSinglePane() { + return u.detailPanel(gtx) + } if u.mode == "phone" || gtx.Constraints.Max.X < gtx.Dp(unit.Dp(720)) { u.phoneSpan = gtx.Constraints.Max.Y listHeight := int(float32(gtx.Constraints.Max.Y) * u.phoneSplit.Value) @@ -1316,8 +1323,7 @@ func (u *ui) headerActions(gtx layout.Context) layout.Dimensions { return layout.Dimensions{} } if u.isVaultLocked() { - btn := material.Button(u.theme, &u.unlockVault, "Unlock") - return btn.Layout(gtx) + return layout.Dimensions{} } return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { diff --git a/main_test.go b/main_test.go index 66453e6..c174c5f 100644 --- a/main_test.go +++ b/main_test.go @@ -2049,6 +2049,52 @@ func TestEnterOnRemoteLifecycleScreenDefaultsToOpenRemoteVault(t *testing.T) { } } +func TestEnterOnLockedScreenDefaultsToUnlockVault(t *testing.T) { + t.Parallel() + + u := newUIWithSession("desktop", &session.Manager{}) + u.masterPassword.SetText("correct horse battery staple") + if err := u.createVaultAction(); err != nil { + t.Fatalf("createVaultAction() error = %v", err) + } + if err := u.lockAction(); err != nil { + t.Fatalf("lockAction() error = %v", err) + } + + u.masterPassword.SetText("correct horse battery staple") + handled := u.handleKeyPress(key.NameReturn, 0) + if !handled { + t.Fatal("handleKeyPress(Return) = false, want true while locked") + } + if u.isVaultLocked() { + t.Fatal("isVaultLocked() = true, want false after unlock") + } + if got := u.masterPassword.Text(); got != "" { + t.Fatalf("masterPassword after unlock = %q, want empty", got) + } +} + +func TestUILockedVaultUsesSingleUnlockPaneAndOmitsSearchFocus(t *testing.T) { + t.Parallel() + + u := newUIWithSession("desktop", &session.Manager{}) + u.masterPassword.SetText("correct horse battery staple") + if err := u.createVaultAction(); err != nil { + t.Fatalf("createVaultAction() error = %v", err) + } + if err := u.lockAction(); err != nil { + t.Fatalf("lockAction() error = %v", err) + } + + if !u.shouldUseLockedSinglePane() { + t.Fatal("shouldUseLockedSinglePane() = false, want true while locked") + } + + if got := u.focusOrder(); !slices.Equal(got, []focusID{detailFocusID(detailFieldPassword)}) { + t.Fatalf("focusOrder() while locked = %v, want only unlock password focus", got) + } +} + func TestUICopyActionsWriteExpectedClipboardContentsAndSanitizedFeedback(t *testing.T) { t.Parallel() diff --git a/ui_forms.go b/ui_forms.go index 23723df..fd20cf9 100644 --- a/ui_forms.go +++ b/ui_forms.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "image/color" "strings" "gioui.org/layout" @@ -454,7 +455,7 @@ func selectorEditorHelp(th *material.Theme, label, help string, editor *widget.E func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(labeledEditorHelp(u.theme, "Master Password", "Used alone or together with a key file to unlock the vault.", &u.masterPassword, true)), + layout.Rigid(u.unlockPasswordField), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(selectorEditorHelp(u.theme, "Key File", "Optional path to a KeePass-compatible key file.", &u.keyFilePath, &u.pickKeyFile, "Choose File", false)), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), @@ -464,6 +465,55 @@ func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions { ) } +func (u *ui) unlockPasswordField(gtx layout.Context) layout.Dimensions { + icon := u.eyeIcon + desc := "Show master password" + mask := rune('•') + if u.showPassword { + icon = u.eyeOffIcon + desc = "Hide master password" + mask = 0 + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(12), "MASTER PASSWORD") + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return outlinedFieldState(gtx, false, func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Alignment: layout.Middle}.Layout(gtx, + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + restore := u.masterPassword.Mask + u.masterPassword.Mask = mask + defer func() { u.masterPassword.Mask = restore }() + gtx.Constraints.Min.X = gtx.Constraints.Max.X + ed := material.Editor(u.theme, &u.masterPassword, "Master Password") + return ed.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + btn := material.IconButton(u.theme, &u.togglePassword, icon, desc) + btn.Background = color.NRGBA{R: 239, G: 236, B: 229, A: 255} + btn.Color = accentColor + btn.Size = unit.Dp(18) + btn.Inset = layout.UniformInset(unit.Dp(8)) + return btn.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), "Used alone or together with a key file to unlock the vault.") + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + ) +} + func labeledEditorWithFocus( th *material.Theme, label string, diff --git a/ui_keyboard.go b/ui_keyboard.go index 3bc0ac7..788c97a 100644 --- a/ui_keyboard.go +++ b/ui_keyboard.go @@ -45,6 +45,10 @@ func (u *ui) handleKeyPress(name key.Name, modifiers key.Modifiers) bool { if u.handleShortcutKey(name, modifiers) { return true } + if u.isVaultLocked() && name == key.NameReturn { + u.runAction("unlock vault", u.unlockAction) + return true + } if u.shouldShowLifecycleSetup() && name == key.NameReturn { if u.lifecycleMode == "remote" { u.runAction("open remote vault", u.openRemoteAction) @@ -95,6 +99,9 @@ func (u *ui) moveKeyboardFocus(delta int) { } func (u *ui) focusOrder() []focusID { + if u.isVaultLocked() { + return []focusID{detailFocusID(detailFieldPassword)} + } order := []focusID{focusSearch} if u.state.Section != appstate.SectionRecycleBin { order = append(order, breadcrumbFocusID(0))