"""Fila de la lista: icono, nombre, descripción y botones Download/Run.""" from __future__ import annotations from pathlib import Path from PySide6.QtCore import QSize, Qt, Signal from PySide6.QtGui import QPalette, QPixmap from PySide6.QtWidgets import ( QFrame, QHBoxLayout, QLabel, QPushButton, QStyle, QVBoxLayout, ) from ..config import Game from ..metadata import GameMeta, icon_path, load_meta from ..paths import repo_dir ICON_SIZE = 64 class GameRow(QFrame): """Widget de una fila. Emite señales cuando se pulsan los botones.""" download_requested = Signal(object) # Game run_requested = Signal(object) # Game delete_requested = Signal(object) # Game def __init__(self, game: Game, root: Path, parent=None) -> None: super().__init__(parent) self.game = game self.root = root self._update_available = False self.setObjectName("gameRow") self.setFrameShape(QFrame.StyledPanel) layout = QHBoxLayout(self) layout.setContentsMargins(10, 8, 10, 8) layout.setSpacing(12) # --- Icono --- self.icon_label = QLabel() self.icon_label.setFixedSize(ICON_SIZE, ICON_SIZE) self.icon_label.setAlignment(Qt.AlignCenter) layout.addWidget(self.icon_label) # --- Texto (nombre + descripción + estado) --- 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) # Texto atenuado que sigue la paleta (claro/oscuro) en vez de un gris fijo. 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) # --- Botons (icones compactes amb tooltip; visibilitat segons l'estat) --- style = self.style() self._icon_download = style.standardIcon(QStyle.SP_ArrowDown) self._icon_update = style.standardIcon(QStyle.SP_BrowserReload) self.download_btn = QPushButton() self.run_btn = QPushButton() self.run_btn.setIcon(style.standardIcon(QStyle.SP_MediaPlay)) self.run_btn.setToolTip("Juga") self.delete_btn = QPushButton() self.delete_btn.setIcon(style.standardIcon(QStyle.SP_TrashIcon)) self.delete_btn.setToolTip("Esborra") for btn in (self.download_btn, self.run_btn, self.delete_btn): btn.setFixedSize(36, 30) btn.setIconSize(QSize(18, 18)) self.download_btn.clicked.connect(lambda: self.download_requested.emit(self.game)) self.run_btn.clicked.connect(lambda: self.run_requested.emit(self.game)) self.delete_btn.clicked.connect(lambda: self.delete_requested.emit(self.game)) btn_box = QHBoxLayout() btn_box.setSpacing(4) btn_box.setAlignment(Qt.AlignRight | Qt.AlignVCenter) btn_box.addWidget(self.download_btn) btn_box.addWidget(self.run_btn) btn_box.addWidget(self.delete_btn) layout.addLayout(btn_box) self.refresh() # ----------------------------------------------------------------- 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) 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() # Visibilitat dels botons segons l'estat: # - Juga / Esborra: només si està descarregat. # - Descarrega: si no està descarregat o hi ha update pendent. self.run_btn.setVisible(installed) self.delete_btn.setVisible(installed) self.download_btn.setVisible((not installed) or self._update_available) if installed and self._update_available: self.download_btn.setIcon(self._icon_update) self.download_btn.setToolTip("Actualitza") else: self.download_btn.setIcon(self._icon_download) self.download_btn.setToolTip("Descarrega") 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;") # --------------------------------------------------------------- busy UI def set_busy(self, busy: bool, message: str = "") -> None: self.download_btn.setEnabled(not busy) self.run_btn.setEnabled(not busy) self.delete_btn.setEnabled(not busy) if busy and message: self.status_label.setText(message) self.status_label.setStyleSheet("color: #d0d060; font-size: 11px;")