diff --git a/main.go b/main.go index 6b08728..2239e73 100644 --- a/main.go +++ b/main.go @@ -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) } diff --git a/ui_api.go b/ui_api.go index d50dd48..ef62230 100644 --- a/ui_api.go +++ b/ui_api.go @@ -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) } diff --git a/ui_forms.go b/ui_forms.go index d080577..39d9e03 100644 --- a/ui_forms.go +++ b/ui_forms.go @@ -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.") }),