Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f3222bc96 | ||
|
|
ac23195996 | ||
|
|
7156458c6f | ||
|
|
e0090fc1c3 |
No files matched your search
@@ -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
|
||||||
@@ -29,6 +29,7 @@ jobs:
|
|||||||
- name: Build AppImage
|
- name: Build AppImage
|
||||||
env:
|
env:
|
||||||
APPIMAGE_EXTRACT_AND_RUN: "1"
|
APPIMAGE_EXTRACT_AND_RUN: "1"
|
||||||
|
UPDATE_INFO_URL: https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/GAMEMAX-LCD-x86_64.AppImage.zsync
|
||||||
run: bash packaging/build_appimage.sh
|
run: bash packaging/build_appimage.sh
|
||||||
|
|
||||||
- name: Smoke test the bundle (no display)
|
- name: Smoke test the bundle (no display)
|
||||||
@@ -38,6 +39,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 +68,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
|
||||||
@@ -105,6 +105,7 @@ 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)
|
- **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)
|
||||||
|
|
||||||
@@ -208,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.
|
||||||
|
- 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 — but the AppImage also carries the zsync URL as embedded
|
||||||
|
update information (releases built after v1.2.3), so `appimageupdatetool ./GAMEMAX-LCD.AppImage`
|
||||||
|
works directly.
|
||||||
|
|
||||||
|
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
|
## Support
|
||||||
|
|
||||||
If this project is useful to you:
|
If this project is useful to you:
|
||||||
|
|||||||
@@ -51,13 +51,19 @@ Terminal=false
|
|||||||
Categories=System;Utility;
|
Categories=System;Utility;
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# 4. package
|
# 4. package (UPDATE_INFO_URL menyematkan info zsync agar appimageupdatetool bisa jalan)
|
||||||
|
UPDATE_INFO_ARG=""
|
||||||
|
if [ -n "${UPDATE_INFO_URL:-}" ]; then
|
||||||
|
UPDATE_INFO_ARG="-u zsync|${UPDATE_INFO_URL}"
|
||||||
|
echo "embedding update information: ${UPDATE_INFO_URL}"
|
||||||
|
fi
|
||||||
if [ ! -x "$BUILD_DIR/appimagetool" ]; then
|
if [ ! -x "$BUILD_DIR/appimagetool" ]; then
|
||||||
curl -fsSL "$APPIMAGETOOL_URL" -o "$BUILD_DIR/appimagetool"
|
curl -fsSL "$APPIMAGETOOL_URL" -o "$BUILD_DIR/appimagetool"
|
||||||
chmod +x "$BUILD_DIR/appimagetool"
|
chmod +x "$BUILD_DIR/appimagetool"
|
||||||
fi
|
fi
|
||||||
OUT="$OUT_DIR/GAMEMAX-LCD-x86_64.AppImage"
|
OUT="$OUT_DIR/GAMEMAX-LCD-x86_64.AppImage"
|
||||||
ARCH=x86_64 "$BUILD_DIR/appimagetool" "$APPDIR" "$OUT"
|
# shellcheck disable=SC2086
|
||||||
|
ARCH=x86_64 "$BUILD_DIR/appimagetool" $UPDATE_INFO_ARG "$APPDIR" "$OUT"
|
||||||
chmod +x "$OUT"
|
chmod +x "$OUT"
|
||||||
echo "built: $OUT"
|
echo "built: $OUT"
|
||||||
echo "verify: $OUT --selftest (or APPIMAGE_EXTRACT_AND_RUN=1 $OUT --selftest)"
|
echo "verify: $OUT --selftest (or APPIMAGE_EXTRACT_AND_RUN=1 $OUT --selftest)"
|
||||||
+152
-11
@@ -8,6 +8,7 @@ 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
|
||||||
@@ -26,11 +27,12 @@ from PySide6.QtGui import (QAction, QColor, QConicalGradient, QDesktopServices,
|
|||||||
QIcon, 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, QMessageBox, QPlainTextEdit, QProgressBar, QPushButton, QSystemTrayIcon,
|
||||||
|
QVBoxLayout, QWidget,
|
||||||
)
|
)
|
||||||
from PySide6.QtNetwork import QLocalServer, QLocalSocket
|
from PySide6.QtNetwork import QLocalServer, QLocalSocket
|
||||||
|
|
||||||
APP_VERSION = "1.2.1"
|
APP_VERSION = "1.3.0"
|
||||||
REPO = "oxpoe/Gamemax-linux-apps"
|
REPO = "oxpoe/Gamemax-linux-apps"
|
||||||
RELEASES_API = f"https://api.github.com/repos/{REPO}/releases/latest"
|
RELEASES_API = f"https://api.github.com/repos/{REPO}/releases/latest"
|
||||||
UPDATE_ASSET = "GAMEMAX-LCD-x86_64.AppImage"
|
UPDATE_ASSET = "GAMEMAX-LCD-x86_64.AppImage"
|
||||||
@@ -64,6 +66,34 @@ WantedBy=default.target
|
|||||||
RULE_TEMPLATE = ('SUBSYSTEM=="hidraw", ATTRS{idVendor}=="5131", ATTRS{idProduct}=="2007", '
|
RULE_TEMPLATE = ('SUBSYSTEM=="hidraw", ATTRS{idVendor}=="5131", ATTRS{idProduct}=="2007", '
|
||||||
'TAG+="uaccess"\n')
|
'TAG+="uaccess"\n')
|
||||||
|
|
||||||
|
APP_QSS = """
|
||||||
|
QWidget { background: #14121a; color: #ece6ff; font-size: 13px; }
|
||||||
|
QLabel#hint { color: #a99fc4; }
|
||||||
|
QGroupBox { border: 1px solid #2b2740; border-radius: 10px; margin-top: 14px;
|
||||||
|
padding: 12px 10px 8px 10px; font-weight: 600; }
|
||||||
|
QGroupBox::title { subcontrol-origin: margin; left: 12px; padding: 0 6px; color: #a99fc4; }
|
||||||
|
QPushButton { background: #242033; border: 1px solid #34304a; border-radius: 8px;
|
||||||
|
padding: 7px 13px; }
|
||||||
|
QPushButton:hover { background: #2c2740; }
|
||||||
|
QPushButton:pressed { background: #201c2e; }
|
||||||
|
QPushButton:disabled { color: #6c6486; border-color: #2a2740; }
|
||||||
|
QPushButton#primary { background: #def750; color: #14121a; border: none; font-weight: 700; }
|
||||||
|
QPushButton#primary:hover { background: #e9ff6a; }
|
||||||
|
QPushButton#primary:disabled { background: #4a5230; color: #2a2e20; }
|
||||||
|
QComboBox, QDoubleSpinBox { background: #1c1926; border: 1px solid #34304a;
|
||||||
|
border-radius: 8px; padding: 6px 8px; }
|
||||||
|
QComboBox QAbstractItemView { background: #1c1926; border: 1px solid #2b2740;
|
||||||
|
selection-background-color: #34304a; }
|
||||||
|
QPlainTextEdit { background: #1c1926; border: 1px solid #2b2740; border-radius: 10px;
|
||||||
|
padding: 6px; }
|
||||||
|
QProgressBar { background: #1c1926; border: 1px solid #2b2740; border-radius: 6px; }
|
||||||
|
QProgressBar::chunk { background: #def750; border-radius: 5px; }
|
||||||
|
QCheckBox { spacing: 8px; }
|
||||||
|
QCheckBox::indicator { width: 15px; height: 15px; border-radius: 4px;
|
||||||
|
border: 1px solid #6c6486; background: #1c1926; }
|
||||||
|
QCheckBox::indicator:checked { background: #def750; border-color: #def750; }
|
||||||
|
QMessageBox { background: #1b1826; }
|
||||||
|
"""
|
||||||
CRASH_LOG = os.path.join(os.path.dirname(CONFIG_PATH), "gui-crash.log")
|
CRASH_LOG = os.path.join(os.path.dirname(CONFIG_PATH), "gui-crash.log")
|
||||||
ACCENT = QColor(222, 247, 80) # warna aksen GAMEMAX dari appConfig.ini
|
ACCENT = QColor(222, 247, 80) # warna aksen GAMEMAX dari appConfig.ini
|
||||||
DIM = QColor(120, 128, 120)
|
DIM = QColor(120, 128, 120)
|
||||||
@@ -96,6 +126,19 @@ def ensure_ca_bundle():
|
|||||||
return
|
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():
|
def latest_release():
|
||||||
"""(tag, url aset) dari rilis terbaru GitHub — dipanggil dari thread pekerja."""
|
"""(tag, url aset) dari rilis terbaru GitHub — dipanggil dari thread pekerja."""
|
||||||
ensure_ca_bundle()
|
ensure_ca_bundle()
|
||||||
@@ -288,15 +331,20 @@ class Window(QWidget):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.tray = tray
|
self.tray = tray
|
||||||
self.prev_stat = gr.stat_snapshot()
|
self.prev_stat = gr.stat_snapshot()
|
||||||
self.setWindowTitle("GAMEMAX IceBurg LCD")
|
self.setWindowTitle(f"GAMEMAX IceBurg LCD v{APP_VERSION}")
|
||||||
self.setWindowIcon(QIcon(icon_path()))
|
self.setWindowIcon(QIcon(icon_path()))
|
||||||
self.setMinimumWidth(640)
|
self.setMinimumWidth(640)
|
||||||
root = QVBoxLayout(self)
|
root = QVBoxLayout(self)
|
||||||
|
self._extra = {k: v for k, v in gr.load_config(CONFIG_PATH).items()
|
||||||
|
if k in ("close_action", "geometry")}
|
||||||
|
geom = self._extra.get("geometry")
|
||||||
|
if isinstance(geom, list) and len(geom) == 4:
|
||||||
|
self.setGeometry(*[int(g) for g in geom])
|
||||||
|
|
||||||
hint = QLabel("Kontrol layar LCD AIO GAMEMAX. Pilih slot kipas/sumber suhu di "
|
hint = QLabel("Kontrol layar LCD AIO GAMEMAX. Pilih slot kipas/sumber suhu di "
|
||||||
"<b>Pengaturan</b> lalu klik <b>Simpan & terapkan</b>.")
|
"<b>Pengaturan</b> lalu klik <b>Simpan & terapkan</b>.")
|
||||||
|
hint.setObjectName("hint")
|
||||||
hint.setWordWrap(True)
|
hint.setWordWrap(True)
|
||||||
hint.setStyleSheet("color:#c8ccc8;")
|
|
||||||
root.addWidget(hint)
|
root.addWidget(hint)
|
||||||
|
|
||||||
# ---- banner mode layar (jelas: normal / kosong / berhenti)
|
# ---- banner mode layar (jelas: normal / kosong / berhenti)
|
||||||
@@ -314,7 +362,7 @@ class Window(QWidget):
|
|||||||
controls = QHBoxLayout()
|
controls = QHBoxLayout()
|
||||||
self.toggle = QPushButton("Aktifkan layar")
|
self.toggle = QPushButton("Aktifkan layar")
|
||||||
self.toggle.setMinimumHeight(38)
|
self.toggle.setMinimumHeight(38)
|
||||||
self.toggle.setStyleSheet("font-weight: bold;")
|
self.toggle.setObjectName("primary")
|
||||||
self.toggle.clicked.connect(lambda: self.start_task("mengubah status layanan", self.on_toggle))
|
self.toggle.clicked.connect(lambda: self.start_task("mengubah status layanan", self.on_toggle))
|
||||||
self.toggle.setToolTip("Mulai/hentikan pengiriman data sensor ke layar LCD")
|
self.toggle.setToolTip("Mulai/hentikan pengiriman data sensor ke layar LCD")
|
||||||
controls.addWidget(self.toggle, 2)
|
controls.addWidget(self.toggle, 2)
|
||||||
@@ -399,6 +447,10 @@ class Window(QWidget):
|
|||||||
links.addWidget(donate)
|
links.addWidget(donate)
|
||||||
links.addWidget(github)
|
links.addWidget(github)
|
||||||
links.addStretch(1)
|
links.addStretch(1)
|
||||||
|
copy_button = QPushButton("Salin report")
|
||||||
|
copy_button.setToolTip("Salin isi report 65 byte ke papan klip (untuk laporan bug)")
|
||||||
|
copy_button.clicked.connect(self.copy_report)
|
||||||
|
links.addWidget(copy_button)
|
||||||
self.check_button = QPushButton("Periksa pembaruan")
|
self.check_button = QPushButton("Periksa pembaruan")
|
||||||
self.check_button.setToolTip("Bandingkan dengan rilis terbaru di GitHub")
|
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))
|
self.check_button.clicked.connect(lambda: self.start_task("memeriksa pembaruan", self.do_check_update))
|
||||||
@@ -433,7 +485,8 @@ 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
|
testing = any(f in sys.argv for f in ("--selftest", "--selftest-actions"))
|
||||||
|
if installed_appimage() and not testing: # hanya pada versi terpasang
|
||||||
QTimer.singleShot(2500, lambda: self.start_task("memeriksa pembaruan",
|
QTimer.singleShot(2500, lambda: self.start_task("memeriksa pembaruan",
|
||||||
self.do_check_update))
|
self.do_check_update))
|
||||||
if gr.load_config(CONFIG_PATH).get("blank"):
|
if gr.load_config(CONFIG_PATH).get("blank"):
|
||||||
@@ -469,6 +522,7 @@ class Window(QWidget):
|
|||||||
|
|
||||||
def config(self):
|
def config(self):
|
||||||
return {
|
return {
|
||||||
|
**self._extra,
|
||||||
"fan_channel": self.fan.currentData() or None,
|
"fan_channel": self.fan.currentData() or None,
|
||||||
"temp_channel": self.temp.currentData() or None,
|
"temp_channel": self.temp.currentData() or None,
|
||||||
"unit": "c", # panel hanya punya label °C
|
"unit": "c", # panel hanya punya label °C
|
||||||
@@ -478,6 +532,67 @@ class Window(QWidget):
|
|||||||
"blank": bool(self.blank.isChecked()),
|
"blank": bool(self.blank.isChecked()),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def copy_report(self):
|
||||||
|
QApplication.clipboard().setText(getattr(self, "_report_hex", ""))
|
||||||
|
self.log("isi report disalin ke papan klip")
|
||||||
|
|
||||||
|
def save_geometry(self):
|
||||||
|
g = self.geometry()
|
||||||
|
self._extra["geometry"] = [g.x(), g.y(), g.width(), g.height()]
|
||||||
|
try:
|
||||||
|
self.write_config(self.config())
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def closeEvent(self, event): # noqa: N802 (Qt)
|
||||||
|
if getattr(self, "_quitting", False):
|
||||||
|
self.save_geometry()
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
action = self._extra.get("close_action", "ask")
|
||||||
|
if action == "tray" and self.tray.available():
|
||||||
|
self.save_geometry()
|
||||||
|
self.hide()
|
||||||
|
event.ignore()
|
||||||
|
return
|
||||||
|
if action == "quit":
|
||||||
|
self.save_geometry()
|
||||||
|
self._quitting = True
|
||||||
|
event.accept()
|
||||||
|
QApplication.quit()
|
||||||
|
return
|
||||||
|
|
||||||
|
box = QMessageBox(self)
|
||||||
|
box.setWindowTitle("Tutup GAMEMAX LCD")
|
||||||
|
box.setText("Sembunyikan ke tray atau keluar sepenuhnya?")
|
||||||
|
box.setInformativeText("Selama tersembunyi di tray, data sensor tetap dikirim ke layar.")
|
||||||
|
remember = QCheckBox("Ingat pilihan ini")
|
||||||
|
box.setCheckBox(remember)
|
||||||
|
hide_btn = box.addButton("Sembunyikan ke tray", QMessageBox.AcceptRole)
|
||||||
|
quit_btn = box.addButton("Keluar", QMessageBox.DestructiveRole)
|
||||||
|
box.addButton("Batal", QMessageBox.RejectRole)
|
||||||
|
if not self.tray.available():
|
||||||
|
hide_btn.setEnabled(False)
|
||||||
|
box.setInformativeText("Tray tidak tersedia di desktop ini — menutup jendela akan "
|
||||||
|
"menghentikan aplikasi.")
|
||||||
|
box.exec()
|
||||||
|
clicked = box.clickedButton()
|
||||||
|
if clicked is hide_btn:
|
||||||
|
if remember.isChecked():
|
||||||
|
self._extra["close_action"] = "tray"
|
||||||
|
self.save_geometry()
|
||||||
|
self.hide()
|
||||||
|
event.ignore()
|
||||||
|
elif clicked is quit_btn:
|
||||||
|
if remember.isChecked():
|
||||||
|
self._extra["close_action"] = "quit"
|
||||||
|
self.save_geometry()
|
||||||
|
self._quitting = True
|
||||||
|
event.accept()
|
||||||
|
QApplication.quit()
|
||||||
|
else:
|
||||||
|
event.ignore()
|
||||||
|
|
||||||
# -------------------------------------------------------------- pembaruan
|
# -------------------------------------------------------------- pembaruan
|
||||||
def do_check_update(self):
|
def do_check_update(self):
|
||||||
tag, url = latest_release()
|
tag, url = latest_release()
|
||||||
@@ -503,6 +618,10 @@ class Window(QWidget):
|
|||||||
ensure_ca_bundle()
|
ensure_ca_bundle()
|
||||||
tmp = target + ".new"
|
tmp = target + ".new"
|
||||||
urllib.request.urlretrieve(url, tmp)
|
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.chmod(tmp, 0o755)
|
||||||
os.replace(tmp, target) # ganti berkas secara atomik
|
os.replace(tmp, target) # ganti berkas secara atomik
|
||||||
# sender di layanan juga diambil dari bundel baru
|
# sender di layanan juga diambil dari bundel baru
|
||||||
@@ -514,7 +633,11 @@ class Window(QWidget):
|
|||||||
os.makedirs(INSTALL_DIR, exist_ok=True)
|
os.makedirs(INSTALL_DIR, exist_ok=True)
|
||||||
shutil.copyfile(sender, os.path.join(INSTALL_DIR, "gamemax_report.py"))
|
shutil.copyfile(sender, os.path.join(INSTALL_DIR, "gamemax_report.py"))
|
||||||
systemctl("restart", SERVICE)
|
systemctl("restart", SERVICE)
|
||||||
return f"diperbarui ke {tag} — tutup jendela ini lalu buka lagi untuk memakai versi baru"
|
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):
|
||||||
@@ -685,6 +808,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:
|
||||||
@@ -793,7 +918,7 @@ class Tray:
|
|||||||
github_action = QAction("GitHub", menu)
|
github_action = QAction("GitHub", menu)
|
||||||
github_action.triggered.connect(lambda: open_link(GITHUB_URL, self.window))
|
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(self.quit_app)
|
||||||
menu.addAction(show)
|
menu.addAction(show)
|
||||||
menu.addAction(toggle)
|
menu.addAction(toggle)
|
||||||
menu.addAction(self.blank_action)
|
menu.addAction(self.blank_action)
|
||||||
@@ -809,6 +934,13 @@ class Tray:
|
|||||||
self.window = Window(self)
|
self.window = Window(self)
|
||||||
self.icon.show()
|
self.icon.show()
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
return QSystemTrayIcon.isSystemTrayAvailable()
|
||||||
|
|
||||||
|
def quit_app(self):
|
||||||
|
self.window._quitting = True
|
||||||
|
self.app.quit()
|
||||||
|
|
||||||
def sync(self, active):
|
def sync(self, active):
|
||||||
blank = bool(gr.load_config(CONFIG_PATH).get("blank"))
|
blank = bool(gr.load_config(CONFIG_PATH).get("blank"))
|
||||||
state = ("layar kosong (nilai nol)" if blank and active
|
state = ("layar kosong (nilai nol)" if blank and active
|
||||||
@@ -845,6 +977,7 @@ def main():
|
|||||||
def _main():
|
def _main():
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
app.setApplicationName("GAMEMAX LCD")
|
app.setApplicationName("GAMEMAX LCD")
|
||||||
|
app.setStyleSheet(APP_QSS)
|
||||||
app.setQuitOnLastWindowClosed(False)
|
app.setQuitOnLastWindowClosed(False)
|
||||||
|
|
||||||
# hanya satu instance: yang kedua memunculkan jendela yang sudah ada lalu keluar
|
# hanya satu instance: yang kedua memunculkan jendela yang sudah ada lalu keluar
|
||||||
@@ -866,7 +999,14 @@ def _main():
|
|||||||
|
|
||||||
tray = Tray(app)
|
tray = Tray(app)
|
||||||
server.newConnection.connect(lambda: tray.show_window())
|
server.newConnection.connect(lambda: tray.show_window())
|
||||||
app.aboutToQuit.connect(lambda: (server.close(), QLocalServer.removeServer(SOCKET_NAME)))
|
def on_quit():
|
||||||
|
task = getattr(tray.window, "_task", None)
|
||||||
|
if task is not None and task.isRunning():
|
||||||
|
task.wait(3000)
|
||||||
|
server.close()
|
||||||
|
QLocalServer.removeServer(SOCKET_NAME)
|
||||||
|
|
||||||
|
app.aboutToQuit.connect(on_quit)
|
||||||
# Jendela selalu dibuka saat diluncurkan; tray tetap ada kalau didukung desktop.
|
# Jendela selalu dibuka saat diluncurkan; tray tetap ada kalau didukung desktop.
|
||||||
tray.show_window()
|
tray.show_window()
|
||||||
if "--tray-only" in sys.argv:
|
if "--tray-only" in sys.argv:
|
||||||
@@ -896,8 +1036,8 @@ def _main():
|
|||||||
("periksa pembaruan", w.do_check_update))
|
("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
|
||||||
@@ -926,6 +1066,7 @@ def _main():
|
|||||||
print("log:", " | ".join(w.logview.toPlainText().splitlines()[-6:]))
|
print("log:", " | ".join(w.logview.toPlainText().splitlines()[-6:]))
|
||||||
return 0
|
return 0
|
||||||
if "--selftest" in sys.argv:
|
if "--selftest" in sys.argv:
|
||||||
|
tray.window._quitting = True # keluar otomatis: lewati dialog tutup
|
||||||
QTimer.singleShot(1200, app.quit)
|
QTimer.singleShot(1200, app.quit)
|
||||||
rc = app.exec()
|
rc = app.exec()
|
||||||
if "--selftest" in sys.argv:
|
if "--selftest" in sys.argv:
|
||||||
|
|||||||
Reference in new issue
Block a user