From 84fea5e5d795c6ec733fe77bdba2be07e0ec0711 Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Wed, 1 Apr 2026 14:58:56 -0700 Subject: [PATCH] Simplify lifecycle and section UI --- main.go | 455 ++++++++++++++++++++++++++-------------------------- ui_forms.go | 83 +++++++++- 2 files changed, 308 insertions(+), 230 deletions(-) diff --git a/main.go b/main.go index 439bc4a..421de21 100644 --- a/main.go +++ b/main.go @@ -29,9 +29,9 @@ import ( "git.julianfamily.org/keepassgo/apiapproval" "git.julianfamily.org/keepassgo/apiaudit" "git.julianfamily.org/keepassgo/apitokens" + "git.julianfamily.org/keepassgo/appstate" keepassassets "git.julianfamily.org/keepassgo/assets" "git.julianfamily.org/keepassgo/autofillcache" - "git.julianfamily.org/keepassgo/appstate" "git.julianfamily.org/keepassgo/clipboard" "git.julianfamily.org/keepassgo/passwords" "git.julianfamily.org/keepassgo/session" @@ -106,7 +106,8 @@ type recentRemoteRecord struct { } type uiPreferences struct { - GroupControlsHidden bool `json:"groupControlsHidden"` + GroupControlsHidden bool `json:"groupControlsHidden"` + LifecycleAdvancedHidden bool `json:"lifecycleAdvancedHidden"` } type entriesSectionState struct { @@ -131,189 +132,191 @@ const ( ) type ui struct { - mode string - theme *material.Theme - logoHorizontal paint.ImageOp - splashSquare paint.ImageOp - search widget.Editor - vaultPath widget.Editor - saveAsPath widget.Editor - remoteBaseURL widget.Editor - remotePath widget.Editor - remoteUsername widget.Editor - remotePassword widget.Editor - masterPassword widget.Editor - keyFilePath widget.Editor - apiTokenName widget.Editor - apiTokenClientName widget.Editor - apiTokenExpiresAt widget.Editor - apiPolicyOperation widget.Editor - apiPolicyPath widget.Editor - apiPolicyEntryID widget.Editor - securityCipher widget.Editor - securityKDF widget.Editor - entryID widget.Editor - entryTitle widget.Editor - entryUsername widget.Editor - entryPassword widget.Editor - entryURL widget.Editor - entryNotes widget.Editor - entryTags widget.Editor - entryPath widget.Editor - entryFields widget.Editor - customFieldKeys []widget.Editor - customFieldValues []widget.Editor - historyIndex widget.Editor - groupName widget.Editor - groupParentPath widget.Editor - passwordProfile widget.Editor - attachmentName widget.Editor - attachmentPath widget.Editor - exportAttachmentPath widget.Editor - list widget.List - groupList widget.List - detailList widget.List - lifecycleList widget.List - copyUser widget.Clickable - copyPass widget.Clickable - copyURL widget.Clickable - lockVault widget.Clickable - unlockVault widget.Clickable - createVault widget.Clickable - openVault widget.Clickable - saveVault widget.Clickable - saveAsVault widget.Clickable - openRemote widget.Clickable - changeMasterKey widget.Clickable - synchronizeVault widget.Clickable - toggleSyncMenu widget.Clickable - openAdvancedSync widget.Clickable - openSecuritySettings widget.Clickable - closeAdvancedSync widget.Clickable - closeSecuritySettings widget.Clickable - runAdvancedSync widget.Clickable - saveSecuritySettings widget.Clickable - editEntry widget.Clickable - cancelEdit widget.Clickable - pickVaultPath widget.Clickable - pickKeyFile widget.Clickable - pickSyncLocalPath widget.Clickable - addEntry widget.Clickable - saveEntry widget.Clickable - duplicateEntry widget.Clickable - deleteEntry widget.Clickable - restoreEntry widget.Clickable - saveTemplate widget.Clickable - deleteTemplate widget.Clickable - instantiateTemplate widget.Clickable - addAttachment widget.Clickable - replaceAttachment widget.Clickable - removeAttachment widget.Clickable - exportAttachment widget.Clickable - restoreHistory widget.Clickable - generatePassword widget.Clickable - createGroup widget.Clickable - moveGroup widget.Clickable - renameGroup widget.Clickable - deleteGroup widget.Clickable - confirmDeleteGroup widget.Clickable - cancelDeleteGroup widget.Clickable - addCustomField widget.Clickable - toggleGroupControls widget.Clickable - togglePasswordInline widget.Clickable - toggleSyncPassword widget.Clickable - showEntries widget.Clickable - showTemplates widget.Clickable - showRecycle widget.Clickable - showAPITokens widget.Clickable - showAPIAudit widget.Clickable - showLocalLifecycle widget.Clickable - showRemoteLifecycle widget.Clickable - showSyncLocal widget.Clickable - showSyncRemote widget.Clickable - showSyncPull widget.Clickable - showSyncPush widget.Clickable - allowApproval widget.Clickable - denyApproval widget.Clickable - cancelApproval widget.Clickable - approvalPermanent widget.Bool - rememberRemoteAuth widget.Bool - apiPolicyAllow widget.Bool - apiPolicyGroupScopeW widget.Bool - apiTokenDisabled widget.Bool - entryClicks []widget.Clickable - apiTokenClicks []widget.Clickable - apiPolicyRemoves []widget.Clickable - apiAuditClicks []widget.Clickable - historyClicks []widget.Clickable - attachmentClicks []widget.Clickable - breadcrumbs []widget.Clickable - groupClicks []widget.Clickable - recentVaultClicks []widget.Clickable - recentRemoteClicks []widget.Clickable - removeCustomFields []widget.Clickable - state appstate.State - visible []entry - currentPath []string - syncedPath []string - selectedHistoryIndex int - showPassword bool - togglePassword widget.Clickable - copyAPITokenSecret widget.Clickable - issueAPIToken widget.Clickable - saveAPIToken widget.Clickable - rotateAPIToken widget.Clickable - disableAPIToken widget.Clickable - revokeAPIToken widget.Clickable - deleteAPIToken widget.Clickable - addAPIPolicyRule widget.Clickable - phoneSplit widget.Float - splitDrag gesture.Drag - splitBase float32 - splitStartY float32 - phoneSpan int - eyeIcon *widget.Icon - eyeOffIcon *widget.Icon - copyIcon *widget.Icon - expandMoreIcon *widget.Icon - expandLessIcon *widget.Icon - chevronDownIcon *widget.Icon - clipboardWriter clipboard.Writer - loadingMessage string - lifecycleMode string - syncSourceMode syncSourceMode - syncDirection syncDirection - syncLocalPath widget.Editor - syncRemoteBaseURL widget.Editor - syncRemotePath widget.Editor - syncRemoteUsername widget.Editor - syncRemotePassword widget.Editor - syncDialogOpen bool - syncMenuOpen bool - securityDialogOpen bool - showSyncPassword bool - keyboardFocus focusID - defaultSaveAsPath string - recentVaultsPath string - uiPreferencesPath string - recentRemotesPath string - autofillCachePath string - editingEntry bool - groupControlsHidden bool - recentVaults []string - recentRemotes []recentRemoteRecord - recentVaultGroups map[string][]string - recentVaultUsedAt map[string]time.Time - entriesState entriesSectionState - deleteGroupPath []string - apiPolicyGroupScope bool - apiTokenSecret string - selectedAuditIndex int - statusExpiresAt time.Time - now func() time.Time - apiHost *api.Host - auditLog *apiaudit.Log - grpcAddress string + mode string + theme *material.Theme + logoHorizontal paint.ImageOp + splashSquare paint.ImageOp + search widget.Editor + vaultPath widget.Editor + saveAsPath widget.Editor + remoteBaseURL widget.Editor + remotePath widget.Editor + remoteUsername widget.Editor + remotePassword widget.Editor + masterPassword widget.Editor + keyFilePath widget.Editor + apiTokenName widget.Editor + apiTokenClientName widget.Editor + apiTokenExpiresAt widget.Editor + apiPolicyOperation widget.Editor + apiPolicyPath widget.Editor + apiPolicyEntryID widget.Editor + securityCipher widget.Editor + securityKDF widget.Editor + entryID widget.Editor + entryTitle widget.Editor + entryUsername widget.Editor + entryPassword widget.Editor + entryURL widget.Editor + entryNotes widget.Editor + entryTags widget.Editor + entryPath widget.Editor + entryFields widget.Editor + customFieldKeys []widget.Editor + customFieldValues []widget.Editor + historyIndex widget.Editor + groupName widget.Editor + groupParentPath widget.Editor + passwordProfile widget.Editor + attachmentName widget.Editor + attachmentPath widget.Editor + exportAttachmentPath widget.Editor + list widget.List + groupList widget.List + detailList widget.List + lifecycleList widget.List + copyUser widget.Clickable + copyPass widget.Clickable + copyURL widget.Clickable + lockVault widget.Clickable + unlockVault widget.Clickable + createVault widget.Clickable + openVault widget.Clickable + saveVault widget.Clickable + saveAsVault widget.Clickable + openRemote widget.Clickable + changeMasterKey widget.Clickable + synchronizeVault widget.Clickable + toggleSyncMenu widget.Clickable + openAdvancedSync widget.Clickable + openSecuritySettings widget.Clickable + closeAdvancedSync widget.Clickable + closeSecuritySettings widget.Clickable + runAdvancedSync widget.Clickable + saveSecuritySettings widget.Clickable + editEntry widget.Clickable + cancelEdit widget.Clickable + pickVaultPath widget.Clickable + pickKeyFile widget.Clickable + pickSyncLocalPath widget.Clickable + addEntry widget.Clickable + saveEntry widget.Clickable + duplicateEntry widget.Clickable + deleteEntry widget.Clickable + restoreEntry widget.Clickable + saveTemplate widget.Clickable + deleteTemplate widget.Clickable + instantiateTemplate widget.Clickable + addAttachment widget.Clickable + replaceAttachment widget.Clickable + removeAttachment widget.Clickable + exportAttachment widget.Clickable + restoreHistory widget.Clickable + generatePassword widget.Clickable + createGroup widget.Clickable + moveGroup widget.Clickable + renameGroup widget.Clickable + deleteGroup widget.Clickable + confirmDeleteGroup widget.Clickable + cancelDeleteGroup widget.Clickable + addCustomField widget.Clickable + toggleGroupControls widget.Clickable + toggleLifecycleAdvanced widget.Clickable + togglePasswordInline widget.Clickable + toggleSyncPassword widget.Clickable + showEntries widget.Clickable + showTemplates widget.Clickable + showRecycle widget.Clickable + showAPITokens widget.Clickable + showAPIAudit widget.Clickable + showLocalLifecycle widget.Clickable + showRemoteLifecycle widget.Clickable + showSyncLocal widget.Clickable + showSyncRemote widget.Clickable + showSyncPull widget.Clickable + showSyncPush widget.Clickable + allowApproval widget.Clickable + denyApproval widget.Clickable + cancelApproval widget.Clickable + approvalPermanent widget.Bool + rememberRemoteAuth widget.Bool + apiPolicyAllow widget.Bool + apiPolicyGroupScopeW widget.Bool + apiTokenDisabled widget.Bool + entryClicks []widget.Clickable + apiTokenClicks []widget.Clickable + apiPolicyRemoves []widget.Clickable + apiAuditClicks []widget.Clickable + historyClicks []widget.Clickable + attachmentClicks []widget.Clickable + breadcrumbs []widget.Clickable + groupClicks []widget.Clickable + recentVaultClicks []widget.Clickable + recentRemoteClicks []widget.Clickable + removeCustomFields []widget.Clickable + state appstate.State + visible []entry + currentPath []string + syncedPath []string + selectedHistoryIndex int + showPassword bool + togglePassword widget.Clickable + copyAPITokenSecret widget.Clickable + issueAPIToken widget.Clickable + saveAPIToken widget.Clickable + rotateAPIToken widget.Clickable + disableAPIToken widget.Clickable + revokeAPIToken widget.Clickable + deleteAPIToken widget.Clickable + addAPIPolicyRule widget.Clickable + phoneSplit widget.Float + splitDrag gesture.Drag + splitBase float32 + splitStartY float32 + phoneSpan int + eyeIcon *widget.Icon + eyeOffIcon *widget.Icon + copyIcon *widget.Icon + expandMoreIcon *widget.Icon + expandLessIcon *widget.Icon + chevronDownIcon *widget.Icon + clipboardWriter clipboard.Writer + loadingMessage string + lifecycleMode string + syncSourceMode syncSourceMode + syncDirection syncDirection + syncLocalPath widget.Editor + syncRemoteBaseURL widget.Editor + syncRemotePath widget.Editor + syncRemoteUsername widget.Editor + syncRemotePassword widget.Editor + syncDialogOpen bool + syncMenuOpen bool + securityDialogOpen bool + showSyncPassword bool + keyboardFocus focusID + defaultSaveAsPath string + recentVaultsPath string + uiPreferencesPath string + recentRemotesPath string + autofillCachePath string + editingEntry bool + groupControlsHidden bool + lifecycleAdvancedHidden bool + recentVaults []string + recentRemotes []recentRemoteRecord + recentVaultGroups map[string][]string + recentVaultUsedAt map[string]time.Time + entriesState entriesSectionState + deleteGroupPath []string + apiPolicyGroupScope bool + apiTokenSecret string + selectedAuditIndex int + statusExpiresAt time.Time + now func() time.Time + apiHost *api.Host + auditLog *apiaudit.Log + grpcAddress string } var ( @@ -407,21 +410,22 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths) lifecycleList: widget.List{ List: layout.List{Axis: layout.Vertical}, }, - state: appstate.State{}, - selectedHistoryIndex: -1, - selectedAuditIndex: -1, - lifecycleMode: "local", - defaultSaveAsPath: paths.DefaultSaveAsPath, - recentVaultsPath: paths.RecentVaultsPath, - uiPreferencesPath: paths.UIPreferencesPath, - recentRemotesPath: paths.RecentRemotesPath, - autofillCachePath: paths.AutofillCachePath, - recentVaultGroups: map[string][]string{}, - recentVaultUsedAt: map[string]time.Time{}, - now: time.Now, - syncSourceMode: syncSourceLocal, - syncDirection: syncDirectionPull, - apiPolicyGroupScope: true, + state: appstate.State{}, + selectedHistoryIndex: -1, + selectedAuditIndex: -1, + lifecycleMode: "local", + defaultSaveAsPath: paths.DefaultSaveAsPath, + recentVaultsPath: paths.RecentVaultsPath, + uiPreferencesPath: paths.UIPreferencesPath, + recentRemotesPath: paths.RecentRemotesPath, + autofillCachePath: paths.AutofillCachePath, + recentVaultGroups: map[string][]string{}, + recentVaultUsedAt: map[string]time.Time{}, + lifecycleAdvancedHidden: true, + now: time.Now, + syncSourceMode: syncSourceLocal, + syncDirection: syncDirectionPull, + apiPolicyGroupScope: true, } u.apiPolicyAllow.Value = true u.apiPolicyGroupScopeW.Value = true @@ -1098,6 +1102,7 @@ func (u *ui) loadUIPreferences() { return } u.groupControlsHidden = prefs.GroupControlsHidden + u.lifecycleAdvancedHidden = prefs.LifecycleAdvancedHidden } func (u *ui) saveUIPreferences() { @@ -1108,7 +1113,8 @@ func (u *ui) saveUIPreferences() { return } content, err := json.MarshalIndent(uiPreferences{ - GroupControlsHidden: u.groupControlsHidden, + GroupControlsHidden: u.groupControlsHidden, + LifecycleAdvancedHidden: u.lifecycleAdvancedHidden, }, "", " ") if err != nil { return @@ -1761,6 +1767,10 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { for u.showRemoteLifecycle.Clicked(gtx) { u.lifecycleMode = "remote" } + for u.toggleLifecycleAdvanced.Clicked(gtx) { + u.lifecycleAdvancedHidden = !u.lifecycleAdvancedHidden + u.saveUIPreferences() + } for u.showSyncLocal.Clicked(gtx) { u.syncSourceMode = syncSourceLocal } @@ -2596,39 +2606,19 @@ func (u *ui) navigationHeader(gtx layout.Context) layout.Dimensions { func (u *ui) sectionBar(gtx layout.Context) layout.Dimensions { return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { - btn := material.Button(u.theme, &u.showEntries, "Entries") - btn.Background = accentColor - btn.Color = color.NRGBA{R: 255, G: 252, B: 247, A: 255} - btn.TextSize = unit.Sp(11) - btn.Inset = layout.Inset{Top: 5, Bottom: 5, Left: 9, Right: 9} - return btn.Layout(gtx) + return sectionTabButton(gtx, u.theme, &u.showEntries, "Entries", u.state.Section == appstate.SectionEntries) }), layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - btn := material.Button(u.theme, &u.showRecycle, "Recycle Bin") - btn.Background = accentColor - btn.Color = color.NRGBA{R: 255, G: 252, B: 247, A: 255} - btn.TextSize = unit.Sp(11) - btn.Inset = layout.Inset{Top: 5, Bottom: 5, Left: 9, Right: 9} - return btn.Layout(gtx) + return sectionTabButton(gtx, u.theme, &u.showRecycle, "Recycle Bin", u.state.Section == appstate.SectionRecycleBin) }), layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - btn := material.Button(u.theme, &u.showAPITokens, "API Tokens") - btn.Background = accentColor - btn.Color = color.NRGBA{R: 255, G: 252, B: 247, A: 255} - btn.TextSize = unit.Sp(11) - btn.Inset = layout.Inset{Top: 5, Bottom: 5, Left: 9, Right: 9} - return btn.Layout(gtx) + return sectionTabButton(gtx, u.theme, &u.showAPITokens, "API Tokens", u.state.Section == appstate.SectionAPITokens) }), layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - btn := material.Button(u.theme, &u.showAPIAudit, "API Audit") - btn.Background = accentColor - btn.Color = color.NRGBA{R: 255, G: 252, B: 247, A: 255} - btn.TextSize = unit.Sp(11) - btn.Inset = layout.Inset{Top: 5, Bottom: 5, Left: 9, Right: 9} - return btn.Layout(gtx) + return sectionTabButton(gtx, u.theme, &u.showAPIAudit, "API Audit", u.state.Section == appstate.SectionAPIAudit) }), ) } @@ -3345,6 +3335,21 @@ func tonedButton(gtx layout.Context, th *material.Theme, click *widget.Clickable return btn.Layout(gtx) } +func sectionTabButton(gtx layout.Context, th *material.Theme, click *widget.Clickable, label string, active bool) layout.Dimensions { + btn := material.Button(th, click, label) + btn.CornerRadius = unit.Dp(10) + btn.TextSize = unit.Sp(11) + btn.Inset = layout.Inset{Top: 5, Bottom: 5, Left: 9, Right: 9} + if active { + btn.Background = accentColor + btn.Color = color.NRGBA{R: 255, G: 252, B: 247, A: 255} + } else { + btn.Background = selectedColor + btn.Color = accentColor + } + return btn.Layout(gtx) +} + func fill(c color.NRGBA) layout.Widget { return func(gtx layout.Context) layout.Dimensions { paint.FillShape(gtx.Ops, c, clip.Rect{Max: gtx.Constraints.Min}.Op()) diff --git a/ui_forms.go b/ui_forms.go index e8b2b89..7560111 100644 --- a/ui_forms.go +++ b/ui_forms.go @@ -3,6 +3,7 @@ package main import ( "fmt" "image/color" + "path/filepath" "strings" "gioui.org/layout" @@ -17,11 +18,11 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions { return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.showLocalLifecycle, "Local Vault") + return sectionTabButton(gtx, u.theme, &u.showLocalLifecycle, "Local Vault", u.lifecycleMode == "local") }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.showRemoteLifecycle, "Remote Vault") + return sectionTabButton(gtx, u.theme, &u.showRemoteLifecycle, "Remote Vault", u.lifecycleMode == "remote") }), ) }), @@ -61,9 +62,18 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { ) }), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(labeledEditorHelp(u.theme, "Cipher", "Supported values: aes256, chacha20", &u.securityCipher, false)), + layout.Rigid(u.lifecycleAdvancedDisclosure), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(labeledEditorHelp(u.theme, "KDF", "Supported values: aes-kdf, argon2", &u.securityKDF, false)), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if u.lifecycleAdvancedHidden { + return layout.Dimensions{} + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(labeledEditorHelp(u.theme, "Cipher", "Used for new vaults and future saves. Supported values: aes256, chacha20.", &u.securityCipher, false)), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(labeledEditorHelp(u.theme, "KDF", "Used for new vaults and future saves. Supported values: aes-kdf, argon2.", &u.securityKDF, false)), + ) + }), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { if u.lifecycleMode == "remote" { @@ -82,6 +92,36 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { ) } +func (u *ui) lifecycleAdvancedDisclosure(gtx layout.Context) layout.Dimensions { + return u.toggleLifecycleAdvanced.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(2)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + icon := u.expandLessIcon + if u.lifecycleAdvancedHidden { + icon = u.expandMoreIcon + } + if icon != nil { + return icon.Layout(gtx, accentColor) + } + lbl := material.Label(u.theme, unit.Sp(16), ">") + if !u.lifecycleAdvancedHidden { + lbl.Text = "v" + } + lbl.Color = accentColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Width: unit.Dp(4)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(12), "Advanced Vault Settings") + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + ) + }) + }) +} + func (u *ui) recentVaultList(gtx layout.Context) layout.Dimensions { if len(u.recentVaults) == 0 { return layout.Dimensions{} @@ -102,6 +142,9 @@ func (u *ui) recentVaultList(gtx layout.Context) layout.Dimensions { for i, path := range u.recentVaults { index := i label := path + if friendly := friendlyRecentVaultLabel(path); friendly != "" { + label = friendly + } children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.recentVaultClicks[index], label) })) @@ -134,7 +177,7 @@ func (u *ui) recentRemoteList(gtx layout.Context) layout.Dimensions { children := make([]layout.FlexChild, 0, len(u.recentRemotes)*2) for i, record := range u.recentRemotes { index := i - label := record.BaseURL + " / " + record.Path + label := friendlyRecentRemoteLabel(record) children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.recentRemoteClicks[index], label) })) @@ -148,6 +191,36 @@ func (u *ui) recentRemoteList(gtx layout.Context) layout.Dimensions { ) } +func friendlyRecentVaultLabel(path string) string { + value := strings.TrimSpace(path) + if value == "" { + return "" + } + base := filepath.Base(value) + if base == "." || base == string(filepath.Separator) || base == "" { + return value + } + return base +} + +func friendlyRecentRemoteLabel(record recentRemoteRecord) string { + baseURL := strings.TrimSpace(record.BaseURL) + path := strings.TrimSpace(record.Path) + if baseURL == "" && path == "" { + return "" + } + host := strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(baseURL, "https://"), "http://")) + host = strings.TrimSuffix(host, "/") + switch { + case host == "": + return path + case path == "": + return host + default: + return host + " ยท " + path + } +} + func (u *ui) attachmentList(gtx layout.Context) layout.Dimensions { items := u.selectedAttachmentItems() if len(items) == 0 {