feat(ui): 接入 vendor 真实图标库 + 图标选择器
- assets/icons/ 共 80+ 张真实品牌图标嵌入 UI 包 - entry 新增 IconKey 字段,持久化到 config.Entry.IconName - iconPickerDialog 网格布局图标选择器 - Add 完成后自动弹出图标选择器,Skip 则用 vendor 占位图 - ⋯ 菜单新增 "Change icon..." 项 - vendorIconFor 解析顺序:catalog → vendor 占位 → qr 兜底
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
|
||||
"git.wxccs.org/iceking2nd/winauth-go/internal/i18n"
|
||||
)
|
||||
|
||||
// iconPickerDialog shows the full catalog of brand icons in a scrollable
|
||||
// grid. The user clicks an icon to select it, then OK to confirm. A
|
||||
// fixed column count keeps row layout cheap (no FlowWrap needed).
|
||||
type iconPickerDialog struct {
|
||||
icons []string
|
||||
selected string
|
||||
clickables []widget.Clickable
|
||||
list widget.List
|
||||
okBtn widget.Clickable
|
||||
cancelBtn widget.Clickable
|
||||
cols int
|
||||
}
|
||||
|
||||
func newIconPickerDialog(currentKey string) *iconPickerDialog {
|
||||
keys := allIconKeys()
|
||||
d := &iconPickerDialog{
|
||||
icons: keys,
|
||||
selected: currentKey,
|
||||
clickables: make([]widget.Clickable, len(keys)),
|
||||
cols: 6,
|
||||
}
|
||||
d.list.Axis = layout.Vertical
|
||||
return d
|
||||
}
|
||||
|
||||
// iconExistsInCatalog reports whether key is in the published icon
|
||||
// catalog (as opposed to the fallback vendor icons).
|
||||
func iconExistsInCatalog(key string) bool {
|
||||
for _, k := range allIconKeys() {
|
||||
if k == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *iconPickerDialog) Layout(
|
||||
gtx layout.Context, th *material.Theme,
|
||||
onDone func(iconKey string, cancel bool),
|
||||
) layout.Dimensions {
|
||||
for i := range d.icons {
|
||||
if d.clickables[i].Clicked(gtx) {
|
||||
d.selected = d.icons[i]
|
||||
}
|
||||
}
|
||||
if d.cancelBtn.Clicked(gtx) {
|
||||
onDone("", true)
|
||||
return layout.Dimensions{Size: gtx.Constraints.Max}
|
||||
}
|
||||
if d.okBtn.Clicked(gtx) {
|
||||
onDone(d.selected, false)
|
||||
return layout.Dimensions{Size: gtx.Constraints.Max}
|
||||
}
|
||||
|
||||
body := func(gtx layout.Context) layout.Dimensions {
|
||||
// Cap body height so the picker scrolls instead of overflowing
|
||||
// the screen on small windows.
|
||||
maxH := gtx.Dp(unit.Dp(320))
|
||||
if gtx.Constraints.Max.Y > maxH {
|
||||
gtx.Constraints.Max.Y = maxH
|
||||
}
|
||||
|
||||
cellDp := unit.Dp(48)
|
||||
cellPx := gtx.Dp(cellDp)
|
||||
cols := d.cols
|
||||
totalRows := (len(d.icons) + cols - 1) / cols
|
||||
|
||||
return material.List(th, &d.list).Layout(gtx, totalRows, func(gtx layout.Context, rowIdx int) layout.Dimensions {
|
||||
start := rowIdx * cols
|
||||
end := start + cols
|
||||
if end > len(d.icons) {
|
||||
end = len(d.icons)
|
||||
}
|
||||
rowIcons := d.icons[start:end]
|
||||
|
||||
children := make([]layout.FlexChild, len(rowIcons))
|
||||
for j, key := range rowIcons {
|
||||
idx := start + j
|
||||
key := key
|
||||
sel := key == d.selected
|
||||
children[j] = layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{
|
||||
Right: unit.Dp(4), Bottom: unit.Dp(4),
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
gtx.Constraints.Min = image.Pt(cellPx, cellPx)
|
||||
gtx.Constraints.Max = image.Pt(cellPx, cellPx)
|
||||
return d.clickables[idx].Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return drawIconCell(gtx, key, sel)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, children...)
|
||||
})
|
||||
}
|
||||
|
||||
return modalCard(gtx, th, i18n.T("dialog_icon_picker_title"),
|
||||
i18n.T("btn_ok"), i18n.T("btn_cancel"),
|
||||
&d.okBtn, &d.cancelBtn, body)
|
||||
}
|
||||
|
||||
// drawIconCell paints one grid cell: optional selection background +
|
||||
// the icon scaled to fit. Returns the fixed cell-size dimensions.
|
||||
func drawIconCell(gtx layout.Context, key string, selected bool) layout.Dimensions {
|
||||
size := gtx.Constraints.Min
|
||||
if selected {
|
||||
rect := clip.Rect{Max: size}.Push(gtx.Ops)
|
||||
paint.ColorOp{Color: activePalette.RingBg}.Add(gtx.Ops)
|
||||
paint.PaintOp{}.Add(gtx.Ops)
|
||||
rect.Pop()
|
||||
}
|
||||
img := vendorIconFor(key)
|
||||
if img != nil {
|
||||
w := widget.Image{
|
||||
Src: paint.NewImageOp(img),
|
||||
Fit: widget.Contain,
|
||||
Position: layout.Center,
|
||||
}
|
||||
w.Layout(gtx)
|
||||
}
|
||||
return layout.Dimensions{Size: size}
|
||||
}
|
||||
Reference in New Issue
Block a user