4 Commits
Author SHA1 Message Date
oxpoe 4711ef5532 refactor(ui): nama aplikasi jadi 'IceBurg LCD' (tanpa merek vendor di UI)
- judul jendela, tooltip tray, notifikasi, nama aplikasi Qt, dan deskripsi unit systemd
- entri menu di install.sh + petunjuk di jendela
- README: catatan bahwa UI tidak memakai merek; repo tetap menyebut merek hanya untuk
  menjelaskan hardware yang didukung
- versi 1.3.1
2026-10-13 09:44:17 +07:00
oxpoe c384df7cac feat: install.sh — pemasangan/pelepasan satu perintah
- unduh rilis terbaru (+ vX.Y.Z), verifikasi sha256, pasang AppImage, sender, unit systemd,
  entri menu, ikon, dan aturan udev
- opsi: --uninstall, --remove-data, --no-service, --version
- udev memakai sudo; kalau dijalankan lewat pipa tanpa kata sandi, perintahnya dicetak
- README: cara satu perintah di bagian Install
2026-10-13 09:25:11 +07:00
oxpoe 1f3222bc96 feat: rilis v1.3.0 — popup tutup (tray/keluar), tampilan modern ringan, salin report, ingat ukuran jendela
- closeEvent: dialog 'Sembunyikan ke tray / Keluar / Batal' + 'Ingat pilihan ini'
  (tersimpan sebagai close_action; tombol sembunyikan dinonaktifkan bila tray tidak tersedia)
- QSS tema gelap modern: kartu membulat, tombol utama beraksen, kontrol bergaya; tanpa aset/efek berat
- tombol 'Salin report' (hex ke papan klip) untuk laporan bug
- ukuran & posisi jendela diingat (config geometry)
- perbaikan: dialog tutup tidak muncul saat keluar otomatis (--selftest), tunggu tugas latar saat keluar
- suite uji: 13 langkah, semua lolos di bundel beku
2026-10-13 09:02:50 +07:00
oxpoe ac23195996 build: sematkan info zsync ke AppImage (appimageupdatetool langsung bisa dipakai)
docs: catat bahwa rilis pasca-v1.2.3 membawa update information
2026-10-13 08:15:55 +07:00
6 changed files with 341 additions and 22 deletions

No files matched your search

