8 Commits
Author SHA1 Message Date
oxpoe 7156458c6f feat(updater): verifikasi SHA-256 sebelum mengganti AppImage; CI menerbitkan .sha256 + .zsync
- do_update: unduh → verifikasi checksum rilis → baru ganti berkas (mismatch = dibatalkan)
- workflow: hasilkan GAMEMAX-LCD-x86_64.AppImage.sha256 dan .zsync (zsyncmake), lampirkan ketiganya
- README: bagian Updates menjelaskan checksum + zsync (delta update opsional)
- versi 1.2.3
2026-10-13 08:10:05 +07:00
oxpoe e0090fc1c3 feat: suite uji mencetak pesan hasil + GAMEMAX_DEBUG menyalin log ke stdout
docs: bagian Updates (pemeriksaan & pembaruan satu klik dari GitHub Releases)
versi 1.2.2
2026-10-13 07:53:51 +07:00
oxpoe a47b5c251c fix(app): arahkan OpenSSL ke CA sistem pada bundel beku (periksa/unduh pembaruan gagal dengan CERTIFICATE_VERIFY_FAILED)
versi 1.2.1
2026-10-13 07:48:00 +07:00
oxpoe 387e428406 feat(app): periksa + perbarui otomatis dari GitHub Releases
- tombol 'Periksa pembaruan' (+ otomatis saat aplikasi mulai pada versi AppImage)
- bila ada rilis lebih baru: tombol 'Perbarui ke vX' — unduh aset rilis, ganti AppImage
  secara atomik, tarik ulang sender dari bundel baru, restart layanan
