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)
This commit is contained in:
1 parent
0885d69015
commit
6e6f9afea9
9 files changed
+312
-72
No files matched your search
+63
-31
@@ -3,6 +3,7 @@ package shortcut
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"lazyrun/internal/icon"
|
||||
@@ -29,44 +30,75 @@ type iconOptions struct {
|
||||
|
||||
// installIconFor menentukan ikon shortcut dan sumbernya.
|
||||
// Urutan: file user → URL user → ikon di folder binary → favicon situs → cari online → ikon default.
|
||||
// Kandidat yang gagal (file rusak, unduhan gagal, SVG tidak bisa diraster) dilewati,
|
||||
// jadi install tidak pernah batal hanya karena ikon.
|
||||
func installIconFor(o iconOptions) (path, source string, err error) {
|
||||
if o.IconPath != "" {
|
||||
path, err = icon.Save(o.IconPath, "", "", o.Slug)
|
||||
return path, iconSourceFile + o.IconPath, err
|
||||
}
|
||||
if len(o.IconData) > 0 && o.IconURL != "" {
|
||||
if path, err := icon.SaveData(o.IconData, o.Slug); err == nil {
|
||||
return path, o.IconURL, nil
|
||||
}
|
||||
}
|
||||
if o.IconURL != "" {
|
||||
if data, ext, err := icon.FromURL(o.IconURL); err == nil {
|
||||
path, err := icon.SaveBytes(data, ext, o.Slug)
|
||||
return path, o.IconURL, err
|
||||
}
|
||||
}
|
||||
if nearby := icon.Nearby(o.NearbyDir, o.NearbyBase); nearby != "" {
|
||||
path, err = icon.Save(nearby, "", "", o.Slug)
|
||||
return path, iconSourceFile + nearby, err
|
||||
}
|
||||
if o.PageURL != "" {
|
||||
if data, ext, err := icon.Fetch(o.PageURL); err == nil {
|
||||
path, err := icon.SaveBytes(data, ext, o.Slug)
|
||||
return path, iconSourceFavicon + o.PageURL, err
|
||||
}
|
||||
}
|
||||
if o.Query != "" {
|
||||
if result, ok := icon.Find(o.Query); ok {
|
||||
if data, ext, err := icon.FromURL(result.URL); err == nil {
|
||||
path, err := icon.SaveBytes(data, ext, o.Slug)
|
||||
return path, iconSourceSearch + o.Query, err
|
||||
attempts := []struct {
|
||||
source string
|
||||
load func() ([]byte, string, error)
|
||||
}{
|
||||
{iconSourceFile + o.IconPath, func() ([]byte, string, error) {
|
||||
if o.IconPath == "" {
|
||||
return nil, "", errSkipIcon
|
||||
}
|
||||
}
|
||||
data, err := os.ReadFile(o.IconPath)
|
||||
return data, strings.ToLower(filepath.Ext(o.IconPath)), err
|
||||
}},
|
||||
{o.IconURL, func() ([]byte, string, error) {
|
||||
// ikon pilihan dari pencarian online: datanya sudah diunduh UI
|
||||
if len(o.IconData) > 0 {
|
||||
return o.IconData, icon.Ext(o.IconData), nil
|
||||
}
|
||||
if o.IconURL == "" {
|
||||
return nil, "", errSkipIcon
|
||||
}
|
||||
return icon.FromURL(o.IconURL)
|
||||
}},
|
||||
{iconSourceFile + icon.Nearby(o.NearbyDir, o.NearbyBase), func() ([]byte, string, error) {
|
||||
nearby := icon.Nearby(o.NearbyDir, o.NearbyBase)
|
||||
if nearby == "" {
|
||||
return nil, "", errSkipIcon
|
||||
}
|
||||
data, err := os.ReadFile(nearby)
|
||||
return data, strings.ToLower(filepath.Ext(nearby)), err
|
||||
}},
|
||||
{iconSourceFavicon + o.PageURL, func() ([]byte, string, error) {
|
||||
if o.PageURL == "" {
|
||||
return nil, "", errSkipIcon
|
||||
}
|
||||
return icon.Fetch(o.PageURL)
|
||||
}},
|
||||
{iconSourceSearch + o.Query, func() ([]byte, string, error) {
|
||||
if o.Query == "" {
|
||||
return nil, "", errSkipIcon
|
||||
}
|
||||
result, ok := icon.Find(o.Query)
|
||||
if !ok {
|
||||
return nil, "", errSkipIcon
|
||||
}
|
||||
return icon.FromURL(result.URL)
|
||||
}},
|
||||
}
|
||||
|
||||
for _, attempt := range attempts {
|
||||
data, ext, err := attempt.load()
|
||||
if err != nil || len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
path, err := icon.SaveBytes(data, ext, o.Slug)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return path, attempt.source, nil
|
||||
}
|
||||
|
||||
path, err = icon.Default(o.Slug)
|
||||
return path, "", err
|
||||
}
|
||||
|
||||
// errSkipIcon menandai kandidat ikon yang tidak tersedia.
|
||||
var errSkipIcon = errors.New("icon source not available")
|
||||
|
||||
// RefreshIcon mengunduh ulang ikon shortcut dari sumber online-nya (sync manual).
|
||||
func RefreshIcon(sc store.Shortcut) (string, error) {
|
||||
var data []byte
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package shortcut
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -129,9 +130,10 @@ func TestInstallExecutableSearchesIconOnline(t *testing.T) {
|
||||
if sc.IconSource != "search:Winbox" {
|
||||
t.Errorf("icon_source = %q", sc.IconSource)
|
||||
}
|
||||
if filepath.Ext(sc.IconPath) != ".svg" {
|
||||
t.Errorf("ikon online harus tersimpan sebagai .svg, dapat %s", sc.IconPath)
|
||||
if filepath.Ext(sc.IconPath) != ".png" {
|
||||
t.Errorf("SVG online harus diraster jadi .png, dapat %s", sc.IconPath)
|
||||
}
|
||||
assertPNG(t, sc.IconPath)
|
||||
}
|
||||
|
||||
func TestInstallWebAppUsesIconURL(t *testing.T) {
|
||||
@@ -164,9 +166,10 @@ func TestInstallWebAppUsesIconURL(t *testing.T) {
|
||||
if sc.IconSource != srv.URL+"/pilih.svg" {
|
||||
t.Errorf("ikon pilihan user harus dipakai, icon_source = %q", sc.IconSource)
|
||||
}
|
||||
if filepath.Ext(sc.IconPath) != ".svg" {
|
||||
if filepath.Ext(sc.IconPath) != ".png" {
|
||||
t.Errorf("ikon = %s", sc.IconPath)
|
||||
}
|
||||
assertPNG(t, sc.IconPath)
|
||||
}
|
||||
|
||||
func TestInstallWebAppUsesIconDataFromUI(t *testing.T) {
|
||||
@@ -187,8 +190,17 @@ func TestInstallWebAppUsesIconDataFromUI(t *testing.T) {
|
||||
if sc.IconSource != "https://api.iconify.design/mdi/example.svg?height=256" {
|
||||
t.Errorf("icon_source = %q", sc.IconSource)
|
||||
}
|
||||
data, err := os.ReadFile(sc.IconPath)
|
||||
if err != nil || string(data) != string(svg) {
|
||||
t.Errorf("ikon tersimpan salah: %v %d byte", err, len(data))
|
||||
assertPNG(t, sc.IconPath)
|
||||
}
|
||||
|
||||
// assertPNG memastikan file ikon berisi PNG (bukan SVG mentah).
|
||||
func assertPNG(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("ikon tidak terbaca: %v", err)
|
||||
}
|
||||
if !bytes.HasPrefix(data, []byte("\x89PNG\r\n\x1a\n")) {
|
||||
t.Errorf("%s bukan PNG: %q", path, data[:min(12, len(data))])
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user