Simplify lifecycle and section UI

This commit is contained in:
Joe Julian
2026-04-01 14:58:56 -07:00
parent 73ff0fb77d
commit 84fea5e5d7
2 changed files with 308 additions and 230 deletions
+230 -225
View File
@@ -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())
+78 -5
View File
@@ -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 {