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/apiapproval"
"git.julianfamily.org/keepassgo/apiaudit" "git.julianfamily.org/keepassgo/apiaudit"
"git.julianfamily.org/keepassgo/apitokens" "git.julianfamily.org/keepassgo/apitokens"
"git.julianfamily.org/keepassgo/appstate"
keepassassets "git.julianfamily.org/keepassgo/assets" keepassassets "git.julianfamily.org/keepassgo/assets"
"git.julianfamily.org/keepassgo/autofillcache" "git.julianfamily.org/keepassgo/autofillcache"
"git.julianfamily.org/keepassgo/appstate"
"git.julianfamily.org/keepassgo/clipboard" "git.julianfamily.org/keepassgo/clipboard"
"git.julianfamily.org/keepassgo/passwords" "git.julianfamily.org/keepassgo/passwords"
"git.julianfamily.org/keepassgo/session" "git.julianfamily.org/keepassgo/session"
@@ -106,7 +106,8 @@ type recentRemoteRecord struct {
} }
type uiPreferences struct { type uiPreferences struct {
GroupControlsHidden bool `json:"groupControlsHidden"` GroupControlsHidden bool `json:"groupControlsHidden"`
LifecycleAdvancedHidden bool `json:"lifecycleAdvancedHidden"`
} }
type entriesSectionState struct { type entriesSectionState struct {
@@ -131,189 +132,191 @@ const (
) )
type ui struct { type ui struct {
mode string mode string
theme *material.Theme theme *material.Theme
logoHorizontal paint.ImageOp logoHorizontal paint.ImageOp
splashSquare paint.ImageOp splashSquare paint.ImageOp
search widget.Editor search widget.Editor
vaultPath widget.Editor vaultPath widget.Editor
saveAsPath widget.Editor saveAsPath widget.Editor
remoteBaseURL widget.Editor remoteBaseURL widget.Editor
remotePath widget.Editor remotePath widget.Editor
remoteUsername widget.Editor remoteUsername widget.Editor
remotePassword widget.Editor remotePassword widget.Editor
masterPassword widget.Editor masterPassword widget.Editor
keyFilePath widget.Editor keyFilePath widget.Editor
apiTokenName widget.Editor apiTokenName widget.Editor
apiTokenClientName widget.Editor apiTokenClientName widget.Editor
apiTokenExpiresAt widget.Editor apiTokenExpiresAt widget.Editor
apiPolicyOperation widget.Editor apiPolicyOperation widget.Editor
apiPolicyPath widget.Editor apiPolicyPath widget.Editor
apiPolicyEntryID widget.Editor apiPolicyEntryID widget.Editor
securityCipher widget.Editor securityCipher widget.Editor
securityKDF widget.Editor securityKDF widget.Editor
entryID widget.Editor entryID widget.Editor
entryTitle widget.Editor entryTitle widget.Editor
entryUsername widget.Editor entryUsername widget.Editor
entryPassword widget.Editor entryPassword widget.Editor
entryURL widget.Editor entryURL widget.Editor
entryNotes widget.Editor entryNotes widget.Editor
entryTags widget.Editor entryTags widget.Editor
entryPath widget.Editor entryPath widget.Editor
entryFields widget.Editor entryFields widget.Editor
customFieldKeys []widget.Editor customFieldKeys []widget.Editor
customFieldValues []widget.Editor customFieldValues []widget.Editor
historyIndex widget.Editor historyIndex widget.Editor
groupName widget.Editor groupName widget.Editor
groupParentPath widget.Editor groupParentPath widget.Editor
passwordProfile widget.Editor passwordProfile widget.Editor
attachmentName widget.Editor attachmentName widget.Editor
attachmentPath widget.Editor attachmentPath widget.Editor
exportAttachmentPath widget.Editor exportAttachmentPath widget.Editor
list widget.List list widget.List
groupList widget.List groupList widget.List
detailList widget.List detailList widget.List
lifecycleList widget.List lifecycleList widget.List
copyUser widget.Clickable copyUser widget.Clickable
copyPass widget.Clickable copyPass widget.Clickable
copyURL widget.Clickable copyURL widget.Clickable
lockVault widget.Clickable lockVault widget.Clickable
unlockVault widget.Clickable unlockVault widget.Clickable
createVault widget.Clickable createVault widget.Clickable
openVault widget.Clickable openVault widget.Clickable
saveVault widget.Clickable saveVault widget.Clickable
saveAsVault widget.Clickable saveAsVault widget.Clickable
openRemote widget.Clickable openRemote widget.Clickable
changeMasterKey widget.Clickable changeMasterKey widget.Clickable
synchronizeVault widget.Clickable synchronizeVault widget.Clickable
toggleSyncMenu widget.Clickable toggleSyncMenu widget.Clickable
openAdvancedSync widget.Clickable openAdvancedSync widget.Clickable
openSecuritySettings widget.Clickable openSecuritySettings widget.Clickable
closeAdvancedSync widget.Clickable closeAdvancedSync widget.Clickable
closeSecuritySettings widget.Clickable closeSecuritySettings widget.Clickable
runAdvancedSync widget.Clickable runAdvancedSync widget.Clickable
saveSecuritySettings widget.Clickable saveSecuritySettings widget.Clickable
editEntry widget.Clickable editEntry widget.Clickable
cancelEdit widget.Clickable cancelEdit widget.Clickable
pickVaultPath widget.Clickable pickVaultPath widget.Clickable
pickKeyFile widget.Clickable pickKeyFile widget.Clickable
pickSyncLocalPath widget.Clickable pickSyncLocalPath widget.Clickable
addEntry widget.Clickable addEntry widget.Clickable
saveEntry widget.Clickable saveEntry widget.Clickable
duplicateEntry widget.Clickable duplicateEntry widget.Clickable
deleteEntry widget.Clickable deleteEntry widget.Clickable
restoreEntry widget.Clickable restoreEntry widget.Clickable
saveTemplate widget.Clickable saveTemplate widget.Clickable
deleteTemplate widget.Clickable deleteTemplate widget.Clickable
instantiateTemplate widget.Clickable instantiateTemplate widget.Clickable
addAttachment widget.Clickable addAttachment widget.Clickable
replaceAttachment widget.Clickable replaceAttachment widget.Clickable
removeAttachment widget.Clickable removeAttachment widget.Clickable
exportAttachment widget.Clickable exportAttachment widget.Clickable
restoreHistory widget.Clickable restoreHistory widget.Clickable
generatePassword widget.Clickable generatePassword widget.Clickable
createGroup widget.Clickable createGroup widget.Clickable
moveGroup widget.Clickable moveGroup widget.Clickable
renameGroup widget.Clickable renameGroup widget.Clickable
deleteGroup widget.Clickable deleteGroup widget.Clickable
confirmDeleteGroup widget.Clickable confirmDeleteGroup widget.Clickable
cancelDeleteGroup widget.Clickable cancelDeleteGroup widget.Clickable
addCustomField widget.Clickable addCustomField widget.Clickable
toggleGroupControls widget.Clickable toggleGroupControls widget.Clickable
togglePasswordInline widget.Clickable toggleLifecycleAdvanced widget.Clickable
toggleSyncPassword widget.Clickable togglePasswordInline widget.Clickable
showEntries widget.Clickable toggleSyncPassword widget.Clickable
showTemplates widget.Clickable showEntries widget.Clickable
showRecycle widget.Clickable showTemplates widget.Clickable
showAPITokens widget.Clickable showRecycle widget.Clickable
showAPIAudit widget.Clickable showAPITokens widget.Clickable
showLocalLifecycle widget.Clickable showAPIAudit widget.Clickable
showRemoteLifecycle widget.Clickable showLocalLifecycle widget.Clickable
showSyncLocal widget.Clickable showRemoteLifecycle widget.Clickable
showSyncRemote widget.Clickable showSyncLocal widget.Clickable
showSyncPull widget.Clickable showSyncRemote widget.Clickable
showSyncPush widget.Clickable showSyncPull widget.Clickable
allowApproval widget.Clickable showSyncPush widget.Clickable
denyApproval widget.Clickable allowApproval widget.Clickable
cancelApproval widget.Clickable denyApproval widget.Clickable
approvalPermanent widget.Bool cancelApproval widget.Clickable
rememberRemoteAuth widget.Bool approvalPermanent widget.Bool
apiPolicyAllow widget.Bool rememberRemoteAuth widget.Bool
apiPolicyGroupScopeW widget.Bool apiPolicyAllow widget.Bool
apiTokenDisabled widget.Bool apiPolicyGroupScopeW widget.Bool
entryClicks []widget.Clickable apiTokenDisabled widget.Bool
apiTokenClicks []widget.Clickable entryClicks []widget.Clickable
apiPolicyRemoves []widget.Clickable apiTokenClicks []widget.Clickable
apiAuditClicks []widget.Clickable apiPolicyRemoves []widget.Clickable
historyClicks []widget.Clickable apiAuditClicks []widget.Clickable
attachmentClicks []widget.Clickable historyClicks []widget.Clickable
breadcrumbs []widget.Clickable attachmentClicks []widget.Clickable
groupClicks []widget.Clickable breadcrumbs []widget.Clickable
recentVaultClicks []widget.Clickable groupClicks []widget.Clickable
recentRemoteClicks []widget.Clickable recentVaultClicks []widget.Clickable
removeCustomFields []widget.Clickable recentRemoteClicks []widget.Clickable
state appstate.State removeCustomFields []widget.Clickable
visible []entry state appstate.State
currentPath []string visible []entry
syncedPath []string currentPath []string
selectedHistoryIndex int syncedPath []string
showPassword bool selectedHistoryIndex int
togglePassword widget.Clickable showPassword bool
copyAPITokenSecret widget.Clickable togglePassword widget.Clickable
issueAPIToken widget.Clickable copyAPITokenSecret widget.Clickable
saveAPIToken widget.Clickable issueAPIToken widget.Clickable
rotateAPIToken widget.Clickable saveAPIToken widget.Clickable
disableAPIToken widget.Clickable rotateAPIToken widget.Clickable
revokeAPIToken widget.Clickable disableAPIToken widget.Clickable
deleteAPIToken widget.Clickable revokeAPIToken widget.Clickable
addAPIPolicyRule widget.Clickable deleteAPIToken widget.Clickable
phoneSplit widget.Float addAPIPolicyRule widget.Clickable
splitDrag gesture.Drag phoneSplit widget.Float
splitBase float32 splitDrag gesture.Drag
splitStartY float32 splitBase float32
phoneSpan int splitStartY float32
eyeIcon *widget.Icon phoneSpan int
eyeOffIcon *widget.Icon eyeIcon *widget.Icon
copyIcon *widget.Icon eyeOffIcon *widget.Icon
expandMoreIcon *widget.Icon copyIcon *widget.Icon
expandLessIcon *widget.Icon expandMoreIcon *widget.Icon
chevronDownIcon *widget.Icon expandLessIcon *widget.Icon
clipboardWriter clipboard.Writer chevronDownIcon *widget.Icon
loadingMessage string clipboardWriter clipboard.Writer
lifecycleMode string loadingMessage string
syncSourceMode syncSourceMode lifecycleMode string
syncDirection syncDirection syncSourceMode syncSourceMode
syncLocalPath widget.Editor syncDirection syncDirection
syncRemoteBaseURL widget.Editor syncLocalPath widget.Editor
syncRemotePath widget.Editor syncRemoteBaseURL widget.Editor
syncRemoteUsername widget.Editor syncRemotePath widget.Editor
syncRemotePassword widget.Editor syncRemoteUsername widget.Editor
syncDialogOpen bool syncRemotePassword widget.Editor
syncMenuOpen bool syncDialogOpen bool
securityDialogOpen bool syncMenuOpen bool
showSyncPassword bool securityDialogOpen bool
keyboardFocus focusID showSyncPassword bool
defaultSaveAsPath string keyboardFocus focusID
recentVaultsPath string defaultSaveAsPath string
uiPreferencesPath string recentVaultsPath string
recentRemotesPath string uiPreferencesPath string
autofillCachePath string recentRemotesPath string
editingEntry bool autofillCachePath string
groupControlsHidden bool editingEntry bool
recentVaults []string groupControlsHidden bool
recentRemotes []recentRemoteRecord lifecycleAdvancedHidden bool
recentVaultGroups map[string][]string recentVaults []string
recentVaultUsedAt map[string]time.Time recentRemotes []recentRemoteRecord
entriesState entriesSectionState recentVaultGroups map[string][]string
deleteGroupPath []string recentVaultUsedAt map[string]time.Time
apiPolicyGroupScope bool entriesState entriesSectionState
apiTokenSecret string deleteGroupPath []string
selectedAuditIndex int apiPolicyGroupScope bool
statusExpiresAt time.Time apiTokenSecret string
now func() time.Time selectedAuditIndex int
apiHost *api.Host statusExpiresAt time.Time
auditLog *apiaudit.Log now func() time.Time
grpcAddress string apiHost *api.Host
auditLog *apiaudit.Log
grpcAddress string
} }
var ( var (
@@ -407,21 +410,22 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
lifecycleList: widget.List{ lifecycleList: widget.List{
List: layout.List{Axis: layout.Vertical}, List: layout.List{Axis: layout.Vertical},
}, },
state: appstate.State{}, state: appstate.State{},
selectedHistoryIndex: -1, selectedHistoryIndex: -1,
selectedAuditIndex: -1, selectedAuditIndex: -1,
lifecycleMode: "local", lifecycleMode: "local",
defaultSaveAsPath: paths.DefaultSaveAsPath, defaultSaveAsPath: paths.DefaultSaveAsPath,
recentVaultsPath: paths.RecentVaultsPath, recentVaultsPath: paths.RecentVaultsPath,
uiPreferencesPath: paths.UIPreferencesPath, uiPreferencesPath: paths.UIPreferencesPath,
recentRemotesPath: paths.RecentRemotesPath, recentRemotesPath: paths.RecentRemotesPath,
autofillCachePath: paths.AutofillCachePath, autofillCachePath: paths.AutofillCachePath,
recentVaultGroups: map[string][]string{}, recentVaultGroups: map[string][]string{},
recentVaultUsedAt: map[string]time.Time{}, recentVaultUsedAt: map[string]time.Time{},
now: time.Now, lifecycleAdvancedHidden: true,
syncSourceMode: syncSourceLocal, now: time.Now,
syncDirection: syncDirectionPull, syncSourceMode: syncSourceLocal,
apiPolicyGroupScope: true, syncDirection: syncDirectionPull,
apiPolicyGroupScope: true,
} }
u.apiPolicyAllow.Value = true u.apiPolicyAllow.Value = true
u.apiPolicyGroupScopeW.Value = true u.apiPolicyGroupScopeW.Value = true
@@ -1098,6 +1102,7 @@ func (u *ui) loadUIPreferences() {
return return
} }
u.groupControlsHidden = prefs.GroupControlsHidden u.groupControlsHidden = prefs.GroupControlsHidden
u.lifecycleAdvancedHidden = prefs.LifecycleAdvancedHidden
} }
func (u *ui) saveUIPreferences() { func (u *ui) saveUIPreferences() {
@@ -1108,7 +1113,8 @@ func (u *ui) saveUIPreferences() {
return return
} }
content, err := json.MarshalIndent(uiPreferences{ content, err := json.MarshalIndent(uiPreferences{
GroupControlsHidden: u.groupControlsHidden, GroupControlsHidden: u.groupControlsHidden,
LifecycleAdvancedHidden: u.lifecycleAdvancedHidden,
}, "", " ") }, "", " ")
if err != nil { if err != nil {
return return
@@ -1761,6 +1767,10 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
for u.showRemoteLifecycle.Clicked(gtx) { for u.showRemoteLifecycle.Clicked(gtx) {
u.lifecycleMode = "remote" u.lifecycleMode = "remote"
} }
for u.toggleLifecycleAdvanced.Clicked(gtx) {
u.lifecycleAdvancedHidden = !u.lifecycleAdvancedHidden
u.saveUIPreferences()
}
for u.showSyncLocal.Clicked(gtx) { for u.showSyncLocal.Clicked(gtx) {
u.syncSourceMode = syncSourceLocal 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 { func (u *ui) sectionBar(gtx layout.Context) layout.Dimensions {
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.Button(u.theme, &u.showEntries, "Entries") return sectionTabButton(gtx, u.theme, &u.showEntries, "Entries", u.state.Section == appstate.SectionEntries)
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)
}), }),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.Button(u.theme, &u.showRecycle, "Recycle Bin") return sectionTabButton(gtx, u.theme, &u.showRecycle, "Recycle Bin", u.state.Section == appstate.SectionRecycleBin)
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)
}), }),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.Button(u.theme, &u.showAPITokens, "API Tokens") return sectionTabButton(gtx, u.theme, &u.showAPITokens, "API Tokens", u.state.Section == appstate.SectionAPITokens)
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)
}), }),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.Button(u.theme, &u.showAPIAudit, "API Audit") return sectionTabButton(gtx, u.theme, &u.showAPIAudit, "API Audit", u.state.Section == appstate.SectionAPIAudit)
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)
}), }),
) )
} }
@@ -3345,6 +3335,21 @@ func tonedButton(gtx layout.Context, th *material.Theme, click *widget.Clickable
return btn.Layout(gtx) 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 { func fill(c color.NRGBA) layout.Widget {
return func(gtx layout.Context) layout.Dimensions { return func(gtx layout.Context) layout.Dimensions {
paint.FillShape(gtx.Ops, c, clip.Rect{Max: gtx.Constraints.Min}.Op()) paint.FillShape(gtx.Ops, c, clip.Rect{Max: gtx.Constraints.Min}.Op())
+78 -5
View File
@@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"image/color" "image/color"
"path/filepath"
"strings" "strings"
"gioui.org/layout" "gioui.org/layout"
@@ -17,11 +18,11 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions { 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(layout.Spacer{Width: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { 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(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(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(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.lifecycleMode == "remote" { 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 { func (u *ui) recentVaultList(gtx layout.Context) layout.Dimensions {
if len(u.recentVaults) == 0 { if len(u.recentVaults) == 0 {
return layout.Dimensions{} return layout.Dimensions{}
@@ -102,6 +142,9 @@ func (u *ui) recentVaultList(gtx layout.Context) layout.Dimensions {
for i, path := range u.recentVaults { for i, path := range u.recentVaults {
index := i index := i
label := path label := path
if friendly := friendlyRecentVaultLabel(path); friendly != "" {
label = friendly
}
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.recentVaultClicks[index], label) 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) children := make([]layout.FlexChild, 0, len(u.recentRemotes)*2)
for i, record := range u.recentRemotes { for i, record := range u.recentRemotes {
index := i index := i
label := record.BaseURL + " / " + record.Path label := friendlyRecentRemoteLabel(record)
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.recentRemoteClicks[index], label) 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 { func (u *ui) attachmentList(gtx layout.Context) layout.Dimensions {
items := u.selectedAttachmentItems() items := u.selectedAttachmentItems()
if len(items) == 0 { if len(items) == 0 {