diff --git a/.golangci.yml b/.golangci.yml index df12035..af27be7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,8 +1,19 @@ linters: enable: - errcheck + - gocyclo - gosimple - govet - ineffassign - staticcheck - unused + +linters-settings: + gocyclo: + min-complexity: 15 + +issues: + exclude-rules: + - path: _test\.go + linters: + - gocyclo diff --git a/internal/appui/app.go b/internal/appui/app.go index b9a57bf..29aff49 100644 --- a/internal/appui/app.go +++ b/internal/appui/app.go @@ -363,8 +363,6 @@ type ui struct { exportAttachment widget.Clickable restoreHistory widget.Clickable generatePassword widget.Clickable - goToRootGroup widget.Clickable - goToParentGroup widget.Clickable createGroup widget.Clickable moveGroup widget.Clickable renameGroup widget.Clickable @@ -2308,19 +2306,6 @@ func (u *ui) currentRemoteRecord() recentRemoteRecord { } } -func (u *ui) selectedRecentRemoteRecord() (recentRemoteRecord, bool) { - record := u.currentRemoteRecord() - if record.BaseURL == "" || record.Path == "" { - return recentRemoteRecord{}, false - } - for _, existing := range u.recentRemotes { - if existing.BaseURL == record.BaseURL && existing.Path == record.Path { - return existing, true - } - } - return recentRemoteRecord{}, false -} - func (u *ui) applyRecentRemoteRecord(record recentRemoteRecord) { u.remoteBaseURL.SetText(record.BaseURL) u.remotePath.SetText(record.Path) @@ -2416,32 +2401,14 @@ func (u *ui) matchingAdvancedSyncRemoteCredentialEntries() []vault.Entry { } remotePath := strings.TrimSpace(u.syncRemotePath.Text()) entries := u.availableRemoteCredentialEntries() - byID := make(map[string]vault.Entry, len(entries)) - for _, entry := range entries { - byID[entry.ID] = entry - } + byID := u.remoteCredentialEntryMap(entries) matches := make([]vault.Entry, 0, len(entries)) seen := make(map[string]struct{}, len(entries)) appendMatch := func(entry vault.Entry) { - if strings.TrimSpace(entry.ID) == "" { - return - } - if _, ok := seen[entry.ID]; ok { - return - } - seen[entry.ID] = struct{}{} - matches = append(matches, entry) - } - for _, entry := range entries { - if !remoteCredentialURLMatches(entry.URL, baseURL) { - continue - } - appendMatch(entry) - } - profilesByID := make(map[string]vault.RemoteProfile) - for _, profile := range u.availableRemoteProfiles() { - profilesByID[profile.ID] = profile + u.appendRemoteCredentialMatch(&matches, seen, entry) } + u.appendURLMatchedRemoteCredentials(baseURL, entries, appendMatch) + profilesByID := u.remoteProfileMap() localVaultPath := strings.TrimSpace(u.vaultPath.Text()) for _, record := range u.recentRemotes { if localVaultPath != "" && strings.TrimSpace(record.LocalVaultPath) != localVaultPath { @@ -2466,6 +2433,119 @@ func (u *ui) matchingAdvancedSyncRemoteCredentialEntries() []vault.Entry { return matches } +func (u *ui) validRemoteProfileSelection(profiles []vault.RemoteProfile) string { + selectedID := strings.TrimSpace(u.selectedVaultRemoteProfileID) + if u.hasRemoteProfileSelection(selectedID, profiles) { + return selectedID + } + if len(profiles) == 1 { + return profiles[0].ID + } + return "" +} + +func (u *ui) validRemoteCredentialSelection(entries []vault.Entry) string { + selectedID := strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID) + if u.hasRemoteCredentialSelection(selectedID, entries) { + return selectedID + } + if len(entries) == 1 { + return entries[0].ID + } + return "" +} + +func (u *ui) hasRemoteProfileSelection(selectedID string, profiles []vault.RemoteProfile) bool { + for _, profile := range profiles { + if profile.ID == selectedID { + return true + } + } + return false +} + +func (u *ui) hasRemoteCredentialSelection(selectedID string, entries []vault.Entry) bool { + for _, entry := range entries { + if entry.ID == selectedID { + return true + } + } + return false +} + +func (u *ui) applySelectedRemoteProfileFields() { + if profile, ok := u.selectedVaultRemoteProfile(); ok { + u.remoteBaseURL.SetText(profile.BaseURL) + u.remotePath.SetText(profile.Path) + } +} + +func (u *ui) syncRecentRemoteBindingSelection() { + if strings.TrimSpace(u.selectedVaultRemoteProfileID) != "" && strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID) != "" { + return + } + record, ok := u.boundRecentRemoteForLocalVault(strings.TrimSpace(u.vaultPath.Text())) + if !ok { + return + } + u.selectedVaultRemoteProfileID = strings.TrimSpace(record.RemoteProfileID) + u.selectedVaultRemoteCredentialEntryID = strings.TrimSpace(record.CredentialEntryID) + u.selectedVaultRemoteSyncMode = normalizeUISyncMode(appstate.SyncMode(record.SyncMode)) + u.applySelectedRemoteProfileFields() +} + +func (u *ui) syncSelectedRemoteBindingMode() { + binding, ok := u.selectedVaultRemoteBinding() + if !ok { + u.selectedVaultRemoteSyncMode = appstate.SyncModeManual + return + } + for _, record := range u.recentRemotes { + if strings.TrimSpace(record.LocalVaultPath) == strings.TrimSpace(binding.LocalVaultPath) && + strings.TrimSpace(record.RemoteProfileID) == strings.TrimSpace(binding.RemoteProfileID) && + strings.TrimSpace(record.CredentialEntryID) == strings.TrimSpace(binding.CredentialEntryID) { + u.selectedVaultRemoteSyncMode = normalizeUISyncMode(appstate.SyncMode(record.SyncMode)) + return + } + } + u.selectedVaultRemoteSyncMode = appstate.SyncModeManual +} + +func (u *ui) remoteCredentialEntryMap(entries []vault.Entry) map[string]vault.Entry { + byID := make(map[string]vault.Entry, len(entries)) + for _, entry := range entries { + byID[entry.ID] = entry + } + return byID +} + +func (u *ui) remoteProfileMap() map[string]vault.RemoteProfile { + profilesByID := make(map[string]vault.RemoteProfile) + for _, profile := range u.availableRemoteProfiles() { + profilesByID[profile.ID] = profile + } + return profilesByID +} + +func (u *ui) appendRemoteCredentialMatch(matches *[]vault.Entry, seen map[string]struct{}, entry vault.Entry) { + if strings.TrimSpace(entry.ID) == "" { + return + } + if _, ok := seen[entry.ID]; ok { + return + } + seen[entry.ID] = struct{}{} + *matches = append(*matches, entry) +} + +func (u *ui) appendURLMatchedRemoteCredentials(baseURL string, entries []vault.Entry, appendMatch func(vault.Entry)) { + for _, entry := range entries { + if remoteCredentialURLMatches(entry.URL, baseURL) { + appendMatch(entry) + } + } +} + func (u *ui) applyAdvancedSyncRemoteCredentialEntry(entry vault.Entry) { u.selectedSyncRemoteCredentialEntryID = strings.TrimSpace(entry.ID) u.syncRemoteUsername.SetText(strings.TrimSpace(entry.Username)) @@ -2632,71 +2712,11 @@ func (u *ui) newRemoteBindingSyncMode() appstate.SyncMode { func (u *ui) syncSavedRemoteBindingSelection() { profiles := u.availableRemoteProfiles() entries := u.availableRemoteCredentialEntries() - - profileID := strings.TrimSpace(u.selectedVaultRemoteProfileID) - if profileID != "" { - var found bool - for _, profile := range profiles { - if profile.ID == profileID { - found = true - break - } - } - if !found { - u.selectedVaultRemoteProfileID = "" - } - } - if strings.TrimSpace(u.selectedVaultRemoteProfileID) == "" && len(profiles) == 1 { - u.selectedVaultRemoteProfileID = profiles[0].ID - } - if profile, ok := u.selectedVaultRemoteProfile(); ok { - u.remoteBaseURL.SetText(profile.BaseURL) - u.remotePath.SetText(profile.Path) - } - - entryID := strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID) - if entryID != "" { - var found bool - for _, entry := range entries { - if entry.ID == entryID { - found = true - break - } - } - if !found { - u.selectedVaultRemoteCredentialEntryID = "" - } - } - if strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID) == "" && len(entries) == 1 { - u.selectedVaultRemoteCredentialEntryID = entries[0].ID - } - if strings.TrimSpace(u.selectedVaultRemoteProfileID) == "" || strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID) == "" { - if record, ok := u.boundRecentRemoteForLocalVault(strings.TrimSpace(u.vaultPath.Text())); ok { - u.selectedVaultRemoteProfileID = strings.TrimSpace(record.RemoteProfileID) - u.selectedVaultRemoteCredentialEntryID = strings.TrimSpace(record.CredentialEntryID) - u.selectedVaultRemoteSyncMode = normalizeUISyncMode(appstate.SyncMode(record.SyncMode)) - if profile, ok := u.selectedVaultRemoteProfile(); ok { - u.remoteBaseURL.SetText(profile.BaseURL) - u.remotePath.SetText(profile.Path) - } - } - } - if binding, ok := u.selectedVaultRemoteBinding(); ok { - for _, record := range u.recentRemotes { - if strings.TrimSpace(record.LocalVaultPath) != strings.TrimSpace(binding.LocalVaultPath) { - continue - } - if strings.TrimSpace(record.RemoteProfileID) != strings.TrimSpace(binding.RemoteProfileID) { - continue - } - if strings.TrimSpace(record.CredentialEntryID) != strings.TrimSpace(binding.CredentialEntryID) { - continue - } - u.selectedVaultRemoteSyncMode = normalizeUISyncMode(appstate.SyncMode(record.SyncMode)) - return - } - } - u.selectedVaultRemoteSyncMode = appstate.SyncModeManual + u.selectedVaultRemoteProfileID = u.validRemoteProfileSelection(profiles) + u.selectedVaultRemoteCredentialEntryID = u.validRemoteCredentialSelection(entries) + u.applySelectedRemoteProfileFields() + u.syncRecentRemoteBindingSelection() + u.syncSelectedRemoteBindingMode() } func (u *ui) boundRecentRemoteForLocalVault(path string) (recentRemoteRecord, bool) { @@ -4115,6 +4135,35 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { u.syncHostedAPI() u.filter() u.processShortcuts(gtx) + u.handleLifecycleClicks(gtx) + u.handleHeaderAndDialogClicks(gtx) + u.handleSettingsClicks(gtx) + u.handleSectionAndSyncClicks(gtx) + u.handleApprovalAndAPIClicks(gtx) + u.handleSelectionClicks(gtx) + u.handleVaultAndEntryClicks(gtx) + u.handleGroupClicks(gtx) + u.handleInputUpdates(gtx) + u.updateViewportLayoutMode(gtx) + inset := layout.UniformInset(unit.Dp(16)) + return layout.Stack{}.Layout(gtx, + layout.Expanded(func(gtx layout.Context) layout.Dimensions { + return layout.Background{}.Layout(gtx, fill(bgColor), func(gtx layout.Context) layout.Dimensions { + return inset.Layout(gtx, u.mainFrame) + }) + }), + layout.Stacked(u.syncDialogOverlay), + layout.Stacked(u.securityDialogOverlay), + layout.Stacked(u.remotePrefsDialogOverlay), + layout.Stacked(u.approvalDialogOverlay), + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + return u.phoneHeaderMenus(gtx) + }), + layout.Stacked(u.statusToast), + ) +} + +func (u *ui) handleLifecycleClicks(gtx layout.Context) { for u.createVault.Clicked(gtx) { u.runAction("create vault", u.createVaultAction) } @@ -4122,11 +4171,35 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { u.startOpenVaultAction() } for u.lifecycleRemoteSyncAction.Clicked(gtx) { - if u.lifecycleBusy() { - continue + if !u.lifecycleBusy() { + u.beginLifecycleRemoteSyncOpen() } - u.beginLifecycleRemoteSyncOpen() } + for u.unlockVault.Clicked(gtx) { + u.startUnlockAction() + } + for u.cancelLifecycleProgress.Clicked(gtx) { + u.cancelLifecycleBusyState() + } + for u.retryLifecycleOpen.Clicked(gtx) { + u.state.ErrorMessage = "" + u.retryLastLifecycleOpen() + } + for u.toggleLifecycleAdvanced.Clicked(gtx) { + if !u.lifecycleBusy() { + u.lifecycleAdvancedHidden = !u.lifecycleAdvancedHidden + u.saveUIPreferences() + } + } +} + +func (u *ui) handleHeaderAndDialogClicks(gtx layout.Context) { + u.handleHeaderActionClicks(gtx) + u.handleDialogControlClicks(gtx) + u.handleBannerClicks(gtx) +} + +func (u *ui) handleHeaderActionClicks(gtx layout.Context) { for u.saveVault.Clicked(gtx) { u.runAction("save vault", u.saveAction) } @@ -4167,24 +4240,12 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { for u.openRemotePrefsHelp.Clicked(gtx) { u.remotePrefsDialogOpen = true } - for u.setStatusBannerShort.Clicked(gtx) { - u.setStatusBannerTTL(2 * time.Second) - } - for u.setStatusBannerStandard.Clicked(gtx) { - u.setStatusBannerTTL(statusBannerDuration) - } - for u.setStatusBannerLong.Clicked(gtx) { - u.setStatusBannerTTL(statusBannerLong) - } - for u.showAllAutofillNotices.Clicked(gtx) { - u.setAutofillNoticePreference(autofillNoticeAll) - } - for u.showApprovalAutofillOnly.Clicked(gtx) { - u.setAutofillNoticePreference(autofillNoticeApprovals) - } - for u.hideAutofillNotices.Clicked(gtx) { - u.setAutofillNoticePreference(autofillNoticeSuppressed) + for u.lockVault.Clicked(gtx) { + u.runAction("lock vault", u.lockAction) } +} + +func (u *ui) handleDialogControlClicks(gtx layout.Context) { for u.closeAdvancedSync.Clicked(gtx) { u.syncDialogOpen = false u.showSyncPassword = false @@ -4201,6 +4262,60 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { for u.saveSecuritySettings.Clicked(gtx) { u.runAction("save settings", u.saveSecuritySettingsAction) } +} + +func (u *ui) handleBannerClicks(gtx layout.Context) { + for u.dismissBanner.Clicked(gtx) { + u.state.ErrorMessage = "" + u.state.StatusMessage = "" + u.statusExpiresAt = time.Time{} + } +} + +func (u *ui) handleSettingsClicks(gtx layout.Context) { + u.handleStatusPreferenceClicks(gtx) + u.handleAutofillPreferenceClicks(gtx) + u.handleAccessibilityClicks(gtx) + u.handleSettingsSyncDefaultClicks(gtx) +} + +func (u *ui) handleStatusPreferenceClicks(gtx layout.Context) { + for u.setStatusBannerShort.Clicked(gtx) { + u.setStatusBannerTTL(2 * time.Second) + } + for u.setStatusBannerStandard.Clicked(gtx) { + u.setStatusBannerTTL(statusBannerDuration) + } + for u.setStatusBannerLong.Clicked(gtx) { + u.setStatusBannerTTL(statusBannerLong) + } +} + +func (u *ui) handleAutofillPreferenceClicks(gtx layout.Context) { + for u.showAllAutofillNotices.Clicked(gtx) { + u.setAutofillNoticePreference(autofillNoticeAll) + } + for u.showApprovalAutofillOnly.Clicked(gtx) { + u.setAutofillNoticePreference(autofillNoticeApprovals) + } + for u.hideAutofillNotices.Clicked(gtx) { + u.setAutofillNoticePreference(autofillNoticeSuppressed) + } + for u.showAutofillApprovalAsk.Clicked(gtx) { + u.autofillFirstFillApprovalMode = autofillFirstFillApprovalAsk + u.saveUIPreferences() + } + for u.showAutofillApprovalAllow.Clicked(gtx) { + u.autofillFirstFillApprovalMode = autofillFirstFillApprovalAllow + u.saveUIPreferences() + } + for u.showAutofillApprovalBlock.Clicked(gtx) { + u.autofillFirstFillApprovalMode = autofillFirstFillApprovalBlock + u.saveUIPreferences() + } +} + +func (u *ui) handleAccessibilityClicks(gtx layout.Context) { for u.settingsDensityDense.Clicked(gtx) { u.settingsDraft.Accessibility.DisplayDensity = displayDensityDense _ = u.applySecuritySettingsLive() @@ -4233,16 +4348,35 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { u.settingsDraft.Accessibility.KeyboardFocus = keyboardFocusProminent _ = u.applySecuritySettingsLive() } - for u.unlockVault.Clicked(gtx) { - u.startUnlockAction() +} + +func (u *ui) handleSettingsSyncDefaultClicks(gtx layout.Context) { + for u.showSettingsSyncLocal.Clicked(gtx) { + u.settingsDraft.Sync.SourceDefault = syncSourceLocal + _ = u.applySecuritySettingsLive() } - for u.cancelLifecycleProgress.Clicked(gtx) { - u.cancelLifecycleBusyState() + for u.showSettingsSyncRemote.Clicked(gtx) { + u.settingsDraft.Sync.SourceDefault = syncSourceRemote + _ = u.applySecuritySettingsLive() } - for u.retryLifecycleOpen.Clicked(gtx) { - u.state.ErrorMessage = "" - u.retryLastLifecycleOpen() + for u.showSettingsSyncPull.Clicked(gtx) { + u.settingsDraft.Sync.DirectionDefault = syncDirectionPull + _ = u.applySecuritySettingsLive() } + for u.showSettingsSyncPush.Clicked(gtx) { + u.settingsDraft.Sync.DirectionDefault = syncDirectionPush + _ = u.applySecuritySettingsLive() + } +} + +func (u *ui) handleSectionAndSyncClicks(gtx layout.Context) { + u.handleSectionClicks(gtx) + u.handleLifecycleModeClicks(gtx) + u.handleSyncChoiceClicks(gtx) + u.handleRemoteBindingClicks(gtx) +} + +func (u *ui) handleSectionClicks(gtx layout.Context) { for u.showEntries.Clicked(gtx) { u.clearDeleteGroupConfirmation() u.showEntriesSection() @@ -4267,28 +4401,25 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { u.clearDeleteGroupConfirmation() u.showAboutSection() } +} + +func (u *ui) handleLifecycleModeClicks(gtx layout.Context) { for u.showLocalLifecycle.Clicked(gtx) { - if u.lifecycleBusy() { - continue + if !u.lifecycleBusy() { + u.lifecycleMode = "local" + u.requestMasterPassFocus = true } - u.lifecycleMode = "local" - u.requestMasterPassFocus = true } for u.showRemoteLifecycle.Clicked(gtx) { - if u.lifecycleBusy() { - continue + if !u.lifecycleBusy() { + u.lifecycleMode = "remote" + u.selectedRemoteConnection = false + u.requestMasterPassFocus = true } - u.lifecycleMode = "remote" - u.selectedRemoteConnection = false - u.requestMasterPassFocus = true - } - for u.toggleLifecycleAdvanced.Clicked(gtx) { - if u.lifecycleBusy() { - continue - } - u.lifecycleAdvancedHidden = !u.lifecycleAdvancedHidden - u.saveUIPreferences() } +} + +func (u *ui) handleSyncChoiceClicks(gtx layout.Context) { for u.showSyncLocal.Clicked(gtx) { u.syncSourceMode = syncSourceLocal } @@ -4301,34 +4432,35 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { for u.showSyncPush.Clicked(gtx) { u.syncDirection = syncDirectionPush } - for u.showSettingsSyncLocal.Clicked(gtx) { - u.settingsDraft.Sync.SourceDefault = syncSourceLocal - _ = u.applySecuritySettingsLive() +} + +func (u *ui) handleRemoteBindingClicks(gtx layout.Context) { + for u.useSavedAdvancedSyncRemote.Clicked(gtx) { + u.openRemoteSyncSetupDialog() } - for u.showSettingsSyncRemote.Clicked(gtx) { - u.settingsDraft.Sync.SourceDefault = syncSourceRemote - _ = u.applySecuritySettingsLive() + for u.openSelectedVaultRemote.Clicked(gtx) { + if !u.lifecycleBusy() { + u.startOpenRemoteAction() + } } - for u.showSettingsSyncPull.Clicked(gtx) { - u.settingsDraft.Sync.DirectionDefault = syncDirectionPull - _ = u.applySecuritySettingsLive() + for u.saveCurrentRemoteBinding.Clicked(gtx) { + u.runAction("save remote binding", u.saveCurrentRemoteBindingAction) } - for u.showSettingsSyncPush.Clicked(gtx) { - u.settingsDraft.Sync.DirectionDefault = syncDirectionPush - _ = u.applySecuritySettingsLive() + for u.removeSelectedRemoteBinding.Clicked(gtx) { + u.runAction("remove remote sync binding", u.removeSelectedRemoteBindingAction) } - for u.showAutofillApprovalAsk.Clicked(gtx) { - u.autofillFirstFillApprovalMode = autofillFirstFillApprovalAsk - u.saveUIPreferences() - } - for u.showAutofillApprovalAllow.Clicked(gtx) { - u.autofillFirstFillApprovalMode = autofillFirstFillApprovalAllow - u.saveUIPreferences() - } - for u.showAutofillApprovalBlock.Clicked(gtx) { - u.autofillFirstFillApprovalMode = autofillFirstFillApprovalBlock - u.saveUIPreferences() + for u.shareCurrentVault.Clicked(gtx) { + u.runAction("share vault", u.shareCurrentVaultAction) } +} + +func (u *ui) handleApprovalAndAPIClicks(gtx layout.Context) { + u.handleApprovalClicks(gtx) + u.handleAPITokenClicks(gtx) + u.handleAPIPolicyClicks(gtx) +} + +func (u *ui) handleApprovalClicks(gtx layout.Context) { for u.allowApproval.Clicked(gtx) { u.runAction("allow API request", func() error { outcome := apiapproval.OutcomeAllowOnce @@ -4358,9 +4490,9 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { return err }) } - for u.lockVault.Clicked(gtx) { - u.runAction("lock vault", u.lockAction) - } +} + +func (u *ui) handleAPITokenClicks(gtx layout.Context) { for u.issueAPIToken.Clicked(gtx) { u.runAction("issue API token", u.issueAPITokenAction) } @@ -4379,6 +4511,21 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { for u.deleteAPIToken.Clicked(gtx) { u.runAction("delete API token", u.deleteAPITokenAction) } + for u.copyAPITokenSecret.Clicked(gtx) { + secret := u.apiTokenSecret + u.runAction("copy API token secret", func() error { + if strings.TrimSpace(secret) == "" { + return fmt.Errorf("no API token secret to copy") + } + if u.clipboardWriter != nil { + return u.clipboardWriter.WriteText(secret) + } + return clipboard.WriteText(secret) + }) + } +} + +func (u *ui) handleAPIPolicyClicks(gtx layout.Context) { for u.addAPIPolicyRule.Clicked(gtx) { u.runAction("add API policy rule", u.addAPIPolicyRuleAction) } @@ -4397,53 +4544,40 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { u.runAction("remove API policy rule", func() error { return u.removeAPIPolicyRuleAction(index) }) } } - for u.copyAPITokenSecret.Clicked(gtx) { - secret := u.apiTokenSecret - u.runAction("copy API token secret", func() error { - if strings.TrimSpace(secret) == "" { - return fmt.Errorf("no API token secret to copy") - } - if u.clipboardWriter != nil { - return u.clipboardWriter.WriteText(secret) - } - return clipboard.WriteText(secret) - }) - } - for u.editEntry.Clicked(gtx) { - u.editingEntry = true - u.loadSelectedEntryIntoEditor() - } - for u.cancelEdit.Clicked(gtx) { - u.editingEntry = false - u.loadSelectedEntryIntoEditor() - } +} + +func (u *ui) handleSelectionClicks(gtx layout.Context) { + u.handleFileSelectionClicks(gtx) + u.handleRecentSelectionClicks(gtx) + u.handleRemoteSelectionClicks(gtx) + u.handleClearSelectionClicks(gtx) +} + +func (u *ui) handleFileSelectionClicks(gtx layout.Context) { for u.pickVaultPath.Clicked(gtx) { - if u.lifecycleBusy() { - continue + if !u.lifecycleBusy() { + u.startChooseVaultPathAction() } - u.startChooseVaultPathAction() } for u.importSharedVault.Clicked(gtx) { - if u.lifecycleBusy() { - continue + if !u.lifecycleBusy() { + u.startImportSharedVaultAction() } - u.startImportSharedVaultAction() } for u.pickKeyFile.Clicked(gtx) { - if u.lifecycleBusy() { - continue + if !u.lifecycleBusy() { + u.runAction("choose key file", func() error { return u.chooseExistingFileAction(&u.keyFilePath) }) } - u.runAction("choose key file", func() error { return u.chooseExistingFileAction(&u.keyFilePath) }) } for u.pickSyncLocalPath.Clicked(gtx) { u.startChooseSyncLocalSourceAction() } +} + +func (u *ui) handleRecentSelectionClicks(gtx layout.Context) { for i := range u.recentVaultClicks { for u.recentVaultClicks[i].Clicked(gtx) { - if u.lifecycleBusy() { - continue - } - if i < len(u.recentVaults) { + if !u.lifecycleBusy() && i < len(u.recentVaults) { u.lifecycleMode = "local" u.vaultPath.SetText(u.recentVaults[i]) u.requestMasterPassFocus = true @@ -4452,16 +4586,16 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { } for i := range u.recentRemoteClicks { for u.recentRemoteClicks[i].Clicked(gtx) { - if u.lifecycleBusy() { - continue - } - if i < len(u.recentRemotes) { + if !u.lifecycleBusy() && i < len(u.recentRemotes) { u.lifecycleMode = "remote" u.applyRecentRemoteRecord(u.recentRemotes[i]) u.requestMasterPassFocus = true } } } +} + +func (u *ui) handleRemoteSelectionClicks(gtx layout.Context) { for i := range u.vaultRemoteProfileClicks { for u.vaultRemoteProfileClicks[i].Clicked(gtx) { profiles := u.availableRemoteProfiles() @@ -4486,24 +4620,9 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { } } } - for u.useSavedAdvancedSyncRemote.Clicked(gtx) { - u.openRemoteSyncSetupDialog() - } - for u.openSelectedVaultRemote.Clicked(gtx) { - if u.lifecycleBusy() { - continue - } - u.startOpenRemoteAction() - } - for u.saveCurrentRemoteBinding.Clicked(gtx) { - u.runAction("save remote binding", u.saveCurrentRemoteBindingAction) - } - for u.removeSelectedRemoteBinding.Clicked(gtx) { - u.runAction("remove remote sync binding", u.removeSelectedRemoteBindingAction) - } - for u.shareCurrentVault.Clicked(gtx) { - u.runAction("share vault", u.shareCurrentVaultAction) - } +} + +func (u *ui) handleClearSelectionClicks(gtx layout.Context) { for u.clearVaultSelection.Clicked(gtx) { if u.lifecycleBusy() { continue @@ -4534,10 +4653,22 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { u.state.StatusMessage = "" u.requestMasterPassFocus = true } - for u.dismissBanner.Clicked(gtx) { - u.state.ErrorMessage = "" - u.state.StatusMessage = "" - u.statusExpiresAt = time.Time{} +} + +func (u *ui) handleVaultAndEntryClicks(gtx layout.Context) { + u.handleEntryEditorClicks(gtx) + u.handleEntryMutationClicks(gtx) + u.handleAttachmentAndCopyClicks(gtx) +} + +func (u *ui) handleEntryEditorClicks(gtx layout.Context) { + for u.editEntry.Clicked(gtx) { + u.editingEntry = true + u.loadSelectedEntryIntoEditor() + } + for u.cancelEdit.Clicked(gtx) { + u.editingEntry = false + u.loadSelectedEntryIntoEditor() } for u.addEntry.Clicked(gtx) { u.state.BeginNewEntry() @@ -4545,6 +4676,9 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { u.entryPath.SetText(strings.Join(u.displayPath(), " / ")) u.editingEntry = true } +} + +func (u *ui) handleEntryMutationClicks(gtx layout.Context) { for u.saveEntry.Clicked(gtx) { u.runAction("save entry", u.saveEntryAction) } @@ -4566,6 +4700,9 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { for u.instantiateTemplate.Clicked(gtx) { u.runAction("instantiate template", u.instantiateSelectedTemplateAction) } +} + +func (u *ui) handleAttachmentAndCopyClicks(gtx layout.Context) { for u.addAttachment.Clicked(gtx) { u.runAction("add attachment", u.addAttachmentAction) } @@ -4593,6 +4730,9 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { for u.restoreHistory.Clicked(gtx) { u.runAction("restore history", u.restoreSelectedHistoryAction) } +} + +func (u *ui) handleGroupClicks(gtx layout.Context) { for u.createGroup.Clicked(gtx) { u.clearDeleteGroupConfirmation() u.runAction("create group", u.createGroupAction) @@ -4628,6 +4768,9 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { u.state.StatusMessage = "" u.statusExpiresAt = time.Time{} } +} + +func (u *ui) handleInputUpdates(gtx layout.Context) { if u.securityDialogOpen { if _, changed := u.securityCipher.Update(gtx); changed { _ = u.applySecuritySettingsLive() @@ -4662,111 +4805,117 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { if _, changed := u.search.Update(gtx); changed { u.filter() } - u.updateViewportLayoutMode(gtx) - inset := layout.UniformInset(unit.Dp(16)) - return layout.Stack{}.Layout(gtx, - layout.Expanded(func(gtx layout.Context) layout.Dimensions { - return layout.Background{}.Layout(gtx, fill(bgColor), func(gtx layout.Context) layout.Dimensions { - return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(u.header), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if u.bannerSurface().Kind == bannerNone { - return layout.Dimensions{} - } - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), - layout.Rigid(u.banner), - layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), - ) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if u.bannerSurface().Kind != bannerNone { - return layout.Dimensions{} - } - if u.autofillStatusSurface().Kind == autofillStatusNone { - return layout.Dimensions{} - } - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), - layout.Rigid(u.autofillStatusCard), - layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), - ) - }), - layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { - if u.shouldShowLifecycleSetup() { - return u.lifecycleScreen(gtx) - } - if u.shouldUseLockedSinglePane() { - return u.detailPanel(gtx) - } - if u.usesCompactViewport() { - u.phoneSpan = gtx.Constraints.Max.Y - listHeight := int(float32(gtx.Constraints.Max.Y) * u.phoneSplit.Value) - if listHeight < gtx.Dp(unit.Dp(180)) { - listHeight = gtx.Dp(unit.Dp(180)) - } - if listHeight > gtx.Constraints.Max.Y-gtx.Dp(unit.Dp(220)) { - listHeight = gtx.Constraints.Max.Y - gtx.Dp(unit.Dp(220)) - } - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - gtx.Constraints.Min.Y = listHeight - gtx.Constraints.Max.Y = listHeight - return u.listPanel(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(u.phoneSlider), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - func() layout.FlexChild { - if u.shouldUseCompactPhoneDetailPane() { - return layout.Rigid(u.detailPanel) - } - return layout.Flexed(1, u.detailPanel) - }(), - ) - } - return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, - layout.Flexed(0.38, u.listPanel), - layout.Rigid(layout.Spacer{Width: unit.Dp(12)}.Layout), - layout.Flexed(0.62, u.detailPanel), - ) - }), - ) - }) - }) - }), - layout.Stacked(func(gtx layout.Context) layout.Dimensions { - if !u.syncDialogOpen { - return layout.Dimensions{} - } - return u.syncDialog(gtx) - }), - layout.Stacked(func(gtx layout.Context) layout.Dimensions { - if !u.securityDialogOpen { - return layout.Dimensions{} - } - return u.securityDialog(gtx) - }), - layout.Stacked(func(gtx layout.Context) layout.Dimensions { - if !u.remotePrefsDialogOpen { - return layout.Dimensions{} - } - return u.remotePrefsDialog(gtx) - }), - layout.Stacked(func(gtx layout.Context) layout.Dimensions { - if _, ok := u.pendingApproval(); !ok { - return layout.Dimensions{} - } - return u.approvalDialog(gtx) - }), - layout.Stacked(func(gtx layout.Context) layout.Dimensions { - return u.phoneHeaderMenus(gtx) - }), - layout.Stacked(u.statusToast), +} + +func (u *ui) mainFrame(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(u.header), + layout.Rigid(u.bannerRow), + layout.Rigid(u.autofillStatusRow), + layout.Flexed(1, u.primaryContent), ) } +func (u *ui) bannerRow(gtx layout.Context) layout.Dimensions { + if u.bannerSurface().Kind == bannerNone { + return layout.Dimensions{} + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), + layout.Rigid(u.banner), + layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), + ) +} + +func (u *ui) autofillStatusRow(gtx layout.Context) layout.Dimensions { + if u.bannerSurface().Kind != bannerNone || u.autofillStatusSurface().Kind == autofillStatusNone { + return layout.Dimensions{} + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), + layout.Rigid(u.autofillStatusCard), + layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), + ) +} + +func (u *ui) primaryContent(gtx layout.Context) layout.Dimensions { + switch { + case u.shouldShowLifecycleSetup(): + return u.lifecycleScreen(gtx) + case u.shouldUseLockedSinglePane(): + return u.detailPanel(gtx) + case u.usesCompactViewport(): + return u.compactPrimaryContent(gtx) + default: + return u.widePrimaryContent(gtx) + } +} + +func (u *ui) compactPrimaryContent(gtx layout.Context) layout.Dimensions { + u.phoneSpan = gtx.Constraints.Max.Y + listHeight := int(float32(gtx.Constraints.Max.Y) * u.phoneSplit.Value) + if min := gtx.Dp(unit.Dp(180)); listHeight < min { + listHeight = min + } + if max := gtx.Constraints.Max.Y - gtx.Dp(unit.Dp(220)); listHeight > max { + listHeight = max + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + gtx.Constraints.Min.Y = listHeight + gtx.Constraints.Max.Y = listHeight + return u.listPanel(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(u.phoneSlider), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + u.compactDetailFlexChild(), + ) +} + +func (u *ui) compactDetailFlexChild() layout.FlexChild { + if u.shouldUseCompactPhoneDetailPane() { + return layout.Rigid(u.detailPanel) + } + return layout.Flexed(1, u.detailPanel) +} + +func (u *ui) widePrimaryContent(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Flexed(0.38, u.listPanel), + layout.Rigid(layout.Spacer{Width: unit.Dp(12)}.Layout), + layout.Flexed(0.62, u.detailPanel), + ) +} + +func (u *ui) syncDialogOverlay(gtx layout.Context) layout.Dimensions { + if !u.syncDialogOpen { + return layout.Dimensions{} + } + return u.syncDialog(gtx) +} + +func (u *ui) securityDialogOverlay(gtx layout.Context) layout.Dimensions { + if !u.securityDialogOpen { + return layout.Dimensions{} + } + return u.securityDialog(gtx) +} + +func (u *ui) remotePrefsDialogOverlay(gtx layout.Context) layout.Dimensions { + if !u.remotePrefsDialogOpen { + return layout.Dimensions{} + } + return u.remotePrefsDialog(gtx) +} + +func (u *ui) approvalDialogOverlay(gtx layout.Context) layout.Dimensions { + if _, ok := u.pendingApproval(); !ok { + return layout.Dimensions{} + } + return u.approvalDialog(gtx) +} + func (u *ui) syncHostedAPI() { if u.apiHost == nil { return @@ -5424,119 +5573,148 @@ func (u *ui) listPanelPrimaryActionRow(gtx layout.Context) layout.Dimensions { func (u *ui) listPanel(gtx layout.Context) layout.Dimensions { panel := card - spacing := u.sectionSpacing() if u.usesCompactViewport() { panel = compactCard } u.ensureNavClickables() if u.usesCompactViewport() { - return panel(gtx, func(gtx layout.Context) layout.Dimensions { - visibleEntries, entryClicks := u.visibleEntrySnapshot() - rows := make([]layout.Widget, 0, 16+len(visibleEntries)) - for _, section := range u.listPanelTopSections() { - switch section { - case listPanelTopSearch: - rows = append(rows, u.listPanelSearchRow) - case listPanelTopNavigation: - rows = append(rows, u.navigationHeader) - case listPanelTopPath: - rows = append(rows, u.pathBar) - case listPanelTopGroup: - rows = append(rows, u.groupBar) - case listPanelTopGroupTools: - rows = append(rows, u.groupControlsSection) - case listPanelTopPrimary: - rows = append(rows, u.listPanelPrimaryActionRow) - } - rows = append(rows, func(gtx layout.Context) layout.Dimensions { - return layout.Spacer{Height: spacing}.Layout(gtx) - }) - } - switch { - case u.state.Section == appstate.SectionAPITokens: - rows = append(rows, u.apiTokenListPanel) - case u.state.Section == appstate.SectionAPIAudit: - rows = append(rows, u.apiAuditListPanel) - case u.state.Section == appstate.SectionAbout: - case len(visibleEntries) == 0: - rows = append(rows, func(gtx layout.Context) layout.Dimensions { - return emptyStatePanel(gtx, u.theme, u.listEmptyState()) - }) - default: - for i := range visibleEntries { - idx := i - rows = append(rows, func(gtx layout.Context) layout.Dimensions { - return u.entryRow(gtx, entryClicks[idx], idx, visibleEntries[idx]) - }) - } - } - return material.List(u.theme, &u.phonePanelList).Layout(gtx, len(rows), func(gtx layout.Context, i int) layout.Dimensions { - return rows[i](gtx) + return panel(gtx, u.compactListPanel) + } + return panel(gtx, u.wideListPanel) +} + +func (u *ui) compactListPanel(gtx layout.Context) layout.Dimensions { + visibleEntries, entryClicks := u.visibleEntrySnapshot() + rows := u.compactListPanelRows(visibleEntries, entryClicks) + return material.List(u.theme, &u.phonePanelList).Layout(gtx, len(rows), func(gtx layout.Context, i int) layout.Dimensions { + return rows[i](gtx) + }) +} + +func (u *ui) compactListPanelRows(visibleEntries []entry, entryClicks []*widget.Clickable) []layout.Widget { + rows := u.compactListPanelTopRows() + switch { + case u.state.Section == appstate.SectionAPITokens: + rows = append(rows, u.apiTokenListPanel) + case u.state.Section == appstate.SectionAPIAudit: + rows = append(rows, u.apiAuditListPanel) + case u.state.Section == appstate.SectionAbout: + case len(visibleEntries) == 0: + rows = append(rows, func(gtx layout.Context) layout.Dimensions { + return emptyStatePanel(gtx, u.theme, u.listEmptyState()) + }) + default: + for i := range visibleEntries { + idx := i + rows = append(rows, func(gtx layout.Context) layout.Dimensions { + return u.entryRow(gtx, entryClicks[idx], idx, visibleEntries[idx]) }) + } + } + return rows +} + +func (u *ui) compactListPanelTopRows() []layout.Widget { + spacing := u.sectionSpacing() + rows := make([]layout.Widget, 0, 16) + for _, section := range u.listPanelTopSections() { + rows = append(rows, u.listPanelTopSectionWidget(section)) + rows = append(rows, func(gtx layout.Context) layout.Dimensions { + return layout.Spacer{Height: spacing}.Layout(gtx) }) } - return panel(gtx, func(gtx layout.Context) layout.Dimensions { - children := make([]layout.FlexChild, 0, 16) - for _, section := range u.listPanelTopSections() { - switch section { - case listPanelTopSearch: - children = append(children, layout.Rigid(u.listPanelSearchRow)) - case listPanelTopNavigation: - children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if u.isVaultLocked() { - return layout.Dimensions{} - } - return u.navigationHeader(gtx) - })) - case listPanelTopPath: - children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if u.isVaultLocked() || (u.state.Section != appstate.SectionEntries && u.state.Section != appstate.SectionRecycleBin) { - return layout.Dimensions{} - } - return u.pathBar(gtx) - })) - case listPanelTopGroup: - children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if u.isVaultLocked() || u.state.Section != appstate.SectionEntries { - return layout.Dimensions{} - } - return u.groupBar(gtx) - })) - case listPanelTopGroupTools: - children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if u.isVaultLocked() || u.state.Section != appstate.SectionEntries { - return layout.Dimensions{} - } - return u.groupControlsSection(gtx) - })) - case listPanelTopPrimary: - children = append(children, layout.Rigid(u.listPanelPrimaryActionRow)) + return rows +} + +func (u *ui) listPanelTopSectionWidget(section listPanelTopSection) layout.Widget { + switch section { + case listPanelTopSearch: + return u.listPanelSearchRow + case listPanelTopNavigation: + return u.navigationHeader + case listPanelTopPath: + return u.pathBar + case listPanelTopGroup: + return u.groupBar + case listPanelTopGroupTools: + return u.groupControlsSection + case listPanelTopPrimary: + return u.listPanelPrimaryActionRow + default: + return func(gtx layout.Context) layout.Dimensions { return layout.Dimensions{} } + } +} + +func (u *ui) wideListPanel(gtx layout.Context) layout.Dimensions { + children := u.wideListPanelChildren() + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...) +} + +func (u *ui) wideListPanelChildren() []layout.FlexChild { + spacing := u.sectionSpacing() + children := make([]layout.FlexChild, 0, 16) + for _, section := range u.listPanelTopSections() { + children = append(children, layout.Rigid(u.wideListPanelTopSectionWidget(section))) + children = append(children, layout.Rigid(layout.Spacer{Height: spacing}.Layout)) + } + children = append(children, layout.Flexed(1, u.wideListPanelBody)) + return children +} + +func (u *ui) wideListPanelTopSectionWidget(section listPanelTopSection) layout.Widget { + switch section { + case listPanelTopSearch: + return u.listPanelSearchRow + case listPanelTopNavigation: + return func(gtx layout.Context) layout.Dimensions { + if u.isVaultLocked() { + return layout.Dimensions{} } - children = append(children, layout.Rigid(layout.Spacer{Height: spacing}.Layout)) + return u.navigationHeader(gtx) } - children = append(children, - layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { - if u.state.Section == appstate.SectionAPITokens { - return u.apiTokenListPanel(gtx) - } - if u.state.Section == appstate.SectionAPIAudit { - return u.apiAuditListPanel(gtx) - } - if u.state.Section == appstate.SectionAbout { - return emptyStatePanel(gtx, u.theme, u.listEmptyState()) - } - if len(u.visible) == 0 { - return emptyStatePanel(gtx, u.theme, u.listEmptyState()) - } - return material.List(u.theme, &u.list).Layout(gtx, len(u.visible), func(gtx layout.Context, i int) layout.Dimensions { - item := u.visible[i] - click := &u.entryClicks[i] - return u.entryRow(gtx, click, i, item) - }) - }), - ) - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...) - }) + case listPanelTopPath: + return func(gtx layout.Context) layout.Dimensions { + if u.isVaultLocked() || (u.state.Section != appstate.SectionEntries && u.state.Section != appstate.SectionRecycleBin) { + return layout.Dimensions{} + } + return u.pathBar(gtx) + } + case listPanelTopGroup: + return func(gtx layout.Context) layout.Dimensions { + if u.isVaultLocked() || u.state.Section != appstate.SectionEntries { + return layout.Dimensions{} + } + return u.groupBar(gtx) + } + case listPanelTopGroupTools: + return func(gtx layout.Context) layout.Dimensions { + if u.isVaultLocked() || u.state.Section != appstate.SectionEntries { + return layout.Dimensions{} + } + return u.groupControlsSection(gtx) + } + case listPanelTopPrimary: + return u.listPanelPrimaryActionRow + default: + return func(gtx layout.Context) layout.Dimensions { return layout.Dimensions{} } + } +} + +func (u *ui) wideListPanelBody(gtx layout.Context) layout.Dimensions { + switch { + case u.state.Section == appstate.SectionAPITokens: + return u.apiTokenListPanel(gtx) + case u.state.Section == appstate.SectionAPIAudit: + return u.apiAuditListPanel(gtx) + case u.state.Section == appstate.SectionAbout, len(u.visible) == 0: + return emptyStatePanel(gtx, u.theme, u.listEmptyState()) + default: + return material.List(u.theme, &u.list).Layout(gtx, len(u.visible), func(gtx layout.Context, i int) layout.Dimensions { + item := u.visible[i] + click := &u.entryClicks[i] + return u.entryRow(gtx, click, i, item) + }) + } } func (u *ui) navigationHeader(gtx layout.Context) layout.Dimensions { @@ -5561,169 +5739,119 @@ func (u *ui) navigationHeaderLabel() string { return "Group Tools" } -func (u *ui) sectionBar(gtx layout.Context) layout.Dimensions { - tabs := []struct { - click *widget.Clickable - label string - compact string - active bool - }{ - {click: &u.showEntries, label: "Entries", compact: "Entries", active: u.state.Section == appstate.SectionEntries}, - {click: &u.showRecycle, label: "Recycle Bin", compact: "Recycle", active: u.state.Section == appstate.SectionRecycleBin}, - {click: &u.showAPITokens, label: "API Tokens", compact: "Tokens", active: u.state.Section == appstate.SectionAPITokens}, - {click: &u.showAPIAudit, label: "API Audit", compact: "Audit", active: u.state.Section == appstate.SectionAPIAudit}, - } - if u.usesCompactViewport() { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Spacing: layout.SpaceBetween}.Layout(gtx, - layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { - return sectionTabButton(gtx, u.theme, tabs[0].click, tabs[0].compact, tabs[0].active) - }), - layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), - layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { - return sectionTabButton(gtx, u.theme, tabs[1].click, tabs[1].compact, tabs[1].active) - }), - ) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Spacing: layout.SpaceBetween}.Layout(gtx, - layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { - return sectionTabButton(gtx, u.theme, tabs[2].click, tabs[2].compact, tabs[2].active) - }), - layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), - layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { - return sectionTabButton(gtx, u.theme, tabs[3].click, tabs[3].compact, tabs[3].active) - }), - ) - }), - ) - } - return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return sectionTabButton(gtx, u.theme, tabs[0].click, tabs[0].label, tabs[0].active) - }), - layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return sectionTabButton(gtx, u.theme, tabs[1].click, tabs[1].label, tabs[1].active) - }), - layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return sectionTabButton(gtx, u.theme, tabs[2].click, tabs[2].label, tabs[2].active) - }), - layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return sectionTabButton(gtx, u.theme, tabs[3].click, tabs[3].label, tabs[3].active) - }), - ) -} - func (u *ui) entryRow(gtx layout.Context, click *widget.Clickable, idx int, item entry) layout.Dimensions { for click.Clicked(gtx) { _ = u.state.ToggleVisibleIndex(idx) u.loadSelectedEntryIntoEditor() } return click.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - inset, titleSize, metaSize, urlSize, pathSize, dividerGap := u.entryRowMetrics() selected := item.ID == u.state.SelectedEntryID focused := u.isFocused(listFocusID(idx)) rowColors := u.listRowColors(selected, focused, u.state.Section == appstate.SectionRecycleBin) row := func(gtx layout.Context) layout.Dimensions { - return layout.UniformInset(inset).Layout(gtx, func(gtx layout.Context) layout.Dimensions { - showPath := strings.TrimSpace(u.search.Text()) != "" || len(u.displayPath()) == 0 || u.state.Section == appstate.SectionRecycleBin - hasUsername := strings.TrimSpace(item.Username) != "" - hasURL := strings.TrimSpace(item.URL) != "" - pathText := strings.Join(u.displayEntryPath(item.Path), " / ") - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, titleSize, item.Title) - lbl.Color = rowColors.Title - return lbl.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !hasUsername { - return layout.Dimensions{} - } - return layout.Spacer{Height: unit.Dp(3)}.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !hasUsername { - return layout.Dimensions{} - } - lbl := material.Label(u.theme, metaSize, item.Username) - lbl.Color = rowColors.Meta - return lbl.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !hasURL { - return layout.Dimensions{} - } - return layout.Spacer{Height: unit.Dp(2)}.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !hasURL { - return layout.Dimensions{} - } - lbl := material.Label(u.theme, urlSize, item.URL) - lbl.Color = rowColors.Secondary - return lbl.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !showPath { - return layout.Dimensions{} - } - return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !showPath { - return layout.Dimensions{} - } - lbl := material.Label(u.theme, pathSize, pathText) - lbl.Color = rowColors.Secondary - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: dividerGap}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - w := gtx.Constraints.Max.X - if w < 1 { - w = 1 - } - paint.FillShape(gtx.Ops, rowColors.Divider, clip.Rect{Max: image.Pt(w, 1)}.Op()) - return layout.Dimensions{Size: image.Pt(w, 1)} - }), - ) - }) + return u.entryRowContent(gtx, item, rowColors) } if selected || focused { - return layout.Stack{}.Layout(gtx, - layout.Expanded(func(gtx layout.Context) layout.Dimensions { - size := gtx.Constraints.Min - if size.X == 0 { - size.X = gtx.Constraints.Max.X - } - if size.Y == 0 { - size.Y = gtx.Constraints.Max.Y - } - paint.FillShape(gtx.Ops, rowColors.Fill, clip.Rect{Max: size}.Op()) - paint.FillShape(gtx.Ops, rowColors.Edge, clip.Rect{Max: image.Pt(5, size.Y)}.Op()) - return layout.Dimensions{Size: size} - }), - layout.Stacked(func(gtx layout.Context) layout.Dimensions { - return row(gtx) - }), - ) + return u.highlightedEntryRow(gtx, rowColors, row) } - bg := panelColor - if u.state.Section == appstate.SectionRecycleBin { - bg = color.NRGBA{R: 249, G: 242, B: 236, A: 255} - } - return layout.Background{}.Layout(gtx, fill(bg), func(gtx layout.Context) layout.Dimensions { - return row(gtx) - }) + return u.standardEntryRow(gtx, row) }) } +func (u *ui) entryRowContent(gtx layout.Context, item entry, rowColors listRowColors) layout.Dimensions { + inset, titleSize, metaSize, urlSize, pathSize, dividerGap := u.entryRowMetrics() + showPath := strings.TrimSpace(u.search.Text()) != "" || len(u.displayPath()) == 0 || u.state.Section == appstate.SectionRecycleBin + hasUsername := strings.TrimSpace(item.Username) != "" + hasURL := strings.TrimSpace(item.URL) != "" + pathText := strings.Join(u.displayEntryPath(item.Path), " / ") + return layout.UniformInset(inset).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, titleSize, item.Title) + lbl.Color = rowColors.Title + return lbl.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !hasUsername { + return layout.Dimensions{} + } + return layout.Spacer{Height: unit.Dp(3)}.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !hasUsername { + return layout.Dimensions{} + } + lbl := material.Label(u.theme, metaSize, item.Username) + lbl.Color = rowColors.Meta + return lbl.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !hasURL { + return layout.Dimensions{} + } + return layout.Spacer{Height: unit.Dp(2)}.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !hasURL { + return layout.Dimensions{} + } + lbl := material.Label(u.theme, urlSize, item.URL) + lbl.Color = rowColors.Secondary + return lbl.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !showPath { + return layout.Dimensions{} + } + return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !showPath { + return layout.Dimensions{} + } + lbl := material.Label(u.theme, pathSize, pathText) + lbl.Color = rowColors.Secondary + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: dividerGap}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + w := gtx.Constraints.Max.X + if w < 1 { + w = 1 + } + paint.FillShape(gtx.Ops, rowColors.Divider, clip.Rect{Max: image.Pt(w, 1)}.Op()) + return layout.Dimensions{Size: image.Pt(w, 1)} + }), + ) + }) +} + +func (u *ui) highlightedEntryRow(gtx layout.Context, rowColors listRowColors, row layout.Widget) layout.Dimensions { + return layout.Stack{}.Layout(gtx, + layout.Expanded(func(gtx layout.Context) layout.Dimensions { + size := gtx.Constraints.Min + if size.X == 0 { + size.X = gtx.Constraints.Max.X + } + if size.Y == 0 { + size.Y = gtx.Constraints.Max.Y + } + paint.FillShape(gtx.Ops, rowColors.Fill, clip.Rect{Max: size}.Op()) + paint.FillShape(gtx.Ops, rowColors.Edge, clip.Rect{Max: image.Pt(5, size.Y)}.Op()) + return layout.Dimensions{Size: size} + }), + layout.Stacked(row), + ) +} + +func (u *ui) standardEntryRow(gtx layout.Context, row layout.Widget) layout.Dimensions { + bg := panelColor + if u.state.Section == appstate.SectionRecycleBin { + bg = color.NRGBA{R: 249, G: 242, B: 236, A: 255} + } + return layout.Background{}.Layout(gtx, fill(bg), row) +} + func (u *ui) phoneSlider(gtx layout.Context) layout.Dimensions { if !u.usesCompactViewport() { return layout.Dimensions{} @@ -5803,256 +5931,318 @@ func (u *ui) detailPanel(gtx layout.Context) layout.Dimensions { } func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions { - panel := layout.Flex{Axis: layout.Vertical} - _ = panel - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild { - if u.isVaultLocked() { - return []layout.FlexChild{ - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(18), "Unlock Vault") - lbl.Color = accentColor - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(14), "Enter the master password, choose a key file, or provide both.") - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), - layout.Rigid(u.unlockPanel), + if u.isVaultLocked() { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, u.lockedDetailChildren()...) + } + if panel := u.staticDetailPanel(); panel != nil { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, panel...) + } + item, ok := u.selectedEntry() + if !ok && !u.editingEntry { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, u.emptyDetailChildren()...) + } + if u.editingEntry { + return u.detailEditorContent(gtx, ok) + } + return u.detailViewContent(gtx, item) +} + +func (u *ui) lockedDetailChildren() []layout.FlexChild { + return []layout.FlexChild{ + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(18), "Unlock Vault") + lbl.Color = accentColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(14), "Enter the master password, choose a key file, or provide both.") + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), + layout.Rigid(u.unlockPanel), + } +} + +func (u *ui) staticDetailPanel() []layout.FlexChild { + switch u.state.Section { + case appstate.SectionAPITokens: + return []layout.FlexChild{layout.Flexed(1, u.apiTokenDetailPanel)} + case appstate.SectionAPIAudit: + return []layout.FlexChild{layout.Flexed(1, u.apiAuditDetailPanel)} + case appstate.SectionAbout: + return []layout.FlexChild{layout.Flexed(1, u.aboutDetailPanel)} + default: + return nil + } +} + +func (u *ui) emptyDetailChildren() []layout.FlexChild { + return []layout.FlexChild{ + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(18), "Entry details") + lbl.Color = accentColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(16), u.detailPlaceholderMessage()) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + } +} + +func (u *ui) detailEditorContent(gtx layout.Context, hasSelected bool) layout.Dimensions { + rows := []layout.Widget{ + func(gtx layout.Context) layout.Dimensions { + title := "New Entry" + if hasSelected { + title = "Edit Entry" } - } - if u.state.Section == appstate.SectionAPITokens { - return []layout.FlexChild{ - layout.Flexed(1, u.apiTokenDetailPanel), - } - } - if u.state.Section == appstate.SectionAPIAudit { - return []layout.FlexChild{ - layout.Flexed(1, u.apiAuditDetailPanel), - } - } - if u.state.Section == appstate.SectionAbout { - return []layout.FlexChild{ - layout.Flexed(1, u.aboutDetailPanel), - } - } - item, ok := u.selectedEntry() - if !ok && !u.editingEntry { - return []layout.FlexChild{ - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(18), "Entry details") - lbl.Color = accentColor - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(16), u.detailPlaceholderMessage()) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - } - } - if u.editingEntry { - rows := []layout.Widget{ - func(gtx layout.Context) layout.Dimensions { - title := "New Entry" - if ok { - title = "Edit Entry" - } - lbl := material.Label(u.theme, unit.Sp(18), title) - lbl.Color = accentColor - return lbl.Layout(gtx) - }, - layout.Spacer{Height: unit.Dp(8)}.Layout, - u.entryEditorPanel, - } - return []layout.FlexChild{ - layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { - return material.List(u.theme, &u.detailList).Layout(gtx, len(rows), func(gtx layout.Context, i int) layout.Dimensions { - return rows[i](gtx) - }) - }), - } - } - password := u.detailPasswordValue() - titleSize := unit.Sp(26) - titlePad := unit.Dp(10) - sectionGap := unit.Dp(6) - cardGap := unit.Dp(8) + lbl := material.Label(u.theme, unit.Sp(18), title) + lbl.Color = accentColor + return lbl.Layout(gtx) + }, + layout.Spacer{Height: unit.Dp(8)}.Layout, + u.entryEditorPanel, + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + return material.List(u.theme, &u.detailList).Layout(gtx, len(rows), func(gtx layout.Context, i int) layout.Dimensions { + return rows[i](gtx) + }) + }), + ) +} + +type detailViewMetrics struct { + titleSize unit.Sp + titlePad unit.Dp + sectionGap unit.Dp + cardGap unit.Dp +} + +func (u *ui) detailViewContent(gtx layout.Context, item entry) layout.Dimensions { + rows := u.detailViewRows(item) + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + return material.List(u.theme, &u.detailList).Layout(gtx, len(rows), func(gtx layout.Context, i int) layout.Dimensions { + return rows[i](gtx) + }) + }), + ) +} + +func (u *ui) detailViewRows(item entry) []layout.Widget { + password := u.detailPasswordValue() + metrics := u.detailViewMetrics() + rows := []layout.Widget{ + u.detailTitleRow(item, metrics), + layout.Spacer{Height: metrics.titlePad}.Layout, + u.detailCompactCopyRow(item), + layout.Spacer{Height: u.recycleDetailGap()}.Layout, + u.recycleDetailNotice, + layout.Spacer{Height: u.recycleDetailCardGap()}.Layout, + u.detailMetadataCard(item, metrics), + layout.Spacer{Height: metrics.sectionGap}.Layout, + u.detailPasswordCard(password), + layout.Spacer{Height: unit.Dp(8)}.Layout, + u.detailNotesCard(item), + layout.Spacer{Height: metrics.cardGap}.Layout, + u.attachmentSummaryPanel, + layout.Spacer{Height: metrics.cardGap}.Layout, + u.historyPanel, + layout.Spacer{Height: metrics.cardGap}.Layout, + u.detailActionRow, + } + return rows +} + +func (u *ui) detailViewMetrics() detailViewMetrics { + metrics := detailViewMetrics{ + titleSize: unit.Sp(26), + titlePad: unit.Dp(10), + sectionGap: unit.Dp(6), + cardGap: unit.Dp(8), + } + if u.denseLayout { + metrics.titlePad = unit.Dp(6) + metrics.sectionGap = unit.Dp(4) + metrics.cardGap = unit.Dp(6) + } + if u.usesCompactViewport() { + metrics.titleSize = unit.Sp(18) + metrics.titlePad = unit.Dp(4) + metrics.sectionGap = unit.Dp(4) + metrics.cardGap = unit.Dp(6) if u.denseLayout { - titlePad = unit.Dp(6) - sectionGap = unit.Dp(4) - cardGap = unit.Dp(6) + metrics.titlePad = unit.Dp(3) + metrics.sectionGap = unit.Dp(3) + metrics.cardGap = unit.Dp(4) } - if u.usesCompactViewport() { - titleSize = unit.Sp(18) - titlePad = unit.Dp(4) - sectionGap = unit.Dp(4) - cardGap = unit.Dp(6) - if u.denseLayout { - titlePad = unit.Dp(3) - sectionGap = unit.Dp(3) - cardGap = unit.Dp(4) - } + } + return metrics +} + +func (u *ui) detailTitleRow(item entry, metrics detailViewMetrics) layout.Widget { + return func(gtx layout.Context) layout.Dimensions { + title := item.Title + if u.state.Section == appstate.SectionRecycleBin { + title = "Recycle Bin Entry" } - rows := []layout.Widget{ - func(gtx layout.Context) layout.Dimensions { - title := item.Title - if u.state.Section == appstate.SectionRecycleBin { - title = "Recycle Bin Entry" - } - lbl := material.Label(u.theme, titleSize, title) - lbl.Color = accentColor - return lbl.Layout(gtx) - }, - layout.Spacer{Height: titlePad}.Layout, - func(gtx layout.Context) layout.Dimensions { - if u.state.Section != appstate.SectionRecycleBin { - if u.usesCompactViewport() { - return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, - layout.Flexed(0.5, func(gtx layout.Context) layout.Dimensions { - return compactTonedButton(gtx, u.theme, &u.copyUser, "Copy Username") - }), - layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), - layout.Flexed(0.5, func(gtx layout.Context) layout.Dimensions { - return compactTonedButton(gtx, u.theme, &u.copyPass, "Copy Password") - }), - ) - } - return layout.Dimensions{} - } - return recycleDetailTitle(gtx, u.theme, item.Title) - }, - layout.Spacer{Height: func() unit.Dp { - if u.state.Section == appstate.SectionRecycleBin { - return unit.Dp(10) - } - return 0 - }()}.Layout, - func(gtx layout.Context) layout.Dimensions { - if u.state.Section != appstate.SectionRecycleBin { - return layout.Dimensions{} - } - return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(12), "This entry is in the recycle bin. Review it, copy from it, or restore it back into the vault.") + lbl := material.Label(u.theme, metrics.titleSize, title) + lbl.Color = accentColor + return lbl.Layout(gtx) + } +} + +func (u *ui) detailCompactCopyRow(item entry) layout.Widget { + return func(gtx layout.Context) layout.Dimensions { + if u.state.Section == appstate.SectionRecycleBin { + return recycleDetailTitle(gtx, u.theme, item.Title) + } + if !u.usesCompactViewport() { + return layout.Dimensions{} + } + return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, + layout.Flexed(0.5, func(gtx layout.Context) layout.Dimensions { + return compactTonedButton(gtx, u.theme, &u.copyUser, "Copy Username") + }), + layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), + layout.Flexed(0.5, func(gtx layout.Context) layout.Dimensions { + return compactTonedButton(gtx, u.theme, &u.copyPass, "Copy Password") + }), + ) + } +} + +func (u *ui) recycleDetailGap() unit.Dp { + if u.state.Section == appstate.SectionRecycleBin { + return unit.Dp(10) + } + return 0 +} + +func (u *ui) recycleDetailCardGap() unit.Dp { + if u.state.Section == appstate.SectionRecycleBin { + return unit.Dp(8) + } + return 0 +} + +func (u *ui) recycleDetailNotice(gtx layout.Context) layout.Dimensions { + if u.state.Section != appstate.SectionRecycleBin { + return layout.Dimensions{} + } + return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(12), "This entry is in the recycle bin. Review it, copy from it, or restore it back into the vault.") + lbl.Color = mutedColor + return lbl.Layout(gtx) + }) +} + +func (u *ui) detailMetadataCard(item entry, metrics detailViewMetrics) layout.Widget { + return func(gtx layout.Context) layout.Dimensions { + return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(detailLine(u.theme, "Path", strings.Join(u.displayEntryPath(item.Path), " / "))), + layout.Rigid(layout.Spacer{Height: metrics.sectionGap}.Layout), + layout.Rigid(detailLine(u.theme, "Username", item.Username)), + layout.Rigid(layout.Spacer{Height: metrics.sectionGap}.Layout), + layout.Rigid(detailLine(u.theme, "URL", item.URL)), + layout.Rigid(layout.Spacer{Height: metrics.sectionGap}.Layout), + layout.Rigid(detailLine(u.theme, "Tags", strings.Join(item.Tags, ", "))), + ) + }) + } +} + +func (u *ui) detailPasswordCard(password string) layout.Widget { + return func(gtx layout.Context) layout.Dimensions { + return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(u.passwordLine("Password", password)), + layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), + layout.Rigid(u.detailCopyActionRow), + ) + }) + } +} + +func (u *ui) detailCopyActionRow(gtx layout.Context) layout.Dimensions { + if u.usesCompactViewport() { + return compactTonedButton(gtx, u.theme, &u.copyURL, "Copy URL") + } + return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.copyPass, "Copy Password") + }), + layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.copyUser, "Copy Username") + }), + layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.copyURL, "Copy URL") + }), + ) +} + +func (u *ui) detailNotesCard(item entry) layout.Widget { + return func(gtx layout.Context) 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(12), "NOTES") lbl.Color = mutedColor return lbl.Layout(gtx) - }) - }, - layout.Spacer{Height: func() unit.Dp { - if u.state.Section == appstate.SectionRecycleBin { - return unit.Dp(8) - } - return 0 - }()}.Layout, - func(gtx layout.Context) layout.Dimensions { - return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(detailLine(u.theme, "Path", strings.Join(u.displayEntryPath(item.Path), " / "))), - layout.Rigid(layout.Spacer{Height: sectionGap}.Layout), - layout.Rigid(detailLine(u.theme, "Username", item.Username)), - layout.Rigid(layout.Spacer{Height: sectionGap}.Layout), - layout.Rigid(detailLine(u.theme, "URL", item.URL)), - layout.Rigid(layout.Spacer{Height: sectionGap}.Layout), - layout.Rigid(detailLine(u.theme, "Tags", strings.Join(item.Tags, ", "))), - ) - }) - }, - layout.Spacer{Height: sectionGap}.Layout, - func(gtx layout.Context) layout.Dimensions { - return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(u.passwordLine("Password", password)), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if u.usesCompactViewport() { - return compactTonedButton(gtx, u.theme, &u.copyURL, "Copy URL") - } - return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.copyPass, "Copy Password") - }), - layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.copyUser, "Copy Username") - }), - layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.copyURL, "Copy URL") - }), - ) - }), - ) - }) - }, - layout.Spacer{Height: unit.Dp(8)}.Layout, - func(gtx layout.Context) 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(12), "NOTES") - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Body1(u.theme, item.Notes) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - ) - }) - }, - layout.Spacer{Height: cardGap}.Layout, - u.attachmentSummaryPanel, - layout.Spacer{Height: cardGap}.Layout, - u.historyPanel, - layout.Spacer{Height: cardGap}.Layout, - func(gtx layout.Context) layout.Dimensions { - switch u.state.Section { - case appstate.SectionTemplates: - return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.editEntry, "Edit Template") - }), - layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.instantiateTemplate, "Instantiate") - }), - layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.deleteTemplate, "Delete Template") - }), - ) - case appstate.SectionRecycleBin: - return tonedButton(gtx, u.theme, &u.restoreEntry, "Restore Entry To Vault") - default: - return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.editEntry, "Edit") - }), - layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.duplicateEntry, "Duplicate") - }), - layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.deleteEntry, "Delete") - }), - ) - } - }, - } - return []layout.FlexChild{ - layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { - return material.List(u.theme, &u.detailList).Layout(gtx, len(rows), func(gtx layout.Context, i int) layout.Dimensions { - return rows[i](gtx) - }) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Body1(u.theme, item.Notes) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + ) + }) + } +} + +func (u *ui) detailActionRow(gtx layout.Context) layout.Dimensions { + switch u.state.Section { + case appstate.SectionTemplates: + return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.editEntry, "Edit Template") }), - } - }()...) + layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.instantiateTemplate, "Instantiate") + }), + layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.deleteTemplate, "Delete Template") + }), + ) + case appstate.SectionRecycleBin: + return tonedButton(gtx, u.theme, &u.restoreEntry, "Restore Entry To Vault") + default: + return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.editEntry, "Edit") + }), + layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.duplicateEntry, "Duplicate") + }), + layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.deleteEntry, "Delete") + }), + ) + } } func (u *ui) banner(gtx layout.Context) layout.Dimensions { diff --git a/internal/appui/ui_forms.go b/internal/appui/ui_forms.go index 4b3d120..1acd90c 100644 --- a/internal/appui/ui_forms.go +++ b/internal/appui/ui_forms.go @@ -22,92 +22,6 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { busy := u.lifecycleBusy() showLocalChooser := u.showLocalVaultChooser() selectedLocalPath := strings.TrimSpace(u.vaultPath.Text()) - advancedSection := func(gtx layout.Context) layout.Dimensions { - if busy { - return layout.Dimensions{} - } - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(u.lifecycleAdvancedDisclosure), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if u.lifecycleAdvancedHidden { - return layout.Dimensions{} - } - if u.lifecycleMode == "remote" { - 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(13), "Vault settings") - lbl.Color = accentColor - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(12), u.lifecycleSecuritySettingsSummary()) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Open Vault Settings") - }), - ) - }) - }), - ) - } - primaryActionsSection := func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - label := "Open Vault" - if busy { - label = "Opening Vault..." - } - if busy { - return passiveTonedButton(gtx, u.theme, label) - } - return tonedButton(gtx, u.theme, &u.openVault, label) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if busy || !u.shouldShowLifecycleRemoteSyncAction() { - return layout.Dimensions{} - } - return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if busy || !u.shouldShowLifecycleRemoteSyncAction() { - return layout.Dimensions{} - } - return tonedButton(gtx, u.theme, &u.lifecycleRemoteSyncAction, u.lifecycleRemoteSyncActionLabel()) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(11), "Need a fresh database instead?") - 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 passiveSectionTab(gtx, u.theme, "Create New Vault", false) - } - return sectionTabButton(gtx, u.theme, &u.createVault, "Create New Vault", false) - }), - ) - } - selectedVaultSection := func(gtx layout.Context) layout.Dimensions { - if busy || selectedLocalPath == "" { - return layout.Dimensions{} - } - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return u.selectedLocalVaultCard(gtx, selectedLocalPath) - }), - ) - } 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") @@ -123,70 +37,7 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { }), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(func(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(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 { - if !showLocalChooser || busy { - return layout.Dimensions{} - } - return u.recentVaultList(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !showLocalChooser || busy || !supportsSharedVaultImport(runtime.GOOS) { - return layout.Dimensions{} - } - return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !showLocalChooser || busy || !supportsSharedVaultImport(runtime.GOOS) { - return layout.Dimensions{} - } - return tonedButton(gtx, u.theme, &u.importSharedVault, "Import Shared Vault") - }), - 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(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 { - switch { - case busy: - return labeledEditorHelp(u.theme, "Vault Path", localVaultPathHelp(), &u.vaultPath, false)(gtx) - case selectedLocalPath == "": - return localPathSelector(u.theme, &u.vaultPath, &u.pickVaultPath)(gtx) - default: - return layout.Dimensions{} - } - }), - ) + return u.lifecycleVaultChooserSection(gtx, busy, showLocalChooser, selectedLocalPath) }), layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { @@ -207,20 +58,187 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { }), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if u.shouldPrioritizeLifecyclePrimaryActions() { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(primaryActionsSection), - layout.Rigid(selectedVaultSection), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(advancedSection), - ) + return u.lifecycleControlsFooter(gtx, busy, selectedLocalPath) + }), + ) +} + +func (u *ui) lifecycleVaultChooserSection(gtx layout.Context, busy, showLocalChooser bool, selectedLocalPath string) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !showLocalChooser { + return layout.Dimensions{} } - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(advancedSection), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(primaryActionsSection), - layout.Rigid(selectedVaultSection), - ) + lbl := material.Label(u.theme, unit.Sp(12), "RECENT VAULTS") + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + 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 { + if !showLocalChooser || busy { + return layout.Dimensions{} + } + return u.recentVaultList(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return u.lifecycleImportSharedVaultButton(gtx, busy, showLocalChooser) + }), + 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(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 { + return u.lifecycleVaultPathSelector(gtx, busy, selectedLocalPath) + }), + ) +} + +func (u *ui) lifecycleImportSharedVaultButton(gtx layout.Context, busy, showLocalChooser bool) layout.Dimensions { + if !showLocalChooser || busy || !supportsSharedVaultImport(runtime.GOOS) { + return layout.Dimensions{} + } + return layout.Flex{Axis: layout.Vertical}.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.importSharedVault, "Import Shared Vault") + }), + ) +} + +func (u *ui) lifecycleVaultPathSelector(gtx layout.Context, busy bool, selectedLocalPath string) layout.Dimensions { + switch { + case busy: + return labeledEditorHelp(u.theme, "Vault Path", localVaultPathHelp(), &u.vaultPath, false)(gtx) + case selectedLocalPath == "": + return localPathSelector(u.theme, &u.vaultPath, &u.pickVaultPath)(gtx) + default: + return layout.Dimensions{} + } +} + +func (u *ui) lifecycleControlsFooter(gtx layout.Context, busy bool, selectedLocalPath string) layout.Dimensions { + if u.shouldPrioritizeLifecyclePrimaryActions() { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { return u.lifecyclePrimaryActionsSection(gtx, busy) }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return u.lifecycleSelectedVaultSection(gtx, busy, selectedLocalPath) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { return u.lifecycleAdvancedSection(gtx, busy) }), + ) + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { return u.lifecycleAdvancedSection(gtx, busy) }), + layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { return u.lifecyclePrimaryActionsSection(gtx, busy) }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return u.lifecycleSelectedVaultSection(gtx, busy, selectedLocalPath) + }), + ) +} + +func (u *ui) lifecycleAdvancedSection(gtx layout.Context, busy bool) layout.Dimensions { + if busy { + return layout.Dimensions{} + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(u.lifecycleAdvancedDisclosure), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(u.lifecycleAdvancedCard), + ) +} + +func (u *ui) lifecycleAdvancedCard(gtx layout.Context) layout.Dimensions { + if u.lifecycleAdvancedHidden || u.lifecycleMode == "remote" { + 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(13), "Vault settings") + lbl.Color = accentColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(12), u.lifecycleSecuritySettingsSummary()) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Open Vault Settings") + }), + ) + }) +} + +func (u *ui) lifecyclePrimaryActionsSection(gtx layout.Context, busy bool) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + label := "Open Vault" + if busy { + return passiveTonedButton(gtx, u.theme, "Opening Vault...") + } + return tonedButton(gtx, u.theme, &u.openVault, label) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if busy || !u.shouldShowLifecycleRemoteSyncAction() { + return layout.Dimensions{} + } + return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if busy || !u.shouldShowLifecycleRemoteSyncAction() { + return layout.Dimensions{} + } + return tonedButton(gtx, u.theme, &u.lifecycleRemoteSyncAction, u.lifecycleRemoteSyncActionLabel()) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(11), "Need a fresh database instead?") + 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 passiveSectionTab(gtx, u.theme, "Create New Vault", false) + } + return sectionTabButton(gtx, u.theme, &u.createVault, "Create New Vault", false) + }), + ) +} + +func (u *ui) lifecycleSelectedVaultSection(gtx layout.Context, busy bool, selectedLocalPath string) layout.Dimensions { + if busy || selectedLocalPath == "" { + return layout.Dimensions{} + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return u.selectedLocalVaultCard(gtx, selectedLocalPath) }), ) } @@ -229,45 +247,6 @@ func (u *ui) shouldPrioritizeLifecyclePrimaryActions() bool { return u.usesCompactViewport() } -func (u *ui) selectedRemoteConnectionCard(gtx layout.Context) layout.Dimensions { - heading := u.selectedRemoteCardHeading() - primary := u.selectedRemoteCardPrimaryText() - details := u.selectedRemoteCardDetailLines() - return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { - children := []layout.FlexChild{ - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(12), heading) - 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), primary) - lbl.Color = accentColor - return lbl.Layout(gtx) - }), - } - for _, line := range details { - line := line - children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout)) - children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(11), line) - lbl.Color = mutedColor - return lbl.Layout(gtx) - })) - } - children = append(children, - 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") - }), - ) - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...) - }) - }) -} - func (u *ui) selectedRemoteCardHeading() string { if u.selectedRemoteUsesLocalCache() { return "CACHED VAULT" @@ -478,71 +457,6 @@ func (u *ui) recentVaultList(gtx layout.Context) layout.Dimensions { ) } -func (u *ui) recentRemoteList(gtx layout.Context) layout.Dimensions { - if len(u.recentRemotes) == 0 { - return layout.Dimensions{} - } - if len(u.recentRemoteClicks) < len(u.recentRemotes) { - u.recentRemoteClicks = make([]widget.Clickable, len(u.recentRemotes)) - } - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(12), "RECENT CONNECTIONS") - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - maxY := gtx.Dp(unit.Dp(180)) - if gtx.Constraints.Max.Y > maxY { - gtx.Constraints.Max.Y = maxY - } - if gtx.Constraints.Min.Y > gtx.Constraints.Max.Y { - gtx.Constraints.Min.Y = gtx.Constraints.Max.Y - } - return material.List(u.theme, &u.recentRemoteListState).Layout(gtx, len(u.recentRemotes), func(gtx layout.Context, i int) layout.Dimensions { - record := u.recentRemotes[i] - label := friendlyRecentRemoteLabel(record) - selected := strings.TrimSpace(u.remoteBaseURL.Text()) == record.BaseURL && strings.TrimSpace(u.remotePath.Text()) == record.Path - return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return recentSelectionCard(gtx, selected, func(gtx layout.Context) layout.Dimensions { - return u.recentRemoteClicks[i].Layout(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(14), label) - 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: "+normalizedRemoteHost(record.BaseURL)) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if len(record.LastGroup) == 0 { - return layout.Dimensions{} - } - lbl := material.Label(u.theme, unit.Sp(11), "Last group: "+strings.Join(u.displayEntryPath(record.LastGroup), " / ")) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - ) - }) - }) - }) - }) - }) - }), - ) -} - func recentSelectionCard(gtx layout.Context, selected bool, w layout.Widget) layout.Dimensions { if !selected { return compactCard(gtx, w) diff --git a/internal/appui/ui_layout_header.go b/internal/appui/ui_layout_header.go index 8e7a75b..31fdf51 100644 --- a/internal/appui/ui_layout_header.go +++ b/internal/appui/ui_layout_header.go @@ -3,14 +3,15 @@ package appui import ( "image" "image/color" - "strings" "gioui.org/layout" "gioui.org/op" "gioui.org/unit" "gioui.org/widget" "gioui.org/widget/material" + "git.julianfamily.org/keepassgo/internal/appui/actions" appuilayout "git.julianfamily.org/keepassgo/internal/appui/layout" + "git.julianfamily.org/keepassgo/internal/vault" ) func (u *ui) header(gtx layout.Context) layout.Dimensions { @@ -201,216 +202,224 @@ func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions { if len(u.vaultRemoteCredentialClicks) < len(credentials) { u.vaultRemoteCredentialClicks = make([]widget.Clickable, len(credentials)) } - actionRows := []layout.Widget{ + actionRows := u.syncMenuActionRows(model) + actionWidth := menuActionWidth(gtx, actionRows) + return intrinsicCompactCard(gtx, func(gtx layout.Context) layout.Dimensions { + rows := u.syncMenuRows(model, profiles, credentials, actionWidth) + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, rows...) + }) +} + +func (u *ui) syncMenuActionRows(model actions.SyncMenuModel) []layout.Widget { + rows := []layout.Widget{ func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.openAdvancedSync, "Open Advanced Sync") }, } if model.ShowShare { - actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions { + rows = append(rows, func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.shareCurrentVault, "Share Vault") }) } if model.ShowRemoteSyncSetupShortcut() { - actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions { + rows = append(rows, func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, model.RemoteSyncSetupShortcutLabel()) }) } if model.ShowDirectRemoteSyncShortcut() { - actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions { + rows = append(rows, func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, model.DirectRemoteSyncShortcutLabel()) }) } if model.ShowRemoteSyncSettingsShortcut() { - actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions { + rows = append(rows, func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, model.RemoteSyncSettingsShortcutLabel()) }) } if model.ShowRemoveRemoteSyncShortcut() { - actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions { + rows = append(rows, func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.removeSelectedRemoteBinding, model.RemoveRemoteSyncShortcutLabel()) }) } - actionWidth := menuActionWidth(gtx, actionRows) - return intrinsicCompactCard(gtx, func(gtx layout.Context) layout.Dimensions { - rows := []layout.FlexChild{ - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(11), "Need another source or direction?") - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !model.ShowShare { - return layout.Dimensions{} - } + return rows +} + +func (u *ui) syncMenuRows(model actions.SyncMenuModel, profiles []vault.RemoteProfile, credentials []vault.Entry, actionWidth int) []layout.FlexChild { + rows := u.syncMenuPrimaryRows(model, actionWidth) + rows = append(rows, u.syncMenuSavedBindingRows(model, profiles, credentials)...) + if model.ShowSaveCurrentBinding { + rows = append(rows, u.syncMenuSaveBindingRows(model)...) + } + return rows +} + +func (u *ui) syncMenuPrimaryRows(model actions.SyncMenuModel, actionWidth int) []layout.FlexChild { + rows := []layout.FlexChild{ + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(11), "Need another source or direction?") + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + } + if model.ShowShare { + rows = append(rows, layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.shareCurrentVault, "Share Vault") + }) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + ) + })) + } + rows = append(rows, u.syncMenuActionRow(actionWidth, &u.openAdvancedSync, "Open Advanced Sync")) + if model.ShowRemoteSyncSetupShortcut() { + rows = append(rows, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout)) + rows = append(rows, u.syncMenuActionRow(actionWidth, &u.useSavedAdvancedSyncRemote, model.RemoteSyncSetupShortcutLabel())) + } + if model.ShowDirectRemoteSyncShortcut() { + rows = append(rows, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout)) + rows = append(rows, u.syncMenuActionRow(actionWidth, &u.openSelectedVaultRemote, model.DirectRemoteSyncShortcutLabel())) + } + if model.ShowRemoteSyncSettingsShortcut() { + rows = append(rows, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout)) + rows = append(rows, u.syncMenuActionRow(actionWidth, &u.useSavedAdvancedSyncRemote, model.RemoteSyncSettingsShortcutLabel())) + } + if model.ShowRemoveRemoteSyncShortcut() { + rows = append(rows, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout)) + rows = append(rows, u.syncMenuActionRow(actionWidth, &u.removeSelectedRemoteBinding, model.RemoveRemoteSyncShortcutLabel())) + } + return rows +} + +func (u *ui) syncMenuActionRow(actionWidth int, click *widget.Clickable, label string) layout.FlexChild { + return layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, click, label) + }) + }) +} + +func (u *ui) syncMenuSavedBindingRows(model actions.SyncMenuModel, profiles []vault.RemoteProfile, credentials []vault.Entry) []layout.FlexChild { + if !u.hasOpenVault() || len(profiles) == 0 || len(credentials) == 0 { + return nil + } + rows := []layout.FlexChild{ + layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(11), model.SavedBindingHeading()) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + } + if !model.ShowSelectors { + rows = append(rows, layout.Rigid(u.syncMenuSavedBindingSummary(model))) + } else { + rows = append(rows, u.syncMenuSelectorRows(model, profiles, credentials)...) + } + if _, ok := u.selectedVaultRemoteProfile(); ok { + if _, ok := u.selectedVaultRemoteCredentialEntry(); ok { + rows = append(rows, + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.openSelectedVaultRemoteButtonLabel()) + }), + ) + } + } + return rows +} + +func (u *ui) syncMenuSavedBindingSummary(model actions.SyncMenuModel) layout.Widget { + return func(gtx layout.Context) layout.Dimensions { + summary := model.SavedBindingSummary + if !summary.OK { + return layout.Dimensions{} + } + return layout.Background{}.Layout(gtx, fill(color.NRGBA{R: 242, G: 245, B: 240, A: 255}), 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 { - return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.shareCurrentVault, "Share Vault") - }) + lbl := material.Label(u.theme, unit.Sp(13), summary.ProfileLabel) + lbl.Color = accentColor + return lbl.Layout(gtx) }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - ) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.openAdvancedSync, "Open Advanced Sync") - }) - }), - } - if model.ShowRemoteSyncSetupShortcut() { - rows = append(rows, - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, model.RemoteSyncSetupShortcutLabel()) - }) - }), - ) - } - if model.ShowDirectRemoteSyncShortcut() { - rows = append(rows, - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, model.DirectRemoteSyncShortcutLabel()) - }) - }), - ) - } - if model.ShowRemoteSyncSettingsShortcut() { - rows = append(rows, - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, model.RemoteSyncSettingsShortcutLabel()) - }) - }), - ) - } - if model.ShowRemoveRemoteSyncShortcut() { - rows = append(rows, - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.removeSelectedRemoteBinding, model.RemoveRemoteSyncShortcutLabel()) - }) - }), - ) - } - if u.hasOpenVault() && len(profiles) > 0 && len(credentials) > 0 { - rows = append(rows, - layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(11), model.SavedBindingHeading()) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - ) - if !model.ShowSelectors { - rows = append(rows, + layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - summary := model.SavedBindingSummary - if !summary.OK { - return layout.Dimensions{} - } - return layout.Background{}.Layout(gtx, fill(color.NRGBA{R: 242, G: 245, B: 240, A: 255}), 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(13), summary.ProfileLabel) - 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(12), "Credential: "+summary.CredentialLabel) - 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(12), summary.SyncLabel) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - ) - }) - }) + lbl := material.Label(u.theme, unit.Sp(12), "Credential: "+summary.CredentialLabel) + 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(12), summary.SyncLabel) + lbl.Color = mutedColor + return lbl.Layout(gtx) }), ) - } else { - for i, profile := range profiles { - i := i - profile := profile - rows = append(rows, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - selected := strings.TrimSpace(u.selectedVaultRemoteProfileID) == profile.ID - return layout.Inset{Bottom: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return recentSelectionCard(gtx, selected, func(gtx layout.Context) layout.Dimensions { - return u.vaultRemoteProfileClicks[i].Layout(gtx, func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(13), profile.Name) - lbl.Color = accentColor - return lbl.Layout(gtx) - }) - }) - }) - }), - ) - } - rows = append(rows, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout)) - for i, entry := range credentials { - i := i - entry := entry - rows = append(rows, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - selected := strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID) == entry.ID - label := entry.Title - if strings.TrimSpace(entry.Username) != "" { - label += " · " + strings.TrimSpace(entry.Username) - } - return layout.Inset{Bottom: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return recentSelectionCard(gtx, selected, func(gtx layout.Context) layout.Dimensions { - return u.vaultRemoteCredentialClicks[i].Layout(gtx, func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(13), label) - lbl.Color = accentColor - return lbl.Layout(gtx) - }) - }) - }) - }), - ) - } + }) + }) + } +} + +func (u *ui) syncMenuSaveBindingRows(model actions.SyncMenuModel) []layout.FlexChild { + return []layout.FlexChild{ + layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(11), model.SaveCurrentRemoteBindingHeading()) + 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.saveCurrentRemoteBinding, model.SaveCurrentRemoteBindingButtonLabel()) + }), + } +} + +func (u *ui) syncMenuSelectorRows(_ actions.SyncMenuModel, profiles []vault.RemoteProfile, credentials []vault.Entry) []layout.FlexChild { + rows := make([]layout.FlexChild, 0, len(profiles)+len(credentials)+4) + for i, profile := range profiles { + i := i + profile := profile + rows = append(rows, layout.Rigid(func(gtx layout.Context) layout.Dimensions { + selected := u.selectedVaultRemoteProfileID == profile.ID + return layout.Inset{Bottom: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return recentSelectionCard(gtx, selected, func(gtx layout.Context) layout.Dimensions { + return u.vaultRemoteProfileClicks[i].Layout(gtx, func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(13), profile.Name) + lbl.Color = accentColor + return lbl.Layout(gtx) + }) + }) + }) + })) + } + rows = append(rows, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout)) + for i, entry := range credentials { + i := i + entry := entry + rows = append(rows, layout.Rigid(func(gtx layout.Context) layout.Dimensions { + selected := u.selectedVaultRemoteCredentialEntryID == entry.ID + label := entry.Title + if entry.Username != "" { + label += " · " + entry.Username } - if _, ok := u.selectedVaultRemoteProfile(); ok { - if _, ok := u.selectedVaultRemoteCredentialEntry(); ok { - rows = append(rows, - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.openSelectedVaultRemoteButtonLabel()) - }), - ) - } - } - } - if model.ShowSaveCurrentBinding { - rows = append(rows, - layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(11), model.SaveCurrentRemoteBindingHeading()) - 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.saveCurrentRemoteBinding, model.SaveCurrentRemoteBindingButtonLabel()) - }), - ) - } - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, rows...) - }) + return layout.Inset{Bottom: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return recentSelectionCard(gtx, selected, func(gtx layout.Context) layout.Dimensions { + return u.vaultRemoteCredentialClicks[i].Layout(gtx, func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(13), label) + lbl.Color = accentColor + return lbl.Layout(gtx) + }) + }) + }) + })) + } + return rows } func intrinsicCompactCard(gtx layout.Context, w layout.Widget) layout.Dimensions {