"""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._delete_mode = 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 set_delete_mode(self, on: bool) -> None: """Activa/desactiva el mode esborrar (canvia el text d'acció a 'Esborra').""" self._delete_mode = on 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: installed = self.is_installed() # Text d'estat (independent del mode esborrar). if not installed: self.status_label.setText("No descarregat") self.status_label.setStyleSheet("color: #cc8855; font-size: 11px;") elif self._update_available: self.status_label.setText("⬆ Actualització 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"Descarregat{ver}") self.status_label.setStyleSheet("color: #6fae6f; font-size: 11px;") # Text d'acció (què fa el clic). if self._delete_mode: # Només es pot esborrar el que està descarregat. self._set_action("Esborra", "#d9534f") if installed else self.action_label.clear() elif not installed: self._set_action("Descarrega", "#4a90d9") elif self._update_available: self._set_action("Actualitza", "#e0a030") else: 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;")