9 Commits

Author SHA1 Message Date
JailDesigner fb65f4f249 Afig directoris de Homebrew al PATH i puja a 1.0.2 2026-05-30 22:01:46 +02:00
JailDesigner be8c886ad1 Versiona la carpeta icon/ (el patró «Icon» del gitignore l'amagava) 2026-05-30 17:18:03 +02:00
JailDesigner fd8eedab76 Usa l'icona real de icon/ al bundle macOS i al «Quant a» 2026-05-30 17:18:03 +02:00
JailDesigner a71a1be88d Puja la versió a 1.0.1 2026-05-30 17:18:03 +02:00
JailDesigner 34811038eb Diàleg «Quant a» centrat amb icona, nom gran i versió en cursiva 2026-05-30 17:18:03 +02:00
JailDesigner 95a76d0d76 Empaqueta jlauncher com a .app + .dmg per a macOS 2026-05-30 17:18:03 +02:00
JailDesigner c879127401 Consola en mode auto-amaga per defecte
En una instal·lació nova (sense settings.json) la consola sortia visible
perquè el defecte era "show". Es passa a "auto" (i també el fallback de
valors desconeguts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:58:26 +02:00
JailDesigner be3cb44ae2 Reordena els jocs al toml: CC, CCAE, JDD, AEE, Orni, Projecte 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:51:56 +02:00
JailDesigner 0085c63ace Diàleg «Quant a» al menú Ajuda i bump a 1.0.0
- Nou menú Ajuda amb «Quant a Jail Launcher…» que mostra nom, versió i
  copyright. AboutRole perquè a macOS Qt el mogui al menú de l'aplicació.
- Versió bumpejada a 1.0.0 (jlauncher.__version__ + pyproject), llegida pel
  diàleg per no duplicar-la.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:49:29 +02:00
16 changed files with 480 additions and 53 deletions
+2
View File
@@ -14,6 +14,8 @@ settings.json
# Icon must end with two \r # Icon must end with two \r
Icon Icon
# En FS case-insensitive (macOS) el patrón «Icon» se traga la carpeta icon/; re-inclúyela.
!/icon/
# Thumbnails # Thumbnails
._* ._*
+22 -5
View File
@@ -88,9 +88,26 @@ fuente se ejecuta con `python -m jlauncher`).
patchelf (Nuitka usa `install_name_tool`). patchelf (Nuitka usa `install_name_tool`).
- `git` en el PATH. - `git` en el PATH.
### macOS ### macOS (.app + .dmg)
Compila en el propio Mac (Nuitka no compila cruzado): `./build.sh` genera Compila en el propio Mac (Nuitka no compila cruzado). En macOS, `./build.sh` no genera un
`jlauncher-v…-darwin-arm64.tar.gz`. Como el binario no va firmado, la primera vez quizá binario suelto sino una **app nativa**:
debas hacer `xattr -dr com.apple.quarantine jlauncher` o abrirlo con clic derecho → Abrir.
Lánzalo desde terminal (`./jlauncher`). ```bash
./build.sh
# -> dist/jlauncher.app
# -> dist/jlauncher-v<versión>-macos-<arch>.dmg (arrastrar la app a Aplicaciones)
```
El icono vive en `icon/`: el build usa `icon/icon.icns` para el bundle y copia
`icon/icon.png` (1024×1024) dentro del `.app` para el diálogo «Quant a». Para cambiarlo,
sustituye esos ficheros (regenerables con `icon/create_icons.py`).
A diferencia del onefile, la `.app` **no** escribe junto a sí misma (rompería al moverla a
`/Applications`): guarda sus datos en
`~/Library/Application Support/jailgames/jlauncher/` (`jlauncher_data/`, `settings.json` y
una copia editable de `games.toml`, sembrada la primera vez desde el bundle).
La app va **sin firma Developer ID** (firma ad-hoc), así que Gatekeeper avisará la primera
vez: ábrela con **clic derecho → Abrir**, o ejecuta
`xattr -dr com.apple.quarantine /Applications/jlauncher.app`.
+96 -22
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Compila jlauncher a un binario standalone con Nuitka y empaqueta un tar.gz de release. # Compila jlauncher con Nuitka y empaqueta un release.
# Requisitos del sistema: python3-dev, gcc, patchelf (ver README). # - Linux: binario onefile + tar.gz.
# - macOS: jlauncher.app (con icono) dentro de un .dmg arrastrable a /Applications.
# Requisitos del sistema (no los instala el script): ver README (compilador C, git…).
set -euo pipefail set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)" HERE="$(cd "$(dirname "$0")" && pwd)"
@@ -13,7 +15,6 @@ if [ -z "$VERSION" ]; then
fi fi
ARCH="$(uname -m)" ARCH="$(uname -m)"
OS="$(uname -s | tr '[:upper:]' '[:lower:]')" OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
RELEASE_NAME="jlauncher-v${VERSION}-${OS}-${ARCH}"
if [ ! -d .venv ]; then if [ ! -d .venv ]; then
echo "[build] creando venv…" echo "[build] creando venv…"
@@ -39,25 +40,98 @@ echo "[build] versión: v${VERSION}"
echo "[build] limpiando artefactos previos…" echo "[build] limpiando artefactos previos…"
rm -rf dist build app.build app.dist app.onefile-build rm -rf dist build app.build app.dist app.onefile-build
echo "[build] compilando (PySide6 onefile; puede tardar varios minutos)…" ICON_ICNS="icon/icon.icns"
.venv/bin/python -m nuitka \ ICON_PNG="icon/icon.png"
--onefile \
--assume-yes-for-downloads \
--enable-plugin=pyside6 \
--include-package=jlauncher \
--output-dir=dist \
--output-filename=jlauncher \
--remove-output \
--lto=yes \
app.py
echo "[build] copiando games.toml junto al binario…" if [ "$OS" = "darwin" ]; then
cp games.toml dist/games.toml # -------------------------------------------------------------------------
# macOS: app bundle + DMG
# -------------------------------------------------------------------------
if [ ! -f "$ICON_ICNS" ]; then
echo "[build] falta ${ICON_ICNS}" >&2
exit 1
fi
echo "[build] empaquetando release ${RELEASE_NAME}.tar.gz…" echo "[build] compilando jlauncher.app (PySide6; puede tardar varios minutos)…"
tar -czf "dist/${RELEASE_NAME}.tar.gz" -C dist jlauncher games.toml .venv/bin/python -m nuitka \
--standalone \
--macos-create-app-bundle \
--macos-app-icon="$ICON_ICNS" \
--macos-app-name=jlauncher \
--macos-app-version="$VERSION" \
--macos-signed-app-name=com.jailgames.jlauncher \
--company-name=jailgames \
--product-name=jlauncher \
--product-version="$VERSION" \
--assume-yes-for-downloads \
--enable-plugin=pyside6 \
--include-package=jlauncher \
--output-dir=dist \
--output-filename=jlauncher \
--remove-output \
app.py
echo "[build] hecho:" # Según la versión, Nuitka nombra el bundle a partir del script de entrada
ls -lh "dist/jlauncher" "dist/games.toml" "dist/${RELEASE_NAME}.tar.gz" # (app.py -> app.app). Lo normalizamos a jlauncher.app.
echo "[build] el binario crea jlauncher_data/ y settings.json junto a sí mismo." APP="dist/jlauncher.app"
echo "[build] distribuir: descomprimir el tar.gz (jlauncher + games.toml juntos)." if [ ! -d "$APP" ]; then
PRODUCED="$(find dist -maxdepth 1 -name '*.app' | head -n1)"
if [ -z "$PRODUCED" ]; then
echo "[build] Nuitka no produjo ningún .app en dist/" >&2
exit 1
fi
rm -rf "$APP"
mv "$PRODUCED" "$APP"
fi
echo "[build] sembrando games.toml y icon.png en Contents/Resources…"
cp games.toml "$APP/Contents/Resources/games.toml"
cp "$ICON_PNG" "$APP/Contents/Resources/icon.png" # usado por el diálogo «Quant a»
# Bundle ad-hoc (sin Developer ID): quitamos quarantine para abrir sin fricción local.
xattr -dr com.apple.quarantine "$APP" 2>/dev/null || true
DMG="dist/jlauncher-v${VERSION}-macos-${ARCH}.dmg"
echo "[build] empaquetando ${DMG}"
STAGE="$(mktemp -d)"
cp -R "$APP" "$STAGE/"
ln -s /Applications "$STAGE/Applications"
rm -f "$DMG"
hdiutil create -volname "jlauncher" -srcfolder "$STAGE" \
-ov -format UDZO "$DMG" >/dev/null
rm -rf "$STAGE"
echo "[build] hecho:"
du -sh "$APP" | sed 's/^/[build] /'
ls -lh "$DMG"
echo "[build] la app guarda datos en ~/Library/Application Support/jailgames/jlauncher/."
echo "[build] distribuir: el .dmg (arrastrar jlauncher.app a Aplicaciones)."
echo "[build] sin firma Developer ID: primera apertura con clic derecho → Abrir."
else
# -------------------------------------------------------------------------
# Linux (y resto): binario onefile + tar.gz
# -------------------------------------------------------------------------
RELEASE_NAME="jlauncher-v${VERSION}-${OS}-${ARCH}"
echo "[build] compilando (PySide6 onefile; puede tardar varios minutos)…"
.venv/bin/python -m nuitka \
--onefile \
--assume-yes-for-downloads \
--enable-plugin=pyside6 \
--include-package=jlauncher \
--output-dir=dist \
--output-filename=jlauncher \
--remove-output \
--lto=yes \
app.py
echo "[build] copiando games.toml junto al binario…"
cp games.toml dist/games.toml
echo "[build] empaquetando release ${RELEASE_NAME}.tar.gz…"
tar -czf "dist/${RELEASE_NAME}.tar.gz" -C dist jlauncher games.toml
echo "[build] hecho:"
ls -lh "dist/jlauncher" "dist/games.toml" "dist/${RELEASE_NAME}.tar.gz"
echo "[build] el binario crea jlauncher_data/ y settings.json junto a sí mismo."
echo "[build] distribuir: descomprimir el tar.gz (jlauncher + games.toml juntos)."
fi
+12 -12
View File
@@ -35,15 +35,6 @@ run_cmd = "make run"
players = "1-2 jugadors" players = "1-2 jugadors"
author = "JailDesigner" author = "JailDesigner"
[[game]]
id = "aee"
name = "Aventures en Egipte"
clone_url = "https://gitea.sustancia.synology.me/jaildesigner-jailgames/aee.git"
build_cmd = ""
run_cmd = "make run"
players = "1 jugador"
author = "JailDesigner"
[[game]] [[game]]
id = "jaildoctors_dilemma" id = "jaildoctors_dilemma"
name = "JailDoctor's Dilemma" name = "JailDoctor's Dilemma"
@@ -54,9 +45,9 @@ players = "1 jugador"
author = "JailDesigner" author = "JailDesigner"
[[game]] [[game]]
id = "projecte_2026" id = "aee"
name = "Projecte 2026" name = "Aventures en Egipte"
clone_url = "https://gitea.sustancia.synology.me/jaildesigner-jailgames/projecte_2026.git" clone_url = "https://gitea.sustancia.synology.me/jaildesigner-jailgames/aee.git"
build_cmd = "" build_cmd = ""
run_cmd = "make run" run_cmd = "make run"
players = "1 jugador" players = "1 jugador"
@@ -70,3 +61,12 @@ build_cmd = ""
run_cmd = "make run" run_cmd = "make run"
players = "1-2 jugadors" players = "1-2 jugadors"
author = "JailDesigner" author = "JailDesigner"
[[game]]
id = "projecte_2026"
name = "Projecte 2026"
clone_url = "https://gitea.sustancia.synology.me/jaildesigner-jailgames/projecte_2026.git"
build_cmd = ""
run_cmd = "make run"
players = "1 jugador"
author = "JailDesigner"
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env python3
import os
import sys
import shutil
import subprocess
from pathlib import Path
def check_dependencies():
"""Verifica que ImageMagick esté instalado"""
try:
subprocess.run(['magick', '--version'], capture_output=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
print("Error: ImageMagick no está instalado o no se encuentra en el PATH")
print("Instala ImageMagick desde: https://imagemagick.org/script/download.php")
sys.exit(1)
# Verificar iconutil solo en macOS
if sys.platform == 'darwin':
try:
# iconutil no tiene --version, mejor usar which o probar con -h
result = subprocess.run(['which', 'iconutil'], capture_output=True, check=True)
if result.returncode == 0:
print("✓ iconutil disponible - se crearán archivos .ico e .icns")
except (subprocess.CalledProcessError, FileNotFoundError):
print("Error: iconutil no está disponible (solo funciona en macOS)")
print("Solo se creará el archivo .ico")
else:
print("️ Sistema no-macOS detectado - solo se creará archivo .ico")
def create_icons(input_file):
"""Crea archivos .icns e .ico a partir de un PNG"""
# Verificar que el archivo existe
if not os.path.isfile(input_file):
print(f"Error: El archivo {input_file} no existe.")
return False
# Obtener información del archivo
file_path = Path(input_file)
file_dir = file_path.parent
file_name = file_path.stem # Nombre sin extensión
file_extension = file_path.suffix
if file_extension.lower() not in ['.png', '.jpg', '.jpeg', '.bmp', '.tiff']:
print(f"Advertencia: {file_extension} puede no ser compatible. Se recomienda usar PNG.")
# Crear archivo .ico usando el método simplificado
ico_output = file_dir / f"{file_name}.ico"
try:
print(f"Creando {ico_output}...")
subprocess.run([
'magick', str(input_file),
'-define', 'icon:auto-resize=256,128,64,48,32,16',
str(ico_output)
], check=True)
print(f"✓ Archivo .ico creado: {ico_output}")
except subprocess.CalledProcessError as e:
print(f"Error creando archivo .ico: {e}")
return False
# Crear archivo .icns (solo en macOS)
if sys.platform == 'darwin':
try:
# Crear carpeta temporal para iconset
temp_folder = file_dir / "icon.iconset"
# Eliminar carpeta temporal si existe
if temp_folder.exists():
shutil.rmtree(temp_folder)
# Crear carpeta temporal
temp_folder.mkdir(parents=True)
# Definir los tamaños y nombres de archivo para .icns
icon_sizes = [
(16, "icon_16x16.png"),
(32, "icon_16x16@2x.png"),
(32, "icon_32x32.png"),
(64, "icon_32x32@2x.png"),
(128, "icon_128x128.png"),
(256, "icon_128x128@2x.png"),
(256, "icon_256x256.png"),
(512, "icon_256x256@2x.png"),
(512, "icon_512x512.png"),
(1024, "icon_512x512@2x.png")
]
print("Generando imágenes para .icns...")
# Crear cada tamaño de imagen
for size, output_name in icon_sizes:
output_path = temp_folder / output_name
subprocess.run([
'magick', str(input_file),
'-resize', f'{size}x{size}',
str(output_path)
], check=True)
# Crear archivo .icns usando iconutil
icns_output = file_dir / f"{file_name}.icns"
print(f"Creando {icns_output}...")
subprocess.run([
'iconutil', '-c', 'icns',
str(temp_folder),
'-o', str(icns_output)
], check=True)
# Limpiar carpeta temporal
if temp_folder.exists():
shutil.rmtree(temp_folder)
print(f"✓ Archivo .icns creado: {icns_output}")
except subprocess.CalledProcessError as e:
print(f"Error creando archivo .icns: {e}")
# Limpiar carpeta temporal en caso de error
if temp_folder.exists():
shutil.rmtree(temp_folder)
return False
else:
print("️ Archivo .icns no creado (solo disponible en macOS)")
return True
def main():
"""Función principal"""
# Verificar argumentos
if len(sys.argv) != 2:
print(f"Uso: {sys.argv[0]} ARCHIVO")
print("Ejemplo: python3 create_icons.py imagen.png")
sys.exit(0)
input_file = sys.argv[1]
# Verificar dependencias
check_dependencies()
# Crear iconos
if create_icons(input_file):
print("\n✅ Proceso completado exitosamente")
else:
print("\n❌ El proceso falló")
sys.exit(1)
if __name__ == "__main__":
main()
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

+1 -1
View File
@@ -1,3 +1,3 @@
"""jlauncher — lanzador de juegos jailgames.""" """jlauncher — lanzador de juegos jailgames."""
__version__ = "0.1.0" __version__ = "1.0.2"
+5 -1
View File
@@ -4,10 +4,11 @@ from __future__ import annotations
import sys import sys
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QApplication, QMessageBox from PySide6.QtWidgets import QApplication, QMessageBox
from .config import load_config from .config import load_config
from .paths import config_file, data_root from .paths import app_icon_path, config_file, data_root
from .ui.main_window import MainWindow from .ui.main_window import MainWindow
from .ui.theme import apply_theme from .ui.theme import apply_theme
@@ -15,6 +16,9 @@ from .ui.theme import apply_theme
def main() -> int: def main() -> int:
app = QApplication(sys.argv) app = QApplication(sys.argv)
app.setApplicationName("jlauncher") app.setApplicationName("jlauncher")
icon_path = app_icon_path()
if icon_path is not None:
app.setWindowIcon(QIcon(str(icon_path)))
# Tema del sistema para el posible diálogo de error previo a la ventana; # Tema del sistema para el posible diálogo de error previo a la ventana;
# MainWindow re-aplica el modo guardado (system/light/dark) y vigila los cambios. # MainWindow re-aplica el modo guardado (system/light/dark) y vigila los cambios.
apply_theme(app) apply_theme(app)
+69 -2
View File
@@ -9,11 +9,16 @@ Ejecutando desde fuente usamos la raíz del proyecto (la carpeta que contiene ``
from __future__ import annotations from __future__ import annotations
import os import os
import shutil
import sys import sys
from pathlib import Path from pathlib import Path
CONFIG_NAME = "games.toml" CONFIG_NAME = "games.toml"
# Carpeta de soporte en macOS cuando corremos como .app: ~/Library/Application Support/…
APP_SUPPORT_VENDOR = "jailgames"
APP_SUPPORT_APP = "jlauncher"
def is_compiled() -> bool: def is_compiled() -> bool:
"""True si corremos como binario Nuitka (define ``__compiled__`` en cada módulo).""" """True si corremos como binario Nuitka (define ``__compiled__`` en cada módulo)."""
@@ -34,14 +39,76 @@ def base_dir() -> Path:
return Path(__file__).resolve().parent.parent return Path(__file__).resolve().parent.parent
def macos_app_bundle() -> Path | None:
"""Ruta del .app si corremos como bundle de macOS (``…/jlauncher.app``); si no, None.
En un app bundle el ejecutable vive en ``…/jlauncher.app/Contents/MacOS/jlauncher``,
dentro de un árbol no escribible al instalarse en ``/Applications``. Detectarlo nos
deja redirigir datos y settings a ``~/Library/Application Support``.
"""
if not is_compiled() or sys.platform != "darwin":
return None
exe = Path(sys.executable).resolve()
for parent in exe.parents:
if parent.suffix == ".app":
return parent
return None
def app_icon_path() -> Path | None:
"""Ruta al PNG del icono para la UI (icono de ventana/Dock y diálogo «Quant a»).
En .app vive en ``Contents/Resources/icon.png``; desde fuente, en ``assets/icon.png``.
Devuelve None si no se encuentra.
"""
bundle = macos_app_bundle()
if bundle is not None:
candidate = bundle / "Contents" / "Resources" / "icon.png"
else:
candidate = base_dir() / "icon" / "icon.png"
return candidate if candidate.exists() else None
def support_dir() -> Path:
"""Carpeta de soporte escribible en macOS (.app); se crea si no existe."""
root = (
Path.home()
/ "Library"
/ "Application Support"
/ APP_SUPPORT_VENDOR
/ APP_SUPPORT_APP
)
root.mkdir(parents=True, exist_ok=True)
return root
def writable_base() -> Path:
"""Base donde escribir datos/settings: Application Support si .app, si no base_dir()."""
if macos_app_bundle() is not None:
return support_dir()
return base_dir()
def config_file() -> Path: def config_file() -> Path:
"""Ruta a games.toml (junto al ejecutable / raíz del proyecto).""" """Ruta a games.toml.
En .app vive en Application Support (editable y persistente); se siembra la primera vez
desde ``Contents/Resources/games.toml`` del bundle. Si no, junto al ejecutable / raíz.
"""
bundle = macos_app_bundle()
if bundle is not None:
target = writable_base() / CONFIG_NAME
if not target.exists():
seed = bundle / "Contents" / "Resources" / CONFIG_NAME
if seed.exists():
shutil.copy2(seed, target)
return target
return base_dir() / CONFIG_NAME return base_dir() / CONFIG_NAME
def data_root(data_dir: str = "jlauncher_data") -> Path: def data_root(data_dir: str = "jlauncher_data") -> Path:
"""Carpeta raíz de datos; se crea si no existe.""" """Carpeta raíz de datos; se crea si no existe."""
root = base_dir() / data_dir root = writable_base() / data_dir
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
return root return root
+29
View File
@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import os
import subprocess import subprocess
from collections.abc import Callable from collections.abc import Callable
from pathlib import Path from pathlib import Path
@@ -11,11 +12,38 @@ from .paths import repo_dir
LogFn = Callable[[str], None] LogFn = Callable[[str], None]
# Directorios habituales de herramientas de línea de comandos (Homebrew, MacPorts…).
# Una .app de macOS lanzada desde Finder/Dock NO hereda el PATH del shell de login,
# así que herramientas como cmake, instaladas con Homebrew en /opt/homebrew/bin
# (Apple Silicon) o /usr/local/bin (Intel), quedan fuera del PATH y `make` no las
# encuentra. Las añadimos explícitamente. En Linux estos paths no existen y se
# filtran solos.
_EXTRA_PATH_DIRS = (
"/opt/homebrew/bin",
"/opt/homebrew/sbin",
"/usr/local/bin",
"/usr/local/sbin",
"/opt/local/bin",
)
def _noop(_: str) -> None: def _noop(_: str) -> None:
pass pass
def _launch_env() -> dict[str, str]:
"""Entorno para los subprocesos con un PATH que incluye los directorios de
herramientas habituales aunque la app se lance desde Finder/Dock."""
env = os.environ.copy()
parts = env.get("PATH", "").split(os.pathsep)
parts = [p for p in parts if p]
for d in _EXTRA_PATH_DIRS:
if d not in parts and os.path.isdir(d):
parts.append(d)
env["PATH"] = os.pathsep.join(parts)
return env
def _stream(cmd: str, cwd: Path, log: LogFn) -> int: def _stream(cmd: str, cwd: Path, log: LogFn) -> int:
"""Ejecuta un comando de shell en cwd, retransmitiendo stdout/err línea a línea.""" """Ejecuta un comando de shell en cwd, retransmitiendo stdout/err línea a línea."""
log(f"$ {cmd} (cwd={cwd})") log(f"$ {cmd} (cwd={cwd})")
@@ -27,6 +55,7 @@ def _stream(cmd: str, cwd: Path, log: LogFn) -> int:
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
text=True, text=True,
bufsize=1, bufsize=1,
env=_launch_env(),
) )
assert proc.stdout is not None assert proc.stdout is not None
for line in proc.stdout: for line in proc.stdout:
+7 -7
View File
@@ -1,4 +1,4 @@
"""Preferencias persistentes en settings.json, junto al ejecutable.""" """Preferencias persistentes en settings.json, junto a los datos de la app."""
from __future__ import annotations from __future__ import annotations
@@ -6,7 +6,7 @@ import json
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from pathlib import Path from pathlib import Path
from .paths import base_dir from .paths import writable_base
SETTINGS_NAME = "settings.json" SETTINGS_NAME = "settings.json"
@@ -22,8 +22,8 @@ _CONSOLE_MODES = ("show", "auto", "hide")
def _valid_console_mode(value) -> str: def _valid_console_mode(value) -> str:
"""Normaliza el modo de consola; cae a 'show' si es desconocido.""" """Normaliza el modo de consola; cae a 'auto' si es desconocido."""
return value if value in _CONSOLE_MODES else "show" return value if value in _CONSOLE_MODES else "auto"
@dataclass @dataclass
@@ -33,7 +33,7 @@ class Settings:
gitea_token: str = "" # token personal de Gitea para repos privados (no se versiona) gitea_token: str = "" # token personal de Gitea para repos privados (no se versiona)
check_updates_on_start: bool = False # comprobar updates automáticamente al iniciar check_updates_on_start: bool = False # comprobar updates automáticamente al iniciar
theme: str = "system" # tema de la UI: "system" | "light" | "dark" theme: str = "system" # tema de la UI: "system" | "light" | "dark"
console_mode: str = "show" # consola de log: "show" | "auto" | "hide" console_mode: str = "auto" # consola de log: "show" | "auto" | "hide"
# Tolerancia a repos offline/inalcanzables (segundos, salvo stall_limit en bytes/s). # Tolerancia a repos offline/inalcanzables (segundos, salvo stall_limit en bytes/s).
git_fetch_timeout: int = 60 # techo para fetch / comprobar update git_fetch_timeout: int = 60 # techo para fetch / comprobar update
git_clone_timeout: int = 900 # techo para clone (repo grande) git_clone_timeout: int = 900 # techo para clone (repo grande)
@@ -43,7 +43,7 @@ class Settings:
def settings_path() -> Path: def settings_path() -> Path:
return base_dir() / SETTINGS_NAME return writable_base() / SETTINGS_NAME
def load_settings() -> Settings: def load_settings() -> Settings:
@@ -60,7 +60,7 @@ def load_settings() -> Settings:
gitea_token=str(data.get("gitea_token", "")), gitea_token=str(data.get("gitea_token", "")),
check_updates_on_start=bool(data.get("check_updates_on_start", False)), check_updates_on_start=bool(data.get("check_updates_on_start", False)),
theme=_valid_theme(data.get("theme", "system")), theme=_valid_theme(data.get("theme", "system")),
console_mode=_valid_console_mode(data.get("console_mode", "show")), console_mode=_valid_console_mode(data.get("console_mode", "auto")),
git_fetch_timeout=int(data.get("git_fetch_timeout", 60)), git_fetch_timeout=int(data.get("git_fetch_timeout", 60)),
git_clone_timeout=int(data.get("git_clone_timeout", 900)), git_clone_timeout=int(data.get("git_clone_timeout", 900)),
http_timeout=int(data.get("http_timeout", 15)), http_timeout=int(data.get("http_timeout", 15)),
+86 -2
View File
@@ -5,10 +5,14 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
from PySide6.QtCore import QEasingCurve, QPropertyAnimation, QThreadPool, Qt, QTimer from PySide6.QtCore import QEasingCurve, QPropertyAnimation, QThreadPool, Qt, QTimer
from PySide6.QtGui import QAction, QActionGroup from PySide6.QtGui import QAction, QActionGroup, QPixmap
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QApplication, QApplication,
QDialog,
QDialogButtonBox,
QHBoxLayout,
QInputDialog, QInputDialog,
QLabel,
QLineEdit, QLineEdit,
QMainWindow, QMainWindow,
QMessageBox, QMessageBox,
@@ -19,8 +23,9 @@ from PySide6.QtWidgets import (
QWidget, QWidget,
) )
from .. import gitops from .. import __version__, gitops
from ..config import Config, Game from ..config import Config, Game
from ..paths import app_icon_path
from ..settings import load_settings, save_settings from ..settings import load_settings, save_settings
from ..workers import CheckUpdatesWorker, DownloadWorker, RunWorker from ..workers import CheckUpdatesWorker, DownloadWorker, RunWorker
from . import theme from . import theme
@@ -246,6 +251,85 @@ class MainWindow(QMainWindow):
self.action_token.triggered.connect(self._configure_token) self.action_token.triggered.connect(self._configure_token)
menu.addAction(self.action_token) menu.addAction(self.action_token)
self._build_help_menu()
def _build_help_menu(self) -> None:
"""Menú Ajuda amb el «Quant a…». A macOS, AboutRole el mou al menú de l'app."""
help_menu = self.menuBar().addMenu("Ajuda")
self.action_about = QAction(f"Quant a {APP_NAME}", self)
self.action_about.setMenuRole(QAction.MenuRole.AboutRole)
self.action_about.triggered.connect(self._show_about)
help_menu.addAction(self.action_about)
def _show_about(self) -> None:
"""Diàleg «Quant a» personalitzat: icona, nom gran i tot centrat."""
dlg = QDialog(self)
dlg.setWindowTitle(f"Quant a {APP_NAME}")
dlg.setModal(True)
lay = QVBoxLayout(dlg)
lay.setContentsMargins(40, 30, 40, 24)
lay.setSpacing(0)
# Logo (si el trobem); s'escala suau des del PNG gran.
icon_path = app_icon_path()
if icon_path is not None:
pix = QPixmap(str(icon_path))
if not pix.isNull():
logo = QLabel(alignment=Qt.AlignCenter)
logo.setPixmap(
pix.scaled(
96, 96, Qt.KeepAspectRatio, Qt.SmoothTransformation
)
)
lay.addWidget(logo)
lay.addSpacing(16)
# Nom de l'app: gran, en negreta i amb el morat de la marca.
name = QLabel(APP_NAME, alignment=Qt.AlignCenter)
nf = name.font()
nf.setPointSize(nf.pointSize() + 13)
nf.setBold(True)
name.setFont(nf)
name.setStyleSheet("color: #7c4dff;")
lay.addWidget(name)
lay.addSpacing(4)
# Versió en cursiva i atenuada.
ver = QLabel(f"Versió {__version__}", alignment=Qt.AlignCenter)
vf = ver.font()
vf.setItalic(True)
vf.setPointSize(vf.pointSize() + 1)
ver.setFont(vf)
ver.setStyleSheet("color: #8a8a8a;")
lay.addWidget(ver)
lay.addSpacing(18)
# Lema discret.
tag = QLabel("Clona, compila i juga · jailgames", alignment=Qt.AlignCenter)
tag.setStyleSheet("color: #8a8a8a;")
lay.addWidget(tag)
lay.addSpacing(14)
copy = QLabel("© 2026 JailDesigner", alignment=Qt.AlignCenter)
cf = copy.font()
cf.setPointSize(cf.pointSize() - 1)
copy.setFont(cf)
lay.addWidget(copy)
lay.addSpacing(24)
# Botó OK centrat.
buttons = QDialogButtonBox(QDialogButtonBox.Ok)
buttons.button(QDialogButtonBox.Ok).setText("D'acord")
buttons.accepted.connect(dlg.accept)
row = QHBoxLayout()
row.addStretch(1)
row.addWidget(buttons)
row.addStretch(1)
lay.addLayout(row)
dlg.exec()
def _build_theme_menu(self, parent_menu) -> None: def _build_theme_menu(self, parent_menu) -> None:
"""Submenú Tema amb tres opcions exclusives: Sistema / Clar / Fosc.""" """Submenú Tema amb tres opcions exclusives: Sistema / Clar / Fosc."""
submenu = parent_menu.addMenu("Tema") submenu = parent_menu.addMenu("Tema")
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "jlauncher" name = "jlauncher"
version = "0.1.0" version = "1.0.1"
description = "Lanzador de juegos jailgames: clona, compila y ejecuta repos Gitea" description = "Lanzador de juegos jailgames: clona, compila y ejecuta repos Gitea"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [