Files
jail-launcher/jlauncher/ui/main_window.py
T

198 lines
7.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.QtGui import QAction
from PySide6.QtWidgets import (
QMainWindow,
QPlainTextEdit,
QScrollArea,
QSplitter,
QVBoxLayout,
QWidget,
)
from ..config import Config, Game
from ..settings import load_settings, save_settings
from ..workers import CheckUpdatesWorker, DownloadWorker, RunWorker
from .game_row import GameRow
APP_NAME = "Jail Launcher"
WINDOW_TITLE = f"© 2026 {APP_NAME} — JailDesigner"
class MainWindow(QMainWindow):
def __init__(self, config: Config, root: Path, parent=None) -> None:
super().__init__(parent)
self.config = config
self.root = root
self.settings = load_settings()
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(WINDOW_TITLE)
self.resize(720, 640)
self._build_menu()
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;")
splitter.addWidget(self.log_view)
splitter.setStretchFactor(0, 3)
splitter.setStretchFactor(1, 1)
self.setCentralWidget(splitter)
# Estado persistido: marcas de update + filtro de ocultar no descargados.
for game_id in self.settings.updates_pending:
if game_id in self.rows:
self.rows[game_id].set_update_available(True)
self._apply_filter()
# --------------------------------------------------------------- menú
def _build_menu(self) -> None:
menu = self.menuBar().addMenu("Opciones")
self.action_hide = QAction("Ocultar juegos no descargados", self, checkable=True)
self.action_hide.setChecked(self.settings.hide_not_downloaded)
self.action_hide.toggled.connect(self._on_toggle_hide)
menu.addAction(self.action_hide)
self.action_check = QAction("Comprobar actualizaciones", self)
self.action_check.triggered.connect(self._check_updates)
menu.addAction(self.action_check)
def _on_toggle_hide(self, checked: bool) -> None:
self.settings.hide_not_downloaded = checked
save_settings(self.settings)
self._apply_filter()
def _apply_filter(self) -> None:
hide = self.action_hide.isChecked()
for row in self.rows.values():
row.setVisible(not (hide and not row.is_installed()))
# ------------------------------------------------------ comprobar updates
def _check_updates(self) -> None:
self.action_check.setEnabled(False)
self._log("=== Comprobando actualizaciones ===")
worker = CheckUpdatesWorker(self.root, self.config.games)
worker.signals.log.connect(self._log)
worker.signals.result.connect(self._mark_update)
worker.signals.finished.connect(self._check_done)
worker.signals.error.connect(self._check_error)
self._track(worker)
self.pool.start(worker)
def _mark_update(self, game_id: str, has_update: bool) -> None:
row = self.rows.get(game_id)
if row is not None:
row.set_update_available(has_update)
pending = set(self.settings.updates_pending)
pending.add(game_id) if has_update else pending.discard(game_id)
self.settings.updates_pending = sorted(pending)
save_settings(self.settings)
def _check_done(self, _payload) -> None:
self.action_check.setEnabled(True)
self._log("=== Comprobación de actualizaciones terminada ===")
def _check_error(self, msg: str) -> None:
self.action_check.setEnabled(True)
self._log(f"!!! Error comprobando actualizaciones: {msg}")
# --------------------------------------------------------------- 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.set_update_available(False) # recién traído del remoto → al día
if game.id in self.settings.updates_pending:
self.settings.updates_pending = [
g for g in self.settings.updates_pending if g != game.id
]
save_settings(self.settings)
self._apply_filter() # un juego antes no instalado puede aparecer ahora
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()