LazyRun v1.0.0: shortcut maker untuk executable dan PWA
- Fungsi A: shortcut dari file executable (ELF/AppImage/script) - Fungsi B: shortcut PWA/URL dengan deteksi browser (native, /opt, Flatpak) - Checkbox Menu Aplikasi / Desktop, desktop-only pakai NoDisplay=true - List + uninstall + deteksi shortcut yang file .desktop-nya hilang - Metadata ~/.config/lazyrun/shortcuts.json, refresh cache XDG/KDE - AppImage + zsync update info via build.sh
This commit is contained in:
1 parent
9cd5a1f82d
commit
da31f7d7d7
31 files changed
+2916
No files matched your search
@@ -0,0 +1,180 @@
|
||||
// Package ui berisi window utama dan dialog LazyRun.
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
|
||||
"lazyrun/assets"
|
||||
"lazyrun/internal/browser"
|
||||
"lazyrun/internal/shortcut"
|
||||
"lazyrun/internal/store"
|
||||
"lazyrun/internal/xdg"
|
||||
)
|
||||
|
||||
type app struct {
|
||||
win fyne.Window
|
||||
store *store.Store
|
||||
browsers []browser.Browser
|
||||
rows *fyne.Container
|
||||
version string
|
||||
}
|
||||
|
||||
// Run menampilkan window utama LazyRun sampai user menutupnya.
|
||||
func Run(a fyne.App, version string) {
|
||||
ui, loadErr := build(a, version)
|
||||
ui.win.Show()
|
||||
if loadErr != nil {
|
||||
dialog.ShowError(loadErr, ui.win)
|
||||
}
|
||||
a.Run()
|
||||
}
|
||||
|
||||
// build menyiapkan window utama tanpa menampilkannya (dipakai Run dan tes headless).
|
||||
func build(a fyne.App, version string) (*app, error) {
|
||||
ui := &app{version: version}
|
||||
a.SetIcon(fyne.NewStaticResource("lazyrun.png", assets.Icon))
|
||||
|
||||
ui.browsers = browser.Detect()
|
||||
ui.rows = container.NewVBox()
|
||||
ui.win = a.NewWindow(T("app_title"))
|
||||
ui.win.Resize(fyne.NewSize(760, 540))
|
||||
|
||||
top := container.NewHBox(
|
||||
widget.NewButton(T("btn_executable"), ui.showExecutableDialog),
|
||||
widget.NewButton(T("btn_webapp"), ui.showWebAppDialog),
|
||||
)
|
||||
bottom := container.NewBorder(nil, nil,
|
||||
widget.NewButton(T("check_updates"), ui.checkUpdates),
|
||||
widget.NewLabel("v"+version))
|
||||
body := container.NewBorder(heading(T("installed")), nil, nil, nil, container.NewVScroll(ui.rows))
|
||||
ui.win.SetContent(container.NewBorder(top, bottom, nil, nil, body))
|
||||
|
||||
st, err := store.Load(xdg.MetadataPath())
|
||||
ui.store = st
|
||||
ui.reload()
|
||||
return ui, err
|
||||
}
|
||||
|
||||
func (ui *app) reload() {
|
||||
if st, err := store.Load(ui.store.Path); err == nil {
|
||||
ui.store = st
|
||||
}
|
||||
ui.rows.RemoveAll()
|
||||
if len(ui.store.Items) == 0 {
|
||||
ui.rows.Add(widget.NewLabel(T("empty")))
|
||||
}
|
||||
for _, sc := range ui.store.Items {
|
||||
ui.rows.Add(ui.row(sc))
|
||||
}
|
||||
}
|
||||
|
||||
func (ui *app) row(sc store.Shortcut) fyne.CanvasObject {
|
||||
title := widget.NewLabelWithStyle(sc.Name, fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
||||
info := widget.NewLabel(describe(sc))
|
||||
|
||||
open := widget.NewButton(T("folder"), func() {
|
||||
if err := exec.Command("xdg-open", filepath.Dir(sc.DesktopPath)).Start(); err != nil {
|
||||
dialog.ShowError(err, ui.win)
|
||||
}
|
||||
})
|
||||
remove := widget.NewButton(T("delete"), func() { ui.confirmDelete(sc) })
|
||||
|
||||
return container.NewBorder(nil, widget.NewSeparator(), nil,
|
||||
container.NewHBox(open, remove),
|
||||
container.NewVBox(title, info))
|
||||
}
|
||||
|
||||
func describe(sc store.Shortcut) string {
|
||||
targets := make([]string, 0, 2)
|
||||
if sc.Targets.Menu {
|
||||
targets = append(targets, T("check_menu"))
|
||||
}
|
||||
if sc.Targets.Desktop {
|
||||
targets = append(targets, T("check_desktop"))
|
||||
}
|
||||
loc := strings.Join(targets, " + ")
|
||||
|
||||
kind := "Executable"
|
||||
if sc.Type == store.TypeWebApp {
|
||||
kind = "Web"
|
||||
}
|
||||
line := kind + " · " + loc
|
||||
if sc.Broken() {
|
||||
line += " · " + T("broken")
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
func (ui *app) confirmDelete(sc store.Shortcut) {
|
||||
content := container.NewVBox(widget.NewLabel(fmt.Sprintf(T("confirm_delete_msg"), sc.Name)))
|
||||
|
||||
var removeBinary *widget.Check
|
||||
if sc.Type == store.TypeExecutable && sc.BinaryPath != "" {
|
||||
removeBinary = widget.NewCheck(T("also_binary"), nil)
|
||||
content.Add(removeBinary)
|
||||
}
|
||||
|
||||
d := dialog.NewCustomConfirm(T("confirm_delete_title"), T("delete"), T("cancel"), content, func(ok bool) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
opts := shortcut.RemoveOptions{}
|
||||
if removeBinary != nil {
|
||||
opts.Binary = removeBinary.Checked
|
||||
}
|
||||
if err := shortcut.Uninstall(sc, opts); err != nil {
|
||||
dialog.ShowError(err, ui.win)
|
||||
}
|
||||
ui.store.Remove(sc.Slug)
|
||||
if err := ui.store.Save(); err != nil {
|
||||
dialog.ShowError(err, ui.win)
|
||||
}
|
||||
ui.reload()
|
||||
}, ui.win)
|
||||
d.Show()
|
||||
}
|
||||
|
||||
// install menjalankan pekerjaan berat (salin file besar / fetch favicon) di luar UI thread.
|
||||
func (ui *app) install(work func() (store.Shortcut, error), done func(store.Shortcut, error)) {
|
||||
progress := dialog.NewProgressInfinite(T("install"), T("working"), ui.win)
|
||||
progress.Show()
|
||||
go func() {
|
||||
sc, err := work()
|
||||
fyne.Do(func() {
|
||||
progress.Hide()
|
||||
done(sc, err)
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
// added menyimpan metadata shortcut baru; dipanggil dari callback install.
|
||||
func (ui *app) added(sc store.Shortcut, err error, closeDialog func()) {
|
||||
if err != nil {
|
||||
dialog.ShowError(err, ui.win)
|
||||
return
|
||||
}
|
||||
ui.store.Add(sc)
|
||||
if err := ui.store.Save(); err != nil {
|
||||
ui.store.Remove(sc.Slug)
|
||||
dialog.ShowError(err, ui.win)
|
||||
return
|
||||
}
|
||||
closeDialog()
|
||||
ui.reload()
|
||||
dialog.ShowInformation(T("success_title"), fmt.Sprintf(T("success_msg"), sc.Name), ui.win)
|
||||
}
|
||||
|
||||
func heading(text string) *widget.Label {
|
||||
return widget.NewLabelWithStyle(text, fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
||||
}
|
||||
|
||||
func spacer() fyne.CanvasObject { return layout.NewSpacer() }
|
||||
Reference in new issue
Block a user