Clarify start, locked, and recycle flows

This commit is contained in:
Joe Julian
2026-04-01 16:29:53 -07:00
parent 1675811aa3
commit 54ec195e61
3 changed files with 168 additions and 74 deletions
+39 -10
View File
@@ -1599,6 +1599,10 @@ func (u *ui) listEmptyMessage() string {
query := strings.TrimSpace(u.search.Text())
if query != "" {
switch u.state.Section {
case appstate.SectionAPITokens:
return fmt.Sprintf("No API tokens match %q. Clear or refine the search.", query)
case appstate.SectionAPIAudit:
return fmt.Sprintf("No audit events match %q. Clear or refine the search.", query)
case appstate.SectionTemplates:
return fmt.Sprintf("No templates match %q. Clear or refine the search.", query)
case appstate.SectionRecycleBin:
@@ -1615,10 +1619,10 @@ func (u *ui) listEmptyMessage() string {
case appstate.SectionTemplates:
return "Templates are not available in this build."
case appstate.SectionRecycleBin:
return "Recycle Bin is empty."
return "Recycle Bin is empty. Deleted entries will appear here until restored."
default:
if len(u.displayPath()) > 0 {
return "No entries in this group yet. Add one or open a subgroup."
return "No entries in this group yet. Add one, search below this point, or open a subgroup."
}
return "Create or open a vault, then add an entry to get started."
}
@@ -1635,14 +1639,20 @@ func (u *ui) detailPlaceholderMessage() string {
}
switch u.state.Section {
case appstate.SectionAPITokens:
return "Select an API token or issue a new one."
return "Select an API token, issue a new one, or search to narrow the list."
case appstate.SectionAPIAudit:
return "Select an audit event to inspect it."
return "Select an audit event to inspect it, or filter the list with Search vault."
case appstate.SectionTemplates:
return "Select a template or start a reusable entry."
case appstate.SectionRecycleBin:
return "Select a recycle-bin entry to review or restore it."
return "Select a deleted entry to review or restore it."
default:
if strings.TrimSpace(u.search.Text()) != "" {
return "Select a matching entry from the filtered list or clear the search."
}
if len(u.displayPath()) == 0 {
return "Select an entry from the vault root or open a group."
}
return "Select an entry or start a new one."
}
}
@@ -2480,13 +2490,23 @@ func (u *ui) headerActions(gtx layout.Context) layout.Dimensions {
}
func (u *ui) syncButtonGroup(gtx layout.Context) layout.Dimensions {
label := "Synchronize"
spacing := unit.Dp(4)
if u.mode == "phone" {
label = "Sync"
spacing = unit.Dp(3)
}
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.Button(u.theme, &u.synchronizeVault, "Synchronize")
btn := material.Button(u.theme, &u.synchronizeVault, label)
btn.CornerRadius = unit.Dp(10)
if u.mode == "phone" {
btn.TextSize = unit.Sp(13)
btn.Inset = layout.Inset{Top: 8, Bottom: 8, Left: 12, Right: 12}
}
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(4)}.Layout),
layout.Rigid(layout.Spacer{Width: spacing}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return u.syncMenuToggle(gtx)
}),
@@ -2505,6 +2525,10 @@ func (u *ui) syncMenuToggle(gtx layout.Context) layout.Dimensions {
btn.Color = color.NRGBA{R: 255, G: 252, B: 247, A: 255}
btn.Size = unit.Dp(18)
btn.Inset = layout.UniformInset(unit.Dp(8))
if u.mode == "phone" {
btn.Size = unit.Dp(16)
btn.Inset = layout.UniformInset(unit.Dp(7))
}
return btn.Layout(gtx)
}
@@ -2712,6 +2736,7 @@ func (u *ui) entryRow(gtx layout.Context, click *widget.Clickable, idx int, item
}
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
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, titleSize, item.Title)
@@ -2730,10 +2755,10 @@ func (u *ui) entryRow(gtx layout.Context, click *widget.Clickable, idx int, item
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if strings.TrimSpace(u.search.Text()) == "" {
if !showPath {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(11), strings.Join(item.Path, " / "))
lbl := material.Label(u.theme, unit.Sp(11), strings.Join(u.displayEntryPath(item.Path), " / "))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
@@ -2761,6 +2786,10 @@ func (u *ui) entryRow(gtx layout.Context, click *widget.Clickable, idx int, item
}
fillColor := selectedColor
edgeColor := selectedEdge
if u.state.Section == appstate.SectionRecycleBin {
fillColor = color.NRGBA{R: 245, G: 234, B: 226, A: 255}
edgeColor = color.NRGBA{R: 144, G: 74, B: 49, A: 255}
}
if u.isFocused(listFocusID(idx)) && item.ID != u.state.SelectedEntryID {
fillColor = color.NRGBA{R: 235, G: 241, B: 238, A: 255}
edgeColor = accentColor
@@ -3265,7 +3294,7 @@ func (u *ui) historyRow(gtx layout.Context, click *widget.Clickable, index int,
func (u *ui) pathBar(gtx layout.Context) layout.Dimensions {
if u.state.Section == appstate.SectionRecycleBin {
lbl := material.Label(u.theme, unit.Sp(13), "Recycle Bin")
lbl := material.Label(u.theme, unit.Sp(13), "Recycle Bin / Deleted entries")
lbl.Color = mutedColor
return lbl.Layout(gtx)
}
+11 -1
View File
@@ -426,9 +426,19 @@ func (u *ui) apiAuditRow(gtx layout.Context, click *widget.Clickable, idx int, e
func (u *ui) apiTokenListPanel(gtx layout.Context) layout.Dimensions {
tokens := u.apiTokens()
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), "Grant scoped gRPC access to external tools. Search matches token name, client, or expiration.")
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
if len(tokens) == 0 {
lbl := material.Label(u.theme, unit.Sp(14), "No API tokens match the current filter.")
text := "No API tokens yet."
if strings.TrimSpace(u.search.Text()) != "" {
text = "No API tokens match the current filter."
}
lbl := material.Label(u.theme, unit.Sp(14), text)
lbl.Color = mutedColor
return lbl.Layout(gtx)
}
+118 -63
View File
@@ -15,6 +15,12 @@ import (
func (u *ui) lifecycleControls(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), "OPEN OR CREATE VAULT")
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 layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
@@ -69,6 +75,12 @@ func (u *ui) lifecycleControls(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), "VAULT FILE")
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(selectorEditorHelp(u.theme, "Vault Path", "Choose the existing .kdbx file to open.", &u.vaultPath, &u.pickVaultPath, "Choose File", false)),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
@@ -179,41 +191,41 @@ func (u *ui) recentVaultList(gtx layout.Context) layout.Dimensions {
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild {
children := make([]layout.FlexChild, 0, len(u.recentVaults)*2)
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 compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
return u.recentVaultClicks[index].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)
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
)
})
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.lifecycleList).Layout(gtx, len(u.recentVaults), func(gtx layout.Context, i int) layout.Dimensions {
path := u.recentVaults[i]
label := path
if friendly := friendlyRecentVaultLabel(path); friendly != "" {
label = friendly
}
return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
return u.recentVaultClicks[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)
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
)
})
})
}))
if i < len(u.recentVaults)-1 {
children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
}
}
return children
}()...)
})
})
})
}),
)
}
@@ -233,38 +245,38 @@ func (u *ui) recentRemoteList(gtx layout.Context) layout.Dimensions {
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild {
children := make([]layout.FlexChild, 0, len(u.recentRemotes)*2)
for i, record := range u.recentRemotes {
index := i
label := friendlyRecentRemoteLabel(record)
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
return u.recentRemoteClicks[index].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), strings.TrimSpace(record.BaseURL))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
)
})
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.lifecycleList).Layout(gtx, len(u.recentRemotes), func(gtx layout.Context, i int) layout.Dimensions {
record := u.recentRemotes[i]
label := friendlyRecentRemoteLabel(record)
return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return compactCard(gtx, 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), strings.TrimSpace(record.BaseURL))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
)
})
})
}))
if i < len(u.recentRemotes)-1 {
children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
}
}
return children
}()...)
})
})
})
}),
)
}
@@ -707,7 +719,50 @@ func selectorEditorHelp(th *material.Theme, label, help string, editor *widget.E
}
func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions {
targetLabel := "Locked vault"
targetValue := "Unlock the active vault to continue."
if u.state.Session != nil {
if strings.TrimSpace(u.remoteBaseURL.Text()) != "" || strings.TrimSpace(u.remotePath.Text()) != "" {
baseURL := strings.TrimSpace(u.remoteBaseURL.Text())
path := strings.TrimSpace(u.remotePath.Text())
targetLabel = "Remote vault"
targetValue = friendlyRecentRemoteLabel(recentRemoteRecord{BaseURL: baseURL, Path: path})
if strings.TrimSpace(targetValue) == "" {
targetValue = "Remote WebDAV vault"
}
} else {
path := strings.TrimSpace(u.vaultPath.Text())
targetLabel = "Local vault"
targetValue = friendlyRecentVaultLabel(path)
if strings.TrimSpace(path) != "" {
targetValue = targetValue + "\n" + path
}
if strings.TrimSpace(targetValue) == "" {
targetValue = "Local vault file"
}
}
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return compactCard(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(12), strings.ToUpper(targetLabel))
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.Body1(u.theme, targetValue)
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
)
})
})
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return u.masterPasswordField(gtx, "Used alone or together with a key file to unlock the vault.")
}),