Files
lazyrun/internal/ui/dialog_icon.go
T
oxpoe 6e6f9afea9 Perbaiki ikon internet yang kadang hilang / bikin crash
- Semua SVG diraster ke PNG lebih dulu (install, sync, thumbnail). SVG dari
  internet sebagian memakai fitur yang tidak didukung renderer sehingga ikon
  tidak muncul atau proses menggambar berhenti.
- fill="currentColor" diganti warna brand (penyebab tersering ikon Material/
  Remix gagal diraster), rasterisasi dibungkus recover
- Thumbnail pencarian: placeholder + cache unduhan + batas 3 unduhan bersamaan
- Kandidat ikon yang gagal dilewati, install tetap lanjut ke ikon default
- Race pada data ikon pilihan diperbaiki (mutex); tes -race hijau
- Baris status modal pencarian dipindah ke barisnya sendiri (tidak bertabrakan
  dengan tombol Back/Search di layar berskala besar)
2026-10-13 08:00:12 +07:00

152 lines
3.8 KiB
Go

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)
}