Tooltip hint, loading bar, edit shortcut, export/import .lazy
- 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
This commit is contained in:
1 parent
c1aed1e286
commit
deaa30689c
19 files changed
+1362
-61
No files matched your search
@@ -0,0 +1,277 @@
|
||||
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"
|
||||
}
|
||||
Reference in new issue
Block a user