- item menu tray 'Periksa pembaruan'
- suite --selftest-actions ikut memeriksa pembaruan
- versi 1.2.0
2026-10-13 07:42:12 +07:00
oxpoe 93ce357900 chore: tombol Sponsor di header repo (FUNDING.yml -> halaman donasi) 2026-10-13 07:26:33 +07:00
oxpoe 53ee1c5102 docs: percantik halaman repo — badge lengkap + diagram alur di README, template issue laporan hardware, CONTRIBUTING, SECURITY, dan kartu social preview 2026-10-13 07:25:59 +07:00
oxpoe 93bad1f461 docs: perbarui pratinjau GUI dengan tombol Donasi/GitHub dan label versi 2026-10-13 07:15:08 +07:00
oxpoe 657a8d10dc feat(app): tombol Donasi + GitHub (jendela & tray), label versi v1.1.0
docs: bagian Support dengan tautan donasi
2026-10-13 07:09:58 +07:00
9 changed files with 337 additions and 9 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
# Shows the ❤ Sponsor button in the repository header.
custom: ["https://donasi.oxpoe.qzz.io/"]
@@ -0,0 +1,57 @@
name: Hardware report
description: Report a GAMEMAX (or other) LCD cooler that works, partly works, or does not work
title: "[hardware] <cooler model>"
labels: ["hardware"]
body:
- type: markdown
attributes:
value: |
Thanks for testing on your hardware! Reports from other panel revisions are how
this project grows. Please fill in what you can.
- type: input
id: cooler
attributes:
label: Cooler / panel model
placeholder: e.g. GAMEMAX IceBurg 360 Digital WH
validations:
required: true
- type: input
id: usb
attributes:
label: USB identity (lsusb)
description: "`lsusb -d 5131:2007` — paste the line, or the VID:PID if it differs"
validations:
required: true
- type: dropdown
id: result
attributes:
label: Result
options:
- Works fully (values correct on the panel)
- Partly works (which fields are wrong?)
- Panel lights up but shows wrong layout
- Not detected / no reaction
validations:
required: true
- type: textarea
id: report
attributes:
label: Output of `python3 src/gamemax_report.py --dry-run`
description: This writes nothing to the device, it only prints the report it would send.
render: text
validations:
required: false
- type: input
id: system
attributes:
label: System
placeholder: distro · kernel · desktop session · motherboard super-I/O (e.g. nct6798)
validations:
required: false
- type: textarea
id: notes
attributes:
label: Notes
description: Which fan channel / temperature source you had to pick, anything unexpected.
validations:
required: false
+18 -2
View File
@@ -21,7 +21,7 @@ jobs:
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y --no-install-recommends \ sudo apt-get install -y --no-install-recommends \
libgl1 libegl1 libxkbcommon0 libdbus-1-3 libfuse2 fuse libgl1 libegl1 libxkbcommon0 libdbus-1-3 libfuse2 fuse zsync
- name: Set up uv - name: Set up uv
uses: astral-sh/setup-uv@v5 uses: astral-sh/setup-uv@v5
@@ -38,6 +38,15 @@ jobs:
run: | run: |
./dist/GAMEMAX-LCD-x86_64.AppImage --selftest ./dist/GAMEMAX-LCD-x86_64.AppImage --selftest
- name: Checksum and zsync metadata
run: |
cd dist
sha256sum GAMEMAX-LCD-x86_64.AppImage > GAMEMAX-LCD-x86_64.AppImage.sha256
zsyncmake -u "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/GAMEMAX-LCD-x86_64.AppImage" \
-o GAMEMAX-LCD-x86_64.AppImage.zsync GAMEMAX-LCD-x86_64.AppImage
cat GAMEMAX-LCD-x86_64.AppImage.sha256
ls -l
- name: Publish release - name: Publish release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
@@ -58,5 +67,12 @@ jobs:
Device access still needs the udev rule once: Device access still needs the udev rule once:
`sudo cp packaging/70-gamemax.rules /etc/udev/rules.d/ && sudo udevadm control --reload` `sudo cp packaging/70-gamemax.rules /etc/udev/rules.d/ && sudo udevadm control --reload`
**Integrity:** verify the download with the published SHA-256, or use
`appimageupdatetool`/AppImageUpdate with the `.zsync` asset for delta updates.
The built-in updater (v1.2.3+) also verifies that checksum before replacing itself.
Independent project, not affiliated with the vendor — see DISCLAIMER.md. Independent project, not affiliated with the vendor — see DISCLAIMER.md.
files: dist/GAMEMAX-LCD-x86_64.AppImage files: |
dist/GAMEMAX-LCD-x86_64.AppImage
dist/GAMEMAX-LCD-x86_64.AppImage.sha256
dist/GAMEMAX-LCD-x86_64.AppImage.zsync
+33
View File
@@ -0,0 +1,33 @@
# Contributing
Thanks for helping — the most valuable contribution is **hardware reports**: which cooler
works, which firmware revision differs, which sensor channel has to be picked. Use the
*Hardware report* issue template, or send a pull request.
## Before opening a pull request
The sender must stay dependency-free (standard library only) and must never write to the
device unless the user asked for it.
```bash
# nothing is written to the panel:
python3 src/gamemax_report.py --dry-run
python3 src/gamemax_report.py --list-sensors
# the tray app self-checks without a display, and can exercise every action:
uv run --with pyside6 python src/gamemax_gui.py --selftest
uv run --with pyside6 python src/gamemax_gui.py --selftest-actions
# build the AppImage (needs uv + network on first run):
bash packaging/build_appimage.sh
```
`--selftest-actions` really does start/stop the service and toggle autostart, then leaves
the service running — expected.
## Style
- Python 3.10+, no new dependencies, comments only where the code cannot explain itself.
- GUI strings are Indonesian (the original author's language); code and docs are English.
- Keep the protocol knowledge in `docs/reverse-engineering.md` and the README table in sync
when you learn something new about the panel.
+47
View File
@@ -8,7 +8,22 @@ Reverse engineered independently from the Windows vendor software
[`docs/reverse-engineering.md`](docs/reverse-engineering.md) (Indonesian). [`docs/reverse-engineering.md`](docs/reverse-engineering.md) (Indonesian).
[![Release](https://img.shields.io/github/v/release/oxpoe/Gamemax-linux-apps?color=def750&label=release)](https://github.com/oxpoe/Gamemax-linux-apps/releases) [![Release](https://img.shields.io/github/v/release/oxpoe/Gamemax-linux-apps?color=def750&label=release)](https://github.com/oxpoe/Gamemax-linux-apps/releases)
[![Release build](https://github.com/oxpoe/Gamemax-linux-apps/actions/workflows/release.yml/badge.svg)](https://github.com/oxpoe/Gamemax-linux-apps/actions/workflows/release.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-def750)](LICENSE) [![License: MIT](https://img.shields.io/badge/license-MIT-def750)](LICENSE)
[![Platform: Linux](https://img.shields.io/badge/platform-Linux-def750)](#compatibility)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-def750)](src/gamemax_report.py)
[![Dependencies: none](https://img.shields.io/badge/sender_dependencies-none-def750)](src/gamemax_report.py)
[![Donate](https://img.shields.io/badge/%E2%9D%A4-donate-def750)](https://donasi.oxpoe.qzz.io/)
```text
hwmon · /proc/stat · cpufreq · meminfo · statvfs
│
▼
gamemax_report.py ──► 65-byte HID report ──► LCD panel
(stdlib only) hidraw, 5131:2007
▲
gamemax_gui.py (tray) ──► systemd user unit + udev rule
```
Author: **oxpoe** · License: **MIT** · Independent project, **not affiliated with the Author: **oxpoe** · License: **MIT** · Independent project, **not affiliated with the
vendor** — please read [DISCLAIMER.md](DISCLAIMER.md) before using it. vendor** — please read [DISCLAIMER.md](DISCLAIMER.md) before using it.
@@ -90,6 +105,8 @@ temperature source (Tctl vs Tccd).
- **Plug and play**: the service waits for the device, streams while present and keeps - **Plug and play**: the service waits for the device, streams while present and keeps
waiting through unplug/replug; a udev rule grants access to the logged-in user waiting through unplug/replug; a udev rule grants access to the logged-in user
- **Single instance**: launching the app again just raises the running window - **Single instance**: launching the app again just raises the running window
- **Self-update from GitHub Releases** (check + one-click update)
- **Donate / GitHub buttons** in the app (window and tray menu)
- **AppImage build** script (`packaging/build_appimage.sh`, PySide6 bundled) - **AppImage build** script (`packaging/build_appimage.sh`, PySide6 bundled)
The GUI labels are in Indonesian. The GUI labels are in Indonesian.
@@ -192,6 +209,36 @@ docs/ reverse-engineering.md — full analysis (Indonesian)
assets/ previews and icon assets/ previews and icon
``` ```
## Updates
The app keeps itself current from GitHub Releases:
- On start (when running from an AppImage) and from the **Periksa pembaruan** button or the
tray menu, it asks the GitHub API for the latest release and compares it with its version.
- When a newer release exists, a **Perbarui ke vX.Y.Z** button appears. One click downloads
the release asset, replaces the running AppImage atomically, refreshes the sender from the
new bundle and restarts the service. Restart the app to use the new version.
- The download is verified against the release's published **SHA-256** before the AppImage is
replaced; a mismatch aborts the update. Older releases without a checksum are updated with a
warning in the log.
- Nothing is downloaded without that click, and no other files are touched.
Each release publishes three assets: the AppImage, its `.sha256`, and a `.zsync` file for
`appimageupdatetool` / AppImageUpdate (delta updates). The zsync file is optional — the
built-in updater does not need it.
If the check fails (offline, proxy), the message appears in the app log. `GAMEMAX_DEBUG=1`
also mirrors the log to stdout for headless runs.
## Support
If this project is useful to you:
- ❤ **Donate:** <https://donasi.oxpoe.qzz.io/> (buttons for it and for this repository are
in the app: window bottom row and the tray menu)
- ⭐ Star the repo, or open an issue with your cooler model and what worked — reports from
other IceBurg Digital revisions are the best way to widen hardware support.
## Credits ## Credits
Reverse engineered and written by **oxpoe**, with the help of Reverse engineered and written by **oxpoe**, with the help of
+14
View File
@@ -0,0 +1,14 @@
# Security
This is a small local tool. The relevant surface:
- It writes 64-byte HID reports to the panel matching `VID 0x5131 / PID 0x2007`
(plus a udev rule that grants the logged-in user access to that `hidraw` node).
- The tray app runs `systemctl --user` and, for the udev rule, `pkexec sh` on a script it
writes to `/tmp`. Nothing else is executed, and no data leaves the machine.
- No secrets are stored. The config file (`~/.config/gamemax-report/config.json`) holds
sensor channel names and display settings only.
If you find a problem — for example a way to make the helper scripts run something else,
or a path that writes outside the documented locations — please open an issue (or a
private security advisory on GitHub). Do not test on hardware you do not own.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

+166 -7
View File
@@ -8,9 +8,13 @@ mengendalikannya dan memakai gamemax_report.py sebagai pengirim.
gamemax_gui.py --tray-only # tanpa jendela gamemax_gui.py --tray-only # tanpa jendela
gamemax_gui.py --selftest # uji muat tanpa tampilan (untuk build AppImage) gamemax_gui.py --selftest # uji muat tanpa tampilan (untuk build AppImage)
""" """
import hashlib
import json import json
import math import math
import os import os
import re
import tempfile
import urllib.request
import shutil import shutil
import subprocess import subprocess
import sys import sys
@@ -18,19 +22,26 @@ import threading
import time import time
import traceback import traceback
from PySide6.QtCore import QPointF, QRectF, Qt, QThread, QTimer, Signal from PySide6.QtCore import QPointF, QRectF, Qt, QThread, QTimer, QUrl, Signal
from PySide6.QtGui import (QAction, QColor, QConicalGradient, QFont, QIcon, from PySide6.QtGui import (QAction, QColor, QConicalGradient, QDesktopServices, QFont,
QPainter, QPen, QPixmap) QIcon, QPainter, QPen, QPixmap)
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QApplication, QCheckBox, QComboBox, QFormLayout, QGroupBox, QHBoxLayout, QLabel, QApplication, QCheckBox, QComboBox, QFormLayout, QGroupBox, QHBoxLayout, QLabel,
QMenu, QPlainTextEdit, QProgressBar, QPushButton, QSystemTrayIcon, QVBoxLayout, QWidget, QMenu, QPlainTextEdit, QProgressBar, QPushButton, QSystemTrayIcon, QVBoxLayout, QWidget,
) )
from PySide6.QtNetwork import QLocalServer, QLocalSocket from PySide6.QtNetwork import QLocalServer, QLocalSocket
APP_VERSION = "1.2.3"
REPO = "oxpoe/Gamemax-linux-apps"
RELEASES_API = f"https://api.github.com/repos/{REPO}/releases/latest"
UPDATE_ASSET = "GAMEMAX-LCD-x86_64.AppImage"
HERE = os.path.dirname(os.path.abspath(__file__)) HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE) sys.path.insert(0, HERE)
import gamemax_report as gr # noqa: E402 import gamemax_report as gr # noqa: E402
DONATE_URL = "https://donasi.oxpoe.qzz.io/"
GITHUB_URL = "https://github.com/oxpoe/Gamemax-linux-apps"
SERVICE = "gamemax-report.service" SERVICE = "gamemax-report.service"
CONFIG_PATH = gr.CONFIG_PATH CONFIG_PATH = gr.CONFIG_PATH
INSTALL_DIR = os.path.expanduser("~/.local/share/gamemax-report") INSTALL_DIR = os.path.expanduser("~/.local/share/gamemax-report")
@@ -69,6 +80,59 @@ def crash_log(text):
pass pass
def version_tuple(tag):
parts = re.findall(r"\d+", tag or "")[:3]
return tuple(int(p) for p in parts) or (0,)
def ensure_ca_bundle():
"""Bundel PyInstaller tidak menemukan CA sistem sendiri; arahkan OpenSSL ke sana."""
if os.environ.get("SSL_CERT_FILE"):
return
for path in ("/etc/ssl/certs/ca-certificates.crt", # Arch/Debian
"/etc/pki/tls/certs/ca-bundle.crt", # Fedora/openSUSE
"/etc/ssl/cert.pem"): # Alpine/BSD
if os.path.exists(path):
os.environ["SSL_CERT_FILE"] = path
return
def verify_sha256(path, sha_url):
"""True/False bila berkas checksum tersedia, None bila rilis tidak menyediakannya."""
try:
with urllib.request.urlopen(sha_url, timeout=10) as resp:
expected = resp.read().decode().split()[0].strip().lower()
except Exception: # noqa: BLE001
return None
if len(expected) != 64:
return None
actual = hashlib.sha256(open(path, "rb").read()).hexdigest()
return actual == expected
def latest_release():
"""(tag, url aset) dari rilis terbaru GitHub — dipanggil dari thread pekerja."""
ensure_ca_bundle()
req = urllib.request.Request(RELEASES_API, headers={
"Accept": "application/vnd.github+json", "User-Agent": "gamemax-lcd"})
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.load(resp)
asset = next((a for a in data.get("assets", []) if a["name"] == UPDATE_ASSET), None)
return data.get("tag_name", ""), (asset or {}).get("browser_download_url", "")
def installed_appimage():
"""Path AppImage yang sedang berjalan (kosong kalau dijalankan dari sumber)."""
return os.environ.get("APPIMAGE", "")
def open_link(url, parent=None):
"""Buka tautan di peramban; kalau gagal, tampilkan pesannya di log."""
if not QDesktopServices.openUrl(QUrl(url)):
if parent is not None:
parent.log(f"tidak bisa membuka tautan: {url}")
def systemctl(*args): def systemctl(*args):
return subprocess.run(["systemctl", "--user", *args], capture_output=True, text=True) return subprocess.run(["systemctl", "--user", *args], capture_output=True, text=True)
@@ -117,10 +181,12 @@ class Task(QThread):
def __init__(self, label, fn): def __init__(self, label, fn):
super().__init__() super().__init__()
self.label, self.fn = label, fn self.label, self.fn = label, fn
self.result = None
def run(self): def run(self):
try: try:
self.done.emit(self.label, True, self.fn() or "") self.result = self.fn()
self.done.emit(self.label, True, self.result if isinstance(self.result, str) else "")
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
self.done.emit(self.label, False, str(exc)) self.done.emit(self.label, False, str(exc))
@@ -336,6 +402,32 @@ class Window(QWidget):
form.addRow("", row) form.addRow("", row)
root.addWidget(settings) root.addWidget(settings)
# ---- tautan
links = QHBoxLayout()
donate = QPushButton("❤ Donasi")
donate.setToolTip(DONATE_URL)
donate.clicked.connect(lambda: open_link(DONATE_URL, self))
github = QPushButton("GitHub")
github.setToolTip(GITHUB_URL)
github.clicked.connect(lambda: open_link(GITHUB_URL, self))
links.addWidget(donate)
links.addWidget(github)
links.addStretch(1)
self.check_button = QPushButton("Periksa pembaruan")
self.check_button.setToolTip("Bandingkan dengan rilis terbaru di GitHub")
self.check_button.clicked.connect(lambda: self.start_task("memeriksa pembaruan", self.do_check_update))
links.addWidget(self.check_button)
self.update_button = QPushButton("Perbarui")
self.update_button.setStyleSheet("font-weight:bold;")
self.update_button.hide()
self.update_button.clicked.connect(self.on_update_clicked)
links.addWidget(self.update_button)
links.addStretch(1)
self.version_label = QLabel(f"v{APP_VERSION}")
self.version_label.setStyleSheet("color:#8a8f8a;")
links.addWidget(self.version_label)
root.addLayout(links)
# ---- detail + log # ---- detail + log
self.detail = QPlainTextEdit() self.detail = QPlainTextEdit()
self.detail.setReadOnly(True) self.detail.setReadOnly(True)
@@ -355,6 +447,9 @@ class Window(QWidget):
self.poll = QTimer(self, timeout=self.refresh, interval=1000) self.poll = QTimer(self, timeout=self.refresh, interval=1000)
self.poll.start() self.poll.start()
self.refresh() self.refresh()
if installed_appimage(): # hanya pada versi terpasang
QTimer.singleShot(2500, lambda: self.start_task("memeriksa pembaruan",
self.do_check_update))
if gr.load_config(CONFIG_PATH).get("blank"): if gr.load_config(CONFIG_PATH).get("blank"):
self.log("PERHATIAN: mode layar kosong sedang aktif dari konfigurasi " self.log("PERHATIAN: mode layar kosong sedang aktif dari konfigurasi "
"(layar menampilkan nilai nol). Hilangkan centang 'Layar kosong' " "(layar menampilkan nilai nol). Hilangkan centang 'Layar kosong' "
@@ -397,6 +492,52 @@ class Window(QWidget):
"blank": bool(self.blank.isChecked()), "blank": bool(self.blank.isChecked()),
} }
# -------------------------------------------------------------- pembaruan
def do_check_update(self):
tag, url = latest_release()
self._pending_update = (tag, url) if url and version_tuple(tag) > version_tuple(APP_VERSION) else None
if not tag:
return "tidak bisa membaca daftar rilis GitHub"
if self._pending_update:
return f"versi baru tersedia: {tag} (terpasang v{APP_VERSION})"
return f"sudah versi terbaru (v{APP_VERSION})"
def on_update_clicked(self):
pending = getattr(self, "_pending_update", None)
target = installed_appimage()
if not pending:
return
if not target:
self.log("tidak berjalan dari AppImage — perbarui manual dari halaman Releases")
return
tag, url = pending
self.start_task(f"mengunduh {tag}", lambda: self.do_update(tag, url, target))
def do_update(self, tag, url, target):
ensure_ca_bundle()
tmp = target + ".new"
urllib.request.urlretrieve(url, tmp)
checked = verify_sha256(tmp, url + ".sha256")
if checked is False:
os.unlink(tmp)
raise RuntimeError("checksum SHA-256 tidak cocok — unduhan dibatalkan")
os.chmod(tmp, 0o755)
os.replace(tmp, target) # ganti berkas secara atomik
# sender di layanan juga diambil dari bundel baru
with tempfile.TemporaryDirectory() as td:
subprocess.run([target, "--appimage-extract"], cwd=td,
capture_output=True, timeout=180)
sender = os.path.join(td, "squashfs-root/usr/bin/_internal/gamemax_report.py")
if os.path.exists(sender):
os.makedirs(INSTALL_DIR, exist_ok=True)
shutil.copyfile(sender, os.path.join(INSTALL_DIR, "gamemax_report.py"))
systemctl("restart", SERVICE)
note = "" if checked is None else " (checksum SHA-256 terverifikasi)"
if checked is None:
note = " (rilis ini tidak menyertakan checksum)"
return (f"diperbarui ke {tag}{note} — tutup jendela ini lalu buka lagi "
"untuk memakai versi baru")
# -------------------------------------------------------------- tugas latar # -------------------------------------------------------------- tugas latar
def start_task(self, label, fn): def start_task(self, label, fn):
if getattr(self, "_task", None) is not None and self._task.isRunning(): if getattr(self, "_task", None) is not None and self._task.isRunning():
@@ -418,6 +559,10 @@ class Window(QWidget):
for button in self.buttons: for button in self.buttons:
button.setEnabled(True) button.setEnabled(True)
self.log(("selesai: " if ok else "GAGAL: ") + label + (f" — {message}" if message else "")) self.log(("selesai: " if ok else "GAGAL: ") + label + (f" — {message}" if message else ""))
pending = getattr(self, "_pending_update", None)
if label == "memeriksa pembaruan" and pending:
self.update_button.setText(f"Perbarui ke {pending[0]}")
self.update_button.show()
self.refresh() self.refresh()
# -------------------------------------------------------------- aksi # -------------------------------------------------------------- aksi
@@ -562,6 +707,8 @@ class Window(QWidget):
def log(self, text): def log(self, text):
self.logview.appendPlainText(text) self.logview.appendPlainText(text)
if os.environ.get("GAMEMAX_DEBUG"):
print(text, flush=True)
def warn_once(self, text): def warn_once(self, text):
if getattr(self, "_warned", None) != text: if getattr(self, "_warned", None) != text:
@@ -662,12 +809,23 @@ class Tray:
toggle = QAction("Aktifkan / matikan", menu) toggle = QAction("Aktifkan / matikan", menu)
toggle.triggered.connect(lambda: self.window.on_toggle()) toggle.triggered.connect(lambda: self.window.on_toggle())
self.blank_action.triggered.connect(lambda checked: self.window.blank.setChecked(checked)) self.blank_action.triggered.connect(lambda checked: self.window.blank.setChecked(checked))
check_action = QAction("Periksa pembaruan", menu)
check_action.triggered.connect(lambda: self.window.start_task(
"memeriksa pembaruan", self.window.do_check_update))
donate_action = QAction("❤ Donasi", menu)
donate_action.triggered.connect(lambda: open_link(DONATE_URL, self.window))
github_action = QAction("GitHub", menu)
github_action.triggered.connect(lambda: open_link(GITHUB_URL, self.window))
quit_action = QAction("Keluar", menu) quit_action = QAction("Keluar", menu)
quit_action.triggered.connect(app.quit) quit_action.triggered.connect(app.quit)
menu.addAction(show) menu.addAction(show)
menu.addAction(toggle) menu.addAction(toggle)
menu.addAction(self.blank_action) menu.addAction(self.blank_action)
menu.addSeparator() menu.addSeparator()
menu.addAction(check_action)
menu.addAction(donate_action)
menu.addAction(github_action)
menu.addSeparator()
menu.addAction(quit_action) menu.addAction(quit_action)
self.icon.setContextMenu(menu) self.icon.setContextMenu(menu)
self.icon.activated.connect( self.icon.activated.connect(
@@ -758,11 +916,12 @@ def _main():
("nyalakan layar", w.on_toggle), ("nyalakan layar", w.on_toggle),
("autostart ON", lambda: w.on_autostart(True)), ("autostart ON", lambda: w.on_autostart(True)),
("kirim uji", w.send_test), ("kirim uji", w.send_test),
("pasang layanan", lambda: w.install_service(w.config()))) ("pasang layanan", lambda: w.install_service(w.config())),
("periksa pembaruan", w.do_check_update))
for name, fn in steps: for name, fn in steps:
try: try:
fn() out = fn()
print(f"[ OK ] {name}") print(f"[ OK ] {name}" + (f" — {out}" if isinstance(out, str) and out else ""))
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
print(f"[GAGAL] {name}: {exc}") print(f"[GAGAL] {name}: {exc}")
# jalur asinkron yang dipakai tombol: centang layar kosong + simpan # jalur asinkron yang dipakai tombol: centang layar kosong + simpan