126 lines
4.4 KiB
Python
126 lines
4.4 KiB
Python
"""Ventana principal: lista de juegos con scroll + panel de log."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from PySide6.QtCore import QThreadPool, Qt
|
|
from PySide6.QtWidgets import (
|
|
QMainWindow,
|
|
QPlainTextEdit,
|
|
QScrollArea,
|
|
QSplitter,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
from ..config import Config, Game
|
|
from ..workers import DownloadWorker, RunWorker
|
|
from .game_row import GameRow
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
def __init__(self, config: Config, root: Path, parent=None) -> None:
|
|
super().__init__(parent)
|
|
self.config = config
|
|
self.root = root
|
|
self.pool = QThreadPool.globalInstance()
|
|
self.rows: dict[str, GameRow] = {}
|
|
# Mantener referencias a los workers en vuelo: si no, Python los recolecta
|
|
# (junto a su objeto de señales) antes de que la señal en cola `finished`
|
|
# llegue al hilo principal, y la UI nunca se refresca.
|
|
self._workers: set = set()
|
|
|
|
self.setWindowTitle("jlauncher — Juegos jailgames")
|
|
self.resize(720, 640)
|
|
|
|
splitter = QSplitter(Qt.Vertical)
|
|
|
|
# --- Lista de juegos con scroll ---
|
|
list_container = QWidget()
|
|
list_layout = QVBoxLayout(list_container)
|
|
list_layout.setContentsMargins(6, 6, 6, 6)
|
|
list_layout.setSpacing(6)
|
|
for game in config.games:
|
|
row = GameRow(game, root)
|
|
row.download_requested.connect(self._on_download)
|
|
row.run_requested.connect(self._on_run)
|
|
self.rows[game.id] = row
|
|
list_layout.addWidget(row)
|
|
list_layout.addStretch(1)
|
|
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
scroll.setWidget(list_container)
|
|
splitter.addWidget(scroll)
|
|
|
|
# --- Panel de log ---
|
|
self.log_view = QPlainTextEdit()
|
|
self.log_view.setReadOnly(True)
|
|
self.log_view.setMaximumBlockCount(5000)
|
|
self.log_view.setStyleSheet(
|
|
"font-family: monospace; font-size: 11px; background:#1e1e1e; color:#d4d4d4;"
|
|
)
|
|
splitter.addWidget(self.log_view)
|
|
splitter.setStretchFactor(0, 3)
|
|
splitter.setStretchFactor(1, 1)
|
|
|
|
self.setCentralWidget(splitter)
|
|
|
|
# --------------------------------------------------------------- helpers
|
|
|
|
def _log(self, text: str) -> None:
|
|
self.log_view.appendPlainText(text)
|
|
|
|
def _track(self, worker) -> None:
|
|
"""Retiene el worker hasta que emite finished/error, evitando que el GC
|
|
se lleve su objeto de señales antes de entregar la señal en cola."""
|
|
worker.setAutoDelete(False)
|
|
self._workers.add(worker)
|
|
worker.signals.finished.connect(lambda *_: self._workers.discard(worker))
|
|
worker.signals.error.connect(lambda *_: self._workers.discard(worker))
|
|
|
|
# --------------------------------------------------------------- acciones
|
|
|
|
def _on_download(self, game: Game) -> None:
|
|
row = self.rows[game.id]
|
|
row.set_busy(True, "Descargando…")
|
|
self._log(f"=== Download: {game.name} ===")
|
|
|
|
worker = DownloadWorker(self.root, game)
|
|
worker.signals.log.connect(self._log)
|
|
worker.signals.finished.connect(lambda _meta, g=game: self._download_done(g))
|
|
worker.signals.error.connect(lambda msg, g=game: self._op_error(g, msg))
|
|
self._track(worker)
|
|
self.pool.start(worker)
|
|
|
|
def _download_done(self, game: Game) -> None:
|
|
self._log(f"=== {game.name}: descarga completada ===")
|
|
row = self.rows[game.id]
|
|
row.set_busy(False)
|
|
row.refresh()
|
|
|
|
def _on_run(self, game: Game) -> None:
|
|
row = self.rows[game.id]
|
|
row.set_busy(True, "Ejecutando…")
|
|
self._log(f"=== Run: {game.name} ===")
|
|
|
|
worker = RunWorker(self.root, game)
|
|
worker.signals.log.connect(self._log)
|
|
worker.signals.finished.connect(lambda code, g=game: self._run_done(g, code))
|
|
worker.signals.error.connect(lambda msg, g=game: self._op_error(g, msg))
|
|
self._track(worker)
|
|
self.pool.start(worker)
|
|
|
|
def _run_done(self, game: Game, code: int) -> None:
|
|
self._log(f"=== {game.name}: finalizó con código {code} ===")
|
|
row = self.rows[game.id]
|
|
row.set_busy(False)
|
|
row.refresh()
|
|
|
|
def _op_error(self, game: Game, msg: str) -> None:
|
|
self._log(f"!!! {game.name}: ERROR: {msg}")
|
|
row = self.rows[game.id]
|
|
row.set_busy(False)
|
|
row.refresh()
|