UI: tombol GitHub + Donasi, tombol Open per shortcut, aksen gradien
- Footer: Check for Updates, GitHub (github.com/oxpoe/lazyrun), Donate (donasi.oxpoe.qzz.io) - Baris shortcut: tombol play untuk menjalankan (pakai exec_args hasil install) - metadata shortcut menyimpan exec_args; ada fallback ke binary/URL untuk data lama - Chip jumlah shortcut di header daftar + garis gradien brand di bawah header - README: link repo & donasi absolut
This commit is contained in:
1 parent
f00662627a
commit
b12efefd6a
10 files changed
+111
-4
No files matched your search
@@ -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
|
from an executable file, or from a web URL as a PWA. Ships as a single-file
|
||||||
AppImage, no terminal needed.
|
AppImage, no terminal needed.
|
||||||
|
|
||||||
|
- **Repo:** <https://github.com/oxpoe/lazyrun>
|
||||||
|
- **Donate:** <https://donasi.oxpoe.qzz.io/>
|
||||||
|
|
||||||
- **Executable shortcut** — picks an ELF/AppImage/script, copies it to
|
- **Executable shortcut** — picks an ELF/AppImage/script, copies it to
|
||||||
`~/.local/bin/`, installs the icon, writes the `.desktop` entry.
|
`~/.local/bin/`, installs the icon, writes the `.desktop` entry.
|
||||||
- **Web app shortcut** — opens the URL as an app: `--app=<url>` for
|
- **Web app shortcut** — opens the URL as an app: `--app=<url>` for
|
||||||
@@ -23,7 +26,7 @@ AppImage, no terminal needed.
|
|||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
Download the AppImage from [Releases](../../releases), then:
|
Download the AppImage from [Releases](https://github.com/oxpoe/lazyrun/releases), then:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
chmod +x lazyrun-*-x86_64.AppImage
|
chmod +x lazyrun-*-x86_64.AppImage
|
||||||
|
|||||||
@@ -3,5 +3,12 @@ package assets
|
|||||||
|
|
||||||
import _ "embed"
|
import _ "embed"
|
||||||
|
|
||||||
|
// Icon adalah ikon aplikasi LazyRun.
|
||||||
|
//
|
||||||
//go:embed icon.png
|
//go:embed icon.png
|
||||||
var Icon []byte
|
var Icon []byte
|
||||||
|
|
||||||
|
// Heart adalah ikon kecil untuk tombol donasi.
|
||||||
|
//
|
||||||
|
//go:embed heart.png
|
||||||
|
var Heart []byte
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 697 B |
@@ -71,6 +71,7 @@ func InstallExecutable(o ExecOptions) (store.Shortcut, error) {
|
|||||||
Slug: sl,
|
Slug: sl,
|
||||||
Name: name,
|
Name: name,
|
||||||
Type: store.TypeExecutable,
|
Type: store.TypeExecutable,
|
||||||
|
ExecArgs: entry.ExecArgs,
|
||||||
BinaryPath: binDst,
|
BinaryPath: binDst,
|
||||||
IconPath: iconPath,
|
IconPath: iconPath,
|
||||||
DesktopPath: deskDst,
|
DesktopPath: deskDst,
|
||||||
@@ -156,6 +157,7 @@ func InstallWebApp(o WebOptions) (store.Shortcut, error) {
|
|||||||
Type: store.TypeWebApp,
|
Type: store.TypeWebApp,
|
||||||
URL: pageURL,
|
URL: pageURL,
|
||||||
Browser: strings.Join(o.Browser.Exec, " "),
|
Browser: strings.Join(o.Browser.Exec, " "),
|
||||||
|
ExecArgs: entry.ExecArgs,
|
||||||
IconPath: iconPath,
|
IconPath: iconPath,
|
||||||
DesktopPath: deskDst,
|
DesktopPath: deskDst,
|
||||||
Targets: store.Targets{Menu: o.Menu, Desktop: o.Desktop},
|
Targets: store.Targets{Menu: o.Menu, Desktop: o.Desktop},
|
||||||
|
|||||||
@@ -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) {
|
func TestInstallExecutableDesktopOnly(t *testing.T) {
|
||||||
home := testEnv(t)
|
home := testEnv(t)
|
||||||
src := fakeBinary(t, home, "tool")
|
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") {
|
if !strings.Contains(string(content), "Exec=/usr/bin/firefox --new-window https://example.com\n") {
|
||||||
t.Errorf("Exec Firefox salah:\n%s", content)
|
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) {
|
func TestInstallWebAppFlatpakBrowser(t *testing.T) {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type Shortcut struct {
|
|||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
URL string `json:"url,omitempty"`
|
URL string `json:"url,omitempty"`
|
||||||
Browser string `json:"browser,omitempty"`
|
Browser string `json:"browser,omitempty"`
|
||||||
|
ExecArgs []string `json:"exec_args,omitempty"`
|
||||||
BinaryPath string `json:"binary_path,omitempty"`
|
BinaryPath string `json:"binary_path,omitempty"`
|
||||||
IconPath string `json:"icon_path,omitempty"`
|
IconPath string `json:"icon_path,omitempty"`
|
||||||
DesktopPath string `json:"desktop_path"`
|
DesktopPath string `json:"desktop_path"`
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -5,8 +5,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/canvas"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"fyne.io/fyne/v2/dialog"
|
"fyne.io/fyne/v2/dialog"
|
||||||
"fyne.io/fyne/v2/theme"
|
"fyne.io/fyne/v2/theme"
|
||||||
@@ -24,6 +26,7 @@ type app struct {
|
|||||||
store *store.Store
|
store *store.Store
|
||||||
browsers []browser.Browser
|
browsers []browser.Browser
|
||||||
rows *fyne.Container
|
rows *fyne.Container
|
||||||
|
count *canvas.Text // angka jumlah shortcut di header daftar
|
||||||
version string
|
version string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +49,8 @@ func build(a fyne.App, version string) (*app, error) {
|
|||||||
|
|
||||||
ui.browsers = browser.Detect()
|
ui.browsers = browser.Detect()
|
||||||
ui.rows = container.NewVBox()
|
ui.rows = container.NewVBox()
|
||||||
|
countChip, count := chipLabel("0", colorNameChipMenu)
|
||||||
|
ui.count = count
|
||||||
ui.win = a.NewWindow(T("app_title"))
|
ui.win = a.NewWindow(T("app_title"))
|
||||||
ui.win.Resize(fyne.NewSize(840, 600))
|
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)
|
header := container.NewBorder(nil, nil, leftHeader, rightHeader)
|
||||||
|
|
||||||
update := widget.NewButtonWithIcon(T("check_updates"), theme.DownloadIcon(), ui.checkUpdates)
|
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(
|
footer := container.NewBorder(
|
||||||
widget.NewSeparator(), nil,
|
widget.NewSeparator(), nil,
|
||||||
update,
|
container.NewHBox(update, github, donate),
|
||||||
caption("v"+version),
|
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(
|
body := container.NewBorder(
|
||||||
top,
|
top,
|
||||||
nil, nil, nil,
|
nil, nil, nil,
|
||||||
@@ -92,6 +101,8 @@ func (ui *app) reload() {
|
|||||||
ui.store = st
|
ui.store = st
|
||||||
}
|
}
|
||||||
ui.rows.RemoveAll()
|
ui.rows.RemoveAll()
|
||||||
|
ui.count.Text = strconv.Itoa(len(ui.store.Items))
|
||||||
|
ui.count.Refresh()
|
||||||
if len(ui.store.Items) == 0 {
|
if len(ui.store.Items) == 0 {
|
||||||
ui.rows.Add(emptyState())
|
ui.rows.Add(emptyState())
|
||||||
}
|
}
|
||||||
@@ -118,6 +129,7 @@ func (ui *app) row(sc store.Shortcut) fyne.CanvasObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actions := container.NewHBox(chips...)
|
actions := container.NewHBox(chips...)
|
||||||
|
actions.Add(iconButton(theme.MediaPlayIcon(), func() { ui.launch(sc) }))
|
||||||
actions.Add(iconButton(theme.FolderOpenIcon(), func() {
|
actions.Add(iconButton(theme.FolderOpenIcon(), func() {
|
||||||
if err := exec.Command("xdg-open", filepath.Dir(sc.DesktopPath)).Start(); err != nil {
|
if err := exec.Command("xdg-open", filepath.Dir(sc.DesktopPath)).Start(); err != nil {
|
||||||
dialog.ShowError(err, ui.win)
|
dialog.ShowError(err, ui.win)
|
||||||
|
|||||||
@@ -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."},
|
"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."},
|
"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"},
|
"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."},
|
"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."},
|
"no_appimageupdate": {"AppImageUpdate is not installed. Install it to update LazyRun automatically.", "AppImageUpdate tidak terpasang. Install dulu supaya LazyRun bisa update otomatis."},
|
||||||
|
|||||||
+14
-1
@@ -62,11 +62,24 @@ func card(content fyne.CanvasObject) fyne.CanvasObject {
|
|||||||
|
|
||||||
// chip adalah label kecil dengan latar berwarna.
|
// chip adalah label kecil dengan latar berwarna.
|
||||||
func chip(text string, bg fyne.ThemeColorName) fyne.CanvasObject {
|
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 := canvas.NewRectangle(theme.Color(bg))
|
||||||
rect.CornerRadius = 7
|
rect.CornerRadius = 7
|
||||||
label := textLine(text, 11, true, theme.ColorNameForeground)
|
label := textLine(text, 11, true, theme.ColorNameForeground)
|
||||||
label.Alignment = fyne.TextAlignCenter
|
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.
|
// iconButton adalah tombol ikon tanpa label, gaya datar.
|
||||||
|
|||||||
Reference in new issue
Block a user