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

175 lines
6.4 KiB
Python

"""Fila de la llista: la fila sencera actua com un botó.
Un clic fa l'acció principal segons l'estat:
- no descarregat → descarrega
- update pendent → actualitza
- descarregat i al dia → juga
Un indicador d'icona a la dreta (passiu) mostra què farà el clic. L'esborrat es
gestiona per fora (mètode a definir); aquest widget només exposa la senyal.
"""
from __future__ import annotations
from pathlib import Path
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QPalette, QPixmap
from PySide6.QtWidgets import (
QFrame,
QHBoxLayout,
QLabel,
QVBoxLayout,
)
from ..config import Game
from ..metadata import GameMeta, icon_path, load_meta
from ..paths import repo_dir
ICON_SIZE = 64
class GameRow(QFrame):
"""Fila clicable. Emet `activated` en clic i `delete_requested` per al futur esborrat."""
activated = Signal(object) # Game — clic sobre la fila (acció principal)
delete_requested = Signal(object) # Game — esborrar la descàrrega local
def __init__(self, game: Game, root: Path, parent=None) -> None:
super().__init__(parent)
self.game = game
self.root = root
self._update_available = False
self._busy = False
self.setObjectName("gameRow")
self.setFrameShape(QFrame.StyledPanel)
# La fila és un botó: cursor de mà i ressaltat en passar el ratolí.
self.setCursor(Qt.PointingHandCursor)
self.setStyleSheet(
"QFrame#gameRow:hover { background: rgba(127, 127, 127, 0.15); }"
)
layout = QHBoxLayout(self)
layout.setContentsMargins(10, 8, 10, 8)
layout.setSpacing(12)
# --- Icona del joc ---
self.icon_label = QLabel()
self.icon_label.setFixedSize(ICON_SIZE, ICON_SIZE)
self.icon_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.icon_label)
# --- Text (nom + descripció + estat) ---
text_box = QVBoxLayout()
text_box.setSpacing(2)
self.name_label = QLabel(game.name)
self.name_label.setStyleSheet("font-weight: bold; font-size: 14px;")
self.desc_label = QLabel("")
self.desc_label.setWordWrap(True)
# Text atenuat que segueix la paleta (clar/fosc) en lloc d'un gris fix.
self.desc_label.setForegroundRole(QPalette.PlaceholderText)
self.status_label = QLabel("")
self.status_label.setStyleSheet("color: #6fae6f; font-size: 11px;")
text_box.addWidget(self.name_label)
text_box.addWidget(self.desc_label)
text_box.addWidget(self.status_label)
text_box.addStretch(1)
layout.addLayout(text_box, stretch=1)
# --- Indicador d'acció (passiu, text): què farà el clic ---
self.action_label = QLabel()
self.action_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.action_label.setMinimumWidth(96)
layout.addWidget(self.action_label)
# Els fills no intercepten el ratolí: tot clic arriba a la fila.
for child in (
self.icon_label,
self.name_label,
self.desc_label,
self.status_label,
self.action_label,
):
child.setAttribute(Qt.WA_TransparentForMouseEvents)
self.refresh()
# ------------------------------------------------------------- clic fila
def mouseReleaseEvent(self, event) -> None:
if (
event.button() == Qt.LeftButton
and not self._busy
and self.rect().contains(event.position().toPoint())
):
self.activated.emit(self.game)
super().mouseReleaseEvent(event)
# ----------------------------------------------------------------- estat
def is_installed(self) -> bool:
return (repo_dir(self.root, self.game.id) / ".git").exists()
def primary_action_is_download(self) -> bool:
"""True si el clic ha de descarregar/actualitzar; False si ha de jugar."""
return (not self.is_installed()) or self._update_available
def set_update_available(self, available: bool) -> None:
"""Marca/desmarca la fila com a 'té actualització pendent'."""
self._update_available = available
self.refresh()
def refresh(self) -> None:
"""Recarrega icona, descripció i estat des de la cache local."""
meta = load_meta(self.root, self.game.id)
self._set_icon()
self.desc_label.setText(meta.description or "(sense descripció encara)")
self._set_status(meta)
def _set_icon(self) -> None:
path = icon_path(self.root, self.game.id)
pixmap = QPixmap(str(path)) if path.exists() else QPixmap()
if pixmap.isNull():
self.icon_label.setText("🎮")
self.icon_label.setStyleSheet("font-size: 32px;")
else:
self.icon_label.setStyleSheet("")
self.icon_label.setPixmap(
pixmap.scaled(
ICON_SIZE,
ICON_SIZE,
Qt.KeepAspectRatio,
Qt.SmoothTransformation,
)
)
def _set_status(self, meta: GameMeta) -> None:
if not self.is_installed():
self.status_label.setText("No descarregat")
self.status_label.setStyleSheet("color: #cc8855; font-size: 11px;")
self._set_action("Descarrega", "#4a90d9")
elif self._update_available:
self.status_label.setText("⬆ Actualització disponible")
self.status_label.setStyleSheet(
"color: #e0a030; font-weight: bold; font-size: 11px;"
)
self._set_action("Actualitza", "#e0a030")
else:
ver = f" {meta.version}" if meta.version else ""
self.status_label.setText(f"Descarregat{ver}")
self.status_label.setStyleSheet("color: #6fae6f; font-size: 11px;")
self._set_action("Juga", "#6fae6f")
def _set_action(self, text: str, color: str) -> None:
self.action_label.setText(text)
self.action_label.setStyleSheet(
f"color: {color}; font-weight: bold; font-size: 13px;"
)
# --------------------------------------------------------------- busy UI
def set_busy(self, busy: bool, message: str = "") -> None:
self._busy = busy
if busy and message:
self.status_label.setText(message)
self.status_label.setStyleSheet("color: #d0d060; font-size: 11px;")