diff --git a/main.go b/main.go index f7c9fcb..4f1b75d 100644 --- a/main.go +++ b/main.go @@ -577,7 +577,6 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths) apiPolicyGroupScope: true, autofillNoticePreference: autofillNoticeAll, backgroundResults: make(chan backgroundActionResult, 8), - requestMasterPassFocus: true, } u.apiPolicyAllow.Value = true u.apiPolicyGroupScopeW.Value = true @@ -598,10 +597,12 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths) u.loadRecentVaults() u.loadRecentRemotes() u.restoreStartupLifecycleTarget() + u.requestMasterPassFocus = u.hasSelectedLifecycleTarget() u.loadUIPreferences() u.loadSettings() u.loadSettingsFormFromPreferences() u.loadSettingsDraft() + u.requestMasterPassFocus = u.hasSelectedLifecycleTarget() u.filter() u.syncAutofillCache() return u @@ -663,6 +664,10 @@ func shouldUsePreviewWindowSize(mode, goos string) bool { return !strings.EqualFold(strings.TrimSpace(goos), "android") } +func supportsDesktopFilePicker(goos string) bool { + return !strings.EqualFold(strings.TrimSpace(goos), "android") +} + func (u *ui) selectedAttachmentItems() []attachmentItem { item, ok := u.selectedEntry() if !ok || len(item.Attachments) == 0 { @@ -1594,6 +1599,15 @@ 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()) != "" + default: + return strings.TrimSpace(u.vaultPath.Text()) != "" + } +} + func (u *ui) latestRecentVault() (string, time.Time) { for _, path := range u.recentVaults { if strings.TrimSpace(path) == "" { @@ -3699,7 +3713,10 @@ func (u *ui) syncDialogContent(gtx layout.Context) layout.Dimensions { }), ) } - return selectorEditorHelp(u.theme, "Local Vault Path", "Choose the other local .kdbx file to synchronize with.", &u.syncLocalPath, &u.pickSyncLocalPath, "Choose File", false)(gtx) + if supportsDesktopFilePicker(runtime.GOOS) { + return selectorEditorHelp(u.theme, "Local Vault Path", "Choose the other local .kdbx file to synchronize with.", &u.syncLocalPath, &u.pickSyncLocalPath, "Choose File", false)(gtx) + } + return labeledEditorHelp(u.theme, "Local Vault Path", "Enter the shared-storage path to the other local .kdbx file to synchronize with.", &u.syncLocalPath, false)(gtx) }), layout.Rigid(layout.Spacer{Height: unit.Dp(14)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { diff --git a/main_test.go b/main_test.go index a8f84a1..4afceec 100644 --- a/main_test.go +++ b/main_test.go @@ -3519,6 +3519,50 @@ func TestUIStartupPreselectsNewestTargetAcrossLocalAndRemote(t *testing.T) { } } +func TestUIStartupDoesNotRequestMasterPasswordFocusWithoutSelectedTarget(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + u := newUIWithSession("phone", &session.Manager{}, statePaths{ + DefaultSaveAsPath: filepath.Join(dir, "vault.kdbx"), + RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"), + RecentRemotesPath: filepath.Join(dir, "recent-remotes.json"), + SettingsPath: filepath.Join(dir, "settings.json"), + UIPreferencesPath: filepath.Join(dir, "ui-prefs.json"), + AutofillCachePath: filepath.Join(dir, "autofill-cache.json"), + }) + + if u.requestMasterPassFocus { + t.Fatal("requestMasterPassFocus = true without a selected startup target, want false") + } +} + +func TestUIStartupRequestsMasterPasswordFocusForSelectedRecentVault(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + paths := statePaths{ + DefaultSaveAsPath: filepath.Join(dir, "vault.kdbx"), + RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"), + RecentRemotesPath: filepath.Join(dir, "recent-remotes.json"), + SettingsPath: filepath.Join(dir, "settings.json"), + UIPreferencesPath: filepath.Join(dir, "ui-prefs.json"), + AutofillCachePath: filepath.Join(dir, "autofill-cache.json"), + } + + first := newUIWithSession("phone", &session.Manager{}, paths) + first.noteRecentVault("/tmp/demo.kdbx") + + reopened := newUIWithSession("phone", &session.Manager{}, paths) + + if got := reopened.vaultPath.Text(); got != "/tmp/demo.kdbx" { + t.Fatalf("vaultPath = %q, want /tmp/demo.kdbx", got) + } + if !reopened.requestMasterPassFocus { + t.Fatal("requestMasterPassFocus = false with a selected startup vault, want true") + } +} + func TestUIGroupToolsDisclosureStatePersists(t *testing.T) { t.Parallel() @@ -4311,6 +4355,17 @@ func TestShouldUsePreviewWindowSizeSkipsAndroid(t *testing.T) { } } +func TestSupportsDesktopFilePicker(t *testing.T) { + t.Parallel() + + if got := supportsDesktopFilePicker("android"); got { + t.Fatal("supportsDesktopFilePicker(android) = true, want false") + } + if got := supportsDesktopFilePicker("linux"); !got { + t.Fatal("supportsDesktopFilePicker(linux) = false, want true") + } +} + func TestEnterOnLocalLifecycleScreenDefaultsToOpenVault(t *testing.T) { t.Parallel() diff --git a/ui_forms.go b/ui_forms.go index cd340cc..1a4ae26 100644 --- a/ui_forms.go +++ b/ui_forms.go @@ -6,6 +6,7 @@ import ( "image/color" "net/url" "path/filepath" + "runtime" "strings" "gioui.org/layout" @@ -27,7 +28,7 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { }), layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - message := "Choose a recent vault or pick a `.kdbx` file, then unlock it." + message := "Choose a recent vault or enter a .kdbx path, then unlock it." if u.lifecycleMode == "remote" { message = "Connect to a remote vault, then unlock it with the KeePass master key." } @@ -182,9 +183,9 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { selectedPath := strings.TrimSpace(u.vaultPath.Text()) switch { case busy: - return labeledEditorHelp(u.theme, "Vault Path", "Choose the existing .kdbx file to open.", &u.vaultPath, false)(gtx) + return labeledEditorHelp(u.theme, "Vault Path", localVaultPathHelp(), &u.vaultPath, false)(gtx) case selectedPath == "": - return selectorEditorHelp(u.theme, "Vault Path", "Choose the existing .kdbx file to open.", &u.vaultPath, &u.pickVaultPath, "Choose File", false)(gtx) + return localPathSelector(u.theme, &u.vaultPath, &u.pickVaultPath)(gtx) default: lastGroup := u.recentVaultGroup(selectedPath) return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { @@ -242,9 +243,9 @@ 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 { if busy { - return labeledEditorHelp(u.theme, "Key File", "Optional path to a KeePass-compatible key file.", &u.keyFilePath, false)(gtx) + return labeledEditorHelp(u.theme, "Key File", keyFileHelp(), &u.keyFilePath, false)(gtx) } - return selectorEditorHelp(u.theme, "Key File", "Optional path to a KeePass-compatible key file.", &u.keyFilePath, &u.pickKeyFile, "Choose File", false)(gtx) + return keyFileSelector(u.theme, &u.keyFilePath, &u.pickKeyFile)(gtx) }), layout.Rigid(func(gtx layout.Context) layout.Dimensions { if busy { @@ -1149,6 +1150,34 @@ func labeledEditorHelp(th *material.Theme, label, help string, editor *widget.Ed return labeledEditorHelpFocus(th, defaultAccessibilityPreferences(), label, help, editor, sensitive, false) } +func localVaultPathHelp() string { + if supportsDesktopFilePicker(runtime.GOOS) { + return "Choose the existing .kdbx file to open." + } + return "Enter the shared-storage path to the existing .kdbx file, for example /sdcard/Download/vault.kdbx." +} + +func keyFileHelp() string { + if supportsDesktopFilePicker(runtime.GOOS) { + return "Optional path to a KeePass-compatible key file." + } + return "Optional shared-storage path to a KeePass-compatible key file." +} + +func localPathSelector(th *material.Theme, editor *widget.Editor, click *widget.Clickable) layout.Widget { + if supportsDesktopFilePicker(runtime.GOOS) { + return selectorEditorHelp(th, "Vault Path", localVaultPathHelp(), editor, click, "Choose File", false) + } + return labeledEditorHelp(th, "Vault Path", localVaultPathHelp(), editor, false) +} + +func keyFileSelector(th *material.Theme, editor *widget.Editor, click *widget.Clickable) layout.Widget { + if supportsDesktopFilePicker(runtime.GOOS) { + return selectorEditorHelp(th, "Key File", keyFileHelp(), editor, click, "Choose File", false) + } + return labeledEditorHelp(th, "Key File", keyFileHelp(), editor, false) +} + func labeledEditorHelpFocus(th *material.Theme, prefs accessibilityPreferences, label, help string, editor *widget.Editor, sensitive bool, focused bool) layout.Widget { return func(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, @@ -1252,7 +1281,7 @@ func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions { return u.masterPasswordField(gtx, "Used alone or together with a key file to unlock the vault.") }), 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(keyFileSelector(u.theme, &u.keyFilePath, &u.pickKeyFile)), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.unlockVault, "Unlock")