Interfície en català, botó Esborra i botons d'icona segons l'estat
This commit is contained in:
@@ -30,16 +30,22 @@ jlauncher_data/
|
||||
icon.png # copiado desde repo/release/icons/icon.png
|
||||
```
|
||||
|
||||
## Botones
|
||||
## Botones (interfaz en catalán)
|
||||
|
||||
- **Download**: si no existe el clone, hace `git clone`. Si existe, trae el remoto
|
||||
- **Descarrega**: si no existe el clone, hace `git clone`. Si existe, trae el remoto
|
||||
**forzado** (`git fetch` + `git reset --hard origin/<rama>` + `git clean -fd`),
|
||||
descartando cualquier cambio local. Después refresca la metadata:
|
||||
- descripción desde la API de Gitea (`/api/v1/repos/<org>/<repo>`),
|
||||
- versión ejecutando `version_cmd` (por defecto `git describe --tags --always`),
|
||||
- icono desde `release/icons/icon.png`.
|
||||
- **Run**: si hay `build_cmd`, compila primero; luego ejecuta `run_cmd`. Para estos
|
||||
- **Juga**: si hay `build_cmd`, compila primero; luego ejecuta `run_cmd`. Para estos
|
||||
juegos basta `run_cmd = "make run"` (compila y ejecuta), con `build_cmd` vacío.
|
||||
- **Esborra**: elimina la descarga local (carpeta del juego en `jlauncher_data/`),
|
||||
sin quitar el juego del `games.toml`.
|
||||
|
||||
Menú **Opcions**: *Amaga els jocs no descarregats* (filtro persistente) y
|
||||
*Comprova actualitzacions* (marca los juegos descargados con commits pendientes).
|
||||
Las preferencias se guardan en `settings.json` junto al ejecutable.
|
||||
|
||||
Las operaciones corren en segundo plano (QThreadPool); el log aparece en el panel inferior.
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ def main() -> int:
|
||||
except Exception as exc: # noqa: BLE001 - mostrar cualquier error de carga al usuario
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
"Error cargando games.toml",
|
||||
f"No se pudo leer la configuración en:\n{cfg_path}\n\n{exc}",
|
||||
"Error carregant games.toml",
|
||||
f"No s'ha pogut llegir la configuració a:\n{cfg_path}\n\n{exc}",
|
||||
)
|
||||
return 1
|
||||
|
||||
|
||||
+18
-8
@@ -17,7 +17,7 @@ from pathlib import Path
|
||||
|
||||
from .config import Game
|
||||
from .metadata import GameMeta, icon_path, load_meta, save_meta
|
||||
from .paths import metadata_dir, repo_dir
|
||||
from .paths import game_dir, metadata_dir, repo_dir
|
||||
|
||||
LogFn = Callable[[str], None]
|
||||
|
||||
@@ -76,6 +76,16 @@ def check_update(root: Path, game: Game, log: LogFn = _noop) -> bool:
|
||||
return int(behind or "0") > 0
|
||||
|
||||
|
||||
def delete_local(root: Path, game: Game, log: LogFn = _noop) -> None:
|
||||
"""Esborra la descàrrega local del joc (clon + metadata), sense tocar el TOML."""
|
||||
target = game_dir(root, game.id)
|
||||
if not target.exists():
|
||||
log(f"{game.name}: no hi ha res a esborrar")
|
||||
return
|
||||
log(f"Esborrant la descàrrega local de {game.name}…")
|
||||
shutil.rmtree(target, ignore_errors=True)
|
||||
|
||||
|
||||
def download(root: Path, game: Game, log: LogFn = _noop) -> GameMeta:
|
||||
"""Clona (si no existe) o trae el remoto forzado (descartando cambios locales).
|
||||
|
||||
@@ -87,13 +97,13 @@ def download(root: Path, game: Game, log: LogFn = _noop) -> GameMeta:
|
||||
branch = _fetch_default_branch(game, log)
|
||||
|
||||
if (repo / ".git").exists():
|
||||
log(f"Actualizando {game.name} (forzado, descartando cambios locales)…")
|
||||
log(f"Actualitzant {game.name} (forçat, descartant canvis locals)…")
|
||||
_run_git(["fetch", "origin", "--prune"], repo, log)
|
||||
target = branch or _detect_origin_head(repo, log) or "HEAD"
|
||||
_run_git(["reset", "--hard", f"origin/{target}"], repo, log)
|
||||
_run_git(["clean", "-fd"], repo, log)
|
||||
else:
|
||||
log(f"Clonando {game.name}…")
|
||||
log(f"Clonant {game.name}…")
|
||||
if repo.exists(): # carpeta a medias sin .git: limpiarla
|
||||
shutil.rmtree(repo, ignore_errors=True)
|
||||
_run_git(["clone", game.clone_url, str(repo)], None, log)
|
||||
@@ -131,14 +141,14 @@ def refresh_metadata(
|
||||
def _copy_icon(root: Path, game: Game, repo: Path, log: LogFn) -> None:
|
||||
src = repo / game.icon_rel
|
||||
if not src.exists():
|
||||
log(f"(sin icono en {game.icon_rel})")
|
||||
log(f"(sense icona a {game.icon_rel})")
|
||||
return
|
||||
metadata_dir(root, game.id).mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
shutil.copyfile(src, icon_path(root, game.id))
|
||||
log(f"Icono actualizado desde {game.icon_rel}")
|
||||
log(f"Icona actualitzada des de {game.icon_rel}")
|
||||
except OSError as exc:
|
||||
log(f"No se pudo copiar el icono: {exc}")
|
||||
log(f"No s'ha pogut copiar la icona: {exc}")
|
||||
|
||||
|
||||
def _read_version(game: Game, repo: Path, log: LogFn) -> str:
|
||||
@@ -154,7 +164,7 @@ def _read_version(game: Game, repo: Path, log: LogFn) -> str:
|
||||
timeout=20,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError) as exc:
|
||||
log(f"version_cmd falló: {exc}")
|
||||
log(f"version_cmd ha fallat: {exc}")
|
||||
return ""
|
||||
out = (proc.stdout or "").strip()
|
||||
return out.splitlines()[0] if out else ""
|
||||
@@ -168,7 +178,7 @@ def _fetch_gitea_info(game: Game, log: LogFn) -> dict | None:
|
||||
with urllib.request.urlopen(req, timeout=_HTTP_TIMEOUT) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
except (urllib.error.URLError, OSError, json.JSONDecodeError, ValueError) as exc:
|
||||
log(f"No se pudo leer la info de Gitea ({game.info_url}): {exc}")
|
||||
log(f"No s'ha pogut llegir la info de Gitea ({game.info_url}): {exc}")
|
||||
return None
|
||||
|
||||
|
||||
|
||||
+4
-4
@@ -43,15 +43,15 @@ def run_game(root: Path, game: Game, log: LogFn = _noop) -> int:
|
||||
repo = repo_dir(root, game.id)
|
||||
if not (repo / ".git").exists():
|
||||
raise FileNotFoundError(
|
||||
f"{game.name} no está descargado. Pulsa Download primero."
|
||||
f"{game.name} no està descarregat. Prem Descarrega primer."
|
||||
)
|
||||
|
||||
if game.build_cmd.strip():
|
||||
log(f"Compilando {game.name}…")
|
||||
log(f"Compilant {game.name}…")
|
||||
code = _stream(game.build_cmd, repo, log)
|
||||
if code != 0:
|
||||
log(f"Compilación falló (código {code}). No se ejecuta.")
|
||||
log(f"La compilació ha fallat (codi {code}). No s'executa.")
|
||||
return code
|
||||
|
||||
log(f"Ejecutando {game.name}…")
|
||||
log(f"Executant {game.name}…")
|
||||
return _stream(game.run_cmd, repo, log)
|
||||
|
||||
+45
-15
@@ -4,14 +4,14 @@ from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtCore import QSize, Qt, Signal
|
||||
from PySide6.QtGui import QPalette, QPixmap
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QStyle,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
@@ -27,6 +27,7 @@ class GameRow(QFrame):
|
||||
|
||||
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)
|
||||
@@ -63,17 +64,31 @@ class GameRow(QFrame):
|
||||
text_box.addStretch(1)
|
||||
layout.addLayout(text_box, stretch=1)
|
||||
|
||||
# --- Botones ---
|
||||
self.download_btn = QPushButton("Download")
|
||||
self.run_btn = QPushButton("Run")
|
||||
for btn in (self.download_btn, self.run_btn):
|
||||
btn.setMinimumWidth(96)
|
||||
btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
# --- 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))
|
||||
btn_box = QVBoxLayout()
|
||||
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()
|
||||
@@ -92,7 +107,7 @@ class GameRow(QFrame):
|
||||
"""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 "(sin descripción todavía)")
|
||||
self.desc_label.setText(meta.description or "(sense descripció encara)")
|
||||
self._set_status(meta)
|
||||
|
||||
def _set_icon(self) -> None:
|
||||
@@ -113,18 +128,32 @@ class GameRow(QFrame):
|
||||
)
|
||||
|
||||
def _set_status(self, meta: GameMeta) -> None:
|
||||
if not self.is_installed():
|
||||
self.status_label.setText("No instalado")
|
||||
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;")
|
||||
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.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"Instalado{ver}")
|
||||
self.status_label.setText(f"Descarregat{ver}")
|
||||
self.status_label.setStyleSheet("color: #6fae6f; font-size: 11px;")
|
||||
|
||||
# --------------------------------------------------------------- busy UI
|
||||
@@ -132,6 +161,7 @@ class GameRow(QFrame):
|
||||
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;")
|
||||
|
||||
+39
-13
@@ -8,6 +8,7 @@ from PySide6.QtCore import QThreadPool, Qt
|
||||
from PySide6.QtGui import QAction
|
||||
from PySide6.QtWidgets import (
|
||||
QMainWindow,
|
||||
QMessageBox,
|
||||
QPlainTextEdit,
|
||||
QScrollArea,
|
||||
QSplitter,
|
||||
@@ -15,6 +16,7 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from .. import gitops
|
||||
from ..config import Config, Game
|
||||
from ..settings import load_settings, save_settings
|
||||
from ..workers import CheckUpdatesWorker, DownloadWorker, RunWorker
|
||||
@@ -53,6 +55,7 @@ class MainWindow(QMainWindow):
|
||||
row = GameRow(game, root)
|
||||
row.download_requested.connect(self._on_download)
|
||||
row.run_requested.connect(self._on_run)
|
||||
row.delete_requested.connect(self._on_delete)
|
||||
self.rows[game.id] = row
|
||||
list_layout.addWidget(row)
|
||||
list_layout.addStretch(1)
|
||||
@@ -82,14 +85,14 @@ class MainWindow(QMainWindow):
|
||||
# --------------------------------------------------------------- menú
|
||||
|
||||
def _build_menu(self) -> None:
|
||||
menu = self.menuBar().addMenu("Opciones")
|
||||
menu = self.menuBar().addMenu("Opcions")
|
||||
|
||||
self.action_hide = QAction("Ocultar juegos no descargados", self, checkable=True)
|
||||
self.action_hide = QAction("Amaga els jocs no descarregats", 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 = QAction("Comprova actualitzacions", self)
|
||||
self.action_check.triggered.connect(self._check_updates)
|
||||
menu.addAction(self.action_check)
|
||||
|
||||
@@ -107,7 +110,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def _check_updates(self) -> None:
|
||||
self.action_check.setEnabled(False)
|
||||
self._log("=== Comprobando actualizaciones ===")
|
||||
self._log("=== Comprovant actualitzacions ===")
|
||||
worker = CheckUpdatesWorker(self.root, self.config.games)
|
||||
worker.signals.log.connect(self._log)
|
||||
worker.signals.result.connect(self._mark_update)
|
||||
@@ -127,11 +130,11 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def _check_done(self, _payload) -> None:
|
||||
self.action_check.setEnabled(True)
|
||||
self._log("=== Comprobación de actualizaciones terminada ===")
|
||||
self._log("=== Comprovació d'actualitzacions acabada ===")
|
||||
|
||||
def _check_error(self, msg: str) -> None:
|
||||
self.action_check.setEnabled(True)
|
||||
self._log(f"!!! Error comprobando actualizaciones: {msg}")
|
||||
self._log(f"!!! Error comprovant actualitzacions: {msg}")
|
||||
|
||||
# --------------------------------------------------------------- helpers
|
||||
|
||||
@@ -146,12 +149,12 @@ class MainWindow(QMainWindow):
|
||||
worker.signals.finished.connect(lambda *_: self._workers.discard(worker))
|
||||
worker.signals.error.connect(lambda *_: self._workers.discard(worker))
|
||||
|
||||
# --------------------------------------------------------------- acciones
|
||||
# --------------------------------------------------------------- accions
|
||||
|
||||
def _on_download(self, game: Game) -> None:
|
||||
row = self.rows[game.id]
|
||||
row.set_busy(True, "Descargando…")
|
||||
self._log(f"=== Download: {game.name} ===")
|
||||
row.set_busy(True, "Descarregant…")
|
||||
self._log(f"=== Descàrrega: {game.name} ===")
|
||||
|
||||
worker = DownloadWorker(self.root, game)
|
||||
worker.signals.log.connect(self._log)
|
||||
@@ -161,7 +164,7 @@ class MainWindow(QMainWindow):
|
||||
self.pool.start(worker)
|
||||
|
||||
def _download_done(self, game: Game) -> None:
|
||||
self._log(f"=== {game.name}: descarga completada ===")
|
||||
self._log(f"=== {game.name}: descàrrega completada ===")
|
||||
row = self.rows[game.id]
|
||||
row.set_busy(False)
|
||||
row.set_update_available(False) # recién traído del remoto → al día
|
||||
@@ -174,8 +177,8 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def _on_run(self, game: Game) -> None:
|
||||
row = self.rows[game.id]
|
||||
row.set_busy(True, "Ejecutando…")
|
||||
self._log(f"=== Run: {game.name} ===")
|
||||
row.set_busy(True, "Executant…")
|
||||
self._log(f"=== Juga: {game.name} ===")
|
||||
|
||||
worker = RunWorker(self.root, game)
|
||||
worker.signals.log.connect(self._log)
|
||||
@@ -185,7 +188,7 @@ class MainWindow(QMainWindow):
|
||||
self.pool.start(worker)
|
||||
|
||||
def _run_done(self, game: Game, code: int) -> None:
|
||||
self._log(f"=== {game.name}: finalizó con código {code} ===")
|
||||
self._log(f"=== {game.name}: ha finalitzat amb codi {code} ===")
|
||||
row = self.rows[game.id]
|
||||
row.set_busy(False)
|
||||
row.refresh()
|
||||
@@ -195,3 +198,26 @@ class MainWindow(QMainWindow):
|
||||
row = self.rows[game.id]
|
||||
row.set_busy(False)
|
||||
row.refresh()
|
||||
|
||||
def _on_delete(self, game: Game) -> None:
|
||||
resp = QMessageBox.question(
|
||||
self,
|
||||
"Esborrar descàrrega",
|
||||
f"Segur que vols esborrar la descàrrega local de «{game.name}»?\n\n"
|
||||
"S'eliminarà el clon i les dades en local (no es treu de la llista).",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No,
|
||||
)
|
||||
if resp != QMessageBox.Yes:
|
||||
return
|
||||
self._log(f"=== Esborra: {game.name} ===")
|
||||
gitops.delete_local(self.root, game, log=self._log)
|
||||
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)
|
||||
row = self.rows[game.id]
|
||||
row.set_update_available(False)
|
||||
row.refresh()
|
||||
self._apply_filter() # si està actiu "amaga no descarregats", ara s'amaga
|
||||
|
||||
Reference in New Issue
Block a user