5 Commits
Author SHA1 Message Date
oxpoe c3d34a2788 fix(packaging): nama entri menu di dalam AppImage jadi 'IceBurg LCD'
AppImage manager menyalin .desktop dari dalam bundel, sehingga entri di app drawer
masih bernama merek vendor walau UI aplikasinya sudah diganti. Kini bundel membawa
Name=IceBurg LCD (GenericName/Comment juga tanpa merek). Versi 1.3.2
2026-10-13 09:51:06 +07:00
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 344 additions and 25 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"
+11 -5
View File
@@ -42,22 +42,28 @@ cp "$ROOT_DIR/src/icon.png" "$APPDIR/gamemax-lcd.png"
cat > "$APPDIR/gamemax-lcd.desktop" <<'EOF' cat > "$APPDIR/gamemax-lcd.desktop" <<'EOF'
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=GAMEMAX IceBurg LCD Name=IceBurg LCD
GenericName=AI LCD controller GenericName=Kontrol LCD AIO
Comment=Status, on/off, monitor, and settings for the GAMEMAX IceBurg AIO LCD Comment=Status, nyala/mati, pemantauan, dan pengaturan LCD AIO IceBurg Digital
Exec=gamemax-lcd Exec=gamemax-lcd
Icon=gamemax-lcd Icon=gamemax-lcd
Terminal=false 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.2"
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: