Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fb65f4f249 | |||
| be8c886ad1 | |||
| fd8eedab76 | |||
| a71a1be88d | |||
| 34811038eb | |||
| 95a76d0d76 | |||
| c879127401 |
@@ -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
|
||||
._*
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 361 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 302 KiB |
@@ -1,3 +1,3 @@
|
||||
"""jlauncher — lanzador de juegos jailgames."""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__version__ = "1.0.2"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user