33c554c29f
Games page gains an "Import crash dump…" button (shown when an AI provider is configured) that parses a Proton/Wine minidump and explains it via the opt-in AI assistant. New stdlib core/minidump.py reads the MDMP streams (crash reason, faulting module, OS/CPU, module list), optionally enriched by minidump_stackwalk if installed. Adds ai_knowledge facts for exception codes + faulting-module signatures, a MinidumpDialog, and CLI parity via `rigdoctor ai dump <file>`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
572 lines
22 KiB
Python
572 lines
22 KiB
Python
"""Games page (M6 in the GUI): pick Steam libraries and browse detected games.
|
|
|
|
Libraries are opt-in — the user checks which ones to scan. The list is loaded from the
|
|
cache instantly, then a background rescan refreshes it and flags games installed since the
|
|
last scan (a "NEW" badge here + a count on the sidebar nav).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import threading
|
|
import time
|
|
|
|
from PySide6.QtCore import Qt, QTimer, Signal
|
|
from PySide6.QtWidgets import (
|
|
QApplication,
|
|
QCheckBox,
|
|
QDialog,
|
|
QFileDialog,
|
|
QFrame,
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QLineEdit,
|
|
QMessageBox,
|
|
QPushButton,
|
|
QScrollArea,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
from ..config import load_config, update_config
|
|
from .diagnostic_dialog import DiagnosticDialog
|
|
from .minidump_dialog import MinidumpDialog
|
|
from .theme import ACCENT, GOOD, MUTED, WARN
|
|
|
|
|
|
def _game_row(name: str, sublabel: str, size: str, is_new: bool, appid: str = "", on_diagnose=None) -> QFrame:
|
|
card = QFrame()
|
|
card.setObjectName("Card")
|
|
h = QHBoxLayout(card)
|
|
h.setContentsMargins(16, 10, 16, 10)
|
|
h.setSpacing(10)
|
|
|
|
left = QVBoxLayout()
|
|
left.setSpacing(2)
|
|
title = QLabel(name)
|
|
title.setStyleSheet("font-weight: 600; background: transparent;")
|
|
title.setWordWrap(True)
|
|
left.addWidget(title)
|
|
if sublabel:
|
|
sub = QLabel(sublabel)
|
|
sub.setObjectName("Muted")
|
|
left.addWidget(sub)
|
|
h.addLayout(left, 1)
|
|
|
|
if is_new:
|
|
badge = QLabel("NEW")
|
|
badge.setStyleSheet(
|
|
f"color: {GOOD}; border: 1px solid {GOOD}; border-radius: 6px; "
|
|
f"padding: 1px 6px; font-weight: 700; background: transparent;"
|
|
)
|
|
h.addWidget(badge, 0, Qt.AlignmentFlag.AlignVCenter)
|
|
|
|
size_label = QLabel(size)
|
|
size_label.setObjectName("Muted")
|
|
size_label.setMinimumWidth(80)
|
|
size_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
|
h.addWidget(size_label, 0)
|
|
|
|
if on_diagnose is not None:
|
|
diag_btn = QPushButton("Run Diagnostic")
|
|
diag_btn.setObjectName("ActionButton")
|
|
diag_btn.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
diag_btn.clicked.connect(lambda: on_diagnose(name, appid))
|
|
h.addWidget(diag_btn, 0)
|
|
return card
|
|
|
|
|
|
class GamesPage(QWidget):
|
|
_libraries_ready = Signal(object) # list[dict(path, label, count, selected)]
|
|
_scanned = Signal(object) # steam.ScanResult
|
|
new_count_changed = Signal(int) # newly-installed game count (for the nav badge)
|
|
_diag_done = Signal(object) # DiagnosticResult — focused capture analyzed
|
|
_dump_parsed = Signal(object) # minidump.MinidumpReport — imported .dmp (or None)
|
|
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.setObjectName("Page")
|
|
self._libraries_ready.connect(self._render_libraries)
|
|
self._scanned.connect(self._render_games)
|
|
self._diag_done.connect(self._on_diag_done)
|
|
self._dump_parsed.connect(self._on_dump_parsed)
|
|
self._busy = False
|
|
self._new_appids: set[str] = set()
|
|
self._extra_games: list = [] # non-Steam (Lutris/Heroic), appended after a scan
|
|
self._diag_game: str | None = None
|
|
|
|
root = QVBoxLayout(self)
|
|
root.setContentsMargins(20, 18, 20, 18)
|
|
root.setSpacing(16)
|
|
|
|
header = QHBoxLayout()
|
|
title = QLabel("Games")
|
|
title.setObjectName("PageTitle")
|
|
header.addWidget(title)
|
|
header.addStretch(1)
|
|
self._status = QLabel("")
|
|
self._status.setObjectName("Muted")
|
|
header.addWidget(self._status)
|
|
# Import a Windows crash dump (.dmp) from a Proton game and analyze it with AI.
|
|
# Shown only when an AI provider is configured (AI analysis is the point).
|
|
self._import_btn = QPushButton("Import crash dump…")
|
|
self._import_btn.clicked.connect(self._import_dump)
|
|
header.addWidget(self._import_btn)
|
|
self._autocap_btn = QPushButton("Auto-capture…")
|
|
self._autocap_btn.clicked.connect(self._show_autocapture)
|
|
header.addWidget(self._autocap_btn)
|
|
self._rescan_btn = QPushButton("Rescan")
|
|
self._rescan_btn.setObjectName("PrimaryButton")
|
|
self._rescan_btn.clicked.connect(self.refresh)
|
|
header.addWidget(self._rescan_btn)
|
|
root.addLayout(header)
|
|
|
|
# In-progress diagnostic banner (hidden until a focused capture is running).
|
|
self._banner = QFrame()
|
|
self._banner.setObjectName("Card")
|
|
self._banner.setStyleSheet(f"#Card {{ border: 1px solid {ACCENT}; }}")
|
|
banner_h = QHBoxLayout(self._banner)
|
|
banner_h.setContentsMargins(16, 10, 16, 10)
|
|
banner_h.setSpacing(10)
|
|
self._banner_label = QLabel("")
|
|
self._banner_label.setWordWrap(True)
|
|
self._banner_label.setStyleSheet(f"color: {ACCENT}; font-weight: 700; background: transparent;")
|
|
banner_h.addWidget(self._banner_label, 1)
|
|
self._finish_btn = QPushButton("Finish && analyze") # && → literal & (not a mnemonic)
|
|
self._finish_btn.setObjectName("ActionButton")
|
|
self._finish_btn.clicked.connect(self._finish_diagnostic)
|
|
banner_h.addWidget(self._finish_btn)
|
|
self._discard_btn = QPushButton("Discard")
|
|
self._discard_btn.clicked.connect(self._discard_diagnostic)
|
|
banner_h.addWidget(self._discard_btn)
|
|
self._banner.hide()
|
|
root.addWidget(self._banner)
|
|
|
|
# Hard-crash banner: a previous diagnostic ended without a clean stop.
|
|
self._crash_banner = QFrame()
|
|
self._crash_banner.setObjectName("Card")
|
|
self._crash_banner.setStyleSheet(f"#Card {{ border: 1px solid {WARN}; }}")
|
|
crash_h = QHBoxLayout(self._crash_banner)
|
|
crash_h.setContentsMargins(16, 10, 16, 10)
|
|
crash_h.setSpacing(10)
|
|
self._crash_label = QLabel("")
|
|
self._crash_label.setWordWrap(True)
|
|
self._crash_label.setStyleSheet(f"color: {WARN}; font-weight: 700; background: transparent;")
|
|
crash_h.addWidget(self._crash_label, 1)
|
|
self._analyze_btn = QPushButton("Analyze crash")
|
|
self._analyze_btn.setObjectName("ActionButton")
|
|
self._analyze_btn.clicked.connect(self._analyze_crash)
|
|
crash_h.addWidget(self._analyze_btn)
|
|
self._dismiss_btn = QPushButton("Dismiss")
|
|
self._dismiss_btn.clicked.connect(self._dismiss_crash)
|
|
crash_h.addWidget(self._dismiss_btn)
|
|
self._crash_banner.hide()
|
|
root.addWidget(self._crash_banner)
|
|
|
|
self._diag_timer = QTimer(self)
|
|
self._diag_timer.setInterval(1000)
|
|
self._diag_timer.timeout.connect(self._poll_diag)
|
|
|
|
# Libraries (opt-in checkboxes)
|
|
lib_card = QFrame()
|
|
lib_card.setObjectName("Card")
|
|
lib_v = QVBoxLayout(lib_card)
|
|
lib_v.setContentsMargins(16, 12, 16, 12)
|
|
lib_v.setSpacing(6)
|
|
lib_head = QLabel("Steam libraries")
|
|
lib_head.setStyleSheet("font-weight: 700; background: transparent;")
|
|
lib_v.addWidget(lib_head)
|
|
self._lib_box = QVBoxLayout()
|
|
self._lib_box.setSpacing(6)
|
|
lib_v.addLayout(self._lib_box)
|
|
self._lib_hint = QLabel("Looking for Steam libraries…")
|
|
self._lib_hint.setObjectName("Muted")
|
|
self._lib_hint.setWordWrap(True)
|
|
lib_v.addWidget(self._lib_hint)
|
|
root.addWidget(lib_card)
|
|
|
|
# Games list
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
|
scroll.setStyleSheet("background: transparent;")
|
|
self._container = QWidget()
|
|
self._list = QVBoxLayout(self._container)
|
|
self._list.setContentsMargins(0, 0, 0, 0)
|
|
self._list.setSpacing(8)
|
|
self._list.setAlignment(Qt.AlignmentFlag.AlignTop)
|
|
scroll.setWidget(self._container)
|
|
root.addWidget(scroll, 1)
|
|
|
|
self._load_cached() # instant display from the last scan
|
|
QTimer.singleShot(400, self.refresh) # then rescan in the background on launch
|
|
self._check_crash() # surface an interrupted (crashed) diagnostic
|
|
self._refresh_import_btn() # show Import only if AI is configured
|
|
|
|
# --- loading ----------------------------------------------------------------------
|
|
|
|
def _load_cached(self) -> None:
|
|
from ..core import steam
|
|
|
|
cache = steam.load_cache() or {}
|
|
self._new_appids = set(cache.get("new_appids", []))
|
|
games = steam.cached_games()
|
|
if games:
|
|
self._populate_games(games, self._new_appids)
|
|
self.new_count_changed.emit(len(self._new_appids))
|
|
|
|
def refresh(self) -> None:
|
|
if self._busy:
|
|
return
|
|
self._busy = True
|
|
self._rescan_btn.setEnabled(False)
|
|
self._status.setText("Scanning Steam libraries…")
|
|
threading.Thread(target=self._work, daemon=True).start()
|
|
|
|
def _work(self) -> None:
|
|
from ..core import launchers, steam
|
|
|
|
try:
|
|
selected = {os.path.realpath(p) for p in steam.selected_library_paths()}
|
|
libs = [
|
|
{"path": lib.path, "label": lib.label, "selected": lib.path in selected,
|
|
"count": len(steam.scan_library(lib.path))}
|
|
for lib in steam.discover_libraries()
|
|
]
|
|
self._libraries_ready.emit(libs)
|
|
try:
|
|
self._extra_games = launchers.scan() # Lutris / Heroic (non-Steam)
|
|
except Exception:
|
|
self._extra_games = []
|
|
self._scanned.emit(steam.rescan())
|
|
except Exception:
|
|
self._scanned.emit(None)
|
|
|
|
# --- rendering --------------------------------------------------------------------
|
|
|
|
def _render_libraries(self, libs) -> None:
|
|
while self._lib_box.count():
|
|
item = self._lib_box.takeAt(0)
|
|
w = item.widget()
|
|
if w is not None:
|
|
w.deleteLater()
|
|
if not libs:
|
|
self._lib_hint.setText("No Steam libraries detected. Is Steam installed?")
|
|
self._lib_hint.show()
|
|
return
|
|
self._lib_hint.hide()
|
|
for lib in libs:
|
|
label = lib["path"]
|
|
if lib["label"]:
|
|
label += f" [{lib['label']}]"
|
|
cb = QCheckBox(f"{label} · {lib['count']} games")
|
|
cb.setChecked(lib["selected"])
|
|
cb.toggled.connect(lambda checked, p=lib["path"]: self._toggle_library(p, checked))
|
|
self._lib_box.addWidget(cb)
|
|
|
|
def _toggle_library(self, path: str, checked: bool) -> None:
|
|
selected = {os.path.realpath(p) for p in (load_config().get("steam_libraries") or [])}
|
|
if checked:
|
|
selected.add(os.path.realpath(path))
|
|
else:
|
|
selected.discard(os.path.realpath(path))
|
|
update_config(steam_libraries=sorted(selected))
|
|
self.refresh()
|
|
|
|
def _render_games(self, result) -> None:
|
|
self._busy = False
|
|
self._rescan_btn.setEnabled(True)
|
|
if result is None:
|
|
self._status.setText("scan failed")
|
|
return
|
|
self._new_appids = set(result.new_appids)
|
|
games = list(result.games) + list(self._extra_games)
|
|
self._populate_games(games, self._new_appids)
|
|
new = len(self._new_appids)
|
|
suffix = f" · {new} new" if new else ""
|
|
non_steam = f" · {len(self._extra_games)} non-Steam" if self._extra_games else ""
|
|
self._status.setText(
|
|
f"{len(games)} games · {time.strftime('%H:%M:%S')}{suffix}{non_steam}"
|
|
)
|
|
self.new_count_changed.emit(new)
|
|
|
|
def _populate_games(self, games, new_appids: set[str]) -> None:
|
|
from ..core import steam
|
|
|
|
while self._list.count():
|
|
item = self._list.takeAt(0)
|
|
w = item.widget()
|
|
if w is not None:
|
|
w.deleteLater()
|
|
|
|
if not games:
|
|
empty = QLabel(
|
|
"No games to show yet — check a Steam library above to scan it for games."
|
|
)
|
|
empty.setObjectName("Muted")
|
|
empty.setWordWrap(True)
|
|
self._list.addWidget(empty)
|
|
self._list.addStretch(1)
|
|
return
|
|
|
|
for g in games:
|
|
launcher = getattr(g, "launcher", "steam")
|
|
if launcher != "steam":
|
|
sublabel, appid = launcher.title(), "" # non-Steam: can't steam:// launch it
|
|
else:
|
|
sublabel, appid = (os.path.basename(g.library.rstrip("/")) or g.library), g.appid
|
|
self._list.addWidget(_game_row(
|
|
g.name,
|
|
sublabel,
|
|
steam.human_size(g.size_bytes),
|
|
g.appid in new_appids,
|
|
appid=appid,
|
|
on_diagnose=self._start_diagnostic,
|
|
))
|
|
self._list.addStretch(1)
|
|
|
|
# --- guided diagnostic (M6/D12) ---------------------------------------------------
|
|
|
|
def _start_diagnostic(self, name: str, appid: str = "") -> None:
|
|
from ..core import diagnostic, steam
|
|
|
|
if diagnostic.is_running():
|
|
QMessageBox.information(
|
|
self, "RigDoctor",
|
|
"A capture is already running — finish or discard it first.")
|
|
return
|
|
|
|
# Tell the user what the flow actually is, and offer to launch the game for them.
|
|
box = QMessageBox(self)
|
|
box.setIcon(QMessageBox.Icon.Information)
|
|
box.setWindowTitle(f"Run Diagnostic — {name}")
|
|
box.setText(f"Record a focused diagnostic while you play {name}?")
|
|
box.setInformativeText(
|
|
"RigDoctor will capture sensors in the background. Then:\n\n"
|
|
"1. Play the game and try to reproduce the freeze / black screen / crash.\n"
|
|
"2. When you're done — or after a hard freeze and reboot — come back here and "
|
|
"click “Finish & analyze”.\n\n"
|
|
"Your readings are saved continuously, so even a hard lock won't lose them."
|
|
)
|
|
launch_btn = box.addButton("Launch game && start", QMessageBox.ButtonRole.AcceptRole)
|
|
start_btn = box.addButton("Start without launching", QMessageBox.ButtonRole.ActionRole)
|
|
box.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
|
|
if not appid:
|
|
launch_btn.setEnabled(False) # no appid → can't ask Steam to launch it
|
|
box.exec()
|
|
clicked = box.clickedButton()
|
|
if clicked not in (launch_btn, start_btn):
|
|
return
|
|
|
|
if diagnostic.start(game=name) is None:
|
|
QMessageBox.warning(self, "RigDoctor", "Couldn't start the capture.")
|
|
return
|
|
launched = steam.launch_game(appid) if clicked is launch_btn else False
|
|
self._diag_game = name
|
|
self._finish_btn.setEnabled(True)
|
|
self._discard_btn.setEnabled(True)
|
|
self._banner.show()
|
|
self._diag_timer.start()
|
|
self._poll_diag()
|
|
if clicked is launch_btn and not launched:
|
|
QMessageBox.information(
|
|
self, "RigDoctor",
|
|
"Recording started, but couldn't launch the game automatically — "
|
|
"launch it yourself, then click “Finish & analyze” when you're done.")
|
|
|
|
def _poll_diag(self) -> None:
|
|
from ..core import diagnostic
|
|
|
|
status = diagnostic.active()
|
|
if not status:
|
|
self._diag_timer.stop() # recorder exited on its own
|
|
return
|
|
samples = status.get("samples", 0)
|
|
lost = " · ⚠ GPU-lost detected" if status.get("gpu_lost") else ""
|
|
game = status.get("game") or self._diag_game or "your game"
|
|
self._banner_label.setText(
|
|
f"● Recording {game} — play it and reproduce the problem, then click "
|
|
f"“Finish & analyze”. ({samples} samples{lost})"
|
|
)
|
|
|
|
def _finish_diagnostic(self) -> None:
|
|
self._diag_timer.stop()
|
|
self._finish_btn.setEnabled(False)
|
|
self._discard_btn.setEnabled(False)
|
|
self._banner_label.setText("Analyzing… (running the health report)")
|
|
threading.Thread(target=self._work_finish, daemon=True).start()
|
|
|
|
def _work_finish(self) -> None:
|
|
from ..core import diagnostic
|
|
|
|
try:
|
|
result = diagnostic.finish()
|
|
except Exception:
|
|
result = None
|
|
self._diag_done.emit(result)
|
|
|
|
def _on_diag_done(self, result) -> None:
|
|
self._banner.hide()
|
|
self._crash_banner.hide()
|
|
self._finish_btn.setEnabled(True)
|
|
self._discard_btn.setEnabled(True)
|
|
self._analyze_btn.setEnabled(True)
|
|
if result is None:
|
|
QMessageBox.warning(self, "RigDoctor", "The diagnostic couldn't be analyzed.")
|
|
return
|
|
DiagnosticDialog(result, self).exec()
|
|
|
|
def _discard_diagnostic(self) -> None:
|
|
from ..core import reccontrol
|
|
|
|
self._diag_timer.stop()
|
|
reccontrol.stop_background()
|
|
self._banner.hide()
|
|
|
|
def _show_autocapture(self) -> None:
|
|
from ..core import wrap
|
|
|
|
option = wrap.launch_option()
|
|
dlg = QDialog(self)
|
|
dlg.setWindowTitle("Auto-capture in Steam")
|
|
dlg.resize(580, 250)
|
|
v = QVBoxLayout(dlg)
|
|
v.setContentsMargins(20, 18, 20, 16)
|
|
v.setSpacing(12)
|
|
info = QLabel(
|
|
"Capture automatically every time you launch a game — no need to click "
|
|
"Run Diagnostic.\n\n"
|
|
"1. In Steam, right-click the game → Properties → Launch Options.\n"
|
|
"2. Paste the line below.\n\n"
|
|
"RigDoctor starts a focused capture when the game launches and stops it on exit. "
|
|
"If the game hard-freezes, you'll get a crash report next time you open RigDoctor."
|
|
)
|
|
info.setWordWrap(True)
|
|
v.addWidget(info)
|
|
row = QHBoxLayout()
|
|
field = QLineEdit(option)
|
|
field.setReadOnly(True)
|
|
row.addWidget(field, 1)
|
|
copy = QPushButton("Copy")
|
|
copy.setObjectName("PrimaryButton")
|
|
copy.clicked.connect(lambda: QApplication.clipboard().setText(option))
|
|
row.addWidget(copy)
|
|
v.addLayout(row)
|
|
buttons = QHBoxLayout()
|
|
buttons.addStretch(1)
|
|
close = QPushButton("Close")
|
|
close.clicked.connect(dlg.accept)
|
|
buttons.addWidget(close)
|
|
v.addLayout(buttons)
|
|
dlg.exec()
|
|
|
|
# --- import a crash dump (.dmp) ---------------------------------------------------
|
|
|
|
def _refresh_import_btn(self) -> None:
|
|
from ..core import ai
|
|
|
|
self._import_btn.setVisible(ai.is_configured())
|
|
|
|
def _import_dump(self) -> None:
|
|
from ..core import ai
|
|
|
|
if not ai.is_configured():
|
|
QMessageBox.information(
|
|
self, "RigDoctor",
|
|
"Set up an AI provider first (Settings → AI assistant) to analyze a crash dump.")
|
|
return
|
|
path, _ = QFileDialog.getOpenFileName(
|
|
self, "Import crash dump", os.path.expanduser("~"),
|
|
"Crash dumps (*.dmp);;All files (*)")
|
|
if not path:
|
|
return
|
|
self._import_btn.setEnabled(False)
|
|
self._status.setText("Parsing crash dump…")
|
|
threading.Thread(target=self._work_import, args=(path,), daemon=True).start()
|
|
|
|
def _work_import(self, path: str) -> None:
|
|
from ..core import minidump
|
|
|
|
try:
|
|
report = minidump.parse(path) # parses + runs minidump_stackwalk if installed
|
|
except Exception:
|
|
report = None
|
|
self._dump_parsed.emit(report)
|
|
|
|
def _on_dump_parsed(self, report) -> None:
|
|
self._import_btn.setEnabled(True)
|
|
self._status.setText("")
|
|
if report is None or not report.ok:
|
|
detail = report.error if report is not None else "Couldn't read the file."
|
|
QMessageBox.warning(
|
|
self, "Import crash dump", f"Couldn't analyze the dump — {detail}")
|
|
return
|
|
MinidumpDialog(report, self).exec()
|
|
|
|
# --- hard-crash recovery ----------------------------------------------------------
|
|
|
|
def _check_crash(self) -> None:
|
|
from ..core import diagnostic
|
|
|
|
info = diagnostic.pending_crash()
|
|
if info is None:
|
|
self._crash_banner.hide()
|
|
return
|
|
game = info.game or "your last game"
|
|
extra = " · ⚠ GPU-lost was captured" if info.gpu_lost else ""
|
|
self._crash_label.setText(
|
|
f"⚠ Your last diagnostic for {game} ended unexpectedly — likely a hard crash "
|
|
f"({info.samples} samples{extra}). Analyze it to see the final readings and the "
|
|
f"likely cause from the system logs."
|
|
)
|
|
self._analyze_btn.setEnabled(True)
|
|
self._crash_banner.show()
|
|
|
|
def _analyze_crash(self) -> None:
|
|
from ..core import diagnostic
|
|
|
|
diagnostic.acknowledge_crash() # don't prompt again for this one
|
|
self._analyze_btn.setEnabled(False)
|
|
self._crash_label.setText("Analyzing the crash (final readings + system logs)…")
|
|
threading.Thread(target=self._work_analyze_crash, daemon=True).start()
|
|
|
|
def _work_analyze_crash(self) -> None:
|
|
from ..core import diagnostic
|
|
|
|
try:
|
|
result = diagnostic.analyze_crash()
|
|
except Exception:
|
|
result = None
|
|
self._diag_done.emit(result)
|
|
|
|
def _dismiss_crash(self) -> None:
|
|
from ..core import diagnostic
|
|
|
|
diagnostic.acknowledge_crash()
|
|
self._crash_banner.hide()
|
|
|
|
# --- nav badge integration --------------------------------------------------------
|
|
|
|
def showEvent(self, event) -> None: # noqa: N802 (Qt override)
|
|
# Viewing the list acknowledges the new games: clear the sidebar badge. The NEW
|
|
# tags stay on the rows for this session so the user can still spot them.
|
|
super().showEvent(event)
|
|
self._refresh_import_btn() # AI may have been configured since this page was built
|
|
if self._new_appids:
|
|
from ..core import steam
|
|
|
|
threading.Thread(target=steam.acknowledge_new, daemon=True).start()
|
|
self.new_count_changed.emit(0)
|
|
|
|
# Reflect a capture that's still running (e.g. started earlier, navigated back).
|
|
from ..core import diagnostic
|
|
|
|
if diagnostic.is_running():
|
|
status = diagnostic.active() or {}
|
|
self._diag_game = status.get("game") or self._diag_game
|
|
self._banner.show()
|
|
if not self._diag_timer.isActive():
|
|
self._diag_timer.start()
|
|
else:
|
|
self._check_crash() # re-surface an interrupted diagnostic if one is pending
|