7 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
15 changed files with 455 additions and 47 deletions
+2
View File
@@ -14,6 +14,8 @@ settings.json
# Icon must end with two \r
Icon
# En FS case-insensitive (macOS) el patrón «Icon» se traga la carpeta icon/; re-inclúyela.
!/icon/
# Thumbnails
._*
+22 -5
View File
@@ -88,9 +88,26 @@ fuente se ejecuta con `python -m jlauncher`).
patchelf (Nuitka usa `install_name_tool`).
- `git` en el PATH.
### macOS
### macOS (.app + .dmg)
Compila en el propio Mac (Nuitka no compila cruzado): `./build.sh` genera
`jlauncher-v…-darwin-arm64.tar.gz`. Como el binario no va firmado, la primera vez quizá
debas hacer `xattr -dr com.apple.quarantine jlauncher` o abrirlo con clic derecho → Abrir.
Lánzalo desde terminal (`./jlauncher`).
Compila en el propio Mac (Nuitka no compila cruzado). En macOS, `./build.sh` no genera un
binario suelto sino una **app nativa**:
```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
# Compila jlauncher a un binario standalone con Nuitka y empaqueta un tar.gz de release.
# Requisitos del sistema: python3-dev, gcc, patchelf (ver README).
# Compila jlauncher con Nuitka y empaqueta un release.
# - 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
HERE="$(cd "$(dirname "$0")" && pwd)"
@@ -13,7 +15,6 @@ if [ -z "$VERSION" ]; then
fi
ARCH="$(uname -m)"
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
RELEASE_NAME="jlauncher-v${VERSION}-${OS}-${ARCH}"
if [ ! -d .venv ]; then
echo "[build] creando venv…"
@@ -39,25 +40,98 @@ echo "[build] versión: v${VERSION}"
echo "[build] limpiando artefactos previos…"
rm -rf dist build app.build app.dist app.onefile-build
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
ICON_ICNS="icon/icon.icns"
ICON_PNG="icon/icon.png"
echo "[build] copiando games.toml junto al binario…"
cp games.toml dist/games.toml
if [ "$OS" = "darwin" ]; then
# -------------------------------------------------------------------------
# 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…"
tar -czf "dist/${RELEASE_NAME}.tar.gz" -C dist jlauncher games.toml
echo "[build] compilando jlauncher.app (PySide6; puede tardar varios minutos)…"
.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:"
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)."
# Según la versión, Nuitka nombra el bundle a partir del script de entrada
# (app.py -> app.app). Lo normalizamos a jlauncher.app.
APP="dist/jlauncher.app"
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
+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."""
__version__ = "1.0.0"
__version__ = "1.0.2"
+5 -1
View File
@@ -4,10 +4,11 @@ from __future__ import annotations
import sys
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QApplication, QMessageBox
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.theme import apply_theme
@@ -15,6 +16,9 @@ from .ui.theme import apply_theme
def main() -> int:
app = QApplication(sys.argv)
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;
# MainWindow re-aplica el modo guardado (system/light/dark) y vigila los cambios.
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
import os
import shutil
import sys
from pathlib import Path
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:
"""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
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:
"""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
def data_root(data_dir: str = "jlauncher_data") -> Path:
"""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)
return root
+29
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import os
import subprocess
from collections.abc import Callable
from pathlib import Path
@@ -11,11 +12,38 @@ from .paths import repo_dir
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:
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:
"""Ejecuta un comando de shell en cwd, retransmitiendo stdout/err línea a línea."""
log(f"$ {cmd} (cwd={cwd})")
@@ -27,6 +55,7 @@ def _stream(cmd: str, cwd: Path, log: LogFn) -> int:
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
env=_launch_env(),
)
assert proc.stdout is not None
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
@@ -6,7 +6,7 @@ import json
from dataclasses import asdict, dataclass, field
from pathlib import Path
from .paths import base_dir
from .paths import writable_base
SETTINGS_NAME = "settings.json"
@@ -22,8 +22,8 @@ _CONSOLE_MODES = ("show", "auto", "hide")
def _valid_console_mode(value) -> str:
"""Normaliza el modo de consola; cae a 'show' si es desconocido."""
return value if value in _CONSOLE_MODES else "show"
"""Normaliza el modo de consola; cae a 'auto' si es desconocido."""
return value if value in _CONSOLE_MODES else "auto"
@dataclass
@@ -33,7 +33,7 @@ class Settings:
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
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).
git_fetch_timeout: int = 60 # techo para fetch / comprobar update
git_clone_timeout: int = 900 # techo para clone (repo grande)
@@ -43,7 +43,7 @@ class Settings:
def settings_path() -> Path:
return base_dir() / SETTINGS_NAME
return writable_base() / SETTINGS_NAME
def load_settings() -> Settings:
@@ -60,7 +60,7 @@ def load_settings() -> Settings:
gitea_token=str(data.get("gitea_token", "")),
check_updates_on_start=bool(data.get("check_updates_on_start", False)),
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_clone_timeout=int(data.get("git_clone_timeout", 900)),
http_timeout=int(data.get("http_timeout", 15)),
+73 -8
View File
@@ -5,10 +5,14 @@ from __future__ import annotations
from pathlib import Path
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 (
QApplication,
QDialog,
QDialogButtonBox,
QHBoxLayout,
QInputDialog,
QLabel,
QLineEdit,
QMainWindow,
QMessageBox,
@@ -21,6 +25,7 @@ from PySide6.QtWidgets import (
from .. import __version__, gitops
from ..config import Config, Game
from ..paths import app_icon_path
from ..settings import load_settings, save_settings
from ..workers import CheckUpdatesWorker, DownloadWorker, RunWorker
from . import theme
@@ -257,13 +262,73 @@ class MainWindow(QMainWindow):
help_menu.addAction(self.action_about)
def _show_about(self) -> None:
QMessageBox.about(
self,
f"Quant a {APP_NAME}",
f"<b>{APP_NAME}</b><br>"
f"Versió {__version__}<br><br>"
"© 2026 JailDesigner",
)
"""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:
"""Submenú Tema amb tres opcions exclusives: Sistema / Clar / Fosc."""
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "jlauncher"
version = "1.0.0"
version = "1.0.1"
description = "Lanzador de juegos jailgames: clona, compila y ejecuta repos Gitea"
requires-python = ">=3.11"
dependencies = [