diff --git a/README.md b/README.md index af29985..e528769 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,9 @@ Desktop app (Go + Fyne) that creates application shortcuts on Linux in a few cli from an executable file, or from a web URL as a PWA. Ships as a single-file AppImage, no terminal needed. +- **Repo:** +- **Donate:** + - **Executable shortcut** — picks an ELF/AppImage/script, copies it to `~/.local/bin/`, installs the icon, writes the `.desktop` entry. - **Web app shortcut** — opens the URL as an app: `--app=` for @@ -23,7 +26,7 @@ AppImage, no terminal needed. ## Install -Download the AppImage from [Releases](../../releases), then: +Download the AppImage from [Releases](https://github.com/oxpoe/lazyrun/releases), then: ```bash chmod +x lazyrun-*-x86_64.AppImage diff --git a/assets/assets.go b/assets/assets.go index b49efeb..f2b05fe 100644 --- a/assets/assets.go +++ b/assets/assets.go @@ -3,5 +3,12 @@ package assets import _ "embed" +// Icon adalah ikon aplikasi LazyRun. +// //go:embed icon.png var Icon []byte + +// Heart adalah ikon kecil untuk tombol donasi. +// +//go:embed heart.png +var Heart []byte diff --git a/assets/heart.png b/assets/heart.png new file mode 100644 index 0000000..b85e8ac Binary files /dev/null and b/assets/heart.png differ diff --git a/internal/shortcut/executable.go b/internal/shortcut/executable.go index a4c55fd..f5ff260 100644 --- a/internal/shortcut/executable.go +++ b/internal/shortcut/executable.go @@ -71,6 +71,7 @@ func InstallExecutable(o ExecOptions) (store.Shortcut, error) { Slug: sl, Name: name, Type: store.TypeExecutable, + ExecArgs: entry.ExecArgs, BinaryPath: binDst, IconPath: iconPath, DesktopPath: deskDst, @@ -156,6 +157,7 @@ func InstallWebApp(o WebOptions) (store.Shortcut, error) { Type: store.TypeWebApp, URL: pageURL, Browser: strings.Join(o.Browser.Exec, " "), + ExecArgs: entry.ExecArgs, IconPath: iconPath, DesktopPath: deskDst, Targets: store.Targets{Menu: o.Menu, Desktop: o.Desktop}, diff --git a/internal/shortcut/install_test.go b/internal/shortcut/install_test.go index 522a8a3..31e87e1 100644 --- a/internal/shortcut/install_test.go +++ b/internal/shortcut/install_test.go @@ -86,6 +86,19 @@ func TestInstallExecutable(t *testing.T) { } } +func TestInstallExecutableStoresExecArgs(t *testing.T) { + home := testEnv(t) + src := fakeBinary(t, home, "args") + + sc, err := InstallExecutable(ExecOptions{BinaryPath: src, Name: "Args", Menu: true}) + if err != nil { + t.Fatal(err) + } + if len(sc.ExecArgs) != 1 || sc.ExecArgs[0] != sc.BinaryPath { + t.Errorf("exec_args untuk tombol Open salah: %v", sc.ExecArgs) + } +} + func TestInstallExecutableDesktopOnly(t *testing.T) { home := testEnv(t) src := fakeBinary(t, home, "tool") @@ -188,6 +201,9 @@ func TestInstallWebAppFirefoxUsesNewWindow(t *testing.T) { if !strings.Contains(string(content), "Exec=/usr/bin/firefox --new-window https://example.com\n") { t.Errorf("Exec Firefox salah:\n%s", content) } + if len(sc.ExecArgs) != 3 || sc.ExecArgs[2] != "https://example.com" { + t.Errorf("exec_args PWA salah: %v", sc.ExecArgs) + } } func TestInstallWebAppFlatpakBrowser(t *testing.T) { diff --git a/internal/store/store.go b/internal/store/store.go index 7dde37c..2fbf7b8 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -27,6 +27,7 @@ type Shortcut struct { Type string `json:"type"` URL string `json:"url,omitempty"` Browser string `json:"browser,omitempty"` + ExecArgs []string `json:"exec_args,omitempty"` BinaryPath string `json:"binary_path,omitempty"` IconPath string `json:"icon_path,omitempty"` DesktopPath string `json:"desktop_path"` diff --git a/internal/ui/links.go b/internal/ui/links.go new file mode 100644 index 0000000..9ebdfda --- /dev/null +++ b/internal/ui/links.go @@ -0,0 +1,51 @@ +package ui + +import ( + "net/url" + "os/exec" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/dialog" + + "lazyrun/internal/store" +) + +// Link aplikasi: repo GitHub LazyRun dan halaman donasi. +const ( + repoURL = "https://github.com/oxpoe/lazyrun" + donateURL = "https://donasi.oxpoe.qzz.io/" +) + +// openURL membuka link di browser default. +func (ui *app) openURL(raw string) { + u, err := url.Parse(raw) + if err == nil { + err = fyne.CurrentApp().OpenURL(u) + } + if err != nil { + dialog.ShowError(err, ui.win) + } +} + +// launch menjalankan shortcut: pakai argv hasil install, fallback ke binary/URL. +func (ui *app) launch(sc store.Shortcut) { + args := sc.ExecArgs + if len(args) == 0 { + switch { + case sc.Type == store.TypeWebApp && sc.URL != "": + args = []string{"xdg-open", sc.URL} + case sc.BinaryPath != "": + args = []string{sc.BinaryPath} + } + } + if len(args) == 0 { + return + } + + cmd := exec.Command(args[0], args[1:]...) + if err := cmd.Start(); err != nil { + dialog.ShowError(err, ui.win) + return + } + go cmd.Wait() // supaya proses anak tidak jadi zombie +} diff --git a/internal/ui/main_window.go b/internal/ui/main_window.go index 9672bd9..b3d7e4a 100644 --- a/internal/ui/main_window.go +++ b/internal/ui/main_window.go @@ -5,8 +5,10 @@ import ( "fmt" "os/exec" "path/filepath" + "strconv" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/theme" @@ -24,6 +26,7 @@ type app struct { store *store.Store browsers []browser.Browser rows *fyne.Container + count *canvas.Text // angka jumlah shortcut di header daftar version string } @@ -46,6 +49,8 @@ func build(a fyne.App, version string) (*app, error) { ui.browsers = browser.Detect() ui.rows = container.NewVBox() + countChip, count := chipLabel("0", colorNameChipMenu) + ui.count = count ui.win = a.NewWindow(T("app_title")) ui.win.Resize(fyne.NewSize(840, 600)) @@ -64,13 +69,17 @@ func build(a fyne.App, version string) (*app, error) { header := container.NewBorder(nil, nil, leftHeader, rightHeader) update := widget.NewButtonWithIcon(T("check_updates"), theme.DownloadIcon(), ui.checkUpdates) + github := widget.NewButton(T("github"), func() { ui.openURL(repoURL) }) + donate := widget.NewButtonWithIcon(T("donate"), + fyne.NewStaticResource("heart.png", assets.Heart), func() { ui.openURL(donateURL) }) footer := container.NewBorder( widget.NewSeparator(), nil, - update, + container.NewHBox(update, github, donate), caption("v"+version), ) - top := container.NewVBox(header, widget.NewSeparator(), sectionLabel(T("installed"))) + top := container.NewVBox(header, accentBar(), + container.NewHBox(sectionLabel(T("installed")), countChip)) body := container.NewBorder( top, nil, nil, nil, @@ -92,6 +101,8 @@ func (ui *app) reload() { ui.store = st } ui.rows.RemoveAll() + ui.count.Text = strconv.Itoa(len(ui.store.Items)) + ui.count.Refresh() if len(ui.store.Items) == 0 { ui.rows.Add(emptyState()) } @@ -118,6 +129,7 @@ func (ui *app) row(sc store.Shortcut) fyne.CanvasObject { } actions := container.NewHBox(chips...) + actions.Add(iconButton(theme.MediaPlayIcon(), func() { ui.launch(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) diff --git a/internal/ui/strings.go b/internal/ui/strings.go index b378c4a..9fbb561 100644 --- a/internal/ui/strings.go +++ b/internal/ui/strings.go @@ -55,6 +55,8 @@ var tr = map[string][2]string{ "need_target": {"Pick at least one location: menu or desktop.", "Pilih minimal satu lokasi: menu atau desktop."}, "no_browser": {"No supported browser found. Install Chrome, Chromium, Brave, Edge, Vivaldi, or Firefox first.", "Browser yang didukung tidak ditemukan. Install Chrome, Chromium, Brave, Edge, Vivaldi, atau Firefox dulu."}, + "github": {"GitHub", "GitHub"}, + "donate": {"Donate", "Donasi"}, "check_updates": {"Check for Updates", "Cek Update"}, "not_appimage": {"This build is not an AppImage (version %s). Update from where you got it.", "Build ini bukan AppImage (versi %s). Update dari sumber kamu mendapatkannya."}, "no_appimageupdate": {"AppImageUpdate is not installed. Install it to update LazyRun automatically.", "AppImageUpdate tidak terpasang. Install dulu supaya LazyRun bisa update otomatis."}, diff --git a/internal/ui/style.go b/internal/ui/style.go index 5e71d69..2b52868 100644 --- a/internal/ui/style.go +++ b/internal/ui/style.go @@ -62,11 +62,24 @@ func card(content fyne.CanvasObject) fyne.CanvasObject { // chip adalah label kecil dengan latar berwarna. func chip(text string, bg fyne.ThemeColorName) fyne.CanvasObject { + object, _ := chipLabel(text, bg) + return object +} + +// chipLabel mengembalikan chip sekaligus teksnya supaya bisa diperbarui. +func chipLabel(text string, bg fyne.ThemeColorName) (fyne.CanvasObject, *canvas.Text) { rect := canvas.NewRectangle(theme.Color(bg)) rect.CornerRadius = 7 label := textLine(text, 11, true, theme.ColorNameForeground) label.Alignment = fyne.TextAlignCenter - return container.NewStack(rect, container.NewPadded(label)) + return container.NewStack(rect, container.NewPadded(label)), label +} + +// accentBar adalah garis gradien brand di bawah header. +func accentBar() fyne.CanvasObject { + bar := canvas.NewLinearGradient(brandIndigo, brandCyan, 90) + bar.SetMinSize(fyne.NewSize(0, 3)) + return bar } // iconButton adalah tombol ikon tanpa label, gaya datar.