package ui import ( "fmt" "strings" "sync" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "lazyrun/internal/icon" ) // iconCache menyimpan ikon yang sudah diunduh supaya pencarian ulang tidak // mengunduh berkali-kali: API ikon publik membatasi jumlah permintaan beruntun. type iconCache struct { mu sync.Mutex items map[string][]byte } func newIconCache() *iconCache { return &iconCache{items: map[string][]byte{}} } func (c *iconCache) load(url string) ([]byte, bool) { c.mu.Lock() defer c.mu.Unlock() data, ok := c.items[url] return data, ok } func (c *iconCache) store(url string, data []byte) { c.mu.Lock() defer c.mu.Unlock() c.items[url] = data } // fetchIcon mengunduh ikon (atau mengambil dari cache). func (ui *app) fetchIcon(url string) ([]byte, error) { if data, ok := ui.icons.load(url); ok { return data, nil } data, _, err := icon.FromURL(url) if err != nil { return nil, err } ui.icons.store(url, data) return data, nil } // showIconSearch menampilkan pencarian ikon online (Iconify) dan mengirim hasil pilihan. func (ui *app) showIconSearch(query string, onPick func(icon.Result, []byte)) { entry := widget.NewEntry() entry.SetText(icon.QueryHint(query)) entry.SetPlaceHolder("whatsapp") status := muted(T("search_hint")) grid := container.NewGridWithColumns(6) results := container.NewVScroll(container.NewPadded(grid)) slots := make(chan struct{}, 3) var d *dialog.CustomDialog search := func() { if strings.TrimSpace(entry.Text) == "" { status.SetText(T("search_hint")) return } status.SetText(T("searching")) grid.RemoveAll() go func() { list, err := icon.SearchBest(entry.Text, 24) fyne.Do(func() { if err != nil { status.SetText(T("search_failed")) return } status.SetText(fmt.Sprintf(T("search_found"), len(list))) for _, result := range list { grid.Add(ui.iconChoice(result, onPick, d, slots)) } }) }() } entry.OnSubmitted = func(string) { search() } // status di barisnya sendiri: supaya tidak bertabrakan dengan tombol // di layar sempit atau ber-penskalaan besar. bottom := container.NewVBox(status, container.NewHBox( spacer(), widget.NewButtonWithIcon(T("back"), theme.NavigateBackIcon(), func() { d.Hide() }), widget.NewButtonWithIcon(T("search"), theme.SearchIcon(), search), )) content := container.NewBorder( container.NewVBox(closeRow(func() { d.Hide() }), entry), bottom, nil, nil, results, ) d = dialog.NewCustom(T("icon_search_title"), "", content, ui.win) showDialog(d, ui.win, 560, 470) // sekaligus menampilkan dialog search() } // iconChoice adalah satu ikon hasil pencarian. Gambarnya diunduh di latar belakang // dan digambar sebagai PNG: SVG dari internet kadang tidak bisa digambar sehingga // thumbnail tidak muncul atau proses menggambar berhenti. func (ui *app) iconChoice(result icon.Result, onPick func(icon.Result, []byte), d *dialog.CustomDialog, slot chan struct{}) fyne.CanvasObject { var mu sync.Mutex var raw []byte button := widget.NewButton("", func() { mu.Lock() picked := raw mu.Unlock() onPick(result, picked) if d != nil { d.Hide() } }) button.Importance = widget.LowImportance button.Icon = theme.NewThemedResource(theme.FileImageIcon()) // placeholder selama diunduh go func() { slot <- struct{}{} defer func() { <-slot }() downloaded, err := ui.fetchIcon(result.URL) if err != nil { return } thumb, err := icon.Rasterize(downloaded, 64) if err != nil { if icon.Ext(downloaded) == ".svg" { return } thumb = downloaded // PNG/JPEG bisa langsung dipakai } mu.Lock() raw = downloaded mu.Unlock() fyne.Do(func() { button.Icon = fyne.NewStaticResource(result.Name+".png", thumb) button.Refresh() }) }() return container.NewGridWrap(fyne.NewSize(52, 52), button) }