- 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
278 lines
7.7 KiB
Go
278 lines
7.7 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/dialog"
|
|
"fyne.io/fyne/v2/storage"
|
|
"fyne.io/fyne/v2/theme"
|
|
"fyne.io/fyne/v2/widget"
|
|
|
|
"lazyrun/internal/browser"
|
|
"lazyrun/internal/db"
|
|
"lazyrun/internal/icon"
|
|
"lazyrun/internal/shortcut"
|
|
"lazyrun/internal/store"
|
|
)
|
|
|
|
var lazyFilter = storage.NewExtensionFileFilter([]string{".lazy"})
|
|
|
|
// exportShortcuts menyimpan daftar shortcut ke satu berkas SQLite (.lazy),
|
|
// mis. "db.lazy", supaya bisa dipindah ke komputer lain.
|
|
func (ui *app) exportShortcuts() {
|
|
save := dialog.NewFileSave(func(w fyne.URIWriteCloser, err error) {
|
|
if err != nil {
|
|
dialog.ShowError(err, ui.win)
|
|
return
|
|
}
|
|
if w == nil {
|
|
return
|
|
}
|
|
path := w.URI().Path()
|
|
w.Close() // berkas kosong dari dialog akan diganti basis data
|
|
if !strings.HasSuffix(strings.ToLower(path), ".lazy") {
|
|
path += ".lazy"
|
|
}
|
|
|
|
if err := db.Export(path, ui.version, ui.store.Items); err != nil {
|
|
dialog.ShowError(err, ui.win)
|
|
return
|
|
}
|
|
dialog.ShowInformation(T("export"), fmt.Sprintf(T("export_done"), len(ui.store.Items), path), ui.win)
|
|
}, ui.win)
|
|
save.SetFileName(db.DefaultName)
|
|
save.SetFilter(lazyFilter)
|
|
save.Show()
|
|
}
|
|
|
|
// importShortcuts menambahkan shortcut dari berkas .lazy tanpa menimpa yang sudah ada.
|
|
func (ui *app) importShortcuts() {
|
|
open := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) {
|
|
if err != nil {
|
|
dialog.ShowError(err, ui.win)
|
|
return
|
|
}
|
|
if r == nil {
|
|
return
|
|
}
|
|
path := r.URI().Path()
|
|
r.Close()
|
|
|
|
items, err := db.Import(path)
|
|
if err != nil {
|
|
dialog.ShowError(err, ui.win)
|
|
return
|
|
}
|
|
if len(items) == 0 {
|
|
dialog.ShowInformation(T("import"), T("import_empty"), ui.win)
|
|
return
|
|
}
|
|
|
|
added, skipped := mergeImported(ui.store, items)
|
|
if err := ui.store.Save(); err != nil {
|
|
dialog.ShowError(err, ui.win)
|
|
return
|
|
}
|
|
ui.reload()
|
|
dialog.ShowInformation(T("import"), fmt.Sprintf(T("import_done"), added, skipped), ui.win)
|
|
}, ui.win)
|
|
open.SetFilter(lazyFilter)
|
|
open.Show()
|
|
}
|
|
|
|
// showEditDialog mengubah shortcut yang sudah ada: binary/URL, ikon, nama, lokasi.
|
|
func (ui *app) showEditDialog(sc store.Shortcut) {
|
|
isWeb := sc.Type == store.TypeWebApp
|
|
|
|
var iconPath, iconURL, binPath string
|
|
var iconData []byte
|
|
|
|
nameEntry := widget.NewEntry()
|
|
nameEntry.SetText(sc.Name)
|
|
iconLabel := muted(iconLabelOf(sc))
|
|
menu := widget.NewCheck(T("check_menu"), nil)
|
|
menu.SetChecked(sc.Targets.Menu)
|
|
desktopChk := widget.NewCheck(T("check_desktop"), nil)
|
|
desktopChk.SetChecked(sc.Targets.Desktop)
|
|
|
|
browseIcon := newButton(T("browse"), theme.FolderOpenIcon(), T("tip_browse"), widget.MediumImportance, func() {
|
|
pickFile(ui.win, imageFilter, func(path string) {
|
|
iconPath, iconURL, iconData = path, "", nil
|
|
iconLabel.SetText(filepath.Base(path))
|
|
})
|
|
})
|
|
onlineIcon := newButton(T("online"), theme.SearchIcon(), T("tip_online"), widget.MediumImportance, func() {
|
|
ui.showIconSearch(nameEntry.Text, func(result icon.Result, data []byte) {
|
|
iconURL, iconPath, iconData = result.URL, "", data
|
|
iconLabel.SetText(result.Name)
|
|
})
|
|
})
|
|
|
|
// widget khusus executable
|
|
binLabel := muted(sc.BinaryPath)
|
|
browseBin := newButton(T("browse"), theme.FolderOpenIcon(), T("tip_browse"), widget.MediumImportance, func() {
|
|
pickFile(ui.win, nil, func(path string) {
|
|
binPath = path
|
|
binLabel.SetText(filepath.Base(path) + " · " + detectedKind(path))
|
|
})
|
|
})
|
|
category := widget.NewSelect(categories, nil)
|
|
category.SetSelected(categoryOf(sc))
|
|
|
|
// widget khusus web app
|
|
urlEntry := widget.NewEntry()
|
|
urlEntry.SetText(sc.URL)
|
|
browserSel := widget.NewSelect(browserNames(ui.browsers), nil)
|
|
browserSel.SetSelected(currentBrowser(ui.browsers, sc.Browser))
|
|
|
|
iconItem := widget.NewFormItem(T("field_icon"), container.NewBorder(nil, nil, nil,
|
|
container.NewHBox(onlineIcon, browseIcon), iconLabel))
|
|
|
|
items := []*widget.FormItem{}
|
|
if isWeb {
|
|
items = append(items,
|
|
widget.NewFormItem(T("field_url"), urlEntry),
|
|
widget.NewFormItem(T("field_name"), nameEntry),
|
|
iconItem,
|
|
widget.NewFormItem(T("field_browser"), browserSel),
|
|
)
|
|
} else {
|
|
items = append(items,
|
|
widget.NewFormItem(T("field_binary"), container.NewBorder(nil, nil, nil, browseBin, binLabel)),
|
|
widget.NewFormItem(T("field_name"), nameEntry),
|
|
iconItem,
|
|
widget.NewFormItem(T("field_category"), category),
|
|
)
|
|
}
|
|
|
|
var d *dialog.CustomDialog
|
|
save := newButton(T("save"), theme.DocumentSaveIcon(), T("tip_save"), widget.HighImportance, func() {
|
|
switch {
|
|
case strings.TrimSpace(nameEntry.Text) == "":
|
|
dialog.ShowInformation(T("err_title"), T("need_name"), ui.win)
|
|
return
|
|
case !menu.Checked && !desktopChk.Checked:
|
|
dialog.ShowInformation(T("err_title"), T("need_target"), ui.win)
|
|
return
|
|
}
|
|
|
|
ui.install(func() (store.Shortcut, error) {
|
|
if isWeb {
|
|
b := ui.browsers[indexOf(ui.browsers, browserSel.Selected)]
|
|
return shortcut.EditWebApp(sc, shortcut.WebOptions{
|
|
URL: urlEntry.Text,
|
|
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: desktopChk.Checked,
|
|
})
|
|
}
|
|
path := binPath
|
|
if path == "" {
|
|
path = sc.BinaryPath
|
|
}
|
|
return shortcut.EditExecutable(sc, shortcut.ExecOptions{
|
|
BinaryPath: path,
|
|
IconPath: iconPath,
|
|
IconURL: iconURL,
|
|
IconData: iconData,
|
|
Name: nameEntry.Text,
|
|
Category: category.Selected,
|
|
Menu: menu.Checked,
|
|
Desktop: desktopChk.Checked,
|
|
})
|
|
}, func(updated store.Shortcut, err error) {
|
|
if err != nil {
|
|
dialog.ShowError(err, ui.win)
|
|
return
|
|
}
|
|
ui.store.Add(updated)
|
|
if err := ui.store.Save(); err != nil {
|
|
dialog.ShowError(err, ui.win)
|
|
return
|
|
}
|
|
d.Hide()
|
|
ui.reload()
|
|
if card, ok := ui.cards[updated.Slug]; ok {
|
|
card.flash(0)
|
|
}
|
|
dialog.ShowInformation(T("edit_title"), fmt.Sprintf(T("edit_saved"), updated.Name), ui.win)
|
|
})
|
|
})
|
|
|
|
hint := textLine(T("edit_hint"), 12, false, colorNameMuted)
|
|
content := container.NewVScroll(container.NewVBox(
|
|
closeRow(func() { d.Hide() }),
|
|
widget.NewForm(items...),
|
|
widget.NewSeparator(),
|
|
sectionTitle(T("section_location")),
|
|
menu,
|
|
desktopChk,
|
|
hint,
|
|
container.NewHBox(logoBox(newLogo(), 20), spacer(),
|
|
newButton(T("cancel"), nil, T("tip_cancel"), widget.MediumImportance, func() { d.Hide() }),
|
|
save),
|
|
))
|
|
|
|
d = dialog.NewCustomWithoutButtons(T("edit_title"), content, ui.win)
|
|
d.SetOnClosed(pushHintTarget(hint))
|
|
showDialog(d, ui.win, 560, 520)
|
|
}
|
|
|
|
// mergeImported menambahkan shortcut hasil impor tanpa menimpa yang sudah ada.
|
|
func mergeImported(target *store.Store, items []store.Shortcut) (added, skipped int) {
|
|
for _, item := range items {
|
|
item.Name = strings.TrimSpace(item.Name)
|
|
if item.Name == "" || item.Slug == "" {
|
|
skipped++
|
|
continue
|
|
}
|
|
if _, exists := target.Get(item.Slug); exists {
|
|
skipped++
|
|
continue
|
|
}
|
|
target.Add(item)
|
|
added++
|
|
}
|
|
return added, skipped
|
|
}
|
|
|
|
// iconLabelOf menampilkan ikon yang sedang dipakai shortcut.
|
|
func iconLabelOf(sc store.Shortcut) string {
|
|
if sc.IconPath == "" {
|
|
return T("icon_auto")
|
|
}
|
|
return filepath.Base(sc.IconPath)
|
|
}
|
|
|
|
// currentBrowser mengembalikan nama browser yang cocok dengan yang tersimpan.
|
|
func currentBrowser(list []browser.Browser, saved string) string {
|
|
for _, b := range list {
|
|
if strings.Join(b.Exec, " ") == saved {
|
|
return b.Name
|
|
}
|
|
}
|
|
if len(list) > 0 {
|
|
return list[0].Name
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// categoryOf mengambil kategori pertama yang tersimpan.
|
|
func categoryOf(sc store.Shortcut) string {
|
|
first, _, _ := strings.Cut(strings.TrimSuffix(sc.Categories, ";"), ";")
|
|
for _, c := range categories {
|
|
if c == first {
|
|
return c
|
|
}
|
|
}
|
|
return "Utility"
|
|
}
|