2 Commits
Author SHA1 Message Date
oxpoe fbf7344b09 Update: deteksi AppImage tanpa $APPIMAGE, cek berkala, indikator di footer
- appImagePath(): pakai $APPIMAGE kalau ada, kalau tidak periksa berkas
  executable sendiri apakah AppImage (dulu cek update dilewati begitu saja)
- cek ulang tiap 4 jam selama aplikasi terbuka, bukan hanya saat start
- versi di footer berubah jadi 'v1.1.0 -> v1.1.1' saat ada update, jadi tetap
  terlihat walau dialognya ditutup dengan Nanti
- tes: appImagePath (env, fallback, bukan AppImage), indikator footer
2026-10-13 08:32:42 +07:00
oxpoe a7e75d95c4 CI: header catatan rilis (cara install + info update) sebelum changelog otomatis 2026-10-13 08:26:38 +07:00
4 changed files with 137 additions and 16 deletions

No files matched your search

+15
View File
@@ -44,8 +44,23 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
cat > /tmp/notes.md <<'NOTES'
Single-file AppImage for Linux x86_64. No root needed, everything stays in
`~/.local/` and `~/.config/lazyrun/`.
```bash
chmod +x lazyrun-*-x86_64.AppImage
./lazyrun-*-x86_64.AppImage
```
Already using LazyRun? It checks this release page on startup and offers to
update itself (delta updates with `AppImageUpdate` when installed).
---
NOTES
gh release create "$GITHUB_REF_NAME" \
dist/lazyrun-*.AppImage \
dist/lazyrun-*.AppImage.zsync \
--title "LazyRun ${GITHUB_REF_NAME}" \
--notes-file /tmp/notes.md \
--generate-notes
+11 -9
View File
@@ -24,14 +24,15 @@ import (
)
type app struct {
win fyne.Window
store *store.Store
browsers []browser.Browser
rows *fyne.Container
cards map[string]*hoverCard
icons *iconCache
count *canvas.Text // angka jumlah shortcut di header daftar
version string
win fyne.Window
store *store.Store
browsers []browser.Browser
rows *fyne.Container
cards map[string]*hoverCard
icons *iconCache
count *canvas.Text // angka jumlah shortcut di header daftar
versionText *canvas.Text // versi di footer; berubah kalau ada update
version string
}
// Run menampilkan window utama LazyRun sampai user menutupnya.
@@ -60,6 +61,7 @@ func build(a fyne.App, version string) (*app, error) {
ui.count = count
ui.win = a.NewWindow(T("app_title"))
ui.win.Resize(fyne.NewSize(840, 600))
ui.versionText = caption("LazyRun v" + version + " · oxpoe")
st, err := store.Load(xdg.MetadataPath())
ui.store = st
@@ -82,7 +84,7 @@ func build(a fyne.App, version string) (*app, error) {
footer := container.NewBorder(
widget.NewSeparator(), nil,
container.NewHBox(update, github, donate),
caption("LazyRun v"+version+" · oxpoe"),
ui.versionText,
)
accent := accentBar()
+55 -7
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
"fyne.io/fyne/v2"
@@ -15,15 +16,45 @@ import (
"lazyrun/internal/update"
)
// recheckInterval adalah jeda pemeriksaan update berkala selama aplikasi terbuka.
const recheckInterval = 4 * time.Hour
// appImagePath mengembalikan lokasi AppImage yang sedang dipakai.
// $APPIMAGE dipakai kalau ada; kalau tidak (mis. dijalankan lewat wrapper),
// berkas executable-nya sendiri diperiksa apakah benar AppImage.
func appImagePath() string {
if path := os.Getenv("APPIMAGE"); path != "" {
return path
}
exe, err := os.Executable()
if err != nil {
return ""
}
if resolved, err := filepath.EvalSymlinks(exe); err == nil {
exe = resolved
}
if update.IsAppImage(exe) {
return exe
}
return ""
}
// checkUpdates dipanggil tombol footer: hasilnya selalu diberitahukan.
func (ui *app) checkUpdates() { ui.checkForUpdate(true) }
// scheduleUpdateCheck memeriksa rilis terbaru beberapa saat setelah aplikasi tampil.
// scheduleUpdateCheck memeriksa rilis terbaru setelah aplikasi tampil,
// lalu mengulanginya berkala selama aplikasi terbuka.
func (ui *app) scheduleUpdateCheck() {
if os.Getenv("APPIMAGE") == "" {
return // dijalankan dari source: jangan ganggu dengan cek update
if appImagePath() == "" {
return // dijalankan dari source / hasil ekstrak: tidak bisa memasang update
}
time.AfterFunc(2*time.Second, func() { ui.checkForUpdate(false) })
ticker := time.NewTicker(recheckInterval)
go func() {
for range ticker.C {
ui.checkForUpdate(false)
}
}()
}
// checkForUpdate mengambil rilis terbaru dari GitHub. manual=false hanya
@@ -45,6 +76,7 @@ func (ui *app) checkForUpdate(manual bool) {
dialog.ShowError(err, ui.win)
}
case update.Newer(ui.version, rel):
ui.markUpdatePending(rel)
ui.showUpdateDialog(rel)
case manual:
dialog.ShowInformation(T("check_updates"), fmt.Sprintf(T("up_to_date"), ui.version), ui.win)
@@ -53,6 +85,16 @@ func (ui *app) checkForUpdate(manual bool) {
}()
}
// markUpdatePending menampilkan versi baru di footer, jadi tetap terlihat
// walau dialognya ditutup dengan "Nanti".
func (ui *app) markUpdatePending(rel update.Release) {
if ui.versionText == nil {
return
}
ui.versionText.Text = fmt.Sprintf("v%s → v%s", ui.version, rel.Version)
ui.versionText.Refresh()
}
// showUpdateDialog menawarkan update: unduh sekarang, buka halaman rilis, atau nanti.
func (ui *app) showUpdateDialog(rel update.Release) {
notes := widget.NewLabel(rel.Notes)
@@ -91,7 +133,7 @@ func (ui *app) showUpdateDialog(rel update.Release) {
// updaterBinary mengembalikan path AppImageUpdate kalau terpasang.
func (ui *app) updaterBinary() string {
if os.Getenv("APPIMAGE") == "" {
if appImagePath() == "" {
return ""
}
path, err := exec.LookPath("AppImageUpdate")
@@ -103,7 +145,7 @@ func (ui *app) updaterBinary() string {
// downloadUpdate mengunduh AppImage terbaru dan menukarnya dengan yang sedang dipakai.
func (ui *app) downloadUpdate(rel update.Release) {
appImage := os.Getenv("APPIMAGE")
appImage := appImagePath()
if appImage == "" {
ui.openURL(rel.PageURL)
return
@@ -126,10 +168,16 @@ func (ui *app) downloadUpdate(rel update.Release) {
// runUpdater memakai AppImageUpdate untuk memasang update (pakai zsync, unduhannya kecil).
func (ui *app) runUpdater(updater string, rel update.Release) {
appImage := appImagePath()
if appImage == "" {
ui.openURL(rel.PageURL)
return
}
progress := dialog.NewProgressInfinite(T("check_updates"), T("update_downloading"), ui.win)
progress.Show()
go func() {
err := exec.Command(updater, os.Getenv("APPIMAGE")).Run()
err := exec.Command(updater, appImage).Run()
fyne.Do(func() {
progress.Hide()
if err != nil {
@@ -157,7 +205,7 @@ func (ui *app) offerRestart() {
// restart menjalankan AppImage yang baru lalu menutup proses lama.
func (ui *app) restart() {
appImage := os.Getenv("APPIMAGE")
appImage := appImagePath()
if appImage == "" {
return
}
+56
View File
@@ -0,0 +1,56 @@
package ui
import (
"os"
"path/filepath"
"testing"
"fyne.io/fyne/v2/test"
"lazyrun/internal/update"
)
func TestAppImagePathFromEnv(t *testing.T) {
t.Setenv("APPIMAGE", "/home/u/Apps/lazyrun-1.0.0-x86_64.AppImage")
if got := appImagePath(); got != "/home/u/Apps/lazyrun-1.0.0-x86_64.AppImage" {
t.Errorf("appImagePath() = %q", got)
}
}
func TestAppImagePathWithoutEnvAndNotAppImage(t *testing.T) {
// test binary bukan AppImage, jadi tidak ada yang bisa diupdate
t.Setenv("APPIMAGE", "")
if got := appImagePath(); got != "" {
t.Errorf("appImagePath() = %q, mau kosong untuk binary biasa", got)
}
}
func TestAppImagePathFallsBackToExecutable(t *testing.T) {
dir := t.TempDir()
// executable tiruan: ELF + magic AI (AppImage)
fake := filepath.Join(dir, "lazyrun.AppImage")
data := make([]byte, 64)
copy(data, []byte{0x7f, 'E', 'L', 'F', 0x02, 0x01, 0x01, 0x00})
copy(data[8:], []byte{'A', 'I', 0x02})
if err := os.WriteFile(fake, data, 0o755); err != nil {
t.Fatal(err)
}
// jalur env kosong -> deteksi dari IsAppImage (di sini diuji langsung)
if !update.IsAppImage(fake) {
t.Fatal("berkas uji harus dikenali sebagai AppImage")
}
}
func TestMarkUpdatePendingSetsFooter(t *testing.T) {
a := test.NewApp()
defer a.Quit()
ui, err := build(a, "1.1.0")
if err != nil {
t.Fatal(err)
}
ui.markUpdatePending(update.Release{Version: "9.9.9"})
if got := ui.versionText.Text; got != "v1.1.0 → v9.9.9" {
t.Errorf("footer tidak menampilkan update: %q", got)
}
}