Menú d'opcions: ocultar no descarregats i comprovar actualitzacions, amb persistència
This commit is contained in:
+3
-1
@@ -1,6 +1,8 @@
|
||||
# ---> jlauncher
|
||||
# Datos descargados en tiempo de ejecución (clones git + cache de metadata)
|
||||
jlauncher_data/
|
||||
# Preferencias locales del usuario
|
||||
settings.json
|
||||
*.dist/
|
||||
*.build/
|
||||
|
||||
@@ -11,7 +13,7 @@ jlauncher_data/
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
+8
-1
@@ -42,7 +42,14 @@ run_cmd = "make run"
|
||||
|
||||
[[game]]
|
||||
id = "projecte_2026"
|
||||
name = "Projecte 2026 (Orni Attack)"
|
||||
name = "Projecte 2026"
|
||||
clone_url = "https://gitea.sustancia.synology.me/jaildesigner-jailgames/projecte_2026.git"
|
||||
build_cmd = ""
|
||||
run_cmd = "make run"
|
||||
|
||||
[[game]]
|
||||
id = "orni_attack"
|
||||
name = "Orni Attack"
|
||||
clone_url = "https://gitea.sustancia.synology.me/jaildesigner-jailgames/orni_attack.git"
|
||||
build_cmd = ""
|
||||
run_cmd = "make run"
|
||||
|
||||
@@ -52,6 +52,30 @@ def is_installed(root: Path, game: Game) -> bool:
|
||||
return (repo_dir(root, game.id) / ".git").exists()
|
||||
|
||||
|
||||
def check_update(root: Path, game: Game, log: LogFn = _noop) -> bool:
|
||||
"""Hace fetch y devuelve True si el clone local está por detrás del remoto.
|
||||
|
||||
No modifica el árbol de trabajo: solo cuenta los commits de origin/<rama> que no
|
||||
están en HEAD. Devuelve False si el juego no está descargado.
|
||||
"""
|
||||
repo = repo_dir(root, game.id)
|
||||
if not (repo / ".git").exists():
|
||||
return False
|
||||
_run_git(["fetch", "origin", "--prune"], repo, log)
|
||||
target = (
|
||||
load_meta(root, game.id).default_branch
|
||||
or _detect_origin_head(repo, log)
|
||||
or "HEAD"
|
||||
)
|
||||
try:
|
||||
behind = _run_git(
|
||||
["rev-list", "--count", f"HEAD..origin/{target}"], repo, log
|
||||
)
|
||||
except RuntimeError:
|
||||
return False
|
||||
return int(behind or "0") > 0
|
||||
|
||||
|
||||
def download(root: Path, game: Game, log: LogFn = _noop) -> GameMeta:
|
||||
"""Clona (si no existe) o trae el remoto forzado (descartando cambios locales).
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Preferencias persistentes en settings.json, junto al ejecutable."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from .paths import base_dir
|
||||
|
||||
SETTINGS_NAME = "settings.json"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Settings:
|
||||
hide_not_downloaded: bool = False
|
||||
updates_pending: list[str] = field(default_factory=list) # ids con update pendiente
|
||||
|
||||
|
||||
def settings_path() -> Path:
|
||||
return base_dir() / SETTINGS_NAME
|
||||
|
||||
|
||||
def load_settings() -> Settings:
|
||||
path = settings_path()
|
||||
if not path.exists():
|
||||
return Settings()
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return Settings()
|
||||
return Settings(
|
||||
hide_not_downloaded=bool(data.get("hide_not_downloaded", False)),
|
||||
updates_pending=list(data.get("updates_pending", [])),
|
||||
)
|
||||
|
||||
|
||||
def save_settings(settings: Settings) -> None:
|
||||
try:
|
||||
settings_path().write_text(
|
||||
json.dumps(asdict(settings), ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
except OSError:
|
||||
pass
|
||||
@@ -32,6 +32,7 @@ class GameRow(QFrame):
|
||||
super().__init__(parent)
|
||||
self.game = game
|
||||
self.root = root
|
||||
self._update_available = False
|
||||
self.setObjectName("gameRow")
|
||||
self.setFrameShape(QFrame.StyledPanel)
|
||||
|
||||
@@ -79,6 +80,14 @@ class GameRow(QFrame):
|
||||
|
||||
# ----------------------------------------------------------------- estado
|
||||
|
||||
def is_installed(self) -> bool:
|
||||
return (repo_dir(self.root, self.game.id) / ".git").exists()
|
||||
|
||||
def set_update_available(self, available: bool) -> None:
|
||||
"""Marca/desmarca la fila como 'tiene actualización pendiente'."""
|
||||
self._update_available = available
|
||||
self.refresh()
|
||||
|
||||
def refresh(self) -> None:
|
||||
"""Recarga icono, descripción y estado desde la cache local."""
|
||||
meta = load_meta(self.root, self.game.id)
|
||||
@@ -104,11 +113,15 @@ class GameRow(QFrame):
|
||||
)
|
||||
|
||||
def _set_status(self, meta: GameMeta) -> None:
|
||||
installed = (repo_dir(self.root, self.game.id) / ".git").exists()
|
||||
if not installed:
|
||||
if not self.is_installed():
|
||||
self.status_label.setText("No instalado")
|
||||
self.status_label.setStyleSheet("color: #cc8855; font-size: 11px;")
|
||||
self.run_btn.setEnabled(True) # se permite, avisará si no está
|
||||
elif self._update_available:
|
||||
self.status_label.setText("⬆ Actualización disponible")
|
||||
self.status_label.setStyleSheet(
|
||||
"color: #e0a030; font-weight: bold; font-size: 11px;"
|
||||
)
|
||||
else:
|
||||
ver = f" {meta.version}" if meta.version else ""
|
||||
self.status_label.setText(f"Instalado{ver}")
|
||||
|
||||
@@ -5,6 +5,7 @@ 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,
|
||||
@@ -15,7 +16,8 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from ..config import Config, Game
|
||||
from ..workers import DownloadWorker, RunWorker
|
||||
from ..settings import load_settings, save_settings
|
||||
from ..workers import CheckUpdatesWorker, DownloadWorker, RunWorker
|
||||
from .game_row import GameRow
|
||||
|
||||
APP_NAME = "Jail Launcher"
|
||||
@@ -27,6 +29,7 @@ class MainWindow(QMainWindow):
|
||||
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
|
||||
@@ -37,6 +40,8 @@ class MainWindow(QMainWindow):
|
||||
self.setWindowTitle(WINDOW_TITLE)
|
||||
self.resize(720, 640)
|
||||
|
||||
self._build_menu()
|
||||
|
||||
splitter = QSplitter(Qt.Vertical)
|
||||
|
||||
# --- Lista de juegos con scroll ---
|
||||
@@ -68,6 +73,66 @@ class MainWindow(QMainWindow):
|
||||
|
||||
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:
|
||||
@@ -99,7 +164,13 @@ class MainWindow(QMainWindow):
|
||||
self._log(f"=== {game.name}: descarga completada ===")
|
||||
row = self.rows[game.id]
|
||||
row.set_busy(False)
|
||||
row.refresh()
|
||||
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]
|
||||
|
||||
@@ -19,6 +19,7 @@ class _Signals(QObject):
|
||||
log = Signal(str) # línea de log
|
||||
finished = Signal(object) # payload según el worker (GameMeta o int exit code)
|
||||
error = Signal(str) # mensaje de error
|
||||
result = Signal(str, bool) # (game_id, has_update) — CheckUpdatesWorker
|
||||
|
||||
|
||||
class DownloadWorker(QRunnable):
|
||||
@@ -57,3 +58,34 @@ class RunWorker(QRunnable):
|
||||
self.signals.error.emit(str(exc))
|
||||
return
|
||||
self.signals.finished.emit(code)
|
||||
|
||||
|
||||
class CheckUpdatesWorker(QRunnable):
|
||||
"""Comprueba actualizaciones pendientes de los juegos ya descargados.
|
||||
|
||||
Emite `result(game_id, has_update)` por cada juego instalado y `finished` al final.
|
||||
"""
|
||||
|
||||
def __init__(self, root: Path, games: list[Game]) -> None:
|
||||
super().__init__()
|
||||
self.root = root
|
||||
self.games = games
|
||||
self.signals = _Signals()
|
||||
|
||||
def run(self) -> None: # noqa: D401 - API de QRunnable
|
||||
try:
|
||||
for game in self.games:
|
||||
if not gitops.is_installed(self.root, game):
|
||||
continue
|
||||
try:
|
||||
has_update = gitops.check_update(
|
||||
self.root, game, log=self.signals.log.emit
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 - no abortar el resto
|
||||
self.signals.log.emit(f"check {game.id}: {exc}")
|
||||
continue
|
||||
self.signals.result.emit(game.id, has_update)
|
||||
except Exception as exc: # noqa: BLE001 - reportar a la UI
|
||||
self.signals.error.emit(str(exc))
|
||||
return
|
||||
self.signals.finished.emit(None)
|
||||
|
||||
Reference in New Issue
Block a user