+1
View File
@@ -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)
+15 -2
View File
@@ -109,7 +109,8 @@ temperature source (Tctl vs Tccd).
- **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)
The GUI labels are in Indonesian. The GUI labels are in Indonesian. The app itself is named **IceBurg LCD** (no vendor brand in
the UI); the repository keeps the hardware and brand name only to describe what it supports.
## Requirements ## Requirements
@@ -123,6 +124,16 @@ The GUI labels are in Indonesian.
## Install ## Install
One command (downloads the latest release, verifies its SHA-256, installs the app, the
systemd user unit, the menu entry and the udev rule):
```bash
curl -fsSL https://raw.githubusercontent.com/oxpoe/Gamemax-linux-apps/main/install.sh | bash
# uninstall: bash install.sh --uninstall (add --remove-data to drop settings too)
```
Manual steps, if you prefer to see every command:
```bash ```bash
# 1. device access for the logged-in user (once, needs root) # 1. device access for the logged-in user (once, needs root)
sudo cp packaging/70-gamemax.rules /etc/udev/rules.d/ sudo cp packaging/70-gamemax.rules /etc/udev/rules.d/
@@ -225,7 +236,9 @@ The app keeps itself current from GitHub Releases:
Each release publishes three assets: the AppImage, its `.sha256`, and a `.zsync` file for Each release publishes three assets: the AppImage, its `.sha256`, and a `.zsync` file for
`appimageupdatetool` / AppImageUpdate (delta updates). The zsync file is optional — the `appimageupdatetool` / AppImageUpdate (delta updates). The zsync file is optional — the
built-in updater does not need it. 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` 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. also mirrors the log to stdout for headless runs.
Executable
+181
View File
@@ -0,0 +1,181 @@
#!/usr/bin/env bash
# GAMEMAX LCD — pemasangan / pelepasan satu perintah.
#
# curl -fsSL https://raw.githubusercontent.com/oxpoe/Gamemax-linux-apps/main/install.sh | bash
#
# Opsi:
# --uninstall lepaskan pemasangan (layanan, aplikasi, config, ikon, entri menu)
# --no-service pasang aplikasi saja, jangan aktifkan layanan/autostart
# --version vX.Y.Z pasang versi rilis tertentu (default: rilis terbaru)
# --remove-data saat --uninstall: hapus juga ~/.config/gamemax-report
#
# UdeV: aturan izin device dipasang dengan sudo. Kalau skrip dijalankan lewat pipa
# (curl | bash) dan sudo butuh kata sandi, perintahnya dicetak untuk dijalankan manual.
set -euo pipefail
REPO="oxpoe/Gamemax-linux-apps"
ASSET="GAMEMAX-LCD-x86_64.AppImage"
RAW="https://raw.githubusercontent.com/$REPO/main"
OPT_DIR="$HOME/.local/opt/gamemax-lcd"
APP="$OPT_DIR/GAMEMAX-LCD.AppImage"
DATA_DIR="$HOME/.local/share/gamemax-report"
UNIT="$HOME/.config/systemd/user/gamemax-report.service"
DESKTOP_DIR="$HOME/.local/share/applications"
ICON_DIR="$HOME/.local/share/icons/hicolor/128x128/apps"
RULE=/etc/udev/rules.d/70-gamemax.rules
SERVICE="gamemax-report.service"
UNINSTALL=0 WITH_SERVICE=1 REMOVE_DATA=0 VERSION="latest"
say() { printf '%s\n' "$*"; }
die() { printf 'galat: %s\n' "$*" >&2; exit 1; }
while [ $# -gt 0 ]; do
case "$1" in
--uninstall) UNINSTALL=1 ;;
--no-service) WITH_SERVICE=0 ;;
--remove-data) REMOVE_DATA=1 ;;
--version) shift; VERSION="${1:?--version butuh nilai seperti v1.3.0}" ;;
-h|--help) sed -n '2,14p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) die "opsi tidak dikenal: $1" ;;
esac
shift
done
need() { command -v "$1" >/dev/null 2>&1 || die "perintah '$1' tidak tersedia"; }
# ---------------------------------------------------------------- uninstall
if [ "$UNINSTALL" = 1 ]; then
say "== melepaskan GAMEMAX LCD =="
command -v systemctl >/dev/null 2>&1 && systemctl --user disable --now "$SERVICE" 2>/dev/null || true
rm -f "$UNIT"
command -v systemctl >/dev/null 2>&1 && { systemctl --user daemon-reload 2>/dev/null || true; \
systemctl --user reset-failed "$SERVICE" 2>/dev/null || true; }
rm -rf "$OPT_DIR" "$DATA_DIR"
rm -f "$DESKTOP_DIR/gamemax-lcd.desktop" "$ICON_DIR/gamemax-lcd.png"
[ "$REMOVE_DATA" = 1 ] && rm -rf "$HOME/.config/gamemax-report"
command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true
if [ -f "$RULE" ]; then
if sudo -n rm -f "$RULE" 2>/dev/null; then
sudo -n udevadm control --reload 2>/dev/null || true
say " aturan udev dihapus"
else
say " aturan udev masih ada (butuh sudo): sudo rm -f $RULE && sudo udevadm control --reload"
fi
fi
say "selesai. Aplikasi, layanan, dan berkas pendukung sudah dilepas."
say "catatan: konfigurasi ~/.config/gamemax-report hanya dihapus dengan --remove-data."
exit 0
fi
# ---------------------------------------------------------------- install
need curl
need sha256sum
command -v systemctl >/dev/null 2>&1 || die "systemd (systemctl) dibutuhkan untuk layanan"
if [ "$VERSION" = "latest" ]; then
BASE="https://github.com/$REPO/releases/latest/download"
else
BASE="https://github.com/$REPO/releases/download/$VERSION"
fi
say "== GAMEMAX LCD: pemasangan ($VERSION) =="
mkdir -p "$OPT_DIR" "$DATA_DIR" "$DESKTOP_DIR" "$ICON_DIR" "$HOME/.config/systemd/user"
say "1/6 mengunduh aplikasi …"
curl -fsSL -o "$APP.part" "$BASE/$ASSET"
curl -fsSL -o "$APP.sha256" "$BASE/$ASSET.sha256"
say "2/6 memverifikasi checksum …"
EXPECTED="$(awk '{print $1}' "$APP.sha256")"
ACTUAL="$(sha256sum "$APP.part" | awk '{print $1}')"
[ "$EXPECTED" = "$ACTUAL" ] || { rm -f "$APP.part"; die "checksum tidak cocok — unduhan dibatalkan"; }
say " ok: ${ACTUAL:0:16}…"
mv -f "$APP.part" "$APP"
chmod +x "$APP"
rm -f "$APP.sha256"
say "3/6 mengambil sender + ikon dari bundel …"
TMP="$(mktemp -d)"
( cd "$TMP" && "$APP" --appimage-extract >/dev/null 2>&1 ) || true
INTERNAL="$TMP/squashfs-root/usr/bin/_internal"
if [ -f "$INTERNAL/gamemax_report.py" ]; then
cp "$INTERNAL/gamemax_report.py" "$DATA_DIR/gamemax_report.py"
else
say " ! sender tidak ditemukan di bundel — unduh dari repo"
curl -fsSL -o "$DATA_DIR/gamemax_report.py" "$RAW/src/gamemax_report.py"
fi
[ -f "$INTERNAL/icon.png" ] && cp "$INTERNAL/icon.png" "$ICON_DIR/gamemax-lcd.png" \
|| curl -fsSL -o "$ICON_DIR/gamemax-lcd.png" "$RAW/src/icon.png"
rm -rf "$TMP"
say "4/6 memasang layanan systemd (autostart + plug and play) …"
cat > "$UNIT" <<EOF
[Unit]
Description=IceBurg LCD — HID sensor display bridge (Linux)
After=graphical-session.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 $DATA_DIR/gamemax_report.py --wait --interval 0.5 --sample 0.5
Restart=always
RestartSec=3
[Install]
WantedBy=default.target
EOF
systemctl --user daemon-reload
if [ "$WITH_SERVICE" = 1 ]; then
systemctl --user enable --now "$SERVICE"
sleep 2
say " layanan: $(systemctl --user is-active "$SERVICE") / $(systemctl --user is-enabled "$SERVICE")"
else
say " dilewati (--no-service)"
fi
say "5/6 memasang entri menu + ikon …"
cat > "$DESKTOP_DIR/gamemax-lcd.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=IceBurg LCD
GenericName=Kontrol LCD AIO
Comment=Status, nyala/mati, pemantauan, dan pengaturan LCD AIO IceBurg Digital
Exec="$APP"
Icon=gamemax-lcd
Terminal=false
Categories=System;Utility;
EOF
command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true
command -v gtk-update-icon-cache >/dev/null 2>&1 && \
gtk-update-icon-cache -f -t "$HOME/.local/share/icons/hicolor" 2>/dev/null || true
say "6/6 aturan udev (izin device) …"
RULE_TMP="$(mktemp)"
curl -fsSL -o "$RULE_TMP" "$RAW/packaging/70-gamemax.rules"
if [ -f "$RULE" ] && cmp -s "$RULE" "$RULE_TMP"; then
say " sudah terpasang"
elif sudo -n install -m 644 "$RULE_TMP" "$RULE" 2>/dev/null; then
sudo -n udevadm control --reload 2>/dev/null || true
sudo -n udevadm trigger --subsystem-match=hidraw 2>/dev/null || true
say " terpasang"
elif [ -t 0 ]; then
sudo install -m 644 "$RULE_TMP" "$RULE" && sudo udevadm control --reload && \
sudo udevadm trigger --subsystem-match=hidraw && say " terpasang"
else
say " ! butuh sudo — jalankan sekali:"
say " sudo install -m 644 $RULE_TMP $RULE && sudo udevadm control --reload && sudo udevadm trigger --subsystem-match=hidraw"
fi
rm -f "$RULE_TMP"
say
say "== selesai =="
say " aplikasi : $APP"
say " sender : $DATA_DIR/gamemax_report.py"
say " menu : GAMEMAX IceBurg LCD"
say " layanan : systemctl --user status $SERVICE"
say " log : journalctl --user -u $SERVICE -f"
say
say "buka aplikasinya untuk mengatur slot kipas/sumber suhu, atau:"
say " $APP --window"
+8 -2
View File
@@ -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)"
+1 -1
View File
@@ -1,5 +1,5 @@
[Unit] [Unit]
Description=GAMEMAX HID sensor display bridge (PcInfoMonitorPro replacement) Description=IceBurg LCD — HID sensor display bridge (Linux)
Documentation=file:///mnt/SSD/Work/REVERSE-EGENERING/analysis/01-recon.md Documentation=file:///mnt/SSD/Work/REVERSE-EGENERING/analysis/01-recon.md
After=graphical-session.target After=graphical-session.target
+135 -17
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""GAMEMAX IceBurg LCD — GUI tray: status, nyala/mati, pemantauan beranimasi, pengaturan. """IceBurg LCD — GUI tray: status, nyala/mati, pemantauan beranimasi, pengaturan.
Streaming tetap dijalankan oleh layanan systemd (gamemax-report.service); GUI ini Streaming tetap dijalankan oleh layanan systemd (gamemax-report.service); GUI ini
mengendalikannya dan memakai gamemax_report.py sebagai pengirim. mengendalikannya dan memakai gamemax_report.py sebagai pengirim.
@@ -27,11 +27,13 @@ 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.3" APP_VERSION = "1.3.1"
APP_NAME = "IceBurg LCD"
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"
@@ -49,7 +51,7 @@ UNIT_PATH = os.path.expanduser(f"~/.config/systemd/user/{SERVICE}")
RULE_PATH = "/etc/udev/rules.d/70-gamemax.rules" RULE_PATH = "/etc/udev/rules.d/70-gamemax.rules"
UNIT_TEMPLATE = """[Unit] UNIT_TEMPLATE = """[Unit]
Description=GAMEMAX HID sensor display bridge (PcInfoMonitorPro replacement) Description=IceBurg LCD — HID sensor display bridge (Linux)
After=graphical-session.target After=graphical-session.target
[Service] [Service]
@@ -65,6 +67,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)
@@ -302,15 +332,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"{APP_NAME} 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 IceBurg Digital. Pilih slot kipas/sumber suhu di "
"<b>Pengaturan</b> lalu klik <b>Simpan &amp; terapkan</b>.") "<b>Pengaturan</b> lalu klik <b>Simpan &amp; 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)
@@ -328,7 +363,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)
@@ -413,6 +448,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))
@@ -447,7 +486,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"):
@@ -483,6 +523,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
@@ -492,6 +533,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(f"Tutup {APP_NAME}")
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()
@@ -745,7 +847,7 @@ class Window(QWidget):
"kosong": "Layar dikosongkan (nilai nol)", "kosong": "Layar dikosongkan (nilai nol)",
"berhenti": "Pengiriman data dihentikan"} "berhenti": "Pengiriman data dihentikan"}
if mode != "normal" or getattr(self, "_mode_seen", False): if mode != "normal" or getattr(self, "_mode_seen", False):
subprocess.run(["notify-send", "-a", "GAMEMAX LCD", "Status layar", subprocess.run(["notify-send", "-a", APP_NAME, "Status layar",
labels[mode]], capture_output=True) labels[mode]], capture_output=True)
self._mode_seen = True self._mode_seen = True
dot = "🟢" if active else "⚪" dot = "🟢" if active else "⚪"
@@ -817,7 +919,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)
@@ -833,11 +935,18 @@ 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
else ("pengiriman dihentikan" if not active else "normal")) else ("pengiriman dihentikan" if not active else "normal"))
self.icon.setToolTip(f"GAMEMAX LCD: {state}") self.icon.setToolTip(f"{APP_NAME}: {state}")
base = icon_path() base = icon_path()
self.icon.setIcon(dimmed_icon(base) if state != "normal" else QIcon(base)) self.icon.setIcon(dimmed_icon(base) if state != "normal" else QIcon(base))
window = getattr(self, "window", None) window = getattr(self, "window", None)
@@ -851,7 +960,7 @@ class Tray:
self.window.raise_() self.window.raise_()
self.window.activateWindow() self.window.activateWindow()
if QSystemTrayIcon.isSystemTrayAvailable(): if QSystemTrayIcon.isSystemTrayAvailable():
self.icon.showMessage("GAMEMAX LCD", "Jendela dibuka dari permintaan kedua.", self.icon.showMessage(APP_NAME, "Jendela dibuka dari permintaan kedua.",
QSystemTrayIcon.Information, 3000) QSystemTrayIcon.Information, 3000)
@@ -868,7 +977,8 @@ def main():
def _main(): def _main():
app = QApplication(sys.argv) app = QApplication(sys.argv)
app.setApplicationName("GAMEMAX LCD") app.setApplicationName(APP_NAME)
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
@@ -877,10 +987,10 @@ def _main():
if probe.waitForConnected(400): if probe.waitForConnected(400):
probe.write(b"raise") probe.write(b"raise")
probe.waitForBytesWritten(400) probe.waitForBytesWritten(400)
print("GAMEMAX LCD sudah berjalan — instance pertama diminta membuka jendelanya.") print(f"{APP_NAME} sudah berjalan — instance pertama diminta membuka jendelanya.")
# Beri tahu lewat notifikasi desktop juga, karena keluarnya proses kedua bisa # Beri tahu lewat notifikasi desktop juga, karena keluarnya proses kedua bisa
# terlihat seperti crash saat diluncurkan dari menu. # terlihat seperti crash saat diluncurkan dari menu.
subprocess.run(["notify-send", "-a", "GAMEMAX LCD", "Sudah berjalan", subprocess.run(["notify-send", "-a", APP_NAME, "Sudah berjalan",
"Aplikasi sudah aktif — jendelanya dibuka, cek taskbar/tray."], "Aplikasi sudah aktif — jendelanya dibuka, cek taskbar/tray."],
capture_output=True) capture_output=True)
return 0 return 0
@@ -890,7 +1000,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:
@@ -950,6 +1067,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: