Backup/restore: - berkas .lazy (format versi 2) kini menyimpan ISI berkas: binary/script dan ikon (maks 512 MB per berkas); format versi 1 tetap bisa diimpor - impor memakai shortcut.Restore: binary ditulis ke ~/.local/bin lokal, ikon dari cadangan (atau diunduh ulang dari icon_source), .desktop ditulis dengan path lokal, salinan Desktop dibuat sesuai target - path dari komputer lama tidak dipakai mentah -> tidak ada lagi 'banyak file missing'; entri yang tidak bisa dipulihkan ditandai dan bisa dibetulkan via edit - tes end-to-end: pasang 3 shortcut -> ekspor -> pulihkan di HOME lain -> semua berkas lokal, script hasil restore benar-benar jalan Script: - script dipasang sebagai wrapper + <slug>.real: wrapper menambahkan ~/.local/bin ke PATH dan cd ke folder asli script (desktop environment menjalankan dengan cwd = home, PATH sesi) - .desktop dapat Path= sesuai folder asli script - opsi 'Run in a terminal window (scripts)' di dialog install & edit - uninstall membuang wrapper + script aslinya
300 lines
8.2 KiB
Go
300 lines
8.2 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 := ui.restoreImported(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)
|
|
terminal := widget.NewCheck(T("check_terminal"), nil)
|
|
terminal.SetChecked(sc.Terminal)
|
|
if sc.Kind != shortcut.KindScript {
|
|
terminal.Hide()
|
|
}
|
|
|
|
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))
|
|
if shortcut.DetectKind(path) == shortcut.KindScript {
|
|
terminal.Show()
|
|
} else {
|
|
terminal.SetChecked(false)
|
|
terminal.Hide()
|
|
}
|
|
})
|
|
})
|
|
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,
|
|
Terminal: terminal.Checked,
|
|
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,
|
|
terminal,
|
|
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)
|
|
}
|
|
|
|
// restoreImported memasang ulang setiap entri cadangan ke mesin ini
|
|
// (binary, ikon, .desktop) lalu menambahkannya ke daftar, tanpa menimpa
|
|
// shortcut yang sudah ada.
|
|
func (ui *app) restoreImported(items []db.Backup) (added, skipped int) {
|
|
for _, backup := range items {
|
|
item := backup.Item
|
|
item.Name = strings.TrimSpace(item.Name)
|
|
if item.Name == "" {
|
|
skipped++
|
|
continue
|
|
}
|
|
if _, exists := ui.store.Get(shortcut.Slug(item.Name)); exists {
|
|
skipped++
|
|
continue
|
|
}
|
|
|
|
restored, err := shortcut.Restore(item, backup.Binary, backup.Icon)
|
|
if err != nil {
|
|
skipped++
|
|
continue
|
|
}
|
|
ui.store.Add(restored)
|
|
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"
|
|
}
|