Ikon otomatis dari internet + cursor mengikuti tema host

- Cari ikon online (Iconify) dari dialog install, plus tombol sync ikon per shortcut
- Auto ikon: file terdekat -> favicon situs (termasuk ikon manifest PWA) -> ikon brand
  yang namanya cocok -> ikon default; sumber ikon disimpan di metadata
- Query dibersihkan (web.whatsapp.com -> whatsapp) dan kata <4 huruf tidak dipakai
  supaya aplikasi tanpa ikon resmi tidak dapat ikon acak
- Unduhan thumbnail dibatasi 4 bersamaan + retry sekali (rate limit Iconify)
- Cursor: XCURSOR_THEME/XCURSOR_SIZE diambil dari config KDE/GTK/~/.icons default,
  dan AppImage tidak lagi membundel libX*/libwayland*/libxkbcommon (dipakai dari host)
- README: bagian ikon + integrasi desktop
This commit is contained in:
oxpoe committed 2026-10-13 08:00:12 +07:00
1 parent 70534bf6e2
commit 7f7cb97a65
19 files changed
+1117 -40

No files matched your search

+63
View File
@@ -3,6 +3,7 @@ package icon
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
@@ -26,6 +27,7 @@ var (
)
// Fetch mengambil favicon dari halaman. Return data + ekstensi file (".png", ".jpg", ".svg").
// Urutan: ikon dari web app manifest (paling besar) → <link rel=icon> → /favicon.ico.
func Fetch(pageURL string) ([]byte, string, error) {
base, err := url.Parse(pageURL)
if err != nil {
@@ -53,6 +55,66 @@ func Fetch(pageURL string) ([]byte, string, error) {
return nil, "", lastErr
}
// manifestIcons membaca <link rel=manifest> lalu daftar ikonnya,
// diurutkan dari ukuran terbesar. Gagal parse bukan error: cukup dilewati.
func manifestIcons(page *url.URL) []iconCandidate {
html, err := get(page.String())
if err != nil {
return nil
}
var manifestURL string
for _, tag := range linkTag.FindAllString(string(html), -1) {
if !strings.Contains(strings.ToLower(attr(relAttr, tag)), "manifest") {
continue
}
if href := attr(hrefAttr, tag); href != "" {
if ref, err := url.Parse(href); err == nil {
manifestURL = page.ResolveReference(ref).String()
}
break
}
}
if manifestURL == "" {
return nil
}
data, err := get(manifestURL)
if err != nil {
return nil
}
var manifest struct {
Icons []struct {
Src string `json:"src"`
Sizes string `json:"sizes"`
} `json:"icons"`
}
if err := json.Unmarshal(data, &manifest); err != nil {
return nil
}
base, err := url.Parse(manifestURL)
if err != nil {
return nil
}
var candidates []iconCandidate
for _, entry := range manifest.Icons {
if entry.Src == "" {
continue
}
ref, err := url.Parse(entry.Src)
if err != nil {
continue
}
score := 0
if size := attr(sizeAttr, `sizes="`+entry.Sizes+`"`); size != "" {
fmt.Sscanf(size, "%d", &score)
}
candidates = append(candidates, iconCandidate{base.ResolveReference(ref).String(), score})
}
sort.SliceStable(candidates, func(i, j int) bool { return candidates[i].score > candidates[j].score })
return candidates
}
// iconCandidate adalah kandidat URL ikon dengan skor prioritas.
type iconCandidate struct {
url string
@@ -90,6 +152,7 @@ func iconCandidates(base *url.URL) []string {
}
sort.SliceStable(found, func(i, j int) bool { return found[i].score > found[j].score })
found = append(manifestIcons(base), found...)
out := make([]string, 0, len(found)+1)
seen := map[string]bool{}