Files
lazyrun/internal/ui/dialog_icon.go
T
oxpoe cad41caaee Hapus tombol kosong bawaan Fyne di modal + semua format ikon dikonversi ke PNG
- dialog.NewCustom selalu menambah tombol dismiss walau labelnya kosong;
  modal kita pakai baris tombol sendiri, jadi pindah ke NewCustomWithoutButtons
  (ditambah tes yang menolak pemakaian dialog.NewCustom lagi)
- Ikon: SVG/webp/gif/bmp/ico/jpeg semuanya dikonversi ke PNG saat dipasang
  (desktop environment tidak konsisten membaca format lain), gambar besar
  diperkecil ke maksimal 512px
- Filter file picker ikon menambah webp/gif/bmp
- Perbaiki data race pada endpoint pencarian ikon (iconifyBase) yang ditemukan
  oleh go test -race; tes UI menunggu goroutine pencarian selesai
- go test -race -count=1 ./... hijau untuk semua paket
2026-10-13 08:00:12 +07:00

149 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.NewCustomWithoutButtons(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.ToPNG(downloaded)
if err != nil {
return // placeholder tetap tampil
}
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)
}