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:
1 parent
70534bf6e2
commit
7f7cb97a65
19 files changed
+1117
-40
No files matched your search
@@ -11,6 +11,7 @@ import (
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
|
||||
"lazyrun/internal/icon"
|
||||
"lazyrun/internal/shortcut"
|
||||
"lazyrun/internal/store"
|
||||
)
|
||||
@@ -26,7 +27,8 @@ var (
|
||||
)
|
||||
|
||||
func (ui *app) showExecutableDialog() {
|
||||
var binPath, iconPath string
|
||||
var binPath, iconPath, iconURL string
|
||||
var iconData []byte
|
||||
|
||||
binLabel := muted(T("none"))
|
||||
iconLabel := muted(T("icon_auto"))
|
||||
@@ -50,10 +52,16 @@ func (ui *app) showExecutableDialog() {
|
||||
})
|
||||
browseIcon := widget.NewButtonWithIcon(T("browse"), theme.FolderOpenIcon(), func() {
|
||||
pickFile(ui.win, imageFilter, func(path string) {
|
||||
iconPath = path
|
||||
iconPath, iconURL, iconData = path, "", nil
|
||||
iconLabel.SetText(filepath.Base(path))
|
||||
})
|
||||
})
|
||||
onlineIcon := widget.NewButtonWithIcon(T("online"), theme.SearchIcon(), func() {
|
||||
ui.showIconSearch(nameEntry.Text, func(result icon.Result, data []byte) {
|
||||
iconURL, iconPath, iconData = result.URL, "", data
|
||||
iconLabel.SetText(result.Name)
|
||||
})
|
||||
})
|
||||
|
||||
var d *dialog.CustomDialog
|
||||
install := widget.NewButtonWithIcon(T("install"), theme.DownloadIcon(), func() {
|
||||
@@ -71,6 +79,8 @@ func (ui *app) showExecutableDialog() {
|
||||
opts := shortcut.ExecOptions{
|
||||
BinaryPath: binPath,
|
||||
IconPath: iconPath,
|
||||
IconURL: iconURL,
|
||||
IconData: iconData,
|
||||
Name: nameEntry.Text,
|
||||
Category: category.Selected,
|
||||
Menu: menu.Checked,
|
||||
@@ -85,7 +95,8 @@ func (ui *app) showExecutableDialog() {
|
||||
|
||||
form := widget.NewForm(
|
||||
widget.NewFormItem(T("field_binary"), container.NewBorder(nil, nil, nil, browseBin, binLabel)),
|
||||
widget.NewFormItem(T("field_icon"), container.NewBorder(nil, nil, nil, browseIcon, iconLabel)),
|
||||
widget.NewFormItem(T("field_icon"), container.NewBorder(nil, nil, nil,
|
||||
container.NewHBox(onlineIcon, browseIcon), iconLabel)),
|
||||
widget.NewFormItem(T("field_name"), nameEntry),
|
||||
widget.NewFormItem(T("field_category"), category),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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"))
|
||||
slots := make(chan struct{}, 4)
|
||||
grid := container.NewGridWithColumns(6)
|
||||
results := container.NewVScroll(container.NewPadded(grid))
|
||||
|
||||
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() }
|
||||
content := container.NewBorder(
|
||||
entry,
|
||||
container.NewBorder(nil, nil, status,
|
||||
container.NewHBox(
|
||||
widget.NewButton(T("cancel"), func() { d.Hide() }),
|
||||
widget.NewButtonWithIcon(T("search"), theme.SearchIcon(), search),
|
||||
)),
|
||||
nil, nil,
|
||||
results,
|
||||
)
|
||||
|
||||
d = dialog.NewCustom(T("icon_search_title"), "", content, ui.win)
|
||||
d.Resize(fyne.NewSize(560, 460))
|
||||
d.Show()
|
||||
search()
|
||||
}
|
||||
|
||||
// iconChoice adalah satu ikon hasil pencarian; gambarnya diunduh di latar belakang.
|
||||
func (ui *app) iconChoice(result icon.Result, onPick func(icon.Result, []byte), d *dialog.CustomDialog, slot chan struct{}) fyne.CanvasObject {
|
||||
var data []byte
|
||||
|
||||
button := widget.NewButton("", func() {
|
||||
onPick(result, data)
|
||||
if d != nil {
|
||||
d.Hide()
|
||||
}
|
||||
})
|
||||
button.Importance = widget.LowImportance
|
||||
|
||||
go func() {
|
||||
slot <- struct{}{} // batasi unduhan bersamaan supaya tidak kena rate limit
|
||||
defer func() { <-slot }()
|
||||
|
||||
downloaded, _, err := icon.FromURL(result.URL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
data = downloaded
|
||||
fyne.Do(func() {
|
||||
button.Icon = fyne.NewStaticResource(result.Name, downloaded)
|
||||
button.Refresh()
|
||||
})
|
||||
}()
|
||||
|
||||
return container.NewGridWrap(fyne.NewSize(52, 52), button)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"fyne.io/fyne/v2/widget"
|
||||
|
||||
"lazyrun/internal/browser"
|
||||
"lazyrun/internal/icon"
|
||||
"lazyrun/internal/shortcut"
|
||||
"lazyrun/internal/store"
|
||||
)
|
||||
@@ -22,7 +23,8 @@ func (ui *app) showWebAppDialog() {
|
||||
return
|
||||
}
|
||||
|
||||
var iconPath string
|
||||
var iconPath, iconURL string
|
||||
var iconData []byte
|
||||
var nameTouched, filling bool
|
||||
|
||||
urlEntry := widget.NewEntry()
|
||||
@@ -55,10 +57,16 @@ func (ui *app) showWebAppDialog() {
|
||||
|
||||
browseIcon := widget.NewButtonWithIcon(T("browse"), theme.FolderOpenIcon(), func() {
|
||||
pickFile(ui.win, imageFilter, func(path string) {
|
||||
iconPath = path
|
||||
iconPath, iconURL, iconData = path, "", nil
|
||||
iconLabel.SetText(filepath.Base(path))
|
||||
})
|
||||
})
|
||||
onlineIcon := widget.NewButtonWithIcon(T("online"), theme.SearchIcon(), func() {
|
||||
ui.showIconSearch(nameEntry.Text, func(result icon.Result, data []byte) {
|
||||
iconURL, iconPath, iconData = result.URL, "", data
|
||||
iconLabel.SetText(result.Name)
|
||||
})
|
||||
})
|
||||
|
||||
var d *dialog.CustomDialog
|
||||
install := widget.NewButtonWithIcon(T("install"), theme.DownloadIcon(), func() {
|
||||
@@ -79,6 +87,8 @@ func (ui *app) showWebAppDialog() {
|
||||
Name: nameEntry.Text,
|
||||
Browser: shortcut.BrowserArg{Name: b.Name, Exec: b.Exec, Firefox: b.Kind == browser.Firefox},
|
||||
IconPath: iconPath,
|
||||
IconURL: iconURL,
|
||||
IconData: iconData,
|
||||
Menu: menu.Checked,
|
||||
Desktop: desktop.Checked,
|
||||
}
|
||||
@@ -93,7 +103,8 @@ func (ui *app) showWebAppDialog() {
|
||||
widget.NewFormItem(T("field_url"), urlEntry),
|
||||
widget.NewFormItem(T("field_name"), nameEntry),
|
||||
widget.NewFormItem(T("field_browser"), picker),
|
||||
widget.NewFormItem(T("field_icon"), container.NewBorder(nil, nil, nil, browseIcon, iconLabel)),
|
||||
widget.NewFormItem(T("field_icon"), container.NewBorder(nil, nil, nil,
|
||||
container.NewHBox(onlineIcon, browseIcon), iconLabel)),
|
||||
)
|
||||
content := container.NewVBox(
|
||||
form,
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
@@ -138,6 +139,9 @@ func (ui *app) row(sc store.Shortcut) *hoverCard {
|
||||
|
||||
actions := container.NewHBox(chips...)
|
||||
actions.Add(iconButton(theme.MediaPlayIcon(), func() { ui.launch(sc) }))
|
||||
if syncable(sc) {
|
||||
actions.Add(iconButton(theme.ViewRefreshIcon(), func() { ui.syncIcon(sc) }))
|
||||
}
|
||||
actions.Add(iconButton(theme.FolderOpenIcon(), func() {
|
||||
if err := exec.Command("xdg-open", filepath.Dir(sc.DesktopPath)).Start(); err != nil {
|
||||
dialog.ShowError(err, ui.win)
|
||||
@@ -192,6 +196,40 @@ func (ui *app) flashCards() {
|
||||
}
|
||||
}
|
||||
|
||||
// syncable berarti ikon shortcut punya sumber online yang bisa diunduh ulang.
|
||||
func syncable(sc store.Shortcut) bool {
|
||||
source := sc.IconSource
|
||||
return strings.HasPrefix(source, "http") ||
|
||||
strings.HasPrefix(source, "favicon:") ||
|
||||
strings.HasPrefix(source, "search:")
|
||||
}
|
||||
|
||||
// syncIcon mengunduh ulang ikon shortcut dari sumber online-nya.
|
||||
func (ui *app) syncIcon(sc store.Shortcut) {
|
||||
progress := dialog.NewProgressInfinite(T("sync_icon"), T("working"), ui.win)
|
||||
progress.Show()
|
||||
go func() {
|
||||
path, err := shortcut.RefreshIcon(sc)
|
||||
fyne.Do(func() {
|
||||
progress.Hide()
|
||||
if err != nil {
|
||||
dialog.ShowError(err, ui.win)
|
||||
return
|
||||
}
|
||||
if i, ok := ui.store.Get(sc.Slug); ok {
|
||||
ui.store.Items[i].IconPath = path
|
||||
if err := ui.store.Save(); err != nil {
|
||||
dialog.ShowError(err, ui.win)
|
||||
}
|
||||
}
|
||||
ui.reload()
|
||||
if card, ok := ui.cards[sc.Slug]; ok {
|
||||
card.flash(0)
|
||||
}
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
func (ui *app) confirmDelete(sc store.Shortcut) {
|
||||
content := container.NewVBox(widget.NewLabel(fmt.Sprintf(T("confirm_delete_msg"), sc.Name)))
|
||||
|
||||
|
||||
+27
-19
@@ -7,24 +7,32 @@ import (
|
||||
|
||||
// tr menyimpan teks UI: index 0 = Inggris, 1 = Indonesia.
|
||||
var tr = map[string][2]string{
|
||||
"app_title": {"LazyRun — lazy shortcut build", "LazyRun — lazy shortcut build"},
|
||||
"tagline": {"lazy shortcut build", "lazy shortcut build"},
|
||||
"btn_executable": {"Executable", "Executable"},
|
||||
"btn_webapp": {"Web App", "Aplikasi Web"},
|
||||
"installed": {"Installed shortcuts", "Shortcut terinstall"},
|
||||
"empty_title": {"Nothing here yet", "Belum ada apa-apa"},
|
||||
"empty_hint": {"Create your first shortcut with the buttons in the corner.", "Buat shortcut pertamamu lewat tombol di pojok kanan atas."},
|
||||
"chip_menu": {"Menu", "Menu"},
|
||||
"chip_desktop": {"Desktop", "Desktop"},
|
||||
"chip_broken": {"file missing", "file hilang"},
|
||||
"none": {"not selected", "belum dipilih"},
|
||||
"icon_auto": {"automatic", "otomatis"},
|
||||
"browse": {"Browse...", "Pilih..."},
|
||||
"install": {"Install", "Install"},
|
||||
"cancel": {"Cancel", "Batal"},
|
||||
"delete": {"Delete", "Hapus"},
|
||||
"folder": {"Folder", "Folder"},
|
||||
"working": {"Working...", "Sedang diproses..."},
|
||||
"app_title": {"LazyRun — lazy shortcut build", "LazyRun — lazy shortcut build"},
|
||||
"tagline": {"lazy shortcut build", "lazy shortcut build"},
|
||||
"btn_executable": {"Executable", "Executable"},
|
||||
"btn_webapp": {"Web App", "Aplikasi Web"},
|
||||
"installed": {"Installed shortcuts", "Shortcut terinstall"},
|
||||
"empty_title": {"Nothing here yet", "Belum ada apa-apa"},
|
||||
"empty_hint": {"Create your first shortcut with the buttons in the corner.", "Buat shortcut pertamamu lewat tombol di pojok kanan atas."},
|
||||
"chip_menu": {"Menu", "Menu"},
|
||||
"chip_desktop": {"Desktop", "Desktop"},
|
||||
"chip_broken": {"file missing", "file hilang"},
|
||||
"none": {"not selected", "belum dipilih"},
|
||||
"icon_auto": {"automatic", "otomatis"},
|
||||
"online": {"Online", "Online"},
|
||||
"search": {"Search", "Cari"},
|
||||
"searching": {"Searching...", "Mencari..."},
|
||||
"search_failed": {"Search failed. Check your connection.", "Pencarian gagal. Cek koneksi kamu."},
|
||||
"search_found": {"%d icons found - click one to use it.", "%d ikon ditemukan - klik satu untuk dipakai."},
|
||||
"search_hint": {"Type a name and press Search.", "Tulis nama lalu tekan Cari."},
|
||||
"icon_search_title": {"Icon from the internet", "Ikon dari internet"},
|
||||
"sync_icon": {"Sync icon", "Sinkron ikon"},
|
||||
"browse": {"Browse...", "Pilih..."},
|
||||
"install": {"Install", "Install"},
|
||||
"cancel": {"Cancel", "Batal"},
|
||||
"delete": {"Delete", "Hapus"},
|
||||
"folder": {"Folder", "Folder"},
|
||||
"working": {"Working...", "Sedang diproses..."},
|
||||
|
||||
"new_exec_title": {"New Executable Shortcut", "Shortcut Executable Baru"},
|
||||
"new_web_title": {"New Web App Shortcut", "Shortcut Web App Baru"},
|
||||
@@ -39,7 +47,7 @@ var tr = map[string][2]string{
|
||||
"check_menu": {"Application menu", "Menu aplikasi"},
|
||||
"check_desktop": {"Desktop", "Desktop"},
|
||||
"exec_hint": {"The binary is copied to ~/.local/bin and can be uninstalled from the list.", "Binary disalin ke ~/.local/bin dan bisa dihapus lagi dari daftar."},
|
||||
"web_hint": {"The icon is fetched from the site's favicon if you leave it empty.", "Ikon diambil otomatis dari favicon situs kalau dibiarkan kosong."},
|
||||
"web_hint": {"The icon comes from the site's favicon, or is looked up online if there is none.", "Ikon diambil dari favicon situs, atau dicari otomatis di internet kalau tidak ada."},
|
||||
|
||||
"confirm_delete_title": {"Uninstall shortcut", "Hapus shortcut"},
|
||||
"confirm_delete_msg": {"Remove %s?", "Hapus %s?"},
|
||||
|
||||
Reference in new issue
Block a user