Compare commits
5 Commits
198732239a
...
17d52e1e45
| Author | SHA1 | Date | |
|---|---|---|---|
| 17d52e1e45 | |||
| efd1b48df0 | |||
| a1ed86ff88 | |||
| e160b01bb0 | |||
| baa03cb63e |
@@ -384,6 +384,9 @@ type ui struct {
|
||||
disableAPIToken widget.Clickable
|
||||
revokeAPIToken widget.Clickable
|
||||
deleteAPIToken widget.Clickable
|
||||
useCurrentGroupForPolicy widget.Clickable
|
||||
useSelectedEntryForPolicy widget.Clickable
|
||||
clearAPIPolicyTarget widget.Clickable
|
||||
addAPIPolicyRule widget.Clickable
|
||||
phoneSplit widget.Float
|
||||
splitDrag gesture.Drag
|
||||
@@ -396,6 +399,7 @@ type ui struct {
|
||||
copyIcon *widget.Icon
|
||||
expandMoreIcon *widget.Icon
|
||||
expandLessIcon *widget.Icon
|
||||
chevronRightIcon *widget.Icon
|
||||
chevronDownIcon *widget.Icon
|
||||
settingsIcon *widget.Icon
|
||||
menuIcon *widget.Icon
|
||||
@@ -615,6 +619,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
||||
u.copyIcon, _ = widget.NewIcon(icons.ContentContentCopy)
|
||||
u.expandMoreIcon, _ = widget.NewIcon(icons.NavigationExpandMore)
|
||||
u.expandLessIcon, _ = widget.NewIcon(icons.NavigationExpandLess)
|
||||
u.chevronRightIcon, _ = widget.NewIcon(icons.NavigationChevronRight)
|
||||
u.chevronDownIcon, _ = widget.NewIcon(icons.NavigationArrowDropDown)
|
||||
u.settingsIcon, _ = widget.NewIcon(icons.ActionSettings)
|
||||
u.menuIcon, _ = widget.NewIcon(icons.NavigationMenu)
|
||||
@@ -645,13 +650,21 @@ func (u *ui) filter() {
|
||||
return
|
||||
}
|
||||
u.visible = visible
|
||||
if len(u.entryClicks) < len(u.visible) {
|
||||
u.entryClicks = make([]widget.Clickable, len(u.visible))
|
||||
u.ensureEntryClicks()
|
||||
}
|
||||
|
||||
func (u *ui) ensureEntryClicks() {
|
||||
if len(u.entryClicks) >= len(u.visible) {
|
||||
return
|
||||
}
|
||||
next := make([]widget.Clickable, len(u.visible))
|
||||
copy(next, u.entryClicks)
|
||||
u.entryClicks = next
|
||||
}
|
||||
|
||||
func (u *ui) visibleEntrySnapshot() ([]entry, []*widget.Clickable) {
|
||||
visible := append([]entry(nil), u.visible...)
|
||||
u.ensureEntryClicks()
|
||||
clicks := make([]*widget.Clickable, len(visible))
|
||||
for i := range visible {
|
||||
clicks[i] = &u.entryClicks[i]
|
||||
@@ -659,6 +672,19 @@ func (u *ui) visibleEntrySnapshot() ([]entry, []*widget.Clickable) {
|
||||
return visible, clicks
|
||||
}
|
||||
|
||||
func (u *ui) searchPlaceholder() string {
|
||||
switch u.state.Section {
|
||||
case appstate.SectionAPITokens:
|
||||
return "Search API tokens"
|
||||
case appstate.SectionAPIAudit:
|
||||
return "Search audit log"
|
||||
case appstate.SectionRecycleBin:
|
||||
return "Search recycle bin"
|
||||
default:
|
||||
return "Search vault"
|
||||
}
|
||||
}
|
||||
|
||||
func defaultStatePaths(stateDir string) statePaths {
|
||||
baseDir := strings.TrimSpace(stateDir)
|
||||
if baseDir == "" {
|
||||
@@ -2582,7 +2608,7 @@ func (u *ui) listEmptyState() emptyState {
|
||||
case appstate.SectionAPITokens:
|
||||
return emptyState{
|
||||
Title: "No matching API tokens",
|
||||
Body: fmt.Sprintf("No API tokens match %q. Clear or refine Search vault to find a token by name, client, or expiration.", query),
|
||||
Body: fmt.Sprintf("No API tokens match %q. Clear or refine Search API tokens to find a token by name, client, or expiration.", query),
|
||||
}
|
||||
case appstate.SectionAPIAudit:
|
||||
return emptyState{
|
||||
@@ -2654,7 +2680,7 @@ func (u *ui) detailPlaceholderMessage() string {
|
||||
case appstate.SectionAPITokens:
|
||||
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, or use Search vault or the quick filters above."
|
||||
return "Select an audit event to inspect it, or use Search audit log or the quick filters above."
|
||||
case appstate.SectionTemplates:
|
||||
return "Select a template or start a reusable entry."
|
||||
case appstate.SectionRecycleBin:
|
||||
@@ -2986,6 +3012,15 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
||||
for u.addAPIPolicyRule.Clicked(gtx) {
|
||||
u.runAction("add API policy rule", u.addAPIPolicyRuleAction)
|
||||
}
|
||||
for u.useCurrentGroupForPolicy.Clicked(gtx) {
|
||||
u.runAction("use current group for API policy", u.useCurrentGroupForPolicyAction)
|
||||
}
|
||||
for u.useSelectedEntryForPolicy.Clicked(gtx) {
|
||||
u.runAction("use selected entry for API policy", u.useSelectedEntryForPolicyAction)
|
||||
}
|
||||
for u.clearAPIPolicyTarget.Clicked(gtx) {
|
||||
u.runAction("clear API policy target", u.clearAPIPolicyTargetAction)
|
||||
}
|
||||
for i := range u.apiPolicyRemoves {
|
||||
for u.apiPolicyRemoves[i].Clicked(gtx) {
|
||||
index := i
|
||||
@@ -4159,7 +4194,7 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
|
||||
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
return u.outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions {
|
||||
editor := material.Editor(u.theme, &u.search, "Search vault")
|
||||
editor := material.Editor(u.theme, &u.search, u.searchPlaceholder())
|
||||
editor.Color = u.theme.Palette.Fg
|
||||
editor.HintColor = mutedColor
|
||||
return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout)
|
||||
@@ -4176,12 +4211,6 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
|
||||
})
|
||||
}
|
||||
}
|
||||
if !u.isVaultLocked() && u.state.Section == appstate.SectionRecycleBin {
|
||||
rows = append(rows, u.recycleBinSectionNotice)
|
||||
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Spacer{Height: spacing}.Layout(gtx)
|
||||
})
|
||||
}
|
||||
if !u.isVaultLocked() && (u.state.Section == appstate.SectionEntries || u.state.Section == appstate.SectionRecycleBin) {
|
||||
rows = append(rows, u.pathBar)
|
||||
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
||||
@@ -4245,18 +4274,6 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
|
||||
return u.navigationHeader(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: spacing}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.isVaultLocked() || u.state.Section != appstate.SectionRecycleBin {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return u.recycleBinSectionNotice(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: func() unit.Dp {
|
||||
if u.isVaultLocked() || u.state.Section != appstate.SectionRecycleBin {
|
||||
return 0
|
||||
}
|
||||
return spacing
|
||||
}()}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.isVaultLocked() || (u.state.Section != appstate.SectionEntries && u.state.Section != appstate.SectionRecycleBin) {
|
||||
return layout.Dimensions{}
|
||||
@@ -4283,7 +4300,7 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
|
||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
}
|
||||
return u.outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions {
|
||||
editor := material.Editor(u.theme, &u.search, "Search vault")
|
||||
editor := material.Editor(u.theme, &u.search, u.searchPlaceholder())
|
||||
editor.Color = u.theme.Palette.Fg
|
||||
editor.HintColor = mutedColor
|
||||
return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout)
|
||||
@@ -5741,10 +5758,6 @@ func recyclePathCard(gtx layout.Context, th *material.Theme, title, body string)
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ui) recycleBinSectionNotice(gtx layout.Context) layout.Dimensions {
|
||||
return recyclePathCard(gtx, u.theme, "Recycle Bin", "Deleted entries are separated from normal browsing so you can review or restore them safely.")
|
||||
}
|
||||
|
||||
func recycleDetailTitle(gtx layout.Context, th *material.Theme, title string) layout.Dimensions {
|
||||
return layout.Background{}.Layout(gtx, fill(color.NRGBA{R: 247, G: 239, B: 231, A: 255}), func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
|
||||
+101
-1
@@ -849,6 +849,28 @@ func TestUIVisibleEntrySnapshotIsStableAfterVisibleMutation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIVisibleEntrySnapshotRegrowsClickableState(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("phone", vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{ID: "1", Title: "Alpha", Path: []string{"Joe", "Internet"}},
|
||||
{ID: "2", Title: "Beta", Path: []string{"Joe", "Internet"}},
|
||||
},
|
||||
})
|
||||
u.state.NavigateToPath([]string{"Joe", "Internet"})
|
||||
u.filter()
|
||||
u.entryClicks = u.entryClicks[:1]
|
||||
|
||||
visible, clicks := u.visibleEntrySnapshot()
|
||||
if len(visible) != 2 || len(clicks) != 2 {
|
||||
t.Fatalf("snapshot lengths = (%d, %d), want (2, 2)", len(visible), len(clicks))
|
||||
}
|
||||
if clicks[1] == nil {
|
||||
t.Fatal("regrown click pointer = nil, want usable clickable state")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIPhoneBackReturnsFromSubscreenToEntries(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -865,6 +887,20 @@ func TestUIPhoneBackReturnsFromSubscreenToEntries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIPhoneBackClosesSettingsDialog(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("phone", vault.Model{})
|
||||
u.securityDialogOpen = true
|
||||
|
||||
if !u.handlePhoneBack() {
|
||||
t.Fatal("handlePhoneBack() = false, want true for open settings dialog")
|
||||
}
|
||||
if u.securityDialogOpen {
|
||||
t.Fatal("securityDialogOpen = true after back, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUISecurityDialogContentDoesNotPanicWithSmallViewport(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -951,7 +987,7 @@ func TestUIAPIAuditMessagesGuideQuickFilters(t *testing.T) {
|
||||
if got := u.listEmptyState(); got.Title != "No API audit events yet" || got.Body != "Connect a trusted client, respond to approval prompts, or issue a token to start recording activity." {
|
||||
t.Fatalf("listEmptyState() = %#v, want updated API audit guidance", got)
|
||||
}
|
||||
if got := u.detailPlaceholderMessage(); got != "Select an audit event to inspect it, or use Search vault or the quick filters above." {
|
||||
if got := u.detailPlaceholderMessage(); got != "Select an audit event to inspect it, or use Search audit log or the quick filters above." {
|
||||
t.Fatalf("detailPlaceholderMessage() = %q, want quick-filter guidance", got)
|
||||
}
|
||||
|
||||
@@ -5195,6 +5231,70 @@ func TestUIListEmptyStateProvidesSectionSpecificGuidance(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUISearchPlaceholderIsContextual(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("desktop", vault.Model{})
|
||||
if got := u.searchPlaceholder(); got != "Search vault" {
|
||||
t.Fatalf("default searchPlaceholder() = %q, want %q", got, "Search vault")
|
||||
}
|
||||
u.showRecycleBinSection()
|
||||
if got := u.searchPlaceholder(); got != "Search recycle bin" {
|
||||
t.Fatalf("recycle searchPlaceholder() = %q, want %q", got, "Search recycle bin")
|
||||
}
|
||||
u.showAPITokensSection()
|
||||
if got := u.searchPlaceholder(); got != "Search API tokens" {
|
||||
t.Fatalf("api token searchPlaceholder() = %q, want %q", got, "Search API tokens")
|
||||
}
|
||||
u.showAPIAuditSection()
|
||||
if got := u.searchPlaceholder(); got != "Search audit log" {
|
||||
t.Fatalf("api audit searchPlaceholder() = %q, want %q", got, "Search audit log")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIAPIPolicyTargetActionsUseCurrentContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("desktop", vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{ID: "lights", Title: "Home Assistant", Path: []string{"Joe", "codex"}},
|
||||
},
|
||||
})
|
||||
u.state.NavigateToPath([]string{"Joe", "codex"})
|
||||
u.filter()
|
||||
u.state.SelectedEntryID = "lights"
|
||||
|
||||
if err := u.useCurrentGroupForPolicyAction(); err != nil {
|
||||
t.Fatalf("useCurrentGroupForPolicyAction() error = %v", err)
|
||||
}
|
||||
if got := u.apiPolicyPath.Text(); got != "codex" {
|
||||
t.Fatalf("apiPolicyPath.Text() = %q, want %q", got, "codex")
|
||||
}
|
||||
if !u.apiPolicyGroupScopeW.Value {
|
||||
t.Fatal("apiPolicyGroupScopeW.Value = false, want true")
|
||||
}
|
||||
|
||||
if err := u.useSelectedEntryForPolicyAction(); err != nil {
|
||||
t.Fatalf("useSelectedEntryForPolicyAction() error = %v", err)
|
||||
}
|
||||
if got := u.apiPolicyEntryID.Text(); got != "lights" {
|
||||
t.Fatalf("apiPolicyEntryID.Text() = %q, want %q", got, "lights")
|
||||
}
|
||||
if u.apiPolicyGroupScopeW.Value {
|
||||
t.Fatal("apiPolicyGroupScopeW.Value = true, want false")
|
||||
}
|
||||
|
||||
if err := u.clearAPIPolicyTargetAction(); err != nil {
|
||||
t.Fatalf("clearAPIPolicyTargetAction() error = %v", err)
|
||||
}
|
||||
if got := u.apiPolicyPath.Text(); got != "" {
|
||||
t.Fatalf("apiPolicyPath.Text() = %q, want empty", got)
|
||||
}
|
||||
if got := u.apiPolicyEntryID.Text(); got != "" {
|
||||
t.Fatalf("apiPolicyEntryID.Text() = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIVisibleBreadcrumbsCompressesAggressivelyOnPhone(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
+1
-1
@@ -471,7 +471,7 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe
|
||||
<uses-sdk android:minSdkVersion="{{.MinSDK}}" android:targetSdkVersion="{{.TargetSDK}}" />
|
||||
{{range .Permissions}} <uses-permission android:name="{{.}}"/>
|
||||
{{end}}{{range .Features}} <uses-feature android:{{.}} android:required="false"/>
|
||||
{{end}}{{.ManifestSnip}} <application {{.IconSnip}} android:label="{{.AppName}}">
|
||||
{{end}}{{.ManifestSnip}} <application {{.IconSnip}} android:label="{{.AppName}}" android:enableOnBackInvokedCallback="false">
|
||||
{{.AppSnip}}
|
||||
<activity android:name="org.gioui.GioActivity"
|
||||
android:label="{{.AppName}}"
|
||||
|
||||
@@ -369,6 +369,57 @@ func (u *ui) addAPIPolicyRuleAction() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) apiPolicyGroupPathSummary() string {
|
||||
path := parsePath(u.apiPolicyPath.Text())
|
||||
if len(path) == 0 {
|
||||
return "No group selected"
|
||||
}
|
||||
return strings.Join(path, " / ")
|
||||
}
|
||||
|
||||
func (u *ui) apiPolicyEntrySummary() string {
|
||||
id := strings.TrimSpace(u.apiPolicyEntryID.Text())
|
||||
if id == "" {
|
||||
return "No entry selected"
|
||||
}
|
||||
if item, ok := u.selectedEntry(); ok && item.ID == id {
|
||||
if strings.TrimSpace(item.Title) != "" {
|
||||
return item.Title + " (" + id + ")"
|
||||
}
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func (u *ui) useCurrentGroupForPolicyAction() error {
|
||||
u.syncCurrentPath()
|
||||
path := u.displayPath()
|
||||
if len(path) == 0 {
|
||||
return fmt.Errorf("navigate to a group first")
|
||||
}
|
||||
u.apiPolicyGroupScope = true
|
||||
u.apiPolicyGroupScopeW.Value = true
|
||||
u.apiPolicyPath.SetText(strings.Join(path, " / "))
|
||||
u.apiPolicyEntryID.SetText("")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) useSelectedEntryForPolicyAction() error {
|
||||
item, ok := u.selectedEntry()
|
||||
if !ok || strings.TrimSpace(item.ID) == "" {
|
||||
return fmt.Errorf("select an entry first")
|
||||
}
|
||||
u.apiPolicyGroupScope = false
|
||||
u.apiPolicyGroupScopeW.Value = false
|
||||
u.apiPolicyEntryID.SetText(item.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) clearAPIPolicyTargetAction() error {
|
||||
u.apiPolicyPath.SetText("")
|
||||
u.apiPolicyEntryID.SetText("")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) removeAPIPolicyRuleAction(index int) error {
|
||||
token, ok := u.selectedAPIToken()
|
||||
if !ok {
|
||||
@@ -1002,9 +1053,44 @@ func (u *ui) apiTokenDetailPanel(gtx layout.Context) layout.Dimensions {
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(labeledEditorHelp(u.theme, "Operation", "Valid operations: "+strings.Join(stringOps(apiOperations()), ", "), &u.apiPolicyOperation, false)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(labeledEditorHelp(u.theme, "Group Path", "Used when group scope is enabled.", &u.apiPolicyPath, false)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(labeledEditorHelp(u.theme, "Entry ID", "Used when group scope is disabled.", &u.apiPolicyEntryID, false)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.apiPolicyGroupScopeW.Value {
|
||||
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(detailLine(u.theme, "Group Path", u.apiPolicyGroupPathSummary())),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.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 {
|
||||
return tonedButton(gtx, u.theme, &u.useCurrentGroupForPolicy, "Use Current Group")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.clearAPIPolicyTarget, "Clear")
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(detailLine(u.theme, "Entry", u.apiPolicyEntrySummary())),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.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 {
|
||||
return tonedButton(gtx, u.theme, &u.useSelectedEntryForPolicy, "Use Selected Entry")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.clearAPIPolicyTarget, "Clear")
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.addAPIPolicyRule, "Add Rule")
|
||||
|
||||
+1
-1
@@ -909,7 +909,7 @@ func (u *ui) groupControlsDisclosure(gtx layout.Context) layout.Dimensions {
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
icon := u.expandLessIcon
|
||||
if u.groupControlsHidden {
|
||||
icon = u.expandMoreIcon
|
||||
icon = u.chevronRightIcon
|
||||
}
|
||||
if icon == nil {
|
||||
lbl := material.Label(u.theme, unit.Sp(16), ">")
|
||||
|
||||
Reference in New Issue
Block a user