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
This commit is contained in:
1 parent
6e6f9afea9
commit
cad41caaee
10 files changed
+269
-73
No files matched your search
@@ -6,6 +6,7 @@ require (
|
|||||||
fyne.io/fyne/v2 v2.8.2
|
fyne.io/fyne/v2 v2.8.2
|
||||||
github.com/fyne-io/oksvg v0.2.1-0.20260918172519-a9af55fa95d9
|
github.com/fyne-io/oksvg v0.2.1-0.20260918172519-a9af55fa95d9
|
||||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef
|
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef
|
||||||
|
golang.org/x/image v0.24.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -36,7 +37,6 @@ require (
|
|||||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
||||||
github.com/stretchr/testify v1.11.1 // indirect
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
github.com/yuin/goldmark v1.8.2 // indirect
|
github.com/yuin/goldmark v1.8.2 // indirect
|
||||||
golang.org/x/image v0.24.0 // indirect
|
|
||||||
golang.org/x/net v0.35.0 // indirect
|
golang.org/x/net v0.35.0 // indirect
|
||||||
golang.org/x/sys v0.30.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package icon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/gif"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/image/bmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func solidImg(size int) image.Image {
|
||||||
|
img := image.NewNRGBA(image.Rect(0, 0, size, size))
|
||||||
|
for x := 0; x < size; x++ {
|
||||||
|
for y := 0; y < size; y++ {
|
||||||
|
img.Set(x, y, color.NRGBA{R: 0x12, G: 0x9f, B: 0x55, A: 0xFF})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return img
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(t *testing.T, fn func(*bytes.Buffer) error) []byte {
|
||||||
|
t.Helper()
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := fn(&buf); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToPNGConvertsFormats(t *testing.T) {
|
||||||
|
src := solidImg(64)
|
||||||
|
|
||||||
|
cases := map[string][]byte{
|
||||||
|
"png": encode(t, func(b *bytes.Buffer) error { return png.Encode(b, src) }),
|
||||||
|
"gif": encode(t, func(b *bytes.Buffer) error { return gif.Encode(b, src, nil) }),
|
||||||
|
"bmp": encode(t, func(b *bytes.Buffer) error { return bmp.Encode(b, src) }),
|
||||||
|
"jpg": encode(t, func(b *bytes.Buffer) error { return jpeg.Encode(b, src, nil) }),
|
||||||
|
"svg": []byte(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"><rect width="8" height="8"/></svg>`),
|
||||||
|
"ico": icoWithPNG(encode(t, func(b *bytes.Buffer) error { return png.Encode(b, solidImg(16)) })),
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, data := range cases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
out, err := ToPNG(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ToPNG(%s) gagal: %v", name, err)
|
||||||
|
}
|
||||||
|
if !bytes.HasPrefix(out, pngMagic) {
|
||||||
|
t.Fatalf("hasil %s bukan PNG", name)
|
||||||
|
}
|
||||||
|
if _, err := png.Decode(bytes.NewReader(out)); err != nil {
|
||||||
|
t.Fatalf("hasil %s tidak bisa di-decode: %v", name, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToPNGRejectsUnknown(t *testing.T) {
|
||||||
|
if _, err := ToPNG([]byte("halo dunia")); err == nil {
|
||||||
|
t.Error("data bukan gambar harus error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToPNGShrinksHugeImages(t *testing.T) {
|
||||||
|
out, err := ToPNG(encode(t, func(b *bytes.Buffer) error { return png.Encode(b, solidImg(1024)) }))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(out) >= 1024*1024 {
|
||||||
|
t.Errorf("gambar besar tidak diperkecil: %d byte", len(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveAlwaysWritesPNG(t *testing.T) {
|
||||||
|
t.Setenv("HOME", t.TempDir())
|
||||||
|
t.Setenv("XDG_DATA_HOME", "")
|
||||||
|
|
||||||
|
// ikon webp tak disupport ditest lewat gif: hasil harus selalu .png
|
||||||
|
path, err := Save(encode(t, func(b *bytes.Buffer) error { return gif.Encode(b, solidImg(32), nil) }), "contoh")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := path[len(path)-4:]; got != ".png" {
|
||||||
|
t.Errorf("path ikon = %s", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package icon
|
||||||
|
|
||||||
|
// decoder gambar tambahan: webp & bmp tidak ada di stdlib,
|
||||||
|
// jpeg/gif perlu di-import supaya terdaftar di image.Decode.
|
||||||
|
import (
|
||||||
|
_ "golang.org/x/image/bmp"
|
||||||
|
_ "golang.org/x/image/webp"
|
||||||
|
|
||||||
|
_ "image/gif"
|
||||||
|
_ "image/jpeg"
|
||||||
|
)
|
||||||
+62
-52
@@ -2,38 +2,30 @@
|
|||||||
package icon
|
package icon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
xdraw "golang.org/x/image/draw"
|
||||||
|
|
||||||
"lazyrun/assets"
|
"lazyrun/assets"
|
||||||
"lazyrun/internal/xdg"
|
"lazyrun/internal/xdg"
|
||||||
)
|
)
|
||||||
|
|
||||||
var exts = []string{".png", ".svg", ".jpg", ".jpeg", ".xpm"}
|
var exts = []string{".png", ".svg", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".ico", ".xpm"}
|
||||||
var nearbyNames = []string{"icon", "logo"}
|
|
||||||
|
|
||||||
// Save memasang ikon untuk shortcut dan mengembalikan path absolutnya.
|
var pngMagic = []byte("\x89PNG\r\n\x1a\n")
|
||||||
// Urutan pilihan: ikon dari user, ikon di folder binary, lalu ikon default LazyRun.
|
|
||||||
func Save(userIcon, nearbyDir, nearbyBase, slug string) (string, error) {
|
// maxSide adalah sisi terpanjang ikon yang disimpan.
|
||||||
if userIcon != "" {
|
const maxSide = 512
|
||||||
return fromFile(userIcon, slug)
|
|
||||||
}
|
|
||||||
for _, name := range append([]string{nearbyBase}, nearbyNames...) {
|
|
||||||
if name == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if p := findNearby(nearbyDir, name); p != "" {
|
|
||||||
return fromFile(p, slug)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Default(slug)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default memasang ikon bawaan LazyRun.
|
// Default memasang ikon bawaan LazyRun.
|
||||||
func Default(slug string) (string, error) {
|
func Default(slug string) (string, error) {
|
||||||
return SaveBytes(assets.Icon, ".png", slug)
|
return Save(assets.Icon, slug)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nearby mencari file ikon bernama base.* di folder dir (dipakai untuk auto-deteksi).
|
// Nearby mencari file ikon bernama base.* di folder dir (dipakai untuk auto-deteksi).
|
||||||
@@ -55,48 +47,66 @@ func findNearby(dir, base string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func fromFile(src, slug string) (string, error) {
|
// Save menulis ikon ke direktori ikon XDG sebagai PNG.
|
||||||
data, err := os.ReadFile(src)
|
//
|
||||||
|
// Semua format (SVG, webp, gif, bmp, ico, jpeg) dikonversi dulu ke PNG karena
|
||||||
|
// desktop environment tidak konsisten membacanya, dan sebagian SVG dari internet
|
||||||
|
// punya fitur yang bikin proses menggambar berhenti di UI.
|
||||||
|
func Save(data []byte, slug string) (string, error) {
|
||||||
|
converted, err := ToPNG(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
ext := strings.ToLower(filepath.Ext(src))
|
|
||||||
// ICO tidak dipakai desktop theme; pakai PNG di dalamnya kalau ada.
|
|
||||||
if ext == ".ico" {
|
|
||||||
png := largestPNGFromICO(data)
|
|
||||||
if png == nil {
|
|
||||||
return "", errors.New("this .ico has no PNG inside it, pick another icon")
|
|
||||||
}
|
|
||||||
return SaveBytes(png, ".png", slug)
|
|
||||||
}
|
|
||||||
return SaveBytes(data, ext, slug)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveBytes menulis data ikon ke direktori ikon XDG.
|
|
||||||
// SVG diraster dulu ke PNG: sebagian ikon internet memakai fitur SVG yang membuat
|
|
||||||
// proses menggambar berhenti (panic) atau tidak tampil di UI/desktop.
|
|
||||||
func SaveBytes(data []byte, ext, slug string) (string, error) {
|
|
||||||
if ext == "" {
|
|
||||||
ext = ".png"
|
|
||||||
}
|
|
||||||
if ext == ".svg" {
|
|
||||||
raster, err := Rasterize(data, 256)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
data, ext = raster, ".png"
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(xdg.IconsDir(), 0o755); err != nil {
|
if err := os.MkdirAll(xdg.IconsDir(), 0o755); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
// shortcut: ikon disimpan apa adanya di hicolor/512x512 tanpa resize;
|
dest := filepath.Join(xdg.IconsDir(), slug+".png")
|
||||||
// desktop environment men-skalanya sendiri. Tambah resize kalau ikon kecil terlihat pecah.
|
return dest, os.WriteFile(dest, converted, 0o644)
|
||||||
dest := filepath.Join(xdg.IconsDir(), slug+ext)
|
|
||||||
return dest, os.WriteFile(dest, data, 0o644)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ext menebak ekstensi file dari isi data (dipakai untuk ikon yang sudah diunduh UI).
|
// Ext menebak ekstensi file dari isi data.
|
||||||
func Ext(data []byte) string { return sniffExt(data) }
|
func Ext(data []byte) string { return sniffExt(data) }
|
||||||
|
|
||||||
// IconBytes adalah ikon default LazyRun.
|
// IconBytes adalah ikon default LazyRun.
|
||||||
func IconBytes() []byte { return assets.Icon }
|
func IconBytes() []byte { return assets.Icon }
|
||||||
|
|
||||||
|
// ToPNG mengubah data gambar apa pun yang didukung menjadi PNG.
|
||||||
|
// Data yang sudah PNG dikembalikan apa adanya.
|
||||||
|
func ToPNG(data []byte) ([]byte, error) {
|
||||||
|
if bytes.HasPrefix(data, pngMagic) {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if sniffExt(data) == ".svg" {
|
||||||
|
return Rasterize(data, 256)
|
||||||
|
}
|
||||||
|
|
||||||
|
// jpeg, gif, bmp, webp dikenali dari isi file
|
||||||
|
if img, _, err := image.Decode(bytes.NewReader(data)); err == nil {
|
||||||
|
return encodePNG(img)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ico bisa berisi PNG langsung
|
||||||
|
if png := largestPNGFromICO(data); png != nil {
|
||||||
|
return png, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("format ikon tidak didukung")
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodePNG menulis gambar jadi PNG, diperkecil kalau lebih besar dari maxSide.
|
||||||
|
func encodePNG(img image.Image) ([]byte, error) {
|
||||||
|
bounds := img.Bounds()
|
||||||
|
if bounds.Dx() > maxSide || bounds.Dy() > maxSide {
|
||||||
|
scale := float64(maxSide) / float64(max(bounds.Dx(), bounds.Dy()))
|
||||||
|
size := image.Rect(0, 0, int(float64(bounds.Dx())*scale), int(float64(bounds.Dy())*scale))
|
||||||
|
scaled := image.NewNRGBA(size)
|
||||||
|
xdraw.CatmullRom.Scale(scaled, size, img, bounds, xdraw.Over, nil)
|
||||||
|
img = scaled
|
||||||
|
}
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
if err := png.Encode(&out, img); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out.Bytes(), nil
|
||||||
|
}
|
||||||
+21
-4
@@ -7,14 +7,30 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
var iconifyBase = "https://api.iconify.design"
|
var (
|
||||||
|
iconifyMu sync.RWMutex
|
||||||
|
iconifyBase = "https://api.iconify.design"
|
||||||
|
)
|
||||||
|
|
||||||
// SetSearchEndpoint mengarahkan pencarian ikon ke endpoint lain (dipakai tes).
|
// SetSearchEndpoint mengarahkan pencarian ikon ke endpoint lain (dipakai tes).
|
||||||
func SetSearchEndpoint(base string) { iconifyBase = base }
|
func SetSearchEndpoint(base string) {
|
||||||
|
iconifyMu.Lock()
|
||||||
|
defer iconifyMu.Unlock()
|
||||||
|
iconifyBase = base
|
||||||
|
}
|
||||||
|
|
||||||
|
// searchEndpoint dibaca setiap kali mencari, jadi harus lewat lock
|
||||||
|
// (tes bisa mengganti endpoint sementara goroutine lain mencari).
|
||||||
|
func searchEndpoint() string {
|
||||||
|
iconifyMu.RLock()
|
||||||
|
defer iconifyMu.RUnlock()
|
||||||
|
return iconifyBase
|
||||||
|
}
|
||||||
|
|
||||||
// Result adalah satu ikon dari pencarian online.
|
// Result adalah satu ikon dari pencarian online.
|
||||||
type Result struct {
|
type Result struct {
|
||||||
@@ -41,7 +57,8 @@ func Search(query string, limit int) ([]Result, error) {
|
|||||||
limit = 32
|
limit = 32
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint := fmt.Sprintf("%s/search?query=%s&limit=%d", iconifyBase, url.QueryEscape(query), limit)
|
base := searchEndpoint()
|
||||||
|
endpoint := fmt.Sprintf("%s/search?query=%s&limit=%d", base, url.QueryEscape(query), limit)
|
||||||
data, err := get(endpoint)
|
data, err := get(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -63,7 +80,7 @@ func Search(query string, limit int) ([]Result, error) {
|
|||||||
// height=256 supaya SVG-nya punya ukuran eksplisit (bukan "1em")
|
// height=256 supaya SVG-nya punya ukuran eksplisit (bukan "1em")
|
||||||
results = append(results, Result{
|
results = append(results, Result{
|
||||||
Name: name,
|
Name: name,
|
||||||
URL: fmt.Sprintf("%s/%s/%s.svg?height=256", iconifyBase, url.PathEscape(prefix), url.PathEscape(icon)),
|
URL: fmt.Sprintf("%s/%s/%s.svg?height=256", base, url.PathEscape(prefix), url.PathEscape(icon)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if len(results) == 0 {
|
if len(results) == 0 {
|
||||||
|
|||||||
@@ -81,11 +81,11 @@ func installIconFor(o iconOptions) (path, source string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, attempt := range attempts {
|
for _, attempt := range attempts {
|
||||||
data, ext, err := attempt.load()
|
data, _, err := attempt.load()
|
||||||
if err != nil || len(data) == 0 {
|
if err != nil || len(data) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
path, err := icon.SaveBytes(data, ext, o.Slug)
|
path, err := icon.Save(data, o.Slug)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -102,22 +102,21 @@ var errSkipIcon = errors.New("icon source not available")
|
|||||||
// RefreshIcon mengunduh ulang ikon shortcut dari sumber online-nya (sync manual).
|
// RefreshIcon mengunduh ulang ikon shortcut dari sumber online-nya (sync manual).
|
||||||
func RefreshIcon(sc store.Shortcut) (string, error) {
|
func RefreshIcon(sc store.Shortcut) (string, error) {
|
||||||
var data []byte
|
var data []byte
|
||||||
var ext string
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case sc.IconSource == "":
|
case sc.IconSource == "":
|
||||||
return "", errors.New("this icon has no online source")
|
return "", errors.New("this icon has no online source")
|
||||||
case strings.HasPrefix(sc.IconSource, iconSourceFavicon):
|
case strings.HasPrefix(sc.IconSource, iconSourceFavicon):
|
||||||
data, ext, err = icon.Fetch(strings.TrimPrefix(sc.IconSource, iconSourceFavicon))
|
data, _, err = icon.Fetch(strings.TrimPrefix(sc.IconSource, iconSourceFavicon))
|
||||||
case strings.HasPrefix(sc.IconSource, iconSourceSearch):
|
case strings.HasPrefix(sc.IconSource, iconSourceSearch):
|
||||||
result, ok := icon.Find(strings.TrimPrefix(sc.IconSource, iconSourceSearch))
|
result, ok := icon.Find(strings.TrimPrefix(sc.IconSource, iconSourceSearch))
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", errors.New("no matching icon found online anymore")
|
return "", errors.New("no matching icon found online anymore")
|
||||||
}
|
}
|
||||||
data, ext, err = icon.FromURL(result.URL)
|
data, _, err = icon.FromURL(result.URL)
|
||||||
case strings.HasPrefix(sc.IconSource, "http"):
|
case strings.HasPrefix(sc.IconSource, "http"):
|
||||||
data, ext, err = icon.FromURL(sc.IconSource)
|
data, _, err = icon.FromURL(sc.IconSource)
|
||||||
default:
|
default:
|
||||||
return "", errors.New("this icon comes from a local file, not the internet")
|
return "", errors.New("this icon comes from a local file, not the internet")
|
||||||
}
|
}
|
||||||
@@ -125,7 +124,7 @@ func RefreshIcon(sc store.Shortcut) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
path, err := icon.SaveBytes(data, ext, sc.Slug)
|
path, err := icon.Save(data, sc.Slug)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ var categories = []string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
imageFilter = storage.NewExtensionFileFilter([]string{".png", ".svg", ".jpg", ".jpeg", ".xpm", ".ico"})
|
imageFilter = storage.NewExtensionFileFilter([]string{".png", ".svg", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".ico", ".xpm"})
|
||||||
stripExts = []string{".AppImage", ".appimage", ".sh", ".bin", ".run", ".desktop"}
|
stripExts = []string{".AppImage", ".appimage", ".sh", ".bin", ".run", ".desktop"}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ func (ui *app) showExecutableDialog() {
|
|||||||
install),
|
install),
|
||||||
))
|
))
|
||||||
|
|
||||||
d = dialog.NewCustom(T("new_exec_title"), "", content, ui.win)
|
d = dialog.NewCustomWithoutButtons(T("new_exec_title"), content, ui.win)
|
||||||
showDialog(d, ui.win, 560, 520)
|
showDialog(d, ui.win, 560, 520)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ func (ui *app) showIconSearch(query string, onPick func(icon.Result, []byte)) {
|
|||||||
results,
|
results,
|
||||||
)
|
)
|
||||||
|
|
||||||
d = dialog.NewCustom(T("icon_search_title"), "", content, ui.win)
|
d = dialog.NewCustomWithoutButtons(T("icon_search_title"), content, ui.win)
|
||||||
showDialog(d, ui.win, 560, 470) // sekaligus menampilkan dialog
|
showDialog(d, ui.win, 560, 470) // sekaligus menampilkan dialog
|
||||||
search()
|
search()
|
||||||
}
|
}
|
||||||
@@ -129,12 +129,9 @@ func (ui *app) iconChoice(result icon.Result, onPick func(icon.Result, []byte),
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
thumb, err := icon.Rasterize(downloaded, 64)
|
thumb, err := icon.ToPNG(downloaded)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if icon.Ext(downloaded) == ".svg" {
|
return // placeholder tetap tampil
|
||||||
return
|
|
||||||
}
|
|
||||||
thumb = downloaded // PNG/JPEG bisa langsung dipakai
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2/test"
|
||||||
|
|
||||||
|
"lazyrun/internal/icon"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestNoEmptyDismissButton memastikan modal tidak memakai dialog.NewCustom,
|
||||||
|
// yang selalu menambah tombol dismiss walau labelnya kosong (tombol hantu
|
||||||
|
// di bagian bawah modal). Kita punya baris tombol sendiri, jadi harus memakai
|
||||||
|
// NewCustomWithoutButtons.
|
||||||
|
func TestNoEmptyDismissButton(t *testing.T) {
|
||||||
|
files, err := filepath.Glob("*.go")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
if strings.HasSuffix(file, "_test.go") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if strings.Contains(string(data), "dialog.NewCustom(") {
|
||||||
|
t.Errorf("%s memakai dialog.NewCustom (menambah tombol kosong); gunakan NewCustomWithoutButtons", file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDialogsOpenWithoutPanic membuka semua modal di test driver.
|
||||||
|
func TestDialogsOpenWithoutPanic(t *testing.T) {
|
||||||
|
a := test.NewApp()
|
||||||
|
defer a.Quit()
|
||||||
|
|
||||||
|
// pencarian ikon diarahkan ke port mati: tes tidak boleh menyentuh internet
|
||||||
|
// dan tidak boleh mengubah UI dari goroutine latar.
|
||||||
|
icon.SetSearchEndpoint("http://127.0.0.1:1")
|
||||||
|
defer icon.SetSearchEndpoint("https://api.iconify.design")
|
||||||
|
|
||||||
|
ui, err := build(a, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ui.win.Show()
|
||||||
|
|
||||||
|
ui.showExecutableDialog()
|
||||||
|
if top := ui.win.Canvas().Overlays().Top(); top == nil {
|
||||||
|
t.Fatal("modal executable tidak muncul")
|
||||||
|
}
|
||||||
|
ui.showWebAppDialog()
|
||||||
|
if len(ui.browsers) > 0 {
|
||||||
|
if top := ui.win.Canvas().Overlays().Top(); top == nil {
|
||||||
|
t.Fatal("modal web app tidak muncul")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.showIconSearch("whatsapp", func(icon.Result, []byte) {})
|
||||||
|
if top := ui.win.Canvas().Overlays().Top(); top == nil {
|
||||||
|
t.Fatal("modal pencarian ikon tidak muncul")
|
||||||
|
}
|
||||||
|
|
||||||
|
// tunggu goroutine pencarian selesai: test driver menjalankan fyne.Do inline,
|
||||||
|
// jadi sisa goroutine bisa bertabrakan dengan tes berikutnya kalau dibiarkan.
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
}
|
||||||
@@ -118,7 +118,7 @@ func (ui *app) showWebAppDialog() {
|
|||||||
install),
|
install),
|
||||||
))
|
))
|
||||||
|
|
||||||
d = dialog.NewCustom(T("new_web_title"), "", content, ui.win)
|
d = dialog.NewCustomWithoutButtons(T("new_web_title"), content, ui.win)
|
||||||
showDialog(d, ui.win, 560, 520)
|
showDialog(d, ui.win, 560, 520)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user