Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0090fc1c3 | ||
|
|
a47b5c251c | ||
|
|
387e428406 | ||
|
|
93ce357900 | ||
|
|
53ee1c5102 | ||
|
|
93bad1f461 | ||
|
|
657a8d10dc |
No files matched your search
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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).
|
||||||
|
|
||||||
[](https://github.com/oxpoe/Gamemax-linux-apps/releases)
|
[](https://github.com/oxpoe/Gamemax-linux-apps/releases)
|
||||||
|
[](https://github.com/oxpoe/Gamemax-linux-apps/actions/workflows/release.yml)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
[](#compatibility)
|
||||||
|
[](src/gamemax_report.py)
|
||||||
|
[](src/gamemax_report.py)
|
||||||
|
[](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,29 @@ 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.
|
||||||
|
- Nothing is downloaded without that click, and no other files are touched.
|
||||||
|
|
||||||
|
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
@@ -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 |
+144
-7
@@ -11,6 +11,9 @@ mengendalikannya dan memakai gamemax_report.py sebagai pengirim.
|
|||||||
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 +21,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.2"
|
||||||
|
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 +79,46 @@ 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 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 +167,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 +388,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 +433,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 +478,44 @@ 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)
|
||||||
|
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)
|
||||||
|
return f"diperbarui ke {tag} — 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 +537,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 +685,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 +787,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 +894,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
|
||||||
|
|||||||
Reference in new issue
Block a user