Files
lazyrun/internal/ui/dialog_icon.go
T
oxpoe deaa30689c Tooltip hint, loading bar, edit shortcut, export/import .lazy
- Keterangan tombol saat hover: baris hint di footer window dan di dalam dialog
  (bukan popup: overlay Fyne menelan klik pertama, tombol jadi perlu 2x klik)
- Dialog proses sekarang pakai progress bar berjalan (bukan spinner diam)
- Tombol edit per shortcut: ganti binary/URL, ikon (file/online), nama, kategori,
  dan lokasi; berkas .desktop + salinan Desktop ditulis ulang, berkas lama dibuang,
  jenis aplikasi dideteksi ulang, binary lama dihapus kalau nama berubah
- Export/Import daftar shortcut ke berkas SQLite .lazy (driver murni Go modernc),
  skema + meta format versi, impor tidak menimpa shortcut yang sudah ada
- Tes: detect kind, edit (rename/ganti binary/ikon/target), db roundtrip, merge impor,
  tooltip (hint muncul/hilang + klik tetap sampai ke tombol), semua paket hijau
- README: bagian backup + hint tombol
2026-10-13 09:12:39 +07:00

168 lines
4.5 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"))
entry.SetPlaceHolder("whatsapp / mikrotik.com")
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 := lookupIcons(entry.Text)
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.
hint := textLine(T("icon_search_hint"), 12, false, colorNameMuted)
bottom := container.NewVBox(hint, status, container.NewHBox(
spacer(),
newButton(T("back"), theme.NavigateBackIcon(), T("tip_back"), widget.MediumImportance, func() { d.Hide() }),
newButton(T("search"), theme.SearchIcon(), T("tip_search"), widget.HighImportance, 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)
d.SetOnClosed(pushHintTarget(hint))
showDialog(d, ui.win, 560, 470) // sekaligus menampilkan dialog
search()
}
// lookupIcons memilih sumber sesuai yang diketik user:
// URL gambar → dipakai langsung, domain situs → ikon resmi situs itu,
// selain itu → pencarian nama di Iconify.
func lookupIcons(query string) ([]icon.Result, error) {
query = strings.TrimSpace(query)
if icon.IsImageURL(query) {
return []icon.Result{{Name: "URL ikon", URL: query}}, nil
}
if domain := icon.Domain(query); domain != "" {
if list, err := icon.SiteIcons(domain); err == nil && len(list) > 0 {
return list, nil
}
}
return icon.SearchBest(query, 24)
}
// 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 := newButton("", nil, result.Name, widget.LowImportance, func() {
mu.Lock()
picked := raw
mu.Unlock()
onPick(result, picked)
if d != nil {
d.Hide()
}
})
button.Icon = theme.NewThemedResource(theme.FileImageIcon()) // placeholder selama diunduh
button.Refresh()
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)
}