forked from Mirrors/RGSX
v2.1.0.0
- Retrobat: automatic `gamelist.xml` update on launch to immediately show scraped images/videos in ES. - System images loading prioritizes explicit `platform_image` from systems JSON. - Auto-detect supported extensions by parsing `es_systems.cfg`; generate and cache automatically `/saves/ports/rgsx/rom_extensions.json`. - Auto-hide unsupported platforms at start if roms folders not exist / not match `es_systems.cfg`) with a toggle to re enable in the Display menu. - Automatic restart after update configuration (beta) - New Display option to change systems grid layout (3x3, 3x4, 4x3, 4x4). - Pause menu reorganized - Translations updated. - Minor display fixes and spacing polish.
This commit is contained in:
34
README.md
34
README.md
@@ -11,18 +11,23 @@ The application supports multiple sources like myrient and 1fichier. These sourc
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **Game downloads** : Support for ZIP files and handling of unsupported extensions thanks to the `info.txt` file in each folder (batocera), which automatically extracts if the system doesn't support archives.
|
||||
- **Game downloads** : Support for ZIP files and handling of unsupported extensions based on EmulationStation's `es_systems.cfg` (and custom `es_systems_*.cfg` on Batocera). RGSX reads allowed extensions per system from these configs and will automatically extract archives when a system doesn't support them.
|
||||
- Downloads require no authentication or account for most sources.
|
||||
- Systems marked `(1fichier)` in the name will only be accessible if you provide your 1fichier API key (see below).
|
||||
- **Download history** : View and re-download previous files.
|
||||
- **Multi-select downloads** : Mark multiple games in the game list with the key mapped to Clear History (default X) to enqueue several downloads in one batch. Press Confirm to start batch.
|
||||
- **Control customization** : Remap keyboard or controller keys to your preference with automatic button name detection from EmulationStation (beta).
|
||||
- **Systems grid layout**: Change the platforms grid (3x3, 3x4, 4x3, 4x4) from the Display menu.
|
||||
- **Show/hide unsupported systems**: Auto-hide platforms whose ROM folder is missing according to `es_systems.cfg`, with a toggle in the Display menu.
|
||||
- **Smarter system images**: Image loading prioritizes explicit `platform_image` from your systems list JSON before falling back to `<platform_name>.png` or folder images.
|
||||
- **Font size adjustment** : If you find the text too small/too large, you can change it in the menu.
|
||||
- **Search mode** : Filter games by name for quick navigation with virtual keyboard on controller.
|
||||
- **Multilingual support** : Interface available in multiple languages. You can choose the language in the menu.
|
||||
- **Error handling** with informative messages and LOG file.
|
||||
- **Adaptive interface** : The interface adapts to all resolutions from 800x600 to 4K (not tested beyond 1920x1080).
|
||||
- **Automatic updates** : the application must be restarted after an update.
|
||||
- **Automatic supported extensions cache**: On first use, RGSX reads `es_systems.cfg` (RetroBat/Batocera) and generates `/saves/ports/rgsx/rom_extensions.json` with allowed extensions per system.
|
||||
- **Retrobat gamelist auto-update**: On Retrobat, the Windows `gamelist.xml` is updated automatically at launch so your images/videos appear in EmulationStation.
|
||||
|
||||
---
|
||||
|
||||
@@ -91,6 +96,13 @@ INFO: for retrobat on first launch, the application will download Python in the
|
||||
- From the pause menu, access history, control help (control display changes depending on which menu you're in) or reconfiguration of keys, languages, font size.
|
||||
- You can also, from the menu, regenerate the cache of the systems/games/images list to be sure to have the latest updates.
|
||||
|
||||
#### Display menu
|
||||
|
||||
- Layout: switch platforms grid between 3x3, 3x4, 4x3, 4x4.
|
||||
- Font size: adjust text scale (accessibility).
|
||||
- Show unsupported systems: toggle visibility for platforms whose ROM folder is missing.
|
||||
- Filter systems: quickly show/hide platforms by name (persistent).
|
||||
|
||||
---
|
||||
|
||||
### Download
|
||||
@@ -148,19 +160,23 @@ RGSX/
|
||||
├── language.py # Multilingual support management.
|
||||
├── accessibility.py # Accessibility settings management.
|
||||
├── utils.py # Utility functions (text wrap, truncation etc.).
|
||||
├── update_gamelist.py # Game list update.
|
||||
├── update_gamelist.py # Game list update (Batocera/Knulli).
|
||||
├── update_gamelist_windows.py # Retrobat-only: auto-update ES gamelist.xml on launch.
|
||||
├── assets/ # Application resources (fonts, executables, music).
|
||||
├── games/ # Game system configuration files.
|
||||
├── images/ # System images.
|
||||
|
||||
├── languages/ # Translation files.
|
||||
└── logs/
|
||||
└── RGSX.log # Log file.
|
||||
|
||||
/saves/ports/RGSX/
|
||||
│
|
||||
├── systems_list.json # Available Systems names / folders / images
|
||||
├── games/ # Links for games.
|
||||
├── images/ # System images.
|
||||
├── rgsx_settings.json # Unified configuration file (settings, accessibility, language, music, symlinks).
|
||||
├── controls.json # Control mapping file (generated after first startup).
|
||||
├── history.json # Download history database (generated after first download).
|
||||
├── rom_extensions.json # Generated from es_systems.cfg: per-system allowed ROM extensions cache.
|
||||
└── 1FichierAPI.txt # 1fichier API key (premium account and + only) (empty by default).
|
||||
```
|
||||
|
||||
@@ -207,6 +223,16 @@ Developed with ❤️ for retro gaming enthusiasts.
|
||||
|
||||
## 🔄 Changelog
|
||||
|
||||
### 2.1.0.0 (2025-09-09)
|
||||
- Retrobat: automatic `gamelist.xml` update on launch to immediately show scraped images/videos in ES.
|
||||
- System image loading prioritizes explicit `platform_image` from systems JSON.
|
||||
- Auto-detect supported extensions by parsing `es_systems.cfg`; generate and cache `/saves/ports/rgsx/rom_extensions.json`.
|
||||
- Auto-hide unsupported platforms (missing ROM folder per `es_systems.cfg`) with a toggle in the Display menu.
|
||||
- New Display option to change systems grid layout (3x3, 3x4, 4x3, 4x4).
|
||||
- Pause menu reorganized to surface the most used items.
|
||||
- Translations updated.
|
||||
- Minor display fixes and spacing polish.
|
||||
|
||||
### 2.0.0.0 (2025-09-05)
|
||||
- Complete sources system overhaul: centralized management through `/saves/ports/rgsx/systems_list.json` (order preserved), automatic platform addition by dropping its JSON file into `/saves/ports/rgsx/games/` (auto-created if missing) — after first creation edit the generated "dossier" field so it matches your downloads folder structure.
|
||||
- Systems visibility filter menu (show/hide platforms with persistent hidden list in settings)
|
||||
|
||||
34
README_FR.md
34
README_FR.md
@@ -10,18 +10,23 @@ L'application prend en charge plusieurs sources comme myrient, 1fichier. Ces sou
|
||||
|
||||
## ✨ Fonctionnalités
|
||||
|
||||
- **Téléchargement de jeux** : Prise en charge des fichiers ZIP et gestion des extensions non supportées grâce au fichier `info.txt` dans chaque dossier (batocera), qui extrait automatiquement si le système ne supporte pas les archives.
|
||||
- **Téléchargement de jeux** : Prise en charge des fichiers ZIP et gestion des extensions non supportées à partir du fichier `es_systems.cfg` d'EmulationStation (et des `es_systems_*.cfg` personnalisés sur Batocera). RGSX lit les extensions autorisées par système depuis ces configurations et extrait automatiquement les archives si le système ne les supporte pas.
|
||||
- Les téléchargements ne nécessitent aucune authentification ni compte pour la plupart.
|
||||
- Les systèmes notés `(1fichier)` dans le nom ne seront accessibles que si vous renseignez votre clé API 1fichier (voir plus bas).
|
||||
- **Historique des téléchargements** : Consultez et retéléchargez les anciens fichiers.
|
||||
- **Téléchargements multi-sélection** : Marquez plusieurs jeux dans la liste avec la touche associée à Vider Historique (par défaut X) pour préparer un lot. Appuyez ensuite sur Confirmer pour lancer les téléchargements en séquence.
|
||||
- **Personnalisation des contrôles** : Remappez les touches du clavier ou de la manette à votre convenance avec détection automatique des noms de boutons depuis EmulationStation(beta).
|
||||
- **Grille des plateformes** : changez la disposition de la grille (3x3, 3x4, 4x3, 4x4) depuis le menu Affichage.
|
||||
- **Afficher/Masquer plateformes non supportées** : masquage automatique des systèmes dont le dossier ROM est absent selon `es_systems.cfg`, avec un interrupteur dans le menu Affichage.
|
||||
- **Images système plus intelligentes** : priorité à l’image explicite `platform_image` issue du JSON des systèmes avant les fallback `<platform_name>.png` ou dossier.
|
||||
- **Changement de taille de police** : Si vous trouvez les écritures trop petites/trop grosses, vous pouvez le changer dans le menu.
|
||||
- **Mode recherche** : Filtrez les jeux par nom pour une navigation rapide avec clavier virtuel sur manette.
|
||||
- **Support multilingue** : Interface disponible en plusieurs langues. Vous pourrez choisir la langue dans le menu.
|
||||
- **Gestion des erreurs** avec messages informatifs et fichier de LOG.
|
||||
- **Interface adaptative** : L'interface s'adapte à toutes résolutions de 800x600 à 4K (non testé au-delà de 1920x1080).
|
||||
- **Mise à jour automatique** : l'application doit être relancée après une mise à jour.
|
||||
- **Cache des extensions supportées** : à la première utilisation, RGSX lit `es_systems.cfg` (RetroBat/Batocera) et génère `/saves/ports/rgsx/rom_extensions.json` avec les extensions autorisées par système.
|
||||
- **Mise à jour automatique de la gamelist (Retrobat)** : sur Retrobat, le `gamelist.xml` Windows est mis à jour automatiquement au lancement pour afficher les images/vidéos dans EmulationStation.
|
||||
|
||||
---
|
||||
|
||||
@@ -89,6 +94,13 @@ INFO : pour retrobat au premier lancement, l'application téléchargera Python d
|
||||
- Depuis le menu pause, accédez à l'historique, à l'aide des contrôles (l'affichage des contrôles change suivant le menu où vous êtes) ou à la reconfiguration des touches, des langues, de la taille de la police.
|
||||
- Vous pouvez aussi, depuis le menu, régénérer le cache de la liste des systèmes/jeux/images pour être sûr d'avoir les dernières mises à jour.
|
||||
|
||||
#### Menu Affichage
|
||||
|
||||
- Disposition: basculez la grille des plateformes entre 3x3, 3x4, 4x3, 4x4.
|
||||
- Taille de police: ajustez l’échelle du texte (accessibilité).
|
||||
- Afficher plateformes non supportées: afficher/masquer les systèmes dont le dossier ROM est absent.
|
||||
- Filtrer les systèmes: afficher/masquer rapidement des plateformes par nom (persistant).
|
||||
|
||||
---
|
||||
|
||||
### Téléchargement
|
||||
@@ -127,6 +139,16 @@ Les logs sont enregistrés dans `roms/ports/RGSX/logs/RGSX.log` sur batocera et
|
||||
|
||||
## 🔄 Journal des modifications
|
||||
|
||||
### 2.1.0.0 (2025-09-09)
|
||||
- Retrobat : mise à jour automatique de `gamelist.xml` au lancement pour afficher immédiatement les images/vidéos dans ES.
|
||||
- Chargement des images systèmes : priorité à `platform_image` défini dans le JSON des systèmes.
|
||||
- Détection automatique des extensions supportées via `es_systems.cfg`; génération et cache dans `/saves/ports/rgsx/rom_extensions.json`.
|
||||
- Masquage automatique des plateformes non supportées (dossier ROM manquant selon `es_systems.cfg`) avec interrupteur dans le menu Affichage.
|
||||
- Nouveau réglage dans Affichage pour changer la grille des plateformes (3x3, 3x4, 4x3, 4x4).
|
||||
- Réorganisation du menu pause pour mettre en avant les options courantes.
|
||||
- Traductions mises à jour.
|
||||
- Corrections visuelles mineures et ajustements d’espacements.
|
||||
|
||||
### 2.0.0.0 (2025-09-05)
|
||||
- Refonte complète du système de sources : gestion centralisée via `/saves/ports/rgsx/systems_list.json` (ordre conservé), ajout automatique d’une plateforme en déposant son fichier JSON dans `/saves/ports/rgsx/games/` (création si absente) — pensez ensuite à éditer le champ "dossier" généré pour qu’il corresponde à votre organisation de téléchargements.
|
||||
- Nouveau menu de filtrage des systèmes (afficher/masquer plateformes avec persistance dans les paramètres)
|
||||
@@ -205,19 +227,23 @@ RGSX/
|
||||
├── language.py # Gestion du support multilingue.
|
||||
├── accessibility.py # Gestion des paramètres d'accessibilité.
|
||||
├── utils.py # Fonctions utilitaires (wrap du texte, troncage etc.).
|
||||
├── update_gamelist.py # Mise à jour de la liste des jeux.
|
||||
├── update_gamelist.py # Mise à jour de la liste des jeux (Batocera/Knulli).
|
||||
├── update_gamelist_windows.py # Spécifique Retrobat : mise à jour auto de gamelist.xml au lancement.
|
||||
├── assets/ # Ressources de l'application (polices, exécutables, musique).
|
||||
├── games/ # Fichiers de configuration des systèmes de jeux.
|
||||
├── images/ # Images des systèmes.
|
||||
|
||||
├── languages/ # Fichiers de traduction.
|
||||
└── logs/
|
||||
└── RGSX.log # Fichier de logs.
|
||||
|
||||
/saves/ports/RGSX/
|
||||
│
|
||||
├── systems_list.json # Liste des systèmes
|
||||
├── games/ # Liens des systèmes
|
||||
├── images/ # Images des systèmes.
|
||||
├── rgsx_settings.json # Fichier de configuration unifié (paramètres, accessibilité, langue, musique, symlinks).
|
||||
├── controls.json # Fichier de mappage des contrôles (généré après le premier démarrage).
|
||||
├── history.json # Base de données de l'historique de téléchargements (généré après le premier téléchargement).
|
||||
├── rom_extensions.json # Généré depuis es_systems.cfg : cache des extensions autorisées par système.
|
||||
└── 1FichierAPI.txt # Clé API 1fichier (compte premium et + uniquement) (vide par défaut).
|
||||
```
|
||||
|
||||
|
||||
@@ -7,12 +7,15 @@ import logging
|
||||
import requests
|
||||
import queue
|
||||
import datetime
|
||||
import subprocess
|
||||
import sys
|
||||
import config
|
||||
|
||||
from display import (
|
||||
init_display, draw_loading_screen, draw_error_screen, draw_platform_grid,
|
||||
draw_progress_screen, draw_controls, draw_virtual_keyboard,
|
||||
draw_extension_warning, draw_pause_menu, draw_controls_help, draw_game_list,
|
||||
draw_display_menu,
|
||||
draw_history_list, draw_clear_history_dialog, draw_cancel_download_dialog,
|
||||
draw_confirm_dialog, draw_redownload_game_cache_dialog, draw_popup, draw_gradient,
|
||||
THEME_COLORS
|
||||
@@ -47,6 +50,33 @@ except Exception as e:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Mise à jour de la gamelist Windows avant toute initialisation graphique (évite les conflits avec ES)
|
||||
def _run_windows_gamelist_update():
|
||||
try:
|
||||
if platform.system() != "Windows":
|
||||
return
|
||||
script_path = os.path.join(config.APP_FOLDER, "update_gamelist_windows.py")
|
||||
if not os.path.exists(script_path):
|
||||
return
|
||||
logger.info("Lancement de update_gamelist_windows.py depuis __main__ (pré-init)")
|
||||
exe = sys.executable or "python"
|
||||
# Exécuter rapidement avec capture sortie pour journaliser tout message utile
|
||||
result = subprocess.run(
|
||||
[exe, script_path],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
cwd=config.APP_FOLDER,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
logger.info(f"update_gamelist_windows.py terminé avec code {result.returncode}")
|
||||
if result.stdout:
|
||||
logger.debug(result.stdout.strip())
|
||||
except Exception as e:
|
||||
logger.exception(f"Échec lors de l'exécution de update_gamelist_windows.py: {e}")
|
||||
|
||||
_run_windows_gamelist_update()
|
||||
|
||||
# Initialisation de Pygame
|
||||
pygame.init()
|
||||
pygame.joystick.init()
|
||||
@@ -54,9 +84,56 @@ logger.debug("------------------------------------------------------------------
|
||||
logger.debug("---------------------------DEBUT LOG--------------------------------")
|
||||
logger.debug("--------------------------------------------------------------------")
|
||||
|
||||
|
||||
#Récupération des noms des joysticks si pas de joystick connecté, verifier si clavier connecté
|
||||
joystick_names = [pygame.joystick.Joystick(i).get_name() for i in range(pygame.joystick.get_count())]
|
||||
if not joystick_names:
|
||||
joystick_names = ["Clavier"]
|
||||
print("Aucun joystick détecté, utilisation du clavier par défaut")
|
||||
logger.debug("Aucun joystick détecté, utilisation du clavier par défaut.")
|
||||
config.joystick = False
|
||||
config.keyboard = True
|
||||
else:
|
||||
config.joystick = True
|
||||
config.keyboard = False
|
||||
print(f"Joysticks détectés: {joystick_names}")
|
||||
logger.debug(f"Joysticks détectés: {joystick_names}, utilisation du joystick par défaut.")
|
||||
# Test des boutons du joystick
|
||||
for name in joystick_names:
|
||||
if "Xbox" in name or "PlayStation" in name or "Logitech" in name:
|
||||
config.xbox_controller = True
|
||||
logger.debug(f"Manette Xbox/PlayStation/Logitech détectée: {name}")
|
||||
print(f"Manette Xbox/PlayStation/Logitech détectée: {name}")
|
||||
break
|
||||
elif "Nintendo" in name:
|
||||
config.nintendo_controller = True
|
||||
logger.debug(f"Manette Nintendo détectée: {name}")
|
||||
print(f"Manette Nintendo détectée: {name}")
|
||||
elif "8Bitdo" in name:
|
||||
config.eightbitdo_controller = True
|
||||
logger.debug(f"Manette 8Bitdo détectée: {name}")
|
||||
print(f"Manette 8Bitdo détectée: {name}")
|
||||
elif "Steam" in name:
|
||||
config.steam_controller = True
|
||||
logger.debug(f"Manette Steam détectée: {name}")
|
||||
print(f"Manette Steam détectée: {name}")
|
||||
elif "TRIMUI Smart Pro" in name:
|
||||
config.trimui_controller = True
|
||||
logger.debug(f"TRIMUI Smart Pro détectée: {name}")
|
||||
print(f"TRIMUI Smart Pro détectée: {name}")
|
||||
else:
|
||||
config.generic_controller = True
|
||||
logger.debug(f"Manette générique détectée: {name}")
|
||||
print(f"Manette générique détectée: {name}")
|
||||
# Chargement des paramètres d'accessibilité
|
||||
config.accessibility_settings = load_accessibility_settings()
|
||||
# Appliquer la grille d'affichage depuis les paramètres
|
||||
try:
|
||||
from rgsx_settings import get_display_grid
|
||||
gcols, grows = get_display_grid()
|
||||
config.GRID_COLS, config.GRID_ROWS = gcols, grows
|
||||
logger.debug(f"Grille d'affichage initiale: {gcols}x{grows}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur chargement grille d'affichage initiale: {e}")
|
||||
for i, scale in enumerate(config.font_scale_options):
|
||||
if scale == config.accessibility_settings.get("font_scale", 1.0):
|
||||
config.current_font_scale_index = i
|
||||
@@ -189,6 +266,18 @@ async def main():
|
||||
|
||||
current_time = pygame.time.get_ticks()
|
||||
|
||||
# Déclenchement d'un redémarrage planifié (permet d'afficher une popup avant)
|
||||
try:
|
||||
pending = getattr(config, 'pending_restart_at', 0)
|
||||
if pending and pygame.time.get_ticks() >= pending:
|
||||
logger.info("Redémarrage planifié déclenché")
|
||||
# Clear the flag to avoid repeated triggers in case restart fails
|
||||
config.pending_restart_at = 0
|
||||
from utils import restart_application
|
||||
restart_application(0)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du déclenchement du redémarrage planifié: {e}")
|
||||
|
||||
# Forcer redraw toutes les 100 ms dans download_progress
|
||||
if config.menu_state == "download_progress" and current_time - last_redraw_time >= 100:
|
||||
config.needs_redraw = True
|
||||
@@ -276,6 +365,11 @@ async def main():
|
||||
if handle_accessibility_events(event):
|
||||
config.needs_redraw = True
|
||||
continue
|
||||
if config.menu_state == "display_menu":
|
||||
# Les événements sont gérés dans controls.handle_controls
|
||||
action = handle_controls(event, sources, joystick, screen)
|
||||
config.needs_redraw = True
|
||||
continue
|
||||
|
||||
if config.menu_state == "controls_help":
|
||||
action = handle_controls(event, sources, joystick, screen)
|
||||
@@ -583,7 +677,6 @@ async def main():
|
||||
config.needs_redraw = True
|
||||
del config.download_tasks[task_id]
|
||||
|
||||
# Popup download_result supprimé : plus de temporisation de 3s
|
||||
|
||||
# Affichage
|
||||
if config.needs_redraw:
|
||||
@@ -637,6 +730,8 @@ async def main():
|
||||
elif config.menu_state == "accessibility_menu":
|
||||
from accessibility import draw_accessibility_menu
|
||||
draw_accessibility_menu(screen)
|
||||
elif config.menu_state == "display_menu":
|
||||
draw_display_menu(screen)
|
||||
elif config.menu_state == "language_select":
|
||||
from display import draw_language_menu
|
||||
draw_language_menu(screen)
|
||||
@@ -854,17 +949,22 @@ async def main():
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
pygame.mixer.music.stop()
|
||||
|
||||
process_name = "emulatorLauncher.exe"
|
||||
result = os.system(f"taskkill /f /im {process_name}")
|
||||
result = subprocess.run(["taskkill", "/f", "/im", "emulatorLauncher.exe"])
|
||||
if result == 0:
|
||||
logger.debug(f"Quitté avec succès: {process_name}")
|
||||
logger.debug(f"Quitté avec succès: emulatorLauncher.exe")
|
||||
else:
|
||||
logger.debug("Error en essayant de quitter emulatorlauncher.")
|
||||
|
||||
pygame.quit()
|
||||
logger.debug("Application terminée")
|
||||
|
||||
result2 = subprocess.run(["batocera-es-swissknife", "--emukill"])
|
||||
if result2 == 0:
|
||||
logger.debug(f"Quitté avec succès")
|
||||
else:
|
||||
logger.debug("Error en essayant de quitter batocera-es-swissknife.")
|
||||
|
||||
|
||||
|
||||
if platform.system() == "Emscripten":
|
||||
asyncio.ensure_future(main())
|
||||
else:
|
||||
|
||||
@@ -5,7 +5,7 @@ import platform
|
||||
from rgsx_settings import load_rgsx_settings, save_rgsx_settings
|
||||
|
||||
# Version actuelle de l'application
|
||||
app_version = "2.0.0.1"
|
||||
app_version = "2.1.0.0"
|
||||
|
||||
def get_operating_system():
|
||||
"""Renvoie le nom du système d'exploitation."""
|
||||
@@ -60,17 +60,14 @@ def get_system_root():
|
||||
|
||||
return "/" if not OPERATING_SYSTEM == "Windows" else os.path.splitdrive(os.getcwd())[0] + os.sep
|
||||
|
||||
|
||||
# Chemins de base
|
||||
SYSTEM_FOLDER = get_system_root()
|
||||
APP_FOLDER = os.path.join(get_application_root(), "RGSX")
|
||||
ROMS_FOLDER = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(APP_FOLDER))), "roms")
|
||||
SAVE_FOLDER = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(APP_FOLDER))), "saves", "ports", "rgsx")
|
||||
BIOS_FOLDER = os.path.dirname(os.path.dirname(os.path.dirname(APP_FOLDER)))
|
||||
RETROBAT_DATA_FOLDER = os.path.dirname(os.path.dirname(os.path.dirname(APP_FOLDER)))
|
||||
|
||||
print(f"BIOS_FOLDER: {BIOS_FOLDER}")
|
||||
print(f"ROMS_FOLDER: {ROMS_FOLDER}")
|
||||
print(f"SAVE_FOLDER: {SAVE_FOLDER}")
|
||||
print(f"RGSX APP_FOLDER: {APP_FOLDER}")
|
||||
|
||||
|
||||
# Configuration du logging
|
||||
@@ -80,17 +77,17 @@ log_file = os.path.join(log_dir, "RGSX.log")
|
||||
|
||||
# Chemins de base
|
||||
GAMELISTXML = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(APP_FOLDER))), "roms", "ports", "gamelist.xml")
|
||||
|
||||
GAMELISTXML_WINDOWS = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(APP_FOLDER))), "roms", "windows", "gamelist.xml")
|
||||
#Dossier /roms/ports/rgsx
|
||||
UPDATE_FOLDER = os.path.join(APP_FOLDER, "update")
|
||||
LANGUAGES_FOLDER = os.path.join(APP_FOLDER, "languages")
|
||||
JSON_EXTENSIONS = os.path.join(APP_FOLDER, "rom_extensions.json")
|
||||
MUSIC_FOLDER = os.path.join(APP_FOLDER, "assets", "music")
|
||||
|
||||
#Dossier /saves/ports/rgsx
|
||||
IMAGES_FOLDER = os.path.join(SAVE_FOLDER, "images")
|
||||
GAMES_FOLDER = os.path.join(SAVE_FOLDER, "games")
|
||||
SOURCES_FILE = os.path.join(SAVE_FOLDER, "systems_list.json")
|
||||
JSON_EXTENSIONS = os.path.join(SAVE_FOLDER, "rom_extensions.json")
|
||||
CONTROLS_CONFIG_PATH = os.path.join(SAVE_FOLDER, "controls.json")
|
||||
HISTORY_PATH = os.path.join(SAVE_FOLDER, "history.json")
|
||||
API_KEY_1FICHIER = os.path.join(SAVE_FOLDER, "1fichierAPI.txt")
|
||||
@@ -112,6 +109,27 @@ xdvdfs_download_exe = os.path.join(OTA_SERVER_URL, "xdvdfs.exe")
|
||||
|
||||
xdvdfs_download_linux = os.path.join(OTA_SERVER_URL, "xdvdfs")
|
||||
|
||||
# Print des chemins pour debug
|
||||
print(f"RETROBAT_DATA_FOLDER: {RETROBAT_DATA_FOLDER}")
|
||||
print(f"ROMS_FOLDER: {ROMS_FOLDER}")
|
||||
print(f"SAVE_FOLDER: {SAVE_FOLDER}")
|
||||
print(f"RGSX APP_FOLDER: {APP_FOLDER}")
|
||||
print(f"RGSX LOGS_FOLDER: {log_dir}")
|
||||
print(f"RGSX SETTINGS PATH: {RGSX_SETTINGS_PATH}")
|
||||
print(f"GAMELISTXML: {GAMELISTXML}")
|
||||
print(f"GAMELISTXML_WINDOWS: {GAMELISTXML_WINDOWS}")
|
||||
print(f"UPDATE_FOLDER: {UPDATE_FOLDER}")
|
||||
print(f"LANGUAGES_FOLDER: {LANGUAGES_FOLDER}")
|
||||
print(f"JSON_EXTENSIONS: {JSON_EXTENSIONS}")
|
||||
print(f"MUSIC_FOLDER: {MUSIC_FOLDER}")
|
||||
print(f"IMAGES_FOLDER: {IMAGES_FOLDER}")
|
||||
print(f"GAMES_FOLDER: {GAMES_FOLDER}")
|
||||
print(f"SOURCES_FILE: {SOURCES_FILE}")
|
||||
print(f"CONTROLS_CONFIG_PATH: {CONTROLS_CONFIG_PATH}")
|
||||
print(f"HISTORY_PATH: {HISTORY_PATH}")
|
||||
|
||||
|
||||
|
||||
# Constantes pour la répétition automatique dans pause_menu
|
||||
REPEAT_DELAY = 350 # Délai initial avant répétition (ms) - augmenté pour éviter les doubles actions
|
||||
REPEAT_INTERVAL = 120 # Intervalle entre répétitions (ms) - ajusté pour une navigation plus contrôlée
|
||||
|
||||
@@ -3,10 +3,11 @@ import pygame # type: ignore
|
||||
import config
|
||||
# Constantes pour la répétition automatique - importées de config.py
|
||||
from config import REPEAT_DELAY, REPEAT_INTERVAL, REPEAT_ACTION_DEBOUNCE
|
||||
from config import CONTROLS_CONFIG_PATH , GRID_COLS, GRID_ROWS
|
||||
from config import CONTROLS_CONFIG_PATH
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from display import draw_validation_transition
|
||||
from network import download_rom, download_from_1fichier, is_1fichier_url
|
||||
from utils import (
|
||||
@@ -32,7 +33,7 @@ VALID_STATES = [
|
||||
"platform", "game", "confirm_exit",
|
||||
"extension_warning", "pause_menu", "controls_help", "history", "controls_mapping",
|
||||
"redownload_game_cache", "restart_popup", "error", "loading", "confirm_clear_history",
|
||||
"language_select", "filter_platforms"
|
||||
"language_select", "filter_platforms", "display_menu"
|
||||
]
|
||||
|
||||
def validate_menu_state(state):
|
||||
@@ -157,19 +158,19 @@ def handle_controls(event, sources, joystick, screen):
|
||||
|
||||
#Plateformes
|
||||
elif config.menu_state == "platform":
|
||||
systems_per_page = GRID_COLS * GRID_ROWS
|
||||
systems_per_page = config.GRID_COLS * config.GRID_ROWS
|
||||
max_index = min(systems_per_page, len(config.platforms) - config.current_page * systems_per_page) - 1
|
||||
current_grid_index = config.selected_platform - config.current_page * systems_per_page
|
||||
row = current_grid_index // GRID_COLS
|
||||
col = current_grid_index % GRID_COLS
|
||||
row = current_grid_index // config.GRID_COLS
|
||||
col = current_grid_index % config.GRID_COLS
|
||||
|
||||
# Espace réservé pour des fonctions helper si nécessaire
|
||||
|
||||
if is_input_matched(event, "down"):
|
||||
# Navigation vers le bas avec gestion des limites de page
|
||||
if current_grid_index + GRID_COLS <= max_index:
|
||||
if current_grid_index + config.GRID_COLS <= max_index:
|
||||
# Déplacement normal vers le bas
|
||||
config.selected_platform += GRID_COLS
|
||||
config.selected_platform += config.GRID_COLS
|
||||
update_key_state("down", True, event.type, event.key if event.type == pygame.KEYDOWN else
|
||||
event.button if event.type == pygame.JOYBUTTONDOWN else
|
||||
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
|
||||
@@ -178,7 +179,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
# Passage à la page suivante si on est en bas de la grille
|
||||
config.current_page += 1
|
||||
new_row = 0 # Première ligne de la nouvelle page
|
||||
config.selected_platform = config.current_page * systems_per_page + new_row * GRID_COLS + col
|
||||
config.selected_platform = config.current_page * systems_per_page + new_row * config.GRID_COLS + col
|
||||
if config.selected_platform >= len(config.platforms):
|
||||
config.selected_platform = len(config.platforms) - 1
|
||||
update_key_state("down", True, event.type, event.key if event.type == pygame.KEYDOWN else
|
||||
@@ -187,9 +188,9 @@ def handle_controls(event, sources, joystick, screen):
|
||||
event.value)
|
||||
elif is_input_matched(event, "up"):
|
||||
# Navigation vers le haut avec gestion des limites de page
|
||||
if current_grid_index - GRID_COLS >= 0:
|
||||
if current_grid_index - config.GRID_COLS >= 0:
|
||||
# Déplacement normal vers le haut
|
||||
config.selected_platform -= GRID_COLS
|
||||
config.selected_platform -= config.GRID_COLS
|
||||
update_key_state("up", True, event.type, event.key if event.type == pygame.KEYDOWN else
|
||||
event.button if event.type == pygame.JOYBUTTONDOWN else
|
||||
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
|
||||
@@ -197,8 +198,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
elif config.current_page > 0:
|
||||
# Passage à la page précédente si on est en haut de la grille
|
||||
config.current_page -= 1
|
||||
new_row = GRID_ROWS - 1 # Dernière ligne de la page précédente
|
||||
config.selected_platform = config.current_page * systems_per_page + new_row * GRID_COLS + col
|
||||
new_row = config.GRID_ROWS - 1 # Dernière ligne de la page précédente
|
||||
config.selected_platform = config.current_page * systems_per_page + new_row * config.GRID_COLS + col
|
||||
if config.selected_platform >= len(config.platforms):
|
||||
config.selected_platform = len(config.platforms) - 1
|
||||
update_key_state("up", True, event.type, event.key if event.type == pygame.KEYDOWN else
|
||||
@@ -216,7 +217,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
elif config.current_page > 0:
|
||||
# Passage à la page précédente si on est à la première colonne
|
||||
config.current_page -= 1
|
||||
config.selected_platform = config.current_page * systems_per_page + row * GRID_COLS + (GRID_COLS - 1)
|
||||
config.selected_platform = config.current_page * systems_per_page + row * config.GRID_COLS + (config.GRID_COLS - 1)
|
||||
if config.selected_platform >= len(config.platforms):
|
||||
config.selected_platform = len(config.platforms) - 1
|
||||
update_key_state("left", True, event.type, event.key if event.type == pygame.KEYDOWN else
|
||||
@@ -224,7 +225,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
|
||||
event.value)
|
||||
elif is_input_matched(event, "right"):
|
||||
if col < GRID_COLS - 1 and current_grid_index < max_index:
|
||||
if col < config.GRID_COLS - 1 and current_grid_index < max_index:
|
||||
# Déplacement normal vers la droite
|
||||
config.selected_platform += 1
|
||||
update_key_state("right", True, event.type, event.key if event.type == pygame.KEYDOWN else
|
||||
@@ -234,7 +235,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
elif (config.current_page + 1) * systems_per_page < len(config.platforms):
|
||||
# Passage à la page suivante si on est à la dernière colonne
|
||||
config.current_page += 1
|
||||
config.selected_platform = config.current_page * systems_per_page + row * GRID_COLS
|
||||
config.selected_platform = config.current_page * systems_per_page + row * config.GRID_COLS
|
||||
if config.selected_platform >= len(config.platforms):
|
||||
config.selected_platform = len(config.platforms) - 1
|
||||
update_key_state("right", True, event.type, event.key if event.type == pygame.KEYDOWN else
|
||||
@@ -245,7 +246,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
# Navigation rapide vers la page suivante
|
||||
if (config.current_page + 1) * systems_per_page < len(config.platforms):
|
||||
config.current_page += 1
|
||||
config.selected_platform = config.current_page * systems_per_page + row * GRID_COLS + col
|
||||
config.selected_platform = config.current_page * systems_per_page + row * config.GRID_COLS + col
|
||||
if config.selected_platform >= len(config.platforms):
|
||||
config.selected_platform = len(config.platforms) - 1
|
||||
# Réinitialiser la répétition pour éviter des comportements inattendus
|
||||
@@ -258,7 +259,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
# Navigation rapide vers la page précédente
|
||||
if config.current_page > 0:
|
||||
config.current_page -= 1
|
||||
config.selected_platform = config.current_page * systems_per_page + row * GRID_COLS + col
|
||||
config.selected_platform = config.current_page * systems_per_page + row * config.GRID_COLS + col
|
||||
if config.selected_platform >= len(config.platforms):
|
||||
config.selected_platform = len(config.platforms) - 1
|
||||
# Réinitialiser la répétition pour éviter des comportements inattendus
|
||||
@@ -514,8 +515,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
platform,
|
||||
load_extensions_json()
|
||||
)
|
||||
ext = os.path.splitext(url)[1].lower()
|
||||
if not is_supported and ext not in ARCHIVE_EXTENSIONS:
|
||||
zip_ok = bool(config.pending_download[3]) # True only if archive and system known
|
||||
if not is_supported and not zip_ok:
|
||||
# Stocker comme pending sans dupliquer l'entrée
|
||||
config.batch_pending_game = (url, platform, game_name, config.pending_download[3])
|
||||
config.previous_menu_state = config.menu_state
|
||||
@@ -588,8 +589,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
platform,
|
||||
load_extensions_json()
|
||||
)
|
||||
ext = os.path.splitext(url)[1].lower()
|
||||
if not is_supported and ext not in ARCHIVE_EXTENSIONS:
|
||||
zip_ok = bool(config.pending_download[3])
|
||||
if not is_supported and not zip_ok:
|
||||
config.previous_menu_state = config.menu_state
|
||||
config.menu_state = "extension_warning"
|
||||
config.extension_confirm_selection = 0
|
||||
@@ -621,8 +622,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
platform,
|
||||
load_extensions_json()
|
||||
)
|
||||
ext = os.path.splitext(url)[1].lower()
|
||||
if not is_supported and ext not in ARCHIVE_EXTENSIONS:
|
||||
zip_ok = bool(config.pending_download[3])
|
||||
if not is_supported and not zip_ok:
|
||||
config.previous_menu_state = config.menu_state
|
||||
config.menu_state = "extension_warning"
|
||||
config.extension_confirm_selection = 0
|
||||
@@ -669,7 +670,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.menu_state = "error"
|
||||
config.error_message = _(
|
||||
"error_api_key"
|
||||
).format(os.join(config.SAVE_FOLDER,"1fichierAPI.txt"))
|
||||
).format(os.path.join(config.SAVE_FOLDER,"1fichierAPI.txt"))
|
||||
config.history[-1]["status"] = "Erreur"
|
||||
config.history[-1]["progress"] = 0
|
||||
config.history[-1]["message"] = "Erreur API : Clé API 1fichier absente"
|
||||
@@ -711,8 +712,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
if not config.pending_download:
|
||||
continue
|
||||
is_supported = is_extension_supported(sanitize_filename(game_name), platform, load_extensions_json())
|
||||
ext = os.path.splitext(url)[1].lower()
|
||||
if not is_supported and ext not in ARCHIVE_EXTENSIONS:
|
||||
zip_ok = bool(config.pending_download[3])
|
||||
if not is_supported and not zip_ok:
|
||||
config.batch_pending_game = (url, platform, game_name, config.pending_download[3])
|
||||
config.previous_menu_state = config.menu_state
|
||||
config.menu_state = "extension_warning"
|
||||
@@ -770,8 +771,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
if not config.pending_download:
|
||||
continue
|
||||
is_supported = is_extension_supported(sanitize_filename(game_name), platform, load_extensions_json())
|
||||
ext = os.path.splitext(url)[1].lower()
|
||||
if not is_supported and ext not in ARCHIVE_EXTENSIONS:
|
||||
zip_ok = bool(config.pending_download[3])
|
||||
if not is_supported and not zip_ok:
|
||||
config.batch_pending_game = (url, platform, game_name, config.pending_download[3])
|
||||
config.previous_menu_state = config.menu_state
|
||||
config.menu_state = "extension_warning"
|
||||
@@ -863,7 +864,13 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.pending_download = check_extension_before_download(game[1], platform, game_name)
|
||||
if config.pending_download:
|
||||
url, platform, game_name, is_zip_non_supported = config.pending_download
|
||||
if is_zip_non_supported and os.path.splitext(url)[1].lower() not in ARCHIVE_EXTENSIONS:
|
||||
# Recalculer le support exact et décider via le flag is_zip_non_supported
|
||||
is_supported = is_extension_supported(
|
||||
sanitize_filename(game_name),
|
||||
platform,
|
||||
load_extensions_json()
|
||||
)
|
||||
if not is_supported and not is_zip_non_supported:
|
||||
config.previous_menu_state = config.menu_state
|
||||
config.menu_state = "extension_warning"
|
||||
config.extension_confirm_selection = 0
|
||||
@@ -990,12 +997,17 @@ def handle_controls(event, sources, joystick, screen):
|
||||
# Menu pause
|
||||
elif config.menu_state == "pause_menu":
|
||||
#logger.debug(f"État pause_menu, selected_option={config.selected_option}, événement={event.type}, valeur={getattr(event, 'value', None)}")
|
||||
if is_input_matched(event, "up"):
|
||||
# Start toggles back to previous state when already in pause
|
||||
if is_input_matched(event, "start"):
|
||||
config.menu_state = validate_menu_state(config.previous_menu_state)
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Start: retour à {config.menu_state} depuis pause_menu")
|
||||
elif is_input_matched(event, "up"):
|
||||
config.selected_option = max(0, config.selected_option - 1)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "down"):
|
||||
# Nombre d'options dynamique (inclut éventuellement l'option source des jeux)
|
||||
total = getattr(config, 'pause_menu_total_options', 9) # fallback 9
|
||||
total = getattr(config, 'pause_menu_total_options', 11) # fallback 11 (Restart added)
|
||||
config.selected_option = min(total - 1, config.selected_option + 1)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "confirm"):
|
||||
@@ -1031,18 +1043,14 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.selected_language_index = 0
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Passage à language_select depuis pause_menu")
|
||||
elif config.selected_option == 4: # Accessibility
|
||||
elif config.selected_option == 4: # Display
|
||||
config.previous_menu_state = validate_menu_state(config.previous_menu_state)
|
||||
config.menu_state = "accessibility_menu"
|
||||
config.menu_state = "display_menu"
|
||||
if not hasattr(config, 'display_menu_selection'):
|
||||
config.display_menu_selection = 0
|
||||
config.needs_redraw = True
|
||||
logger.debug("Passage au menu accessibilité")
|
||||
elif config.selected_option == 5: # Filter platforms
|
||||
# Ne pas écraser previous_menu_state; il référence l'état avant l'ouverture du pause menu
|
||||
config.menu_state = "filter_platforms"
|
||||
config.selected_filter_index = 0
|
||||
config.filter_platforms_scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
elif config.selected_option == 6: # Source toggle (index shifted by new option)
|
||||
logger.debug("Passage au menu affichage")
|
||||
elif config.selected_option == 5: # Source toggle (index shifted by removal of filter)
|
||||
try:
|
||||
from rgsx_settings import get_sources_mode, set_sources_mode
|
||||
current_mode = get_sources_mode()
|
||||
@@ -1058,13 +1066,13 @@ def handle_controls(event, sources, joystick, screen):
|
||||
logger.info(f"Changement du mode des sources vers {new_mode}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur changement mode sources: {e}")
|
||||
elif config.selected_option == 7: # Redownload game cache
|
||||
elif config.selected_option == 6: # Redownload game cache
|
||||
config.previous_menu_state = validate_menu_state(config.previous_menu_state)
|
||||
config.menu_state = "redownload_game_cache"
|
||||
config.redownload_confirm_selection = 0
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Passage à redownload_game_cache depuis pause_menu")
|
||||
elif config.selected_option == 8: # Music toggle
|
||||
elif config.selected_option == 7: # Music toggle
|
||||
config.music_enabled = not config.music_enabled
|
||||
save_music_config()
|
||||
if config.music_enabled:
|
||||
@@ -1076,7 +1084,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
pygame.mixer.music.stop()
|
||||
config.needs_redraw = True
|
||||
logger.info(f"Musique {'activée' if config.music_enabled else 'désactivée'} via menu pause")
|
||||
elif config.selected_option == 9: # Symlink option
|
||||
elif config.selected_option == 8: # Symlink option
|
||||
from rgsx_settings import set_symlink_option, get_symlink_option
|
||||
current_status = get_symlink_option()
|
||||
success, message = set_symlink_option(not current_status)
|
||||
@@ -1084,6 +1092,9 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.popup_timer = 3000 if success else 5000
|
||||
config.needs_redraw = True
|
||||
logger.info(f"Symlink option {'activée' if not current_status else 'désactivée'} via menu pause")
|
||||
elif config.selected_option == 9: # Restart
|
||||
from utils import restart_application
|
||||
restart_application(2000)
|
||||
elif config.selected_option == 10: # Quit
|
||||
config.previous_menu_state = validate_menu_state(config.previous_menu_state)
|
||||
config.menu_state = "confirm_exit"
|
||||
@@ -1102,6 +1113,88 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
logger.debug("Retour au menu pause depuis controls_help")
|
||||
|
||||
# Menu Affichage (layout, police, unsupported)
|
||||
elif config.menu_state == "display_menu":
|
||||
sel = getattr(config, 'display_menu_selection', 0)
|
||||
if is_input_matched(event, "up"):
|
||||
config.display_menu_selection = (sel - 1) % 4
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "down"):
|
||||
config.display_menu_selection = (sel + 1) % 4
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm"):
|
||||
sel = getattr(config, 'display_menu_selection', 0)
|
||||
# 0: layout change
|
||||
if sel == 0 and (is_input_matched(event, "left") or is_input_matched(event, "right")):
|
||||
layouts = [(3,3),(3,4),(4,3),(4,4)]
|
||||
try:
|
||||
idx = layouts.index((config.GRID_COLS, config.GRID_ROWS))
|
||||
except ValueError:
|
||||
idx = 1
|
||||
idx = (idx - 1) % len(layouts) if is_input_matched(event, "left") else (idx + 1) % len(layouts)
|
||||
new_cols, new_rows = layouts[idx]
|
||||
try:
|
||||
from rgsx_settings import set_display_grid
|
||||
set_display_grid(new_cols, new_rows)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur set_display_grid: {e}")
|
||||
config.GRID_COLS = new_cols
|
||||
config.GRID_ROWS = new_rows
|
||||
config.needs_redraw = True
|
||||
# Redémarrage automatique pour appliquer proprement la modification de layout
|
||||
try:
|
||||
from utils import restart_application
|
||||
# Montrer brièvement l'info puis redémarrer
|
||||
config.menu_state = "restart_popup"
|
||||
config.popup_message = _("popup_restarting")
|
||||
config.popup_timer = 2000
|
||||
restart_application(2000)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du redémarrage après changement de layout: {e}")
|
||||
# 1: font size adjust
|
||||
elif sel == 1 and (is_input_matched(event, "left") or is_input_matched(event, "right")):
|
||||
from accessibility import save_accessibility_settings
|
||||
opts = getattr(config, 'font_scale_options', [0.75, 1.0, 1.25, 1.5, 1.75])
|
||||
idx = getattr(config, 'current_font_scale_index', 1)
|
||||
idx = max(0, idx - 1) if is_input_matched(event, "left") else min(len(opts)-1, idx + 1)
|
||||
if idx != getattr(config, 'current_font_scale_index', 1):
|
||||
config.current_font_scale_index = idx
|
||||
scale = opts[idx]
|
||||
config.accessibility_settings["font_scale"] = scale
|
||||
try:
|
||||
save_accessibility_settings(config.accessibility_settings)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur sauvegarde accessibilité: {e}")
|
||||
try:
|
||||
config.init_font()
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur init polices: {e}")
|
||||
config.needs_redraw = True
|
||||
# 2: toggle unsupported
|
||||
elif sel == 2 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
|
||||
try:
|
||||
from rgsx_settings import get_show_unsupported_platforms, set_show_unsupported_platforms
|
||||
current = get_show_unsupported_platforms()
|
||||
new_val = set_show_unsupported_platforms(not current)
|
||||
from utils import load_sources
|
||||
load_sources()
|
||||
config.popup_message = _("menu_show_unsupported_enabled") if new_val else _("menu_show_unsupported_disabled")
|
||||
config.popup_timer = 3000
|
||||
config.needs_redraw = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur toggle unsupported: {e}")
|
||||
# 3: open filter platforms menu
|
||||
elif sel == 3 and (is_input_matched(event, "confirm") or is_input_matched(event, "right")):
|
||||
# Remember return target so the filter menu can go back to display
|
||||
config.filter_return_to = "display_menu"
|
||||
config.menu_state = "filter_platforms"
|
||||
config.selected_filter_index = 0
|
||||
config.filter_platforms_scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "cancel"):
|
||||
config.menu_state = "pause_menu"
|
||||
config.needs_redraw = True
|
||||
|
||||
# Remap controls
|
||||
elif config.menu_state == "controls_mapping":
|
||||
if is_input_matched(event, "cancel"):
|
||||
@@ -1137,9 +1230,12 @@ def handle_controls(event, sources, joystick, screen):
|
||||
logger.debug("Dossier images supprimé avec succès")
|
||||
config.menu_state = "restart_popup"
|
||||
config.popup_message = _("popup_redownload_success")
|
||||
config.popup_timer = 5000 # 5 secondes
|
||||
config.popup_timer = 2000 # bref message
|
||||
config.needs_redraw = True
|
||||
logger.debug("Passage à restart_popup")
|
||||
# Redémarrage automatique
|
||||
from utils import restart_application
|
||||
restart_application(2000)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la suppression du fichier sources.json ou dossiers: {e}")
|
||||
config.menu_state = "error"
|
||||
@@ -1150,9 +1246,11 @@ def handle_controls(event, sources, joystick, screen):
|
||||
logger.debug("Fichier sources.json non trouvé, passage à restart_popup")
|
||||
config.menu_state = "restart_popup"
|
||||
config.popup_message = _("popup_no_cache")
|
||||
config.popup_timer = 5000 # 5 secondes
|
||||
config.popup_timer = 2000
|
||||
config.needs_redraw = True
|
||||
logger.debug("Passage à restart_popup")
|
||||
from utils import restart_application
|
||||
restart_application(2000)
|
||||
else: # Non
|
||||
config.menu_state = validate_menu_state(config.previous_menu_state)
|
||||
config.needs_redraw = True
|
||||
@@ -1213,7 +1311,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
logger.debug("Annulation de la sélection de langue, retour au menu pause")
|
||||
|
||||
# Menu filtre plateformes
|
||||
# Menu filtre plateformes
|
||||
elif config.menu_state == "filter_platforms":
|
||||
total_items = len(config.filter_platforms_selection)
|
||||
action_buttons = 4
|
||||
@@ -1271,7 +1369,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
load_sources()
|
||||
# Recalibrer la sélection et la page courante si elles dépassent la nouvelle liste visible
|
||||
try:
|
||||
systems_per_page = GRID_COLS * GRID_ROWS
|
||||
systems_per_page = config.GRID_COLS * config.GRID_ROWS
|
||||
if config.current_page * systems_per_page >= len(config.platforms):
|
||||
config.current_page = 0
|
||||
if config.selected_platform >= len(config.platforms):
|
||||
@@ -1281,12 +1379,32 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.current_page = 0
|
||||
config.selected_platform = 0
|
||||
config.filter_platforms_dirty = False
|
||||
config.menu_state = "pause_menu"
|
||||
# Return either to display menu or pause menu depending on origin
|
||||
target = getattr(config, 'filter_return_to', 'pause_menu')
|
||||
config.menu_state = target
|
||||
if target == 'display_menu':
|
||||
# reset display selection to the Filter item for convenience
|
||||
config.display_menu_selection = 3
|
||||
else:
|
||||
config.selected_option = 5 # keep pointer on Filter in pause menu
|
||||
config.filter_return_to = None
|
||||
elif btn_idx == 3: # back
|
||||
config.menu_state = "pause_menu"
|
||||
target = getattr(config, 'filter_return_to', 'pause_menu')
|
||||
config.menu_state = target
|
||||
if target == 'display_menu':
|
||||
config.display_menu_selection = 3
|
||||
else:
|
||||
config.selected_option = 5
|
||||
config.filter_return_to = None
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "cancel"):
|
||||
config.menu_state = "pause_menu"
|
||||
target = getattr(config, 'filter_return_to', 'pause_menu')
|
||||
config.menu_state = target
|
||||
if target == 'display_menu':
|
||||
config.display_menu_selection = 3
|
||||
else:
|
||||
config.selected_option = 5
|
||||
config.filter_return_to = None
|
||||
config.needs_redraw = True
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ THEME_COLORS = {
|
||||
"button_hover": (255, 0, 255, 220), # Rose
|
||||
# Générique
|
||||
"text": (255, 255, 255), # blanc
|
||||
# Texte sélectionné (alias pour compatibilité)
|
||||
"text_selected": (0, 255, 0), # utilise le même vert que fond_lignes
|
||||
# Erreur
|
||||
"error_text": (255, 0, 0), # rouge
|
||||
# Avertissement
|
||||
@@ -439,8 +441,8 @@ def draw_platform_grid(screen):
|
||||
margin_right = int(config.screen_width * 0.026)
|
||||
margin_top = int(config.screen_height * 0.140)
|
||||
margin_bottom = int(config.screen_height * 0.0648)
|
||||
num_cols = 3
|
||||
num_rows = 4
|
||||
num_cols = getattr(config, 'GRID_COLS', 3)
|
||||
num_rows = getattr(config, 'GRID_ROWS', 4)
|
||||
systems_per_page = num_cols * num_rows
|
||||
|
||||
available_width = config.screen_width - margin_left - margin_right
|
||||
@@ -820,12 +822,22 @@ def draw_history_list(screen):
|
||||
screen.blit(text_surface, text_rect)
|
||||
return
|
||||
|
||||
available_height = config.screen_height - title_height - extra_margin_top - extra_margin_bottom - 2 * margin_top_bottom
|
||||
items_per_page = available_height // line_height
|
||||
# Espace visible garanti entre le titre et la liste, et au-dessus du footer
|
||||
top_gap = 20
|
||||
bottom_reserved = 70 # réserve pour le footer (barre des contrôles) + marge visuelle (réduit)
|
||||
|
||||
# Positionner la liste juste après le titre, avec un espace dédié
|
||||
# Utiliser le rectangle du titre déjà dessiné pour une meilleure précision
|
||||
title_bottom = title_rect_inflated.bottom
|
||||
rect_y = title_bottom + top_gap
|
||||
|
||||
# Calculer l'espace disponible en bas en réservant une zone pour le footer
|
||||
available_height = max(0, config.screen_height - rect_y - bottom_reserved)
|
||||
# Déterminer le nombre d'éléments par page en tenant compte de l'en-tête et des marges internes
|
||||
items_per_page = max(1, (available_height - header_height - 2 * margin_top_bottom) // line_height)
|
||||
|
||||
rect_height = header_height + items_per_page * line_height + 2 * margin_top_bottom
|
||||
rect_x = (config.screen_width - rect_width) // 2
|
||||
rect_y = title_height + extra_margin_top + (config.screen_height - title_height - extra_margin_top - extra_margin_bottom - rect_height) // 2
|
||||
|
||||
config.history_scroll_offset = max(0, min(config.history_scroll_offset, max(0, len(history) - items_per_page)))
|
||||
if config.current_history_item < config.history_scroll_offset:
|
||||
@@ -1090,9 +1102,6 @@ def draw_progress_screen(screen):
|
||||
# Limiter le pourcentage entre 0 et 100 pour l'affichage de la barre
|
||||
progress_width = int(bar_width * (min(100, max(0, progress_percent)) / 100))
|
||||
|
||||
|
||||
## Ancienne fonction draw_popup_result_download supprimée (popup de fin de téléchargement retiré)
|
||||
|
||||
# Écran avertissement extension non supportée téléchargement
|
||||
def draw_extension_warning(screen):
|
||||
"""Affiche un avertissement pour une extension non reconnue ou un fichier ZIP."""
|
||||
@@ -1103,7 +1112,7 @@ def draw_extension_warning(screen):
|
||||
game_name = "Inconnu"
|
||||
else:
|
||||
url, platform, game_name, is_zip_non_supported = config.pending_download
|
||||
logger.debug(f"config.pending_download: url={url}, platform={platform}, game_name={game_name}, is_zip_non_supported={is_zip_non_supported}")
|
||||
# Log réduit: pas de détail verbeux ici
|
||||
is_zip = is_zip_non_supported
|
||||
if not game_name:
|
||||
game_name = "Inconnu"
|
||||
@@ -1116,7 +1125,6 @@ def draw_extension_warning(screen):
|
||||
|
||||
max_width = config.screen_width - 80
|
||||
lines = wrap_text(message, config.font, max_width)
|
||||
logger.debug(f"Lignes générées : {lines}")
|
||||
|
||||
try:
|
||||
line_height = config.font.get_height() + 5
|
||||
@@ -1217,9 +1225,9 @@ def draw_language_menu(screen):
|
||||
button_height = 60
|
||||
button_width = 300
|
||||
button_spacing = 20
|
||||
|
||||
total_height = len(available_languages) * (button_height + button_spacing) - button_spacing
|
||||
start_y = (config.screen_height - total_height) // 2
|
||||
|
||||
# Démarrer la liste juste sous le titre avec le même écart que les boutons
|
||||
start_y = title_bg_rect.bottom + button_spacing
|
||||
|
||||
for i, lang_code in enumerate(available_languages):
|
||||
# Obtenir le nom de la langue
|
||||
@@ -1245,10 +1253,69 @@ def draw_language_menu(screen):
|
||||
instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, config.screen_height - 50))
|
||||
screen.blit(instruction_surface, instruction_rect)
|
||||
|
||||
def draw_display_menu(screen):
|
||||
"""Affiche le sous-menu Affichage (layout, taille de police, systèmes non supportés)."""
|
||||
screen.blit(OVERLAY, (0, 0))
|
||||
|
||||
# États actuels
|
||||
layout_str = f"{getattr(config, 'GRID_COLS', 3)}x{getattr(config, 'GRID_ROWS', 4)}"
|
||||
font_scale = config.accessibility_settings.get("font_scale", 1.0)
|
||||
from rgsx_settings import get_show_unsupported_platforms
|
||||
show_unsupported = get_show_unsupported_platforms()
|
||||
|
||||
# Libellés
|
||||
options = [
|
||||
f"{_('display_layout')}: {layout_str}",
|
||||
_("accessibility_font_size").format(f"{font_scale:.1f}"),
|
||||
_("menu_show_unsupported_on") if show_unsupported else _("menu_show_unsupported_off"),
|
||||
_("menu_filter_platforms"),
|
||||
]
|
||||
|
||||
selected = getattr(config, 'display_menu_selection', 0)
|
||||
|
||||
# Dimensions du cadre (cohérent avec le menu pause)
|
||||
title_text = _("menu_display")
|
||||
title_surface = config.title_font.render(title_text, True, THEME_COLORS["text"])
|
||||
title_height = title_surface.get_height() + 10
|
||||
menu_width = int(config.screen_width * 0.7)
|
||||
button_height = int(config.screen_height * 0.0463)
|
||||
margin_top_bottom = 20
|
||||
vertical_spacing = 10
|
||||
menu_height = title_height + len(options) * (button_height + vertical_spacing) + 2 * margin_top_bottom
|
||||
menu_x = (config.screen_width - menu_width) // 2
|
||||
menu_y = (config.screen_height - menu_height) // 2
|
||||
|
||||
# Cadre
|
||||
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (menu_x, menu_y, menu_width, menu_height), border_radius=12)
|
||||
pygame.draw.rect(screen, THEME_COLORS["border"], (menu_x, menu_y, menu_width, menu_height), 2, border_radius=12)
|
||||
|
||||
# Titre centré dans le cadre
|
||||
title_rect = title_surface.get_rect(center=(config.screen_width // 2, menu_y + margin_top_bottom + title_surface.get_height() // 2))
|
||||
screen.blit(title_surface, title_rect)
|
||||
|
||||
# Boutons des options
|
||||
for i, option_text in enumerate(options):
|
||||
y = menu_y + margin_top_bottom + title_height + i * (button_height + vertical_spacing)
|
||||
draw_stylized_button(
|
||||
screen,
|
||||
option_text,
|
||||
menu_x + 20,
|
||||
y,
|
||||
menu_width - 40,
|
||||
button_height,
|
||||
selected=(i == selected)
|
||||
)
|
||||
|
||||
# Aide en bas de l'écran
|
||||
instruction_text = _("language_select_instruction")
|
||||
instruction_surface = config.small_font.render(instruction_text, True, THEME_COLORS["text"])
|
||||
instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, config.screen_height - 50))
|
||||
screen.blit(instruction_surface, instruction_rect)
|
||||
|
||||
def draw_pause_menu(screen, selected_option):
|
||||
"""Dessine le menu pause avec un style moderne."""
|
||||
screen.blit(OVERLAY, (0, 0))
|
||||
from rgsx_settings import get_symlink_option, get_sources_mode
|
||||
from rgsx_settings import get_symlink_option, get_sources_mode, get_show_unsupported_platforms
|
||||
mode = get_sources_mode()
|
||||
source_label = _("games_source_rgsx") if mode == "rgsx" else _("games_source_custom")
|
||||
if config.music_enabled:
|
||||
@@ -1262,13 +1329,13 @@ def draw_pause_menu(screen, selected_option):
|
||||
_("menu_remap_controls"), # 1
|
||||
_("menu_history"), # 2
|
||||
_("menu_language"), # 3
|
||||
_("menu_accessibility"), # 4
|
||||
_("menu_filter_platforms"), # 5 new filter option
|
||||
f"{_('menu_games_source_prefix')}: {source_label}", # 5
|
||||
_("menu_display"), # 4 new merged display menu
|
||||
f"{_('menu_games_source_prefix')}: {source_label}", # 5 (shifted left)
|
||||
_("menu_redownload_cache"), # 6
|
||||
music_option, # 7
|
||||
symlink_option, # 8
|
||||
_("menu_quit") # 9
|
||||
_("menu_restart"), # 9 (new)
|
||||
_("menu_quit") # 10
|
||||
]
|
||||
menu_width = int(config.screen_width * 0.8)
|
||||
line_height = config.font.get_height() + 10
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"download_canceled": "Download vom Benutzer abgebrochen.",
|
||||
|
||||
"extension_warning_zip": "Die Datei '{0}' ist ein Archiv und Batocera unterstützt keine Archive für dieses System. Die automatische Extraktion der Datei erfolgt nach dem Download, fortfahren?",
|
||||
"extension_warning_unsupported": "Die Erweiterung der Datei '{0}' wird laut der Datei info.txt von Batocera nicht unterstützt. Möchtest du fortfahren?",
|
||||
"extension_warning_unsupported": "Die Dateierweiterung für '{0}' wird laut der Konfiguration es_systems.cfg von Batocera nicht unterstützt. Möchtest du fortfahren?",
|
||||
|
||||
"confirm_exit": "Anwendung beenden?",
|
||||
"confirm_clear_history": "Verlauf löschen?",
|
||||
@@ -76,10 +76,13 @@
|
||||
"menu_history": "Verlauf",
|
||||
"menu_language": "Sprache",
|
||||
"menu_accessibility": "Barrierefreiheit",
|
||||
"menu_display": "Anzeige",
|
||||
"display_layout": "Anzeigelayout",
|
||||
"menu_redownload_cache": "Spieleliste aktualisieren",
|
||||
"menu_music_toggle": "Musik ein/aus",
|
||||
"menu_music_enabled": "Musik aktiviert: {0}",
|
||||
"menu_music_disabled": "Musik deaktiviert",
|
||||
"menu_restart": "Neustart",
|
||||
"menu_filter_platforms": "Systeme filtern",
|
||||
"filter_platforms_title": "Systemsichtbarkeit",
|
||||
"filter_all": "Alle anzeigen",
|
||||
@@ -88,11 +91,16 @@
|
||||
"filter_back": "Zurück",
|
||||
"filter_platforms_info": "Sichtbar: {0} | Versteckt: {1} / Gesamt: {2}",
|
||||
"filter_unsaved_warning": "Ungespeicherte Änderungen",
|
||||
"menu_show_unsupported_on": "Nicht unterstützte Systeme anzeigen: Ja",
|
||||
"menu_show_unsupported_off": "Nicht unterstützte Systeme anzeigen: Nein",
|
||||
"menu_show_unsupported_enabled": "Sichtbarkeit nicht unterstützter Systeme aktiviert",
|
||||
"menu_show_unsupported_disabled": "Sichtbarkeit nicht unterstützter Systeme deaktiviert",
|
||||
"menu_quit": "Beenden",
|
||||
|
||||
"button_yes": "Ja",
|
||||
"button_no": "Nein",
|
||||
"button_OK": "OK",
|
||||
"popup_restarting": "Neustart...",
|
||||
|
||||
"controls_hold_message": "3 Sekunden halten für: '{0}'",
|
||||
"controls_skip_message": "Drücke Esc, um zu überspringen (nur PC)",
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"download_canceled": "Download canceled by user.",
|
||||
|
||||
"extension_warning_zip": "The file '{0}' is an archive and Batocera does not support archives for this system. Automatic extraction will occur after download, continue?",
|
||||
"extension_warning_unsupported": "The file extension for '{0}' is not supported by Batocera according to the info.txt file. Do you want to continue?",
|
||||
"extension_warning_unsupported": "The file extension for '{0}' is not supported by Batocera according to the es_systems.cfg configuration. Do you want to continue?",
|
||||
|
||||
"confirm_exit": "Exit application?",
|
||||
"confirm_clear_history": "Clear history?",
|
||||
@@ -76,10 +76,13 @@
|
||||
"menu_history": "History",
|
||||
"menu_language": "Language",
|
||||
"menu_accessibility": "Accessibility",
|
||||
"menu_display": "Display",
|
||||
"display_layout": "Display layout",
|
||||
"menu_redownload_cache": "Update games list",
|
||||
"menu_music_toggle": "Toggle music",
|
||||
"menu_music_enabled": "Music enabled: {0}",
|
||||
"menu_music_disabled": "Music disabled",
|
||||
"menu_restart": "Restart",
|
||||
"menu_filter_platforms": "Filter systems",
|
||||
"filter_platforms_title": "Systems visibility",
|
||||
"filter_all": "Show all",
|
||||
@@ -88,11 +91,16 @@
|
||||
"filter_back": "Back",
|
||||
"filter_platforms_info": "Visible: {0} | Hidden: {1} / Total: {2}",
|
||||
"filter_unsaved_warning": "Unsaved changes",
|
||||
"menu_show_unsupported_on": "Show unsupported systems: Yes",
|
||||
"menu_show_unsupported_off": "Show unsupported systems: No",
|
||||
"menu_show_unsupported_enabled": "Unsupported systems visibility enabled",
|
||||
"menu_show_unsupported_disabled": "Unsupported systems visibility disabled",
|
||||
"menu_quit": "Quit",
|
||||
|
||||
"button_yes": "Yes",
|
||||
"button_no": "No",
|
||||
"button_OK": "OK",
|
||||
"popup_restarting": "Restarting...",
|
||||
|
||||
"controls_hold_message": "Hold for 3s for: '{0}'",
|
||||
"controls_skip_message": "Press Esc to skip (PC only)",
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"download_canceled": "Descarga cancelada por el usuario.",
|
||||
|
||||
"extension_warning_zip": "El archivo '{0}' es un archivo comprimido y Batocera no soporta archivos comprimidos para este sistema. La extracción automática del archivo se realizará después de la descarga, ¿continuar?",
|
||||
"extension_warning_unsupported": "La extensión del archivo '{0}' no es soportada por Batocera según el archivo info.txt. ¿Deseas continuar?",
|
||||
"extension_warning_unsupported": "La extensión del archivo '{0}' no está soportada por Batocera según la configuración es_systems.cfg. ¿Deseas continuar?",
|
||||
|
||||
"confirm_exit": "¿Salir de la aplicación?",
|
||||
"confirm_clear_history": "¿Vaciar el historial?",
|
||||
@@ -77,10 +77,13 @@
|
||||
"menu_history": "Historial",
|
||||
"menu_language": "Idioma",
|
||||
"menu_accessibility": "Accesibilidad",
|
||||
"menu_display": "Pantalla",
|
||||
"display_layout": "Distribución",
|
||||
"menu_redownload_cache": "Actualizar lista de juegos",
|
||||
"menu_music_toggle": "Activar/Desactivar música",
|
||||
"menu_music_enabled": "Música activada: {0}",
|
||||
"menu_music_disabled": "Música desactivada",
|
||||
"menu_restart": "Reiniciar",
|
||||
"menu_filter_platforms": "Filtrar sistemas",
|
||||
"filter_platforms_title": "Visibilidad de sistemas",
|
||||
"filter_all": "Mostrar todo",
|
||||
@@ -89,11 +92,16 @@
|
||||
"filter_back": "Volver",
|
||||
"filter_platforms_info": "Visibles: {0} | Ocultos: {1} / Total: {2}",
|
||||
"filter_unsaved_warning": "Cambios no guardados",
|
||||
"menu_show_unsupported_on": "Mostrar sistemas no soportados: Sí",
|
||||
"menu_show_unsupported_off": "Mostrar sistemas no soportados: No",
|
||||
"menu_show_unsupported_enabled": "Visibilidad de sistemas no soportados activada",
|
||||
"menu_show_unsupported_disabled": "Visibilidad de sistemas no soportados desactivada",
|
||||
"menu_quit": "Salir",
|
||||
|
||||
"button_yes": "Sí",
|
||||
"button_no": "No",
|
||||
"button_OK": "OK",
|
||||
"popup_restarting": "Reiniciando...",
|
||||
|
||||
"controls_hold_message": "Mantén presionado durante 3s para: '{0}'",
|
||||
"controls_skip_message": "Presiona Esc para omitir (solo PC)",
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
"download_canceled": "Téléchargement annulé par l'utilisateur.",
|
||||
|
||||
"extension_warning_zip": "Le fichier '{0}' est une archive et Batocera ne prend pas en charge les archives pour ce système. L'extraction automatique du fichier aura lieu après le téléchargement, continuer ?",
|
||||
"extension_warning_unsupported": "L'extension du fichier '{0}' n'est pas supportée par Batocera d'après le fichier info.txt. Voulez-vous continuer ?",
|
||||
"extension_warning_unsupported": "L'extension du fichier '{0}' n'est pas supportée par Batocera d'après la configuration es_systems.cfg. Voulez-vous continuer ?",
|
||||
|
||||
"confirm_exit": "Quitter l'application ?",
|
||||
"confirm_clear_history": "Vider l'historique ?",
|
||||
@@ -73,11 +73,14 @@
|
||||
"menu_history": "Historique",
|
||||
"menu_language": "Langue",
|
||||
"menu_accessibility": "Accessibilité",
|
||||
"menu_display": "Affichage",
|
||||
"display_layout": "Disposition",
|
||||
"menu_redownload_cache": "Mettre à jour la liste des jeux",
|
||||
"menu_quit": "Quitter",
|
||||
"menu_music_toggle": "Activer/Désactiver la musique",
|
||||
"menu_music_enabled": "Musique activée : {0}",
|
||||
"menu_music_disabled": "Musique désactivée",
|
||||
"menu_restart": "Redémarrer",
|
||||
"menu_filter_platforms": "Filtrer les systèmes",
|
||||
"filter_platforms_title": "Affichage des systèmes",
|
||||
"filter_all": "Tout afficher",
|
||||
@@ -86,10 +89,15 @@
|
||||
"filter_back": "Retour",
|
||||
"filter_platforms_info": "Visibles: {0} | Masqués: {1} / Total: {2}",
|
||||
"filter_unsaved_warning": "Modifications non sauvegardées",
|
||||
"menu_show_unsupported_on": "Afficher systèmes non supportés : Oui",
|
||||
"menu_show_unsupported_off": "Afficher systèmes non supportés : Non",
|
||||
"menu_show_unsupported_enabled": "Affichage systèmes non supportés activé",
|
||||
"menu_show_unsupported_disabled": "Affichage systèmes non supportés désactivé",
|
||||
|
||||
"button_yes": "Oui",
|
||||
"button_no": "Non",
|
||||
"button_OK": "OK",
|
||||
"popup_restarting": "Redémarrage...",
|
||||
|
||||
"controls_hold_message": "Maintenez pendant 3s pour : '{0}'",
|
||||
"controls_skip_message": "Appuyez sur Échap pour passer(Pc only)",
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"download_canceled": "Download cancelado pelo usuário.",
|
||||
|
||||
"extension_warning_zip": "O arquivo '{0}' é um arquivo compactado e o Batocera não suporta arquivos compactados para este sistema. A extração automática ocorrerá após o download, continuar?",
|
||||
"extension_warning_unsupported": "A extensão do arquivo '{0}' não é suportada pelo Batocera segundo o arquivo info.txt. Deseja continuar?",
|
||||
"extension_warning_unsupported": "A extensão do arquivo '{0}' não é suportada pelo Batocera segundo a configuração es_systems.cfg. Deseja continuar?",
|
||||
|
||||
"confirm_exit": "Sair da aplicação?",
|
||||
"confirm_clear_history": "Limpar histórico?",
|
||||
@@ -76,10 +76,13 @@
|
||||
"menu_history": "Histórico",
|
||||
"menu_language": "Idioma",
|
||||
"menu_accessibility": "Acessibilidade",
|
||||
"menu_display": "Exibição",
|
||||
"display_layout": "Layout de exibição",
|
||||
"menu_redownload_cache": "Atualizar lista de jogos",
|
||||
"menu_music_toggle": "Ativar/Desativar música",
|
||||
"menu_music_enabled": "Música ativada: {0}",
|
||||
"menu_music_disabled": "Música desativada",
|
||||
"menu_restart": "Reiniciar",
|
||||
"menu_filter_platforms": "Filtrar sistemas",
|
||||
"filter_platforms_title": "Visibilidade dos sistemas",
|
||||
"filter_all": "Mostrar todos",
|
||||
@@ -88,11 +91,16 @@
|
||||
"filter_back": "Voltar",
|
||||
"filter_platforms_info": "Visíveis: {0} | Ocultos: {1} / Total: {2}",
|
||||
"filter_unsaved_warning": "Alterações não salvas",
|
||||
"menu_show_unsupported_on": "Mostrar sistemas não suportados: Sim",
|
||||
"menu_show_unsupported_off": "Mostrar sistemas não suportados: Não",
|
||||
"menu_show_unsupported_enabled": "Visibilidade de sistemas não suportados ativada",
|
||||
"menu_show_unsupported_disabled": "Visibilidade de sistemas não suportados desativada",
|
||||
"menu_quit": "Sair",
|
||||
|
||||
"button_yes": "Sim",
|
||||
"button_no": "Não",
|
||||
"button_OK": "OK",
|
||||
"popup_restarting": "Reiniciando...",
|
||||
|
||||
"controls_hold_message": "Mantenha pressionado por 3s para: '{0}'",
|
||||
"controls_skip_message": "Pressione Esc para ignorar (somente PC)",
|
||||
|
||||
@@ -183,17 +183,21 @@ async def check_for_updates():
|
||||
config.needs_redraw = True
|
||||
logger.debug("Mise à jour terminée avec succès")
|
||||
|
||||
# Configurer la popup pour afficher le message de succès
|
||||
config.menu_state = "update_result"
|
||||
# Message succès de mise à jour
|
||||
# Configurer la popup puis redémarrer automatiquement
|
||||
config.menu_state = "restart_popup"
|
||||
config.update_result_message = _("network_update_success").format(latest_version)
|
||||
# Utiliser aussi le système générique de popup pour affichage
|
||||
config.popup_message = config.update_result_message
|
||||
config.popup_timer = 5000 # 5 secondes
|
||||
config.popup_timer = 2000
|
||||
config.update_result_error = False
|
||||
config.update_result_start_time = pygame.time.get_ticks()
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Affichage de la popup de mise à jour réussie")
|
||||
logger.debug(f"Affichage de la popup de mise à jour réussie, redémarrage imminent")
|
||||
|
||||
try:
|
||||
from utils import restart_application
|
||||
restart_application(2000)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du redémarrage après mise à jour: {e}")
|
||||
|
||||
return True, _("network_update_success_message")
|
||||
else:
|
||||
@@ -270,10 +274,10 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
platform_folder = normalize_platform_name(platform)
|
||||
dest_dir = apply_symlink_path(config.ROMS_FOLDER, platform_folder)
|
||||
|
||||
# Spécifique: si le système est "00 BIOS" on force le dossier BIOS
|
||||
if platform == "00 BIOS":
|
||||
dest_dir = config.BIOS_FOLDER
|
||||
logger.debug(f"Plateforme '00 BIOS' détectée, destination forcée vers BIOS_FOLDER: {dest_dir}")
|
||||
# Spécifique: si le système est "BIOS" on force le dossier BIOS
|
||||
if platform == "- BIOS by TMCTV":
|
||||
dest_dir = config.RETROBAT_DATA_FOLDER
|
||||
logger.debug(f"Plateforme 'BIOS' détectée, destination forcée vers RETROBAT_DATA_FOLDER: {dest_dir}")
|
||||
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
if not os.access(dest_dir, os.W_OK):
|
||||
@@ -557,8 +561,8 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
|
||||
# Spécifique: si le système est "00 BIOS" on force le dossier BIOS
|
||||
if platform == "00 BIOS":
|
||||
dest_dir = config.BIOS_FOLDER
|
||||
logger.debug(f"Plateforme '00 BIOS' détectée, destination forcée vers BIOS_FOLDER: {dest_dir}")
|
||||
dest_dir = config.RETROBAT_DATA_FOLDER
|
||||
logger.debug(f"Plateforme '00 BIOS' détectée, destination forcée vers RETROBAT_DATA_FOLDER: {dest_dir}")
|
||||
|
||||
logger.debug(f"Vérification répertoire destination: {dest_dir}")
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
|
||||
@@ -25,6 +25,9 @@ def load_rgsx_settings():
|
||||
"accessibility": {
|
||||
"font_scale": 1.0
|
||||
},
|
||||
"display": {
|
||||
"grid": "3x4"
|
||||
},
|
||||
"symlink": {
|
||||
"enabled": False,
|
||||
"target_directory": ""
|
||||
@@ -32,7 +35,8 @@ def load_rgsx_settings():
|
||||
"sources": {
|
||||
"mode": "rgsx",
|
||||
"custom_url": ""
|
||||
}
|
||||
},
|
||||
"show_unsupported_platforms": False
|
||||
}
|
||||
|
||||
try:
|
||||
@@ -160,3 +164,44 @@ def get_sources_zip_url(fallback_url):
|
||||
# Pas de fallback : retourner None pour signaler une source vide
|
||||
return None
|
||||
return fallback_url
|
||||
|
||||
# ----------------------- Unsupported platforms toggle ----------------------- #
|
||||
|
||||
def get_show_unsupported_platforms(settings=None):
|
||||
"""Retourne True si l'affichage des systèmes non supportés est activé."""
|
||||
if settings is None:
|
||||
settings = load_rgsx_settings()
|
||||
return bool(settings.get("show_unsupported_platforms", False))
|
||||
|
||||
|
||||
def set_show_unsupported_platforms(enabled: bool):
|
||||
"""Active/désactive l'affichage des systèmes non supportés et sauvegarde."""
|
||||
settings = load_rgsx_settings()
|
||||
settings["show_unsupported_platforms"] = bool(enabled)
|
||||
save_rgsx_settings(settings)
|
||||
return settings["show_unsupported_platforms"]
|
||||
|
||||
# ----------------------- Display layout (grid) ----------------------- #
|
||||
|
||||
def get_display_grid(settings=None):
|
||||
"""Retourne (cols, rows) pour la grille d'affichage, par défaut (3,4)."""
|
||||
if settings is None:
|
||||
settings = load_rgsx_settings()
|
||||
disp = settings.get("display", {})
|
||||
grid = disp.get("grid", "3x4")
|
||||
try:
|
||||
cols, rows = map(int, grid.lower().split("x"))
|
||||
return cols, rows
|
||||
except Exception:
|
||||
return 3, 4
|
||||
|
||||
def set_display_grid(cols: int, rows: int):
|
||||
"""Définit et sauvegarde la grille d'affichage (cols x rows) parmi options autorisées."""
|
||||
allowed = {(3,3), (3,4), (4,3), (4,4)}
|
||||
if (cols, rows) not in allowed:
|
||||
cols, rows = 3, 4
|
||||
settings = load_rgsx_settings()
|
||||
disp = settings.setdefault("display", {})
|
||||
disp["grid"] = f"{cols}x{rows}"
|
||||
save_rgsx_settings(settings)
|
||||
return cols, rows
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
138
ports/RGSX/update_gamelist_windows.py
Normal file
138
ports/RGSX/update_gamelist_windows.py
Normal file
@@ -0,0 +1,138 @@
|
||||
import os
|
||||
import xml.dom.minidom
|
||||
import xml.etree.ElementTree as ET
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
RGSX_ENTRY = {
|
||||
"path": "./RGSX Retrobat.bat",
|
||||
# 'name' left optional to preserve ES-chosen display name if already present
|
||||
"name": "RGSX",
|
||||
"desc": "Retro Games Sets X - Games Downloader",
|
||||
"image": "./images/RGSX.png",
|
||||
"video": "./videos/RGSX.mp4",
|
||||
"marquee": "./images/RGSX.png",
|
||||
"thumbnail": "./images/RGSX.png",
|
||||
"fanart": "./images/RGSX.png",
|
||||
# Avoid forcing rating to not conflict with ES metadata; set only if absent
|
||||
# "rating": "1",
|
||||
"releasedate": "20250620T165718",
|
||||
"developer": "RetroGameSets.fr",
|
||||
"genre": "Various / Utilities"
|
||||
}
|
||||
|
||||
def _get_root_dir():
|
||||
"""Détecte le dossier racine RetroBat sans importer config."""
|
||||
# Ce script est dans .../roms/ports/RGSX/
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
# Remonter à .../roms/ports/
|
||||
ports_dir = os.path.dirname(here)
|
||||
# Remonter à .../roms/
|
||||
roms_dir = os.path.dirname(ports_dir)
|
||||
# Remonter à la racine RetroBat
|
||||
root_dir = os.path.dirname(roms_dir)
|
||||
return root_dir
|
||||
|
||||
|
||||
def update_gamelist():
|
||||
try:
|
||||
root_dir = _get_root_dir()
|
||||
gamelist_xml = os.path.join(root_dir, "roms", "windows", "gamelist.xml")
|
||||
# Si le fichier n'existe pas, est vide ou non valide, créer une nouvelle structure
|
||||
if not os.path.exists(gamelist_xml) or os.path.getsize(gamelist_xml) == 0:
|
||||
logger.info(f"Création de {gamelist_xml}")
|
||||
root = ET.Element("gameList")
|
||||
else:
|
||||
try:
|
||||
logger.info(f"Lecture de {gamelist_xml}")
|
||||
tree = ET.parse(gamelist_xml)
|
||||
root = tree.getroot()
|
||||
if root.tag != "gameList":
|
||||
logger.info(f"{gamelist_xml} n'a pas de balise <gameList>, création d'une nouvelle structure")
|
||||
root = ET.Element("gameList")
|
||||
except ET.ParseError:
|
||||
logger.info(f"{gamelist_xml} est invalide, création d'une nouvelle structure")
|
||||
root = ET.Element("gameList")
|
||||
|
||||
# Rechercher une entrée existante pour ce chemin
|
||||
game_elem = None
|
||||
for game in root.findall("game"):
|
||||
path = game.find("path")
|
||||
if path is not None and path.text == RGSX_ENTRY["path"]:
|
||||
game_elem = game
|
||||
break
|
||||
|
||||
if game_elem is None:
|
||||
# Créer une nouvelle entrée si absente
|
||||
game_elem = ET.SubElement(root, "game")
|
||||
for key, value in RGSX_ENTRY.items():
|
||||
elem = ET.SubElement(game_elem, key)
|
||||
elem.text = value
|
||||
logger.info("Nouvelle entrée RGSX ajoutée")
|
||||
else:
|
||||
# Fusionner: préserver les champs gérés par ES, compléter/mettre à jour nos champs
|
||||
def ensure(tag, value):
|
||||
elem = game_elem.find(tag)
|
||||
if elem is None:
|
||||
elem = ET.SubElement(game_elem, tag)
|
||||
if elem.text is None or elem.text.strip() == "":
|
||||
elem.text = value
|
||||
|
||||
# S'assurer du chemin
|
||||
ensure("path", RGSX_ENTRY["path"])
|
||||
# Ne pas écraser le nom s'il existe déjà (ES peut le définir selon le fichier)
|
||||
name_elem = game_elem.find("name")
|
||||
existing_name = ""
|
||||
if name_elem is not None and name_elem.text:
|
||||
existing_name = name_elem.text.strip()
|
||||
if not existing_name:
|
||||
ensure("name", RGSX_ENTRY.get("name", "RGSX"))
|
||||
|
||||
# Champs d'habillage que nous voulons imposer/mettre à jour
|
||||
for tag in ("desc", "image", "video", "marquee", "thumbnail", "fanart", "developer", "genre", "releasedate"):
|
||||
val = RGSX_ENTRY.get(tag)
|
||||
if val:
|
||||
elem = game_elem.find(tag)
|
||||
if elem is None:
|
||||
elem = ET.SubElement(game_elem, tag)
|
||||
# Toujours aligner ces champs sur nos valeurs pour garder l'expérience RGSX
|
||||
elem.text = val
|
||||
|
||||
# Ne pas toucher aux champs: playcount, lastplayed, gametime, lang, favorite, kidgame, hidden, rating
|
||||
logger.info("Entrée RGSX mise à jour (fusion)")
|
||||
|
||||
# Générer le XML avec minidom pour une indentation correcte
|
||||
rough_string = '<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring(root, encoding='unicode')
|
||||
parsed = xml.dom.minidom.parseString(rough_string)
|
||||
pretty_xml = parsed.toprettyxml(indent="\t", encoding='utf-8').decode('utf-8')
|
||||
# Supprimer les lignes vides inutiles générées par minidom
|
||||
pretty_xml = '\n'.join(line for line in pretty_xml.split('\n') if line.strip())
|
||||
with open(gamelist_xml, 'w', encoding='utf-8') as f:
|
||||
f.write(pretty_xml)
|
||||
logger.info(f"{gamelist_xml} mis à jour avec succès")
|
||||
|
||||
# Définir les permissions
|
||||
try:
|
||||
os.chmod(gamelist_xml, 0o644)
|
||||
except Exception:
|
||||
# Sur Windows, chmod peut être partiel; ignorer silencieusement
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la mise à jour de la gamelist Windows: {e}")
|
||||
raise
|
||||
|
||||
def load_gamelist(path):
|
||||
"""Charge le fichier gamelist.xml."""
|
||||
try:
|
||||
tree = ET.parse(path)
|
||||
return tree.getroot()
|
||||
except (FileNotFoundError, ET.ParseError) as e:
|
||||
logging.error(f"Erreur lors de la lecture de {path} : {e}")
|
||||
return None
|
||||
|
||||
if __name__ == "__main__":
|
||||
update_gamelist()
|
||||
@@ -7,6 +7,7 @@ import logging
|
||||
import platform
|
||||
import subprocess
|
||||
import config
|
||||
import glob
|
||||
import threading
|
||||
from rgsx_settings import load_rgsx_settings, save_rgsx_settings
|
||||
import zipfile
|
||||
@@ -16,6 +17,7 @@ import config
|
||||
from history import save_history
|
||||
from language import _
|
||||
from datetime import datetime
|
||||
import sys
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -26,6 +28,50 @@ logging.getLogger("requests").setLevel(logging.WARNING)
|
||||
# Liste globale pour stocker les systèmes avec une erreur 404
|
||||
unavailable_systems = []
|
||||
|
||||
# Cache/process flags for extensions generation/loading
|
||||
|
||||
|
||||
def restart_application(delay_ms: int = 2000):
|
||||
"""Schedule a restart with a visible popup; actual restart happens in the main loop.
|
||||
|
||||
- Sets popup_restarting and schedules config.pending_restart_at = now + delay_ms.
|
||||
- Main loop (__main__) detects pending_restart_at and calls restart_application(0) to perform the execl.
|
||||
"""
|
||||
try:
|
||||
# Show popup and schedule
|
||||
if hasattr(config, 'popup_message'):
|
||||
try:
|
||||
config.popup_message = _("popup_restarting")
|
||||
except Exception:
|
||||
config.popup_message = "Restarting..."
|
||||
config.popup_timer = max(config.popup_timer, int(delay_ms)) if hasattr(config, 'popup_timer') else int(delay_ms)
|
||||
config.menu_state = getattr(config, 'menu_state', 'restart_popup') or 'restart_popup'
|
||||
config.needs_redraw = True
|
||||
# Schedule actual restart in main loop
|
||||
now = pygame.time.get_ticks() if hasattr(pygame, 'time') else 0
|
||||
config.pending_restart_at = now + max(0, int(delay_ms))
|
||||
logger.debug(f"Redémarrage planifié dans {delay_ms} ms (pending_restart_at={getattr(config, 'pending_restart_at', 0)})")
|
||||
|
||||
# If delay_ms is 0, perform immediately here
|
||||
if int(delay_ms) <= 0:
|
||||
try:
|
||||
try:
|
||||
pygame.mixer.music.stop()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
pygame.quit()
|
||||
except Exception:
|
||||
pass
|
||||
exe = sys.executable or "python"
|
||||
os.execl(exe, exe, *sys.argv)
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to restart immediately: {e}")
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to schedule restart: {e}")
|
||||
_extensions_cache = None # type: ignore
|
||||
_extensions_json_regenerated = False
|
||||
|
||||
|
||||
# Détection système non-PC
|
||||
def detect_non_pc():
|
||||
@@ -45,13 +91,165 @@ def detect_non_pc():
|
||||
|
||||
# Fonction pour charger le fichier JSON des extensions supportées
|
||||
def load_extensions_json():
|
||||
"""Charge le fichier JSON contenant les extensions supportées."""
|
||||
"""Charge le JSON des extensions supportées.
|
||||
- Régénère une seule fois par exécution (au premier appel ou si le fichier est absent).
|
||||
- Met en cache le résultat pour éviter les relectures et logs répétés.
|
||||
"""
|
||||
global _extensions_cache, _extensions_json_regenerated
|
||||
try:
|
||||
with open(config.JSON_EXTENSIONS, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
# Retour immédiat si déjà en cache
|
||||
if _extensions_cache is not None:
|
||||
return _extensions_cache
|
||||
|
||||
os.makedirs(os.path.dirname(config.JSON_EXTENSIONS), exist_ok=True)
|
||||
|
||||
# Régénération unique au premier appel (ou si le fichier est manquant)
|
||||
if not _extensions_json_regenerated or not os.path.exists(config.JSON_EXTENSIONS):
|
||||
try:
|
||||
generated = generate_extensions_json_from_es_systems()
|
||||
if generated:
|
||||
with open(config.JSON_EXTENSIONS, 'w', encoding='utf-8') as wf:
|
||||
json.dump(generated, wf, ensure_ascii=False, indent=2)
|
||||
logger.info(f"rom_extensions régénéré ({len(generated)} systèmes): {config.JSON_EXTENSIONS}")
|
||||
else:
|
||||
logger.warning("Aucune donnée générée depuis es_systems.cfg; on conserve l'existant si présent")
|
||||
_extensions_json_regenerated = True
|
||||
except Exception as ge:
|
||||
logger.error(f"Échec lors de la régénération de {config.JSON_EXTENSIONS} depuis es_systems.cfg: {ge}")
|
||||
|
||||
# Lecture du fichier (nouveau ou existant)
|
||||
if os.path.exists(config.JSON_EXTENSIONS):
|
||||
with open(config.JSON_EXTENSIONS, 'r', encoding='utf-8') as f:
|
||||
_extensions_cache = json.load(f)
|
||||
return _extensions_cache
|
||||
_extensions_cache = []
|
||||
return _extensions_cache
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la lecture de {config.JSON_EXTENSIONS}: {e}")
|
||||
_extensions_cache = []
|
||||
return _extensions_cache
|
||||
|
||||
def _detect_es_systems_cfg_paths():
|
||||
"""Retourne une liste de chemins possibles pour es_systems.cfg selon l'OS.
|
||||
- RetroBat (Windows): {config.RETROBAT_DATA_FOLDER}\\system\\templates\\emulationstation\\es_systems.cfg
|
||||
- Batocera (Linux): /usr/share/emulationstation/es_systems.cfg
|
||||
Ajoute aussi les fichiers customs: /userdata/system/configs/emulationstation/es_systems_*.cfg
|
||||
"""
|
||||
candidates = []
|
||||
try:
|
||||
if platform.system() == 'Windows':
|
||||
base = getattr(config, 'RETROBAT_DATA_FOLDER', None)
|
||||
if base:
|
||||
candidates.append(os.path.join(base, 'system', 'templates', 'emulationstation', 'es_systems.cfg'))
|
||||
else:
|
||||
# Batocera / Linux classiques
|
||||
candidates.append('/usr/share/emulationstation/es_systems.cfg')
|
||||
candidates.append('/etc/emulationstation/es_systems.cfg')
|
||||
# Batocera customs
|
||||
custom_dir = '/userdata/system/configs/emulationstation'
|
||||
try:
|
||||
for p in glob.glob(os.path.join(custom_dir, 'es_systems_*.cfg')):
|
||||
candidates.append(p)
|
||||
direct_cfg = os.path.join(custom_dir, 'es_systems.cfg')
|
||||
if os.path.exists(direct_cfg):
|
||||
candidates.append(direct_cfg)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
existing = [p for p in candidates if p and os.path.exists(p)]
|
||||
# Logs réduits: on ne conserve que les résumés plus loin
|
||||
return existing
|
||||
|
||||
def _parse_es_systems_cfg(cfg_path):
|
||||
"""Parse un es_systems.cfg minimalement pour extraire (folder, extensions).
|
||||
Retourne une liste de dicts: { 'folder': <str>, 'extensions': [..] }
|
||||
- folder: dérivé de la balise <path> en prenant la partie après 'roms/' (ou '\\roms\\' sous Windows)
|
||||
- extensions: liste normalisée de .ext (point + minuscule)
|
||||
"""
|
||||
try:
|
||||
# Lire tel quel (pas besoin d'un parseur XML strict, mais ElementTree suffit)
|
||||
import xml.etree.ElementTree as ET
|
||||
# Log détaillé supprimé pour alléger les traces
|
||||
tree = ET.parse(cfg_path)
|
||||
root = tree.getroot()
|
||||
out = []
|
||||
for sys_elem in root.findall('system'):
|
||||
path_text = (sys_elem.findtext('path') or '').strip()
|
||||
ext_text = (sys_elem.findtext('extension') or '').strip()
|
||||
if not path_text:
|
||||
continue
|
||||
# Extraire le dossier après 'roms'
|
||||
folder = None
|
||||
norm = path_text.replace('\\', '/').lower()
|
||||
marker = '/roms/'
|
||||
if marker in norm:
|
||||
after = norm.split(marker, 1)[1]
|
||||
folder = after.strip().strip('/\\')
|
||||
if not folder:
|
||||
# fallback: si le chemin finit par .../roms/<folder>
|
||||
parts = norm.strip('/').split('/')
|
||||
if len(parts) >= 2 and parts[-2] == 'roms':
|
||||
folder = parts[-1]
|
||||
if not folder:
|
||||
continue
|
||||
|
||||
# Extensions: split par espaces, normaliser en .ext
|
||||
exts = []
|
||||
for tok in ext_text.split():
|
||||
tok = tok.strip().lower()
|
||||
if not tok:
|
||||
continue
|
||||
if not tok.startswith('.'):
|
||||
# Certaines entrées peuvent omettre le point
|
||||
tok = '.' + tok
|
||||
exts.append(tok)
|
||||
# Dédupliquer tout en conservant l'ordre
|
||||
seen = set()
|
||||
norm_exts = []
|
||||
for e in exts:
|
||||
if e not in seen:
|
||||
seen.add(e)
|
||||
norm_exts.append(e)
|
||||
out.append({'folder': folder, 'extensions': norm_exts})
|
||||
# Résumé final affiché ailleurs
|
||||
return out
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur parsing es_systems.cfg ({cfg_path}): {e}")
|
||||
return []
|
||||
|
||||
def generate_extensions_json_from_es_systems():
|
||||
"""Essaie de construire la liste des extensions à partir des es_systems.cfg disponibles.
|
||||
Priorité: RetroBat si présent, sinon Batocera. Fusionne si plusieurs trouvés, en préférant RetroBat.
|
||||
"""
|
||||
combined = {}
|
||||
paths = _detect_es_systems_cfg_paths()
|
||||
if not paths:
|
||||
logger.warning("Aucun chemin es_systems.cfg détecté (RetroBat/Batocera)")
|
||||
return []
|
||||
# Prioriser RetroBat en tête si présent
|
||||
def score(p):
|
||||
return 0 if 'templates' in p.replace('\\', '/').lower() else 1
|
||||
for cfg in sorted(paths, key=score):
|
||||
if not os.path.exists(cfg):
|
||||
continue
|
||||
items = _parse_es_systems_cfg(cfg)
|
||||
for itm in items:
|
||||
folder = itm['folder']
|
||||
exts = itm['extensions']
|
||||
if folder in combined:
|
||||
# Fusionner: ajouter extensions manquantes
|
||||
present = set(combined[folder])
|
||||
for e in exts:
|
||||
if e not in present:
|
||||
combined[folder].append(e)
|
||||
present.add(e)
|
||||
else:
|
||||
combined[folder] = list(exts)
|
||||
# Convertir en liste triée par dossier
|
||||
result = [{'folder': k, 'extensions': v} for k, v in sorted(combined.items(), key=lambda x: x[0])]
|
||||
logger.info(f"Extensions combinées totales: {len(result)} systèmes")
|
||||
return result
|
||||
|
||||
def check_extension_before_download(url, platform, game_name):
|
||||
"""Vérifie l'extension avant de lancer le téléchargement et retourne un tuple de 4 éléments."""
|
||||
@@ -66,10 +264,14 @@ def check_extension_before_download(url, platform, game_name):
|
||||
extension = os.path.splitext(sanitized_name)[1].lower()
|
||||
is_archive = extension in (".zip", ".rar")
|
||||
|
||||
# Déterminer si le système (dossier) est connu dans extensions_data
|
||||
dest_folder_name = _get_dest_folder_name(platform)
|
||||
system_known = any(s.get("folder") == dest_folder_name for s in extensions_data)
|
||||
|
||||
if is_supported:
|
||||
logger.debug(f"L'extension de {sanitized_name} est supportée pour {platform}")
|
||||
return (url, platform, game_name, False)
|
||||
elif is_archive:
|
||||
elif is_archive and system_known:
|
||||
logger.debug(f"Archive {extension.upper()} détectée pour {sanitized_name}, extraction automatique prévue")
|
||||
return (url, platform, game_name, True)
|
||||
else:
|
||||
@@ -91,10 +293,10 @@ def is_extension_supported(filename, platform_key, extensions_data):
|
||||
if platform_dict.get("platform_name") == platform_key:
|
||||
dest_dir = os.path.join(config.ROMS_FOLDER, platform_dict.get("folder"))
|
||||
break
|
||||
|
||||
|
||||
if not dest_dir:
|
||||
logger.warning(f"Aucun dossier 'folder' trouvé pour la plateforme {platform}")
|
||||
dest_dir = os.path.join(os.path.dirname(os.path.dirname(config.APP_FOLDER)), platform)
|
||||
logger.warning(f"Aucun dossier 'folder' trouvé pour la plateforme {platform_key}")
|
||||
dest_dir = os.path.join(os.path.dirname(os.path.dirname(config.APP_FOLDER)), platform_key)
|
||||
|
||||
dest_folder_name = os.path.basename(dest_dir)
|
||||
for i, system in enumerate(extensions_data):
|
||||
@@ -106,6 +308,20 @@ def is_extension_supported(filename, platform_key, extensions_data):
|
||||
return False
|
||||
|
||||
|
||||
def _get_dest_folder_name(platform_key: str) -> str:
|
||||
"""Retourne le nom du dossier de destination pour une plateforme (basename du dossier)."""
|
||||
dest_dir = None
|
||||
for platform_dict in config.platform_dicts:
|
||||
if platform_dict.get("platform_name") == platform_key:
|
||||
folder = platform_dict.get("folder")
|
||||
if folder:
|
||||
dest_dir = os.path.join(config.ROMS_FOLDER, folder)
|
||||
break
|
||||
if not dest_dir:
|
||||
dest_dir = os.path.join(os.path.dirname(os.path.dirname(config.APP_FOLDER)), platform_key)
|
||||
return os.path.basename(dest_dir)
|
||||
|
||||
|
||||
|
||||
|
||||
# Fonction pour charger sources.json
|
||||
@@ -218,6 +434,39 @@ def load_sources():
|
||||
hidden = set(settings.get("hidden_platforms", [])) if isinstance(settings, dict) else set()
|
||||
all_sorted_names = [s.get("platform_name", "") for s in sorted_for_display]
|
||||
visible_names = [n for n in all_sorted_names if n and n not in hidden]
|
||||
|
||||
# Masquer automatiquement les systèmes dont le dossier ROM n'existe pas (selon le toggle)
|
||||
unsupported = []
|
||||
try:
|
||||
from rgsx_settings import get_show_unsupported_platforms
|
||||
show_unsupported = get_show_unsupported_platforms(settings)
|
||||
sources_by_name = {s.get("platform_name", ""): s for s in sources if isinstance(s, dict)}
|
||||
for name in list(visible_names):
|
||||
entry = sources_by_name.get(name) or {}
|
||||
folder = entry.get("folder")
|
||||
# Conserver BIOS même sans dossier, et ignorer entrées sans folder
|
||||
bios_name = name.strip()
|
||||
if not folder or bios_name == "- BIOS by TMCTV -" or bios_name == "- BIOS":
|
||||
continue
|
||||
expected_dir = os.path.join(config.ROMS_FOLDER, folder)
|
||||
if not os.path.isdir(expected_dir):
|
||||
unsupported.append(name)
|
||||
if show_unsupported:
|
||||
config.unsupported_platforms = unsupported
|
||||
else:
|
||||
if unsupported:
|
||||
# Filtrer la liste visible
|
||||
visible_names = [n for n in visible_names if n not in set(unsupported)]
|
||||
config.unsupported_platforms = unsupported
|
||||
# Log concis + détaillé en DEBUG uniquement
|
||||
logger.info(f"Plateformes masquées (dossier rom absent): {len(unsupported)}")
|
||||
logger.debug("Détails plateformes masquées: " + ", ".join(unsupported))
|
||||
else:
|
||||
config.unsupported_platforms = []
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur détection plateformes non supportées (dossiers manquants): {e}")
|
||||
config.unsupported_platforms = []
|
||||
|
||||
config.platforms = visible_names
|
||||
config.platform_names = {p: p for p in config.platforms}
|
||||
# Nouveau mapping par nom pour éviter décalages index après tri d'affichage
|
||||
@@ -303,7 +552,7 @@ def load_games(platform_id):
|
||||
else:
|
||||
logger.warning(f"Format de fichier jeux inattendu pour {platform_id}: {type(data)}")
|
||||
|
||||
logger.debug(f"Jeux chargés pour {platform_id} depuis {os.path.basename(game_file)}: {len(normalized)} entrées")
|
||||
logger.debug(f"{os.path.basename(game_file)}: {len(normalized)} jeux")
|
||||
return normalized
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du chargement des jeux pour {platform_id}: {e}")
|
||||
@@ -451,27 +700,57 @@ def wrap_text(text, font, max_width):
|
||||
return lines
|
||||
|
||||
def load_system_image(platform_dict):
|
||||
"""Charge une image système avec priorité:
|
||||
1. Fichier nommé exactement <platform_name>.png
|
||||
2. Champ platform_image si non vide
|
||||
3. Fallback default.png"""
|
||||
"""Charge une image système avec la priorité suivante:
|
||||
1. platform_image explicite s'il est défini
|
||||
2. <platform_name>.png
|
||||
3. <folder>.png si disponible
|
||||
4. Recherche fallback dans le dossier images de l'app (APP_FOLDER/images) avec le même ordre
|
||||
5. default.png (dans SAVE_FOLDER/images), sinon default.png de l'app
|
||||
|
||||
Cela évite d'échouer lorsque le nom affiché ne correspond pas au fichier image
|
||||
et respecte un mapping explicite fourni par systems_list.json."""
|
||||
platform_name = platform_dict.get("platform_name", "unknown")
|
||||
preferred_filename = f"{platform_name}.png"
|
||||
preferred_path = os.path.join(config.IMAGES_FOLDER, preferred_filename)
|
||||
folder_name = platform_dict.get("folder") or ""
|
||||
|
||||
# Normaliser platform_image pouvant être vide
|
||||
platform_image_field = platform_dict.get("platform_image") or ""
|
||||
explicit_image_path = os.path.join(config.IMAGES_FOLDER, platform_image_field) if platform_image_field else None
|
||||
default_path = os.path.join(config.IMAGES_FOLDER, "default.png")
|
||||
# Dossiers d'images
|
||||
save_images = config.IMAGES_FOLDER
|
||||
app_images = os.path.join(config.APP_FOLDER, "images")
|
||||
|
||||
# Candidats, par ordre de priorité
|
||||
candidates = []
|
||||
platform_image_field = (platform_dict.get("platform_image") or "").strip()
|
||||
if platform_image_field:
|
||||
candidates.append(os.path.join(save_images, platform_image_field))
|
||||
candidates.append(os.path.join(save_images, f"{platform_name}.png"))
|
||||
if folder_name:
|
||||
candidates.append(os.path.join(save_images, f"{folder_name}.png"))
|
||||
|
||||
# Fallback: images packagées avec l'app
|
||||
if platform_image_field:
|
||||
candidates.append(os.path.join(app_images, platform_image_field))
|
||||
candidates.append(os.path.join(app_images, f"{platform_name}.png"))
|
||||
if folder_name:
|
||||
candidates.append(os.path.join(app_images, f"{folder_name}.png"))
|
||||
|
||||
# Charger le premier fichier existant
|
||||
try:
|
||||
if os.path.exists(preferred_path):
|
||||
return pygame.image.load(preferred_path).convert_alpha()
|
||||
if explicit_image_path and os.path.exists(explicit_image_path):
|
||||
return pygame.image.load(explicit_image_path).convert_alpha()
|
||||
if os.path.exists(default_path):
|
||||
return pygame.image.load(default_path).convert_alpha()
|
||||
logger.error(f"Aucune image trouvée pour {platform_name} (cherché: {preferred_path}, {explicit_image_path}, default.png)")
|
||||
for path in candidates:
|
||||
if path and os.path.exists(path):
|
||||
return pygame.image.load(path).convert_alpha()
|
||||
|
||||
# default.png (save d'abord, sinon app)
|
||||
default_save = os.path.join(save_images, "default.png")
|
||||
if os.path.exists(default_save):
|
||||
return pygame.image.load(default_save).convert_alpha()
|
||||
default_app = os.path.join(app_images, "default.png")
|
||||
if os.path.exists(default_app):
|
||||
return pygame.image.load(default_app).convert_alpha()
|
||||
|
||||
logger.error(
|
||||
f"Aucune image trouvée pour {platform_name}. Candidats: "
|
||||
+ ", ".join(candidates)
|
||||
+ f"; default cherchés: {default_save}, {default_app}"
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du chargement de l'image pour {platform_name} : {str(e)}")
|
||||
|
||||
@@ -2,8 +2,16 @@
|
||||
setlocal EnableDelayedExpansion
|
||||
|
||||
:: Fichier de log
|
||||
if not exist %CD%\logs MD %CD%\logs
|
||||
set LOG_FILE=%CD%\logs\Retrobat_RGSX_log.txt
|
||||
if not exist "%CD%\logs" MD "%CD%\logs"
|
||||
set "LOG_FILE=%CD%\logs\Retrobat_RGSX_log.txt"
|
||||
:: Fichier de log (chemin absolu pour fiabilité)
|
||||
:: Détecter la racine (ROOT_DIR) d'abord pour construire un chemin stable
|
||||
set CURRENT_DIR=%CD%
|
||||
pushd "%CURRENT_DIR%\..\.."
|
||||
set "ROOT_DIR=%CD%"
|
||||
popd
|
||||
if not exist "%ROOT_DIR%\roms\windows\logs" MD "%ROOT_DIR%\roms\windows\logs"
|
||||
set "LOG_FILE=%ROOT_DIR%\roms\windows\logs\Retrobat_RGSX_log.txt"
|
||||
|
||||
:: Ajouter un horodatage au début du log
|
||||
echo [%DATE% %TIME%] Script start >> "%LOG_FILE%"
|
||||
@@ -25,9 +33,13 @@ popd
|
||||
:: Définir le chemin du script principal selon les spécifications
|
||||
set "MAIN_SCRIPT=%ROOT_DIR%\roms\ports\RGSX\__main__.py"
|
||||
|
||||
:: Definir le chemin du script de mise à jour de la gamelist Windows
|
||||
set "UPDATE_GAMELIST_SCRIPT=%ROOT_DIR%\roms\ports\RGSX\update_gamelist_windows.py"
|
||||
|
||||
:: Convertir les chemins relatifs en absolus avec pushd/popd
|
||||
pushd "%ROOT_DIR%\system\tools\Python"
|
||||
set "PYTHON_EXE_FULL=%ROOT_DIR%\system\tools\Python\!PYTHON_EXE!"
|
||||
set "PYTHONW_EXE_FULL=%ROOT_DIR%\system\tools\Python\pythonw.exe"
|
||||
popd
|
||||
|
||||
:: Afficher et logger les variables
|
||||
@@ -37,6 +49,7 @@ echo CURRENT_DIR : !CURRENT_DIR! >> "%LOG_FILE%"
|
||||
echo ROOT_DIR : !ROOT_DIR! >> "%LOG_FILE%"
|
||||
echo PYTHON_EXE_FULL : !PYTHON_EXE_FULL! >> "%LOG_FILE%"
|
||||
echo MAIN_SCRIPT : !MAIN_SCRIPT! >> "%LOG_FILE%"
|
||||
echo UPDATE_GAMELIST_SCRIPT : !UPDATE_GAMELIST_SCRIPT! >> "%LOG_FILE%"
|
||||
|
||||
:: Vérifier si l'exécutable Python existe
|
||||
echo Checking python.exe...
|
||||
@@ -101,16 +114,35 @@ if not exist "!MAIN_SCRIPT!" (
|
||||
echo __main__.py found.
|
||||
echo [%DATE% %TIME%] __main__.py found. >> "%LOG_FILE%"
|
||||
|
||||
:: Exécuter le script Python
|
||||
echo Executing __main__.py...
|
||||
echo [%DATE% %TIME%] Executing "!MAIN_SCRIPT!" with !PYTHON_EXE_FULL! >> "%LOG_FILE%"
|
||||
"!PYTHON_EXE_FULL!" "!MAIN_SCRIPT!" >> "%LOG_FILE%" 2>&1
|
||||
if %ERRORLEVEL% equ 0 (
|
||||
:: L'étape de mise à jour de la gamelist est désormais appelée depuis __main__.py
|
||||
echo [%DATE% %TIME%] Skipping external gamelist update (handled in app). >> "%LOG_FILE%"
|
||||
|
||||
echo Launching __main__.py (attached)...
|
||||
echo [%DATE% %TIME%] Preparing to launch main. >> "%LOG_FILE%"
|
||||
|
||||
:: Assurer le bon dossier de travail pour l'application
|
||||
cd /d "%ROOT_DIR%\roms\ports\RGSX"
|
||||
|
||||
:: Forcer les drivers SDL côté Windows et réduire le bruit console
|
||||
set PYGAME_HIDE_SUPPORT_PROMPT=1
|
||||
set SDL_VIDEODRIVER=windows
|
||||
set SDL_AUDIODRIVER=directsound
|
||||
echo [%DATE% %TIME%] CWD before launch: %CD% >> "%LOG_FILE%"
|
||||
|
||||
:: Lancer l'application dans la même console et attendre sa fin
|
||||
:: Forcer python.exe pour capturer la sortie
|
||||
set "PY_MAIN_EXE=!PYTHON_EXE_FULL!"
|
||||
echo [%DATE% %TIME%] Using interpreter: !PY_MAIN_EXE! >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] Launching "!MAIN_SCRIPT!" now... >> "%LOG_FILE%"
|
||||
"!PY_MAIN_EXE!" "!MAIN_SCRIPT!" >> "%LOG_FILE%" 2>&1
|
||||
set EXITCODE=!ERRORLEVEL!
|
||||
echo [%DATE% %TIME%] __main__.py exit code: !EXITCODE! >> "%LOG_FILE%"
|
||||
if "!EXITCODE!"=="0" (
|
||||
echo Execution finished successfully.
|
||||
echo [%DATE% %TIME%] Execution of __main__.py finished successfully. >> "%LOG_FILE%"
|
||||
) else (
|
||||
echo Error: Failed to execute __main__.py (code %ERRORLEVEL%).
|
||||
echo [%DATE% %TIME%] Error: Failed to execute __main__.py with error code %ERRORLEVEL%. >> "%LOG_FILE%"
|
||||
echo Error: Failed to execute __main__.py (code !EXITCODE!).
|
||||
echo [%DATE% %TIME%] Error: Failed to execute __main__.py with error code !EXITCODE!. >> "%LOG_FILE%"
|
||||
goto :error
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user