diff --git a/internal/ui/main_window.go b/internal/ui/main_window.go index dd4eb68..de3337b 100644 --- a/internal/ui/main_window.go +++ b/internal/ui/main_window.go @@ -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() diff --git a/internal/ui/update.go b/internal/ui/update.go index 4c3eb29..8eeefb1 100644 --- a/internal/ui/update.go +++ b/internal/ui/update.go @@ -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 } diff --git a/internal/ui/update_test.go b/internal/ui/update_test.go new file mode 100644 index 0000000..eb4bda4 --- /dev/null +++ b/internal/ui/update_test.go @@ -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) + } +}