diff --git a/README.md b/README.md index 1fbd49f..177b735 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,23 @@ Everything stays inside the home directory — no root, no system directories. Filenames use a slug of the app name (`WhatsApp Web` → `whatsapp-web.desktop`); the `Name=` field keeps the name you typed. +## Updates + +LazyRun checks the **latest release** of this repository once, a few seconds after +start (only when running as an AppImage) and offers to install it: + +- if [AppImageUpdate](https://github.com/AppImageCommunity/AppImageUpdate) is in + `$PATH` it is used, so only the changed blocks are downloaded (zsync); +- otherwise the AppImage is downloaded, verified (ELF + AppImage magic bytes) and + swapped in atomically — if anything fails the current AppImage stays untouched; +- *Restart now* in the dialog runs the new version. + +*Check for Updates* in the footer runs the same flow manually. + +**Publishing a release** so updates reach users: tag it `vX.Y.Z`, upload +`lazyrun-X.Y.Z-x86_64.AppImage` plus the `.zsync` file produced by `build.sh` as +release assets, and mark it as the latest release. + ## Build Requirements: Go 1.24+, cgo, and X11/OpenGL development headers (Fyne). diff --git a/internal/ui/dialog_test.go b/internal/ui/dialog_test.go index 9330baa..c5ee1bd 100644 --- a/internal/ui/dialog_test.go +++ b/internal/ui/dialog_test.go @@ -3,6 +3,7 @@ package ui import ( "os" "path/filepath" + "regexp" "strings" "testing" "time" @@ -70,3 +71,40 @@ func TestDialogsOpenWithoutPanic(t *testing.T) { // jadi sisa goroutine bisa bertabrakan dengan tes berikutnya kalau dibiarkan. time.Sleep(300 * time.Millisecond) } + +// TestStringsCoverUsedKeys memastikan setiap kunci T() yang dipakai ada di tabel +// terjemahan (kalau tidak, UI menampilkan nama kunci mentah). +func TestStringsCoverUsedKeys(t *testing.T) { + files, err := filepath.Glob("*.go") + if err != nil { + t.Fatal(err) + } + used := map[string]bool{} + for _, file := range files { + if strings.HasSuffix(file, "_test.go") { + continue + } + data, err := os.ReadFile(file) + if err != nil { + t.Fatal(err) + } + for _, m := range regexp.MustCompile(`T\("([a-z_]+)"\)`).FindAllStringSubmatch(string(data), -1) { + used[m[1]] = true + } + } + if len(used) == 0 { + t.Fatal("tidak ada kunci T() yang ditemukan") + } + for key := range used { + if _, ok := tr[key]; !ok { + t.Errorf("kunci %q dipakai tapi tidak ada di tabel terjemahan", key) + } + } + + // dan sebaliknya: kunci terjemahan yang tidak terpakai sebaiknya dihapus + for key := range tr { + if !used[key] { + t.Errorf("kunci %q ada di tabel terjemahan tapi tidak dipakai", key) + } + } +} diff --git a/internal/ui/strings.go b/internal/ui/strings.go index 9607661..1dc996b 100644 --- a/internal/ui/strings.go +++ b/internal/ui/strings.go @@ -30,8 +30,8 @@ var tr = map[string][2]string{ "browse": {"Browse...", "Pilih..."}, "install": {"Install", "Install"}, "cancel": {"Cancel", "Batal"}, + "back": {"Back", "Kembali"}, "delete": {"Delete", "Hapus"}, - "folder": {"Folder", "Folder"}, "working": {"Working...", "Sedang diproses..."}, "new_exec_title": {"New Executable Shortcut", "Shortcut Executable Baru"}, @@ -63,11 +63,20 @@ 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."}, + "github": {"GitHub", "GitHub"}, + "donate": {"Donate", "Donasi"}, + "check_updates": {"Check for Updates", "Cek Update"}, + + "update_available": {"LazyRun %s is available (you are on %s).", "LazyRun %s sudah tersedia (kamu pakai %s)."}, + "update_hint": {"The new AppImage is downloaded and swapped in; restart afterwards to use it.", "AppImage baru diunduh lalu ditukar; restart setelahnya untuk memakainya."}, + "update_now": {"Update now", "Update sekarang"}, + "update_later": {"Later", "Nanti"}, + "update_page": {"Release page", "Halaman rilis"}, + "update_downloading": {"Downloading update...", "Mengunduh update..."}, + "update_installed": {"Update installed. Restart LazyRun to use the new version.", "Update terpasang. Restart LazyRun untuk memakai versi baru."}, + "update_restart": {"Restart now", "Restart sekarang"}, + "up_to_date": {"LazyRun %s is the latest version.", "LazyRun %s adalah versi terbaru."}, + "dev_build": {"This is a development build (%s), update check is skipped.", "Ini build development (%s), cek update dilewati."}, } var lang = detectLang() diff --git a/internal/ui/update.go b/internal/ui/update.go index 9fbfcad..4c3eb29 100644 --- a/internal/ui/update.go +++ b/internal/ui/update.go @@ -59,11 +59,10 @@ func (ui *app) showUpdateDialog(rel update.Release) { notes.Wrapping = fyne.TextWrapWord notes.Importance = widget.LowImportance - body := container.NewVScroll(container.NewVBox( - heading(fmt.Sprintf(T("update_available"), rel.Version, ui.version)), - muted(T("update_hint")), - notes, - )) + // catatan rilis di tengah (bukan top): VScroll di Fyne punya MinSize kecil, + // jadi kalau dipasang sebagai top dari Border isinya terpotong. + head := heading(fmt.Sprintf(T("update_available"), rel.Version, ui.version)) + body := container.NewVScroll(container.NewVBox(muted(T("update_hint")), notes)) var d *dialog.CustomDialog buttons := container.NewHBox( @@ -86,7 +85,7 @@ func (ui *app) showUpdateDialog(rel update.Release) { })) } - d = dialog.NewCustomWithoutButtons(T("check_updates"), container.NewBorder(body, buttons, nil, nil), ui.win) + d = dialog.NewCustomWithoutButtons(T("check_updates"), container.NewBorder(head, buttons, nil, nil, body), ui.win) showDialog(d, ui.win, 560, 440) }