retructuration pour ajouter compatibilité retrobat

This commit is contained in:
skymike03
2025-07-31 14:37:17 +02:00
parent fc2ecf655a
commit 358b9e9fa5
40 changed files with 28 additions and 6 deletions

181
ports/RGSX/README.md Normal file
View File

@@ -0,0 +1,181 @@
# 🎮 Retro Game Sets Xtra (RGSX)
RGSX est une application Python basée sur Pygame.
---
## ✨ 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.
- 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.
- **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.
- **Mode recherche** : Filtrez les jeux par nom pour une navigation rapide avec clavier virtuel sur manette.
- **Support multilingue** : Interface disponible en plusieurs langues.
- **Gestion des erreurs** avec messages informatifs.
- **Interface réactive** : L'interface s'adapte à toutes résolutions de 800x600 à 4K (non testé au-delà de 1920x1080).
- **Mise à jour automatique** (bug d'affichage à améliorer lors d'une mise à jour) : l'application doit être relancée après sa fermeture automatique.
---
## 🖥️ Prérequis
### Système d'exploitation
- Batocera ou Knulli
### Matériel
- Manette (optionnelle, mais recommandée pour une expérience optimale) ou Clavier.
### Espace disque
- Espace suffisant pour stocker les ROMs, images et fichiers de configuration.
---
## 🚀 Installation
### Méthode 1 : Ligne de commande
- Sur batocera PC acceder à F1>Applications>xTERM ou
- Depuis un autre pc sur le réseau avec application Putty, powershell SSH ou autre
Entrez la commande :
## `curl -L bit.ly/rgsx-install | sh`
Patientez et regardez le retour à l'écran ou sur la commande (à améliorer).
Mettez à jour la liste des jeux via : `Menu > Paramètres de jeux > Mettre à jour la liste des jeux `.
Vous trouverez RGSX dans le système "PORTS" ou "Jeux Amateurs et portages" et dans `/roms/ports/RGSX`
---
### Méthode 2 : Copie manuelle
- Téléchargez le contenu du dépôt en zip : https://github.com/RetroGameSets/RGSX/archive/refs/heads/main.zip
- Extrayez le tout dans `roms/ports/RGSX` (le dossier RGSX devra être créé manuellement). Attention de bien respecter la structure indiquée plus bas.
- Mettez à jour la liste des jeux via le menu :
`Paramètres de jeux > Mettre à jour la liste`.
## 🏁 1er démarrage
---
> ## IMPORTANT
> Si vous avez une clé API 1Fichier, vous devez la renseigner dans
> `/saves/ports/RGSX/1FichierAPI.txt`
> si vous souhaitez télécharger depuis des liens 1Fichier.
---
- Lancez RGSX depuis ports.
- Au premier lancement, l'application importera automatiquement la configuration des contrôles depuis EmulationStation si disponible.
- Configurez les contrôles si nécessaire. Ils pourront être reconfigurés via le menu pause par la suite.
- Supprimez le fichier `/saves/ports/RGSX/controls.json` en cas de problème puis relancez l'application.
- L'application téléchargera toutes les données nécessaires automatiquement ensuite.
---
## 🕹️ Utilisation
### Navigation dans les menus
- Utilisez les touches directionnelles (D-Pad, flèches du clavier) pour naviguer entre les plateformes, jeux et options.
- Appuyez sur la touche configurée comme start (par défaut, **P** ou bouton Start sur la manette) pour ouvrir le menu pause.
- 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.
- Vous pouvez aussi, depuis le menu, régénérer la liste des systèmes/jeux/images pour être sûr d'avoir les dernières mises à jour.
---
### Téléchargement
- Sélectionnez une plateforme, puis un jeu.
- Appuyez sur la touche configurée confirm (par défaut, **Entrée** ou bouton **A**) pour lancer le téléchargement.
- Suivez la progression dans le menu `download_progress`.
---
### Personnalisation des contrôles
- Dans le menu pause, sélectionnez **Remap controls**.
- Suivez les instructions à l'écran pour mapper chaque action en maintenant la touche ou le bouton pendant 3 secondes.
- Les noms des boutons s'affichent automatiquement selon votre manette (A, B, X, Y, LB, RB, LT, RT, etc.).
- La configuration est compatible avec toutes les manettes supportées par EmulationStation.
---
### Historique
- Accédez à l'historique des téléchargements via le menu pause ou en appuyant sur la touche history (par défaut, **H**).
- Sélectionnez un jeu pour le retélécharger si nécessaire.
- Videz l'historique via le bouton progress dans le menu historique.
---
### Logs
Les logs sont enregistrés dans `/roms/ports/RGSX/logs/RGSX.log` pour diagnostiquer les problèmes.
---
## 📁 Structure du projet
```
/roms/ports/
RGSX-INSTALL.log # LOG d'installation uniquement
RGSX/
├── main.py # Point d'entrée principal de l'application.
├── controls.py # Gestion des événements clavier/manette/souris et navigation dans les menus.
├── controls_mapper.py # Configuration des contrôles avec détection automatique des noms de boutons.
├── es_input_parser.py # Parseur de configuration EmulationStation pour l'import automatique des contrôles.
├── display.py # Rendu des interfaces graphiques avec Pygame.
├── config.py # Configuration globale (chemins, paramètres, etc.).
├── network.py # Gestion des téléchargements de jeux.
├── history.py # Gestion de l'historique des téléchargements.
├── language.py # Gestion du support multilingue.
├── utils.py # Fonctions utilitaires (wrap du texte, troncage etc.).
└── logs/
└── RGSX.log # Fichier de logs.
/saves/ports/
RGSX/
├── controls.json # Fichier de mappage des contrôles (généré après le 1er demarrage)
├── history.json # Base de données de l'historique de téléchargements (généré après le 1er téléchargement)
└── 1FichierAPI.txt # Clé API 1fichier (compte premium et + uniquement) (vide par defaut)
```
---
## 🤝 Contribution
### Signaler un bug
1. Consultez les logs dans `/roms/ports/RGSX/logs/RGSX.log`.
2. Ouvrez une issue sur GitHub avec une description détaillée et les logs pertinents.
### Proposer une fonctionnalité
- Soumettez une issue avec une description claire de la fonctionnalité proposée.
- Expliquez comment elle s'intègre dans l'application.
### Contribuer au code
1. Forkez le dépôt et créez une branche pour votre fonctionnalité ou correction :
git checkout -b feature/nom-de-votre-fonctionnalité
2. Testez vos modifications sur Batocera.
3. Soumettez une pull request avec une description détaillée.
---
## ⚠️ Problèmes connus / À implémenter
- Gestion des téléchargements multiples
---
## 📝 Licence
Ce projet est libre. Vous êtes libre de l'utiliser, le modifier et le distribuer selon les termes de cette licence.
Développé avec ❤️ pour les amateurs de jeux rétro.

4
ports/RGSX/RGSX.sh Normal file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
# Supprimer SDL_VIDEODRIVER=fbcon pour laisser SDL choisir le pilote
# export SDL_VIDEODRIVER=fbcon
/usr/bin/python3 /userdata/roms/ports/RGSX

View File

@@ -0,0 +1,119 @@
@echo on
setlocal EnableDelayedExpansion
:: Définir le fichier de log
if not exist %CD%\logs MD %CD%\logs
set LOG_FILE=%CD%\logs\Retrobat_log.txt
:: Ajouter un horodatage au début du log
echo [%DATE% %TIME%] Démarrage du script >> "%LOG_FILE%"
:: Afficher un message de démarrage
cls
echo Exécution de __main__.py pour RetroBat...
echo [%DATE% %TIME%] Exécution de __main__.py pour RetroBat >> "%LOG_FILE%"
:: Définir les chemins relatifs
set TOOLS_FOLDER=..\..\..\system\tools
set PYTHON_EXE=python.exe
set MAIN_SCRIPT=__main__.py
set CURRENT_DIR=%CD%
set "PYTHON_EXE_FULL=%CURRENT_DIR%\!TOOLS_FOLDER!\Python\!PYTHON_EXE!"
set "MAIN_SCRIPT_FULL=%CURRENT_DIR%\!MAIN_SCRIPT!"
:: Afficher et logger les variables
echo TOOLS_FOLDER : !TOOLS_FOLDER!
echo [%DATE% %TIME%] TOOLS_FOLDER : !TOOLS_FOLDER! >> "%LOG_FILE%"
echo PYTHON_EXE : !PYTHON_EXE!
echo [%DATE% %TIME%] PYTHON_EXE : !PYTHON_EXE! >> "%LOG_FILE%"
echo MAIN_SCRIPT : !MAIN_SCRIPT!
echo [%DATE% %TIME%] MAIN_SCRIPT : !MAIN_SCRIPT! >> "%LOG_FILE%"
echo CURRENT_DIR : !CURRENT_DIR!
echo [%DATE% %TIME%] CURRENT_DIR : !CURRENT_DIR! >> "%LOG_FILE%"
echo PYTHON_EXE_FULL : !PYTHON_EXE_FULL!
echo [%DATE% %TIME%] PYTHON_EXE_FULL : !PYTHON_EXE_FULL! >> "%LOG_FILE%"
echo MAIN_SCRIPT_FULL : !MAIN_SCRIPT_FULL!
echo [%DATE% %TIME%] MAIN_SCRIPT_FULL : !MAIN_SCRIPT_FULL! >> "%LOG_FILE%"
:: Vérifier si l'exécutable Python existe
echo Vérification de python.exe...
echo [%DATE% %TIME%] Vérification de python.exe à !PYTHON_EXE_FULL! >> "%LOG_FILE%"
if not exist "!PYTHON_EXE_FULL!" (
echo Python.exe non trouvé. Préparation du téléchargement...
echo [%DATE% %TIME%] Python.exe non trouvé. Préparation du téléchargement... >> "%LOG_FILE%"
:: Définir les chemins pour le téléchargement et l'extraction
set ZIP_URL=https://retrogamesets.fr/softs/python.zip
echo ZIP_URL : !ZIP_URL!
echo [%DATE% %TIME%] ZIP_URL : !ZIP_URL! >> "%LOG_FILE%"
if not exist "!TOOLS_FOLDER!\Python" (
echo Création du dossier !TOOLS_FOLDER!\Python...
echo [%DATE% %TIME%] Création du dossier !TOOLS_FOLDER!\Python... >> "%LOG_FILE%"
mkdir "!TOOLS_FOLDER!\Python"
)
set ZIP_FILE=!TOOLS_FOLDER!\python.zip
echo ZIP_FILE : !ZIP_FILE!
echo [%DATE% %TIME%] ZIP_FILE : !ZIP_FILE! >> "%LOG_FILE%"
echo Téléchargement de python.zip...
echo [%DATE% %TIME%] Téléchargement de python.zip depuis !ZIP_URL!... >> "%LOG_FILE%"
:: Afficher un message de progression pendant le téléchargement
echo Téléchargement en cours...
curl -L "!ZIP_URL!" -o "!ZIP_FILE!"
if exist "!ZIP_FILE!" (
echo Téléchargement terminé. Extraction de python.zip...
echo [%DATE% %TIME%] Téléchargement terminé. Extraction de python.zip vers !TOOLS_FOLDER!\Python... >> "%LOG_FILE%"
:: Afficher des messages de progression pendant l'extraction
echo Extraction en cours...
tar -xf "!ZIP_FILE!" -C "!TOOLS_FOLDER!" --strip-components=0
echo Extraction terminée.
echo [%DATE% %TIME%] Extraction terminée. >> "%LOG_FILE%"
del /q "!ZIP_FILE!"
echo Fichier python.zip supprimé.
echo [%DATE% %TIME%] Fichier python.zip supprimé. >> "%LOG_FILE%"
) else (
echo Erreur : Échec du téléchargement de python.zip.
echo [%DATE% %TIME%] Erreur : Échec du téléchargement de python.zip. >> "%LOG_FILE%"
goto :error
)
:: Vérifier à nouveau si python.exe existe après extraction
if not exist "!PYTHON_EXE_FULL!" (
echo Erreur : python.exe n'a pas été trouvé après extraction à !PYTHON_EXE_FULL!.
echo [%DATE% %TIME%] Erreur : python.exe n'a pas été trouvé après extraction à !PYTHON_EXE_FULL! >> "%LOG_FILE%"
goto :error
)
)
echo python.exe trouvé.
echo [%DATE% %TIME%] python.exe trouvé. >> "%LOG_FILE%"
:: Vérifier si le script Python existe
echo Vérification de __main__.py...
echo [%DATE% %TIME%] Vérification de __main__.py à !MAIN_SCRIPT_FULL! >> "%LOG_FILE%"
if not exist "!MAIN_SCRIPT_FULL!" (
echo Erreur : __main__.py n'a pas été trouvé à !MAIN_SCRIPT_FULL!.
echo [%DATE% %TIME%] Erreur : __main__.py n'a pas été trouvé à !MAIN_SCRIPT_FULL! >> "%LOG_FILE%"
goto :error
)
echo __main__.py trouvé.
echo [%DATE% %TIME%] __main__.py trouvé. >> "%LOG_FILE%"
:: Exécuter le script Python
echo Exécution de __main__.py...
echo [%DATE% %TIME%] Exécution de __main__.py avec !PYTHON_EXE_FULL! >> "%LOG_FILE%"
"!PYTHON_EXE_FULL!" "!MAIN_SCRIPT_FULL!"
if %ERRORLEVEL% equ 0 (
echo Exécution terminée avec succès.
echo [%DATE% %TIME%] Exécution de __main__.py terminée avec succès. >> "%LOG_FILE%"
) else (
echo Erreur : Échec de l'exécution de __main__.py (code %ERRORLEVEL%).
echo [%DATE% %TIME%] Erreur : Échec de l'exécution de __main__.py avec code d'erreur %ERRORLEVEL%. >> "%LOG_FILE%"
goto :error
)
:end
echo Tâche terminée.
echo [%DATE% %TIME%] Tâche terminée avec succès. >> "%LOG_FILE%"
exit /b 0
:error
echo Une erreur s'est produite.
echo [%DATE% %TIME%] Une erreur s'est produite. >> "%LOG_FILE%"
exit /b 1

789
ports/RGSX/__main__.py Normal file
View File

@@ -0,0 +1,789 @@
import os
os.environ["SDL_FBDEV"] = "/dev/fb0"
import pygame # type: ignore
import asyncio
import platform
import logging
import requests
import queue
import datetime
from display import init_display, draw_loading_screen, draw_error_screen, draw_platform_grid, draw_progress_screen, draw_controls, draw_virtual_keyboard, draw_popup_result_download, draw_extension_warning, draw_pause_menu, draw_controls_help, draw_game_list, 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
from language import handle_language_menu_events, _
from network import test_internet, download_rom, is_1fichier_url, download_from_1fichier, check_for_updates
from controls import handle_controls, validate_menu_state, process_key_repeats
from controls_mapper import load_controls_config, map_controls, draw_controls_mapping, ACTIONS
from utils import detect_non_pc, load_sources, check_extension_before_download, extract_zip_data, play_random_music
from history import load_history, save_history
import config
from config import OTA_data_ZIP
# Configuration du logging
try:
os.makedirs(config.log_dir, exist_ok=True)
logging.basicConfig(
filename=config.log_file,
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s'
)
except Exception as e:
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logging.error(f"Échec de la configuration du logging dans {config.log_file}: {str(e)}")
logger = logging.getLogger(__name__)
# Initialisation de Pygame et des polices
pygame.init()
config.init_font()
pygame.joystick.init()
logger.debug("--------------------------------------------------------------------")
logger.debug("---------------------------DEBUT LOG--------------------------------")
logger.debug("--------------------------------------------------------------------")
# Chargement des paramètres d'accessibilité
from utils import load_accessibility_settings
config.accessibility_settings = load_accessibility_settings()
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
break
# Chargement et initialisation de la langue
from language import initialize_language
initialize_language()
# Détection du système non-PC
config.is_non_pc = detect_non_pc()
# Initialisation de lécran
screen = init_display()
clock = pygame.time.Clock()
pygame.display.set_caption("RGSX")
# Initialisation des polices via config
config.init_font()
# Mise à jour de la résolution dans config
config.screen_width, config.screen_height = pygame.display.get_surface().get_size()
logger.debug(f"Résolution d'écran : {config.screen_width}x{config.screen_height}")
# Vérification des dossiers pour le débogage
logger.debug(f"SYSTEM_FOLDER: {config.SYSTEM_FOLDER}")
logger.debug(f"ROMS_FOLDER: {config.ROMS_FOLDER}")
logger.debug(f"SAVE_FOLDER: {config.SAVE_FOLDER}")
logger.debug(f"APP_FOLDER: {config.APP_FOLDER}")
# Initialisation des variables de grille
config.current_page = 0
config.selected_platform = 0
config.selected_key = (0, 0)
config.transition_state = "none"
# Initialisation des variables de répétition
config.repeat_action = None
config.repeat_key = None
config.repeat_start_time = 0
config.repeat_last_action = 0
# Initialisation des variables pour la popup de musique
# Dossier musique Batocera
music_folder = os.path.join(config.APP_FOLDER, "assets", "music")
music_files = [f for f in os.listdir(music_folder) if f.lower().endswith(('.ogg', '.mp3'))]
current_music = None # Variable pour suivre la musique en cours
config.music_folder = music_folder
config.music_files = music_files
config.current_music = current_music
if music_files:
current_music = play_random_music(music_files, music_folder, current_music)
else:
logger.debug("Aucune musique trouvée dans config.APP_FOLDER/assets/music")
config.current_music = current_music # Met à jour la musique en cours dans config
# Chargement de l'historique
config.history = load_history()
logger.debug(f"Historique de téléchargement : {len(config.history)} entrées")
# Vérification et chargement de la configuration des contrôles
config.controls_config = load_controls_config()
# Vérifier si la configuration est vide (pas de fichier ou importation échouée)
if not config.controls_config:
# Si pas de configuration, on commence par les configurer
config.menu_state = "controls_mapping"
config.needs_redraw = True # Forcer le redraw immédiatement
logger.info("Aucune configuration de contrôles disponible, configuration manuelle nécessaire")
logger.debug("Menu initial: mappage des contrôles")
else:
# Sinon, chargement normal
config.menu_state = "loading"
logger.debug("Menu chargement normal")
# Initialisation du gamepad
joystick = None
if pygame.joystick.get_count() > 0:
joystick = pygame.joystick.Joystick(0)
joystick.init()
logger.debug("Gamepad initialisé")
# Initialisation du mixer Pygame
pygame.mixer.pre_init(44100, -16, 2, 4096)
pygame.mixer.init()
from utils import load_music_config
load_music_config()
# Boucle principale
async def main():
# amazonq-ignore-next-line
global current_music, music_files, music_folder
logger.debug("Début main")
running = True
loading_step = "none"
sources = []
config.last_state_change_time = 0
config.debounce_delay = 50
config.update_triggered = False
last_redraw_time = pygame.time.get_ticks()
config.last_frame_time = pygame.time.get_ticks() # Initialisation pour éviter erreur
while running:
clock.tick(30) # Limite à 60 FPS
if config.update_triggered:
logger.debug("Mise à jour déclenchée, arrêt de la boucle principale")
break
current_time = pygame.time.get_ticks()
# 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
last_redraw_time = current_time
# Forcer redraw toutes les 100 ms dans history avec téléchargement actif
if config.menu_state == "history" and any(entry["status"] == "Téléchargement" for entry in config.history):
if current_time - last_redraw_time >= 100:
config.needs_redraw = True
last_redraw_time = current_time
# logger.debug("Forcing redraw in history state due to active download")
# Gestion de la fin du popup
if config.menu_state == "restart_popup" and config.popup_timer > 0:
config.popup_timer -= (current_time - config.last_frame_time)
config.needs_redraw = True
if config.popup_timer <= 0:
config.menu_state = validate_menu_state(config.previous_menu_state)
config.popup_message = ""
config.popup_timer = 0
config.needs_redraw = True
logger.debug(f"Fermeture automatique du popup, retour à {config.menu_state}")
# Gestion de la fin du popup update_result
if config.menu_state == "update_result" and current_time - config.update_result_start_time > 5000:
config.menu_state = "platform" # Retour à l'écran des plateformes
config.update_result_message = ""
config.update_result_error = False
config.needs_redraw = True
logger.debug("Fin popup update_result, retour à platform")
# Gestion de la répétition automatique des actions
process_key_repeats(sources, joystick, screen)
# Gestion des événements
events = pygame.event.get()
for event in events:
# Gestion directe des événements pour le menu de langue
if config.menu_state == "language_select":
if handle_language_menu_events(event, screen):
config.needs_redraw = True
continue
if event.type == pygame.USEREVENT + 1: # Événement de fin de musique
logger.debug("Fin de la musique détectée, lecture d'une nouvelle musique aléatoire")
current_music = play_random_music(music_files, music_folder, current_music)
continue
if event.type == pygame.QUIT:
config.menu_state = "confirm_exit"
config.confirm_selection = 0
config.needs_redraw = True
logger.debug("Événement QUIT détecté, passage à confirm_exit")
continue
start_config = config.controls_config.get("start", {})
if start_config and (
(event.type == pygame.KEYDOWN and start_config.get("type") == "key" and event.key == start_config.get("key")) or
(event.type == pygame.JOYBUTTONDOWN and start_config.get("type") == "button" and event.button == start_config.get("button")) or
(event.type == pygame.JOYAXISMOTION and start_config.get("type") == "axis" and event.axis == start_config.get("axis") and abs(event.value) > 0.5 and (1 if event.value > 0 else -1) == start_config.get("direction")) or
(event.type == pygame.JOYHATMOTION and start_config.get("type") == "hat" and event.value == tuple(start_config.get("value") if isinstance(start_config.get("value"), list) else start_config.get("value"))) or
(event.type == pygame.MOUSEBUTTONDOWN and start_config.get("type") == "mouse" and event.button == start_config.get("button"))
):
if config.menu_state not in ["pause_menu", "controls_help", "controls_mapping", "history", "confirm_clear_history"]:
config.previous_menu_state = config.menu_state
config.menu_state = "pause_menu"
config.selected_option = 0
config.needs_redraw = True
logger.debug(f"Ouverture menu pause depuis {config.previous_menu_state}")
continue
if config.menu_state == "pause_menu":
action = handle_controls(event, sources, joystick, screen)
config.needs_redraw = True
#logger.debug(f"Événement transmis à handle_controls dans pause_menu: {event.type}")
continue
if config.menu_state == "accessibility_menu":
from accessibility import handle_accessibility_events
if handle_accessibility_events(event):
config.needs_redraw = True
continue
if config.menu_state == "controls_help":
action = handle_controls(event, sources, joystick, screen)
config.needs_redraw = True
#logger.debug(f"Événement transmis à handle_controls dans controls_help: {event.type}")
continue
if config.menu_state == "confirm_clear_history":
action = handle_controls(event, sources, joystick, screen)
config.needs_redraw = True
continue
if config.menu_state == "confirm_cancel_download":
action = handle_controls(event, sources, joystick, screen)
config.needs_redraw = True
continue
if config.menu_state == "redownload_game_cache":
action = handle_controls(event, sources, joystick, screen)
config.needs_redraw = True
#logger.debug(f"Événement transmis à handle_controls dans redownload_game_cache: {event.type}")
continue
if config.menu_state == "extension_warning":
action = handle_controls(event, sources, joystick, screen)
config.needs_redraw = True
if action == "confirm":
if config.pending_download and config.extension_confirm_selection == 0: # Oui
url, platform, game_name, is_zip_non_supported = config.pending_download
logger.debug(f"Téléchargement confirmé après avertissement: {game_name} pour {platform} depuis {url}")
task_id = str(pygame.time.get_ticks())
config.history.append({
"platform": platform,
"game_name": game_name,
"status": "downloading",
"progress": 0,
"url": url,
"timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
})
config.current_history_item = len(config.history) - 1
save_history(config.history)
config.download_tasks[task_id] = (
asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported, task_id)),
url, game_name, platform
)
config.menu_state = "history"
config.pending_download = None
config.needs_redraw = True
logger.debug(f"Téléchargement démarré pour {game_name}, task_id={task_id}")
elif config.extension_confirm_selection == 1: # Non
config.menu_state = config.previous_menu_state
config.pending_download = None
config.needs_redraw = True
logger.debug("Téléchargement annulé, retour à l'état précédent")
continue
if config.menu_state in ["platform", "game", "error", "confirm_exit", "download_progress", "download_result", "history"]:
action = handle_controls(event, sources, joystick, screen)
config.needs_redraw = True
if action == "quit":
running = False
logger.debug("Action quit détectée, arrêt de l'application")
elif action == "download" and config.menu_state == "game" and config.filtered_games:
game = config.filtered_games[config.current_game]
game_name = game[0] if isinstance(game, (list, tuple)) else game
platform = config.platforms[config.current_platform]["name"] # Utiliser le nom de la plateforme
url = game[1] if isinstance(game, (list, tuple)) and len(game) > 1 else None
if url:
logger.debug(f"Vérification pour {game_name}, URL: {url}")
# Ajouter une entrée temporaire à l'historique
config.history.append({
"platform": platform,
"game_name": game_name,
"status": "downloading",
"progress": 0,
"message": _("download_initializing"),
"url": url,
"timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
})
config.current_history_item = len(config.history) - 1 # Sélectionner l'entrée en cours
if is_1fichier_url(url):
if not config.API_KEY_1FICHIER:
config.previous_menu_state = config.menu_state
config.menu_state = "error"
config.error_message = (
f"Attention il faut renseigner sa clé API (premium only) dans le fichier {os.path.join(config.SAVE_FOLDER, '1fichierAPI.txt')}"
)
# Mettre à jour l'entrée temporaire avec l'erreur
config.history[-1]["status"] = "Erreur"
config.history[-1]["progress"] = 0
config.history[-1]["message"] = "Erreur API : Clé API 1fichier absente"
save_history(config.history)
config.needs_redraw = True
logger.error("Clé API 1fichier absente")
config.pending_download = None
continue
is_supported, message, is_zip_non_supported = check_extension_before_download(url, platform, game_name)
if not is_supported:
config.pending_download = (url, platform, game_name, is_zip_non_supported)
config.menu_state = "extension_warning"
config.extension_confirm_selection = 0
config.needs_redraw = True
logger.debug(f"Extension non reconnue pour lien 1fichier, passage à extension_warning pour {game_name}")
# Supprimer l'entrée temporaire si erreur
config.history.pop()
else:
config.previous_menu_state = config.menu_state
logger.debug(f"Previous menu state défini: {config.previous_menu_state}")
# Lancer le téléchargement dans une tâche asynchrone
task_id = str(pygame.time.get_ticks())
config.download_tasks[task_id] = (
asyncio.create_task(download_from_1fichier(url, platform, game_name, is_zip_non_supported)),
url, game_name, platform
)
config.menu_state = "history" # Passer à l'historique
config.needs_redraw = True
logger.debug(f"Téléchargement 1fichier démarré pour {game_name}, passage à l'historique")
else:
is_supported, message, is_zip_non_supported = check_extension_before_download(url, platform, game_name)
if not is_supported:
config.pending_download = (url, platform, game_name, is_zip_non_supported)
config.menu_state = "extension_warning"
config.extension_confirm_selection = 0
config.needs_redraw = True
logger.debug(f"Extension non reconnue, passage à extension_warning pour {game_name}")
# Supprimer l'entrée temporaire si erreur
config.history.pop()
else:
config.previous_menu_state = config.menu_state
logger.debug(f"Previous menu state défini: {config.previous_menu_state}")
# Lancer le téléchargement dans une tâche asynchrone
task_id = str(pygame.time.get_ticks())
config.download_tasks[task_id] = (
asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported)),
url, game_name, platform
)
config.menu_state = "history" # Passer à l'historique
config.needs_redraw = True
logger.debug(f"Téléchargement démarré pour {game_name}, passage à l'historique")
elif action == "redownload" and config.menu_state == "history" and config.history:
entry = config.history[config.current_history_item]
platform = entry["platform"]
game_name = entry["game_name"]
for game in config.games:
if game[0] == game_name and config.platforms[config.current_platform] == platform:
url = game[1]
logger.debug(f"Vérification pour retéléchargement de {game_name}, URL: {url}")
if is_1fichier_url(url):
if not config.API_KEY_1FICHIER:
config.previous_menu_state = config.menu_state
config.menu_state = "error"
config.error_message = (
f"Attention il faut renseigner sa clé API (premium only) dans le fichier {os.path.join(config.SAVE_FOLDER, '1fichierAPI.txt')}"
)
config.needs_redraw = True
logger.error("Clé API 1fichier absente")
config.pending_download = None
continue
is_supported, message, is_zip_non_supported = check_extension_before_download(url, platform, game_name)
if not is_supported:
config.pending_download = (url, platform, game_name, is_zip_non_supported)
config.menu_state = "extension_warning"
config.extension_confirm_selection = 0
config.needs_redraw = True
logger.debug(f"Extension non reconnue pour lien 1fichier, passage à extension_warning pour {game_name}")
else:
config.previous_menu_state = config.menu_state
logger.debug(f"Previous menu state défini: {config.previous_menu_state}")
success, message = download_from_1fichier(url, platform, game_name, is_zip_non_supported)
config.download_result_message = message
config.download_result_error = not success
config.download_result_start_time = pygame.time.get_ticks()
config.menu_state = "download_result"
config.download_progress.clear()
config.pending_download = None
config.needs_redraw = True
logger.debug(f"Retéléchargement 1fichier terminé pour {game_name}, succès={success}, message={message}")
else:
is_supported, message, is_zip_non_supported = check_extension_before_download(url, platform, game_name)
if not is_supported:
config.pending_download = (url, platform, game_name, is_zip_non_supported)
config.menu_state = "extension_warning"
config.extension_confirm_selection = 0
config.needs_redraw = True
logger.debug(f"Extension non reconnue pour retéléchargement, passage à extension_warning pour {game_name}")
else:
config.previous_menu_state = config.menu_state
logger.debug(f"Previous menu state défini: {config.previous_menu_state}")
success, message = download_rom(url, platform, game_name, is_zip_non_supported)
config.download_result_message = message
config.download_result_error = not success
config.download_result_start_time = pygame.time.get_ticks()
config.menu_state = "download_result"
config.download_progress.clear()
config.pending_download = None
config.needs_redraw = True
logger.debug(f"Retéléchargement terminé pour {game_name}, succès={success}, message={message}")
break
# Gestion des téléchargements
if config.download_tasks:
for task_id, (task, url, game_name, platform) in list(config.download_tasks.items()):
if task.done():
try:
success, message = await task
if "http" in message:
message = message.split("https://")[0].strip()
for entry in config.history:
if entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]:
entry["status"] = "Download_OK" if success else "Erreur"
entry["progress"] = 100 if success else 0
entry["message"] = message
save_history(config.history)
config.needs_redraw = True
logger.debug(f"Téléchargement terminé: {game_name}, succès={success}, message={message}, task_id={task_id}")
break
config.download_result_message = message
config.download_result_error = not success
config.download_result_start_time = pygame.time.get_ticks()
config.menu_state = "download_result"
config.download_progress.clear()
config.pending_download = None
config.needs_redraw = True
del config.download_tasks[task_id]
except Exception as e:
message = f"Erreur lors du téléchargement: {str(e)}"
if "http" in message:
message = message.split("https://")[0].strip()
for entry in config.history:
if entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]:
entry["status"] = "Erreur"
entry["progress"] = 0
entry["message"] = message
save_history(config.history)
config.needs_redraw = True
logger.debug(f"Erreur téléchargement: {game_name}, message={message}, task_id={task_id}")
break
config.download_result_message = message
config.download_result_error = True
config.download_result_start_time = pygame.time.get_ticks()
config.menu_state = "download_result"
config.download_progress.clear()
config.pending_download = None
config.needs_redraw = True
del config.download_tasks[task_id]
else:
# Traiter les mises à jour de progression
progress_queue = queue.Queue()
while not progress_queue.empty():
data = progress_queue.get()
# logger.debug(f"Progress queue data received: {data}, task_id={task_id}")
if len(data) != 3 or data[0] != task_id: # Ignorer les données d'une autre tâche
logger.debug(f"Ignoring queue data for task_id={data[0]}, expected={task_id}")
continue
if isinstance(data[1], bool): # Fin du téléchargement
success, message = data[1], data[2]
for entry in config.history:
if entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]:
entry["status"] = "Download_OK" if success else "Erreur"
entry["progress"] = 100 if success else 0
entry["message"] = message
save_history(config.history)
config.needs_redraw = True
logger.debug(f"Final update in history: status={entry['status']}, progress={entry['progress']}%, message={message}, task_id={task_id}")
break
else:
downloaded, total_size = data[1], data[2]
progress = (downloaded / total_size * 100) if total_size > 0 else 0
for entry in config.history:
if entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]:
entry["progress"] = progress
entry["status"] = "Téléchargement"
config.needs_redraw = True
# logger.debug(f"Progress updated in history: {progress:.1f}% for {game_name}, task_id={task_id}")
break
config.download_result_message = message
config.download_result_error = True
config.download_result_start_time = pygame.time.get_ticks()
config.menu_state = "download_result"
config.download_progress.clear()
config.pending_download = None
config.needs_redraw = True
del config.download_tasks[task_id]
# Gestion de la fin du popup download_result
if config.menu_state == "download_result" and current_time - config.download_result_start_time > 3000:
config.menu_state = "history" # Rester dans l'historique après le popup
config.download_progress.clear()
config.pending_download = None
config.needs_redraw = True
logger.debug(f"Fin popup download_result, retour à history")
# Affichage
if config.needs_redraw:
draw_gradient(screen, THEME_COLORS["background_top"], THEME_COLORS["background_bottom"])
if config.menu_state == "controls_mapping":
# Ne rien faire ici, la gestion est faite dans la section spécifique
pass
elif config.menu_state == "loading":
draw_loading_screen(screen)
elif config.menu_state == "error":
draw_error_screen(screen)
elif config.menu_state == "update_result":
draw_popup_result_download(screen, config.update_result_message, config.update_result_error)
elif config.menu_state == "platform":
draw_platform_grid(screen)
elif config.menu_state == "game":
if not config.search_mode:
draw_game_list(screen)
if config.search_mode:
draw_game_list(screen)
if config.is_non_pc:
draw_virtual_keyboard(screen)
elif config.menu_state == "download_progress":
draw_progress_screen(screen)
elif config.menu_state == "download_result":
draw_popup_result_download(screen, config.download_result_message, config.download_result_error)
elif config.menu_state == "confirm_exit":
draw_confirm_dialog(screen)
elif config.menu_state == "extension_warning":
draw_extension_warning(screen)
elif config.menu_state == "pause_menu":
draw_pause_menu(screen, config.selected_option)
#logger.debug("Rendu de draw_pause_menu")
elif config.menu_state == "controls_help":
draw_controls_help(screen, config.previous_menu_state)
elif config.menu_state == "history":
draw_history_list(screen)
# logger.debug("Screen updated with draw_history_list")
elif config.menu_state == "confirm_clear_history":
draw_clear_history_dialog(screen)
elif config.menu_state == "confirm_cancel_download":
draw_cancel_download_dialog(screen)
elif config.menu_state == "redownload_game_cache":
draw_redownload_game_cache_dialog(screen)
elif config.menu_state == "restart_popup":
draw_popup(screen)
elif config.menu_state == "accessibility_menu":
from accessibility import draw_accessibility_menu
draw_accessibility_menu(screen)
elif config.menu_state == "language_select":
from display import draw_language_menu
draw_language_menu(screen)
else:
config.menu_state = "platform"
draw_platform_grid(screen)
config.needs_redraw = True
logger.error(f"État de menu non valide détecté: {config.menu_state}, retour à platform")
draw_controls(screen, config.menu_state, getattr(config, 'current_music_name', None), getattr(config, 'music_popup_start_time', 0))
pygame.display.flip()
config.needs_redraw = False
# logger.debug("Screen flipped with pygame.display.flip()")
# Gestion de l'état controls_mapping
if config.menu_state == "controls_mapping":
logger.debug("Avant appel de map_controls")
try:
# Vérifier si le fichier de contrôles existe déjà
controls_file_exists = os.path.exists(config.CONTROLS_CONFIG_PATH)
logger.debug(f"Vérification du fichier controls.json: {controls_file_exists} à {config.CONTROLS_CONFIG_PATH}")
if controls_file_exists:
# Si le fichier existe déjà, passer directement à l'état loading
config.menu_state = "loading"
logger.debug("Fichier controls.json existe déjà, passage direct à l'état loading")
config.needs_redraw = True
else:
# Forcer l'affichage de l'interface de mappage des contrôles
action = ACTIONS[0]
draw_controls_mapping(screen, action, None, True, 0.0)
pygame.display.flip()
logger.debug("Interface de mappage des contrôles affichée")
# Appeler map_controls pour gérer la configuration
success = map_controls(screen)
logger.debug(f"map_controls terminé, succès={success}")
if success:
config.controls_config = load_controls_config()
# Toujours passer à l'état loading après la configuration des contrôles
config.menu_state = "loading"
logger.debug("Passage à l'état loading après mappage")
config.needs_redraw = True
else:
config.menu_state = "error"
config.error_message = "Échec du mappage des contrôles"
config.needs_redraw = True
logger.debug("Échec du mappage, passage à l'état error")
except Exception as e:
logger.error(f"Erreur lors de l'appel de map_controls : {str(e)}")
config.menu_state = "error"
config.error_message = f"Erreur dans map_controls: {str(e)}"
config.needs_redraw = True
# Gestion de l'état loading
elif config.menu_state == "loading":
if loading_step == "none":
loading_step = "test_internet"
config.current_loading_system = "Test de connexion..."
config.loading_progress = 0.0
config.needs_redraw = True
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
elif loading_step == "test_internet":
#logger.debug("Exécution de test_internet()")
if test_internet():
loading_step = "check_ota"
config.current_loading_system = "Verification Mise à jour en cours... Patientez..."
config.loading_progress = 20.0
config.needs_redraw = True
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
else:
config.menu_state = "error"
config.error_message = "Pas de connexion Internet. Vérifiez votre réseau."
config.needs_redraw = True
logger.debug(f"Erreur : {config.error_message}")
elif loading_step == "check_ota":
logger.debug("Exécution de check_for_updates()")
success, message = await check_for_updates()
logger.debug(f"Résultat de check_for_updates : success={success}, message={message}")
if not success:
config.menu_state = "error"
config.error_message = message
config.needs_redraw = True
logger.debug(f"Erreur OTA : {message}")
else:
loading_step = "check_data"
config.current_loading_system = "Téléchargement des jeux et images ..."
config.loading_progress = 50.0
config.needs_redraw = True
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
elif loading_step == "check_data":
games_data_dir = os.path.join(config.APP_FOLDER, "games")
is_data_empty = not os.path.exists(games_data_dir) or not any(os.scandir(games_data_dir))
if is_data_empty:
config.current_loading_system = "Téléchargement du Dossier Data initial..."
config.loading_progress = 30.0
config.needs_redraw = True
logger.debug("Dossier Data vide, début du téléchargement du ZIP")
try:
zip_path = os.path.join(config.APP_FOLDER, "data_download.zip")
headers = {'User-Agent': 'Mozilla/5.0'}
with requests.get(OTA_data_ZIP, stream=True, headers=headers, timeout=30) as response:
response.raise_for_status()
total_size = int(response.headers.get('content-length', 0))
logger.debug(f"Taille totale du ZIP : {total_size} octets")
downloaded = 0
os.makedirs(os.path.dirname(zip_path), exist_ok=True)
with open(zip_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
config.download_progress[OTA_data_ZIP] = {
"downloaded_size": downloaded,
"total_size": total_size,
"status": "Téléchargement",
"progress_percent": (downloaded / total_size * 100) if total_size > 0 else 0
}
config.loading_progress = 15.0 + (35.0 * downloaded / total_size) if total_size > 0 else 15.0
config.needs_redraw = True
await asyncio.sleep(0)
logger.debug(f"ZIP téléchargé : {zip_path}")
config.current_loading_system = "Extraction du Dossier Data initial..."
config.loading_progress = 60.0
config.needs_redraw = True
dest_dir = config.APP_FOLDER
success, message = extract_zip_data(zip_path, dest_dir, OTA_data_ZIP)
if success:
logger.debug(f"Extraction réussie : {message}")
config.loading_progress = 70.0
config.needs_redraw = True
else:
raise Exception(f"Échec de l'extraction : {message}")
except Exception as e:
logger.error(f"Erreur lors du téléchargement/extraction du Dossier Data : {str(e)}")
config.menu_state = "error"
config.error_message = f"Échec du téléchargement/extraction du Dossier Data : {str(e)}"
config.needs_redraw = True
loading_step = "load_sources"
if os.path.exists(zip_path):
os.remove(zip_path)
continue
if os.path.exists(zip_path):
os.remove(zip_path)
logger.debug(f"Fichier ZIP {zip_path} supprimé")
loading_step = "load_sources"
config.current_loading_system = "Chargement des systèmes..."
config.loading_progress = 80.0
config.needs_redraw = True
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
else:
loading_step = "load_sources"
config.current_loading_system = "Chargement des systèmes..."
config.loading_progress = 80.0
config.needs_redraw = True
logger.debug(f"Dossier Data non vide, passage à {loading_step}")
elif loading_step == "load_sources":
sources = load_sources()
if not sources:
config.menu_state = "error"
config.error_message = "Échec du chargement de sources.json"
config.needs_redraw = True
logger.debug("Erreur : Échec du chargement de sources.json")
else:
config.menu_state = "platform"
config.loading_progress = 100.0
config.current_loading_system = ""
config.needs_redraw = True
logger.debug(f"Fin chargement, passage à platform, progress={config.loading_progress}")
# Gestion de l'état de transition
if config.transition_state == "to_game":
config.transition_progress += 1
if config.transition_progress >= config.transition_duration:
config.menu_state = "game"
config.transition_state = "idle"
config.transition_progress = 0.0
config.needs_redraw = True
logger.debug("Transition terminée, passage à game")
config.last_frame_time = current_time
clock.tick(60)
await asyncio.sleep(0.01)
pygame.mixer.music.stop()
pygame.quit()
logger.debug("Application terminée")
if platform.system() == "Emscripten":
asyncio.ensure_future(main())
else:
if __name__ == "__main__":
asyncio.run(main())

129
ports/RGSX/accessibility.py Normal file
View File

@@ -0,0 +1,129 @@
import pygame #type:ignore
import config
from utils import save_accessibility_settings
from language import _
def draw_accessibility_menu(screen):
"""Affiche le menu d'accessibilité avec curseur pour la taille de police."""
from display import OVERLAY, THEME_COLORS, draw_stylized_button
screen.blit(OVERLAY, (0, 0))
# Titre
title_text = _("menu_accessibility")
title_surface = config.title_font.render(title_text, True, THEME_COLORS["text"])
title_rect = title_surface.get_rect(center=(config.screen_width // 2, config.screen_height // 4))
# Fond du titre
title_bg_rect = title_rect.inflate(40, 20)
pygame.draw.rect(screen, THEME_COLORS["button_idle"], title_bg_rect, border_radius=10)
pygame.draw.rect(screen, THEME_COLORS["border"], title_bg_rect, 2, border_radius=10)
screen.blit(title_surface, title_rect)
# Curseur de taille de police
current_scale = config.font_scale_options[config.current_font_scale_index]
font_text = _("accessibility_font_size").format(f"{current_scale:.1f}")
# Position du curseur
cursor_y = config.screen_height // 2
cursor_width = 400
cursor_height = 60
cursor_x = (config.screen_width - cursor_width) // 2
# Fond du curseur
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (cursor_x, cursor_y, cursor_width, cursor_height), border_radius=10)
pygame.draw.rect(screen, THEME_COLORS["border"], (cursor_x, cursor_y, cursor_width, cursor_height), 2, border_radius=10)
# Flèches gauche/droite
arrow_size = 30
left_arrow_x = cursor_x + 20
right_arrow_x = cursor_x + cursor_width - arrow_size - 20
arrow_y = cursor_y + (cursor_height - arrow_size) // 2
# Flèche gauche
left_color = THEME_COLORS["fond_lignes"] if config.current_font_scale_index > 0 else THEME_COLORS["border"]
pygame.draw.polygon(screen, left_color, [
(left_arrow_x + arrow_size, arrow_y),
(left_arrow_x, arrow_y + arrow_size // 2),
(left_arrow_x + arrow_size, arrow_y + arrow_size)
])
# Flèche droite
right_color = THEME_COLORS["fond_lignes"] if config.current_font_scale_index < len(config.font_scale_options) - 1 else THEME_COLORS["border"]
pygame.draw.polygon(screen, right_color, [
(right_arrow_x, arrow_y),
(right_arrow_x + arrow_size, arrow_y + arrow_size // 2),
(right_arrow_x, arrow_y + arrow_size)
])
# Texte au centre
text_surface = config.font.render(font_text, True, THEME_COLORS["text"])
text_rect = text_surface.get_rect(center=(cursor_x + cursor_width // 2, cursor_y + cursor_height // 2))
screen.blit(text_surface, text_rect)
# Instructions
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 - 100))
screen.blit(instruction_surface, instruction_rect)
def handle_accessibility_events(event):
"""Gère les événements du menu d'accessibilité avec support clavier et manette."""
# Gestion des touches du clavier
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT and config.current_font_scale_index > 0:
config.current_font_scale_index -= 1
update_font_scale()
return True
elif event.key == pygame.K_RIGHT and config.current_font_scale_index < len(config.font_scale_options) - 1:
config.current_font_scale_index += 1
update_font_scale()
return True
elif event.key == pygame.K_RETURN or event.key == pygame.K_ESCAPE:
config.menu_state = "pause_menu"
return True
# Gestion des boutons de la manette
elif event.type == pygame.JOYBUTTONDOWN:
if event.button == 0: # Bouton A (valider)
config.menu_state = "pause_menu"
return True
elif event.button == 1: # Bouton B (annuler)
config.menu_state = "pause_menu"
return True
# Gestion du D-pad
elif event.type == pygame.JOYHATMOTION:
if event.value == (-1, 0): # Gauche
if config.current_font_scale_index > 0:
config.current_font_scale_index -= 1
update_font_scale()
return True
elif event.value == (1, 0): # Droite
if config.current_font_scale_index < len(config.font_scale_options) - 1:
config.current_font_scale_index += 1
update_font_scale()
return True
# Gestion du joystick analogique (axe horizontal)
elif event.type == pygame.JOYAXISMOTION:
if event.axis == 0 and abs(event.value) > 0.5: # Joystick gauche horizontal
if event.value < -0.5 and config.current_font_scale_index > 0: # Gauche
config.current_font_scale_index -= 1
update_font_scale()
return True
elif event.value > 0.5 and config.current_font_scale_index < len(config.font_scale_options) - 1: # Droite
config.current_font_scale_index += 1
update_font_scale()
return True
return False
def update_font_scale():
"""Met à jour l'échelle de police et sauvegarde."""
new_scale = config.font_scale_options[config.current_font_scale_index]
config.accessibility_settings["font_scale"] = new_scale
save_accessibility_settings(config.accessibility_settings)
# Réinitialiser les polices
config.init_font()
config.needs_redraw = True

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

207
ports/RGSX/config.py Normal file
View File

@@ -0,0 +1,207 @@
import pygame # type: ignore
import os
import sys
import logging
# Version actuelle de l'application
app_version = "1.9.7.9"
def get_application_root():
"""Détermine le dossier de l'application de manière portable."""
try:
# Obtenir le chemin absolu du fichier config.py
current_file = os.path.abspath(__file__)
# Remonter au dossier parent de config.py (par exemple, dossier de l'application)
app_root = os.path.dirname(os.path.dirname(current_file))
return app_root
except NameError:
# Si __file__ n'est pas défini (par exemple, exécution dans un REPL)
return os.path.abspath(os.getcwd())
def get_system_root():
"""Détermine le dossier racine du système de fichiers (par exemple, /userdata ou C:\\)."""
try:
if sys.platform.startswith("win"):
# Sur Windows, extraire la lettre de disque
current_path = os.path.abspath(__file__)
drive, _ = os.path.splitdrive(current_path)
system_root = drive + os.sep
return system_root
else:
# Sur Linux/Batocera, remonter jusqu'à atteindre /userdata ou /
current_path = os.path.abspath(__file__)
current_dir = current_path
while current_dir != os.path.dirname(current_dir): # Tant qu'on peut remonter
parent_dir = os.path.dirname(current_dir)
if os.path.basename(parent_dir) == "userdata": # Vérifier si le parent est userdata
system_root = parent_dir
return system_root
current_dir = parent_dir
# Si userdata n'est pas trouvé, retourner /
return "/"
except NameError:
# Si __file__ n'est pas défini, utiliser le répertoire de travail actuel
return "/" if not sys.platform.startswith("win") 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")
# Configuration du logging
logger = logging.getLogger(__name__)
log_dir = os.path.join(APP_FOLDER, "logs")
log_file = os.path.join(log_dir, "RGSX.log")
# Chemins de base
UPDATE_FOLDER = os.path.join(APP_FOLDER, "update")
GAMELISTXML = os.path.join(APP_FOLDER, "gamelist.xml")
IMAGES_FOLDER = os.path.join(APP_FOLDER, "images", "systemes")
GAMES_FOLDER = os.path.join(APP_FOLDER, "games")
CONTROLS_CONFIG_PATH = os.path.join(SAVE_FOLDER, "controls.json")
HISTORY_PATH = os.path.join(SAVE_FOLDER, "history.json")
LANGUAGE_CONFIG_PATH = os.path.join(SAVE_FOLDER, "language.json")
JSON_EXTENSIONS = os.path.join(APP_FOLDER, "rom_extensions.json")
MUSIC_CONFIG_PATH = os.path.join(SAVE_FOLDER, "music_config.json")
# URL
OTA_SERVER_URL = "https://retrogamesets.fr/softs/"
OTA_VERSION_ENDPOINT = os.path.join(OTA_SERVER_URL, "version.json")
OTA_UPDATE_ZIP = os.path.join(OTA_SERVER_URL, "RGSX.zip")
OTA_data_ZIP = os.path.join(OTA_SERVER_URL, "rgsx-data.zip")
# 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
REPEAT_ACTION_DEBOUNCE = 150 # Délai anti-rebond pour répétitions (ms) - augmenté pour éviter les doubles actions
# Variables d'état
platforms = []
current_platform = 0
accessibility_mode = False # Mode accessibilité pour les polices agrandies
accessibility_settings = {"font_scale": 1.0}
font_scale_options = [0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]
current_font_scale_index = 3 # Index pour 1.0
platform_names = {} # {platform_id: platform_name}
games = []
current_game = 0
menu_state = "popup"
confirm_choice = False
scroll_offset = 0
visible_games = 15
popup_start_time = 0
last_progress_update = 0
needs_redraw = True
transition_state = "idle"
transition_progress = 0.0
transition_duration = 18
games_count = {}
music_enabled = True # Par défaut la musique est activée
API_KEY_1FICHIER = "" # Initialisation de la variable globale pour la clé API
# Variables pour la sélection de langue
selected_language_index = 0
loading_progress = 0.0
current_loading_system = ""
error_message = ""
repeat_action = None
repeat_start_time = 0
repeat_last_action = 0
repeat_key = None
filtered_games = []
search_mode = False
search_query = ""
filter_active = False
extension_confirm_selection = 0
pending_download = None
controls_config = {}
selected_option = 0
previous_menu_state = None
history = [] # Liste des entrées d'historique avec platform, game_name, status, url, progress, message, timestamp
download_progress = {}
download_tasks = {} # Dictionnaire pour les tâches de téléchargement
download_result_message = ""
download_result_error = False
download_result_start_time = 0
needs_redraw = False
current_history_item = 0
history_scroll_offset = 0 # Offset pour le défilement de l'historique
visible_history_items = 15 # Nombre d'éléments d'historique visibles (ajusté dynamiquement)
confirm_clear_selection = 0 # confirmation clear historique
confirm_cancel_selection = 0 # confirmation annulation téléchargement
last_state_change_time = 0 # Temps du dernier changement d'état pour debounce
debounce_delay = 200 # Délai de debounce en millisecondes
platform_dicts = [] # Liste des dictionnaires de plateformes
selected_key = (0, 0) # Position du curseur dans le clavier virtuel
is_non_pc = True # Indicateur pour plateforme non-PC (par exemple, console)
redownload_confirm_selection = 0 # Sélection pour la confirmation de redownload
popup_message = "" # Message à afficher dans les popups
popup_timer = 0 # Temps restant pour le popup en millisecondes (0 = inactif)
last_frame_time = pygame.time.get_ticks()
current_music_name = None
music_popup_start_time = 0
GRID_COLS = 3 # Number of columns in the platform grid
GRID_ROWS = 4 # Number of rows in the platform grid
# Résolution de l'écran fallback
# Utilisée si la résolution définie dépasse les capacités de l'écran
SCREEN_WIDTH = 800
"""Largeur de l'écran en pixels."""
SCREEN_HEIGHT = 600
"""Hauteur de l'écran en pixels."""
# Polices
FONT = None
"""Police par défaut pour l'affichage, initialisée via init_font()."""
progress_font = None
"""Police pour l'affichage de la progression."""
title_font = None
"""Police pour les titres."""
search_font = None
"""Police pour la recherche."""
small_font = None
"""Police pour les petits textes."""
def init_font():
"""Initialise les polices après pygame.init()."""
global font, progress_font, title_font, search_font, small_font
font_scale = accessibility_settings.get("font_scale", 1.0)
try:
font_path = os.path.join(APP_FOLDER, "assets", "Pixel-UniCode.ttf")
font = pygame.font.Font(font_path, int(36 * font_scale))
title_font = pygame.font.Font(font_path, int(48 * font_scale))
search_font = pygame.font.Font(font_path, int(48 * font_scale))
progress_font = pygame.font.Font(font_path, int(36 * font_scale))
small_font = pygame.font.Font(font_path, int(28 * font_scale))
logger.debug(f"Polices Pixel-UniCode initialisées (font_scale: {font_scale})")
except Exception as e:
try:
font = pygame.font.SysFont("arial", int(48 * font_scale))
title_font = pygame.font.SysFont("arial", int(60 * font_scale))
search_font = pygame.font.SysFont("arial", int(60 * font_scale))
progress_font = pygame.font.SysFont("arial", int(36 * font_scale))
small_font = pygame.font.SysFont("arial", int(28 * font_scale))
logger.debug(f"Polices Arial initialisées (font_scale: {font_scale})")
except Exception as e2:
logger.error(f"Erreur lors de l'initialisation des polices : {e2}")
font = None
progress_font = None
title_font = None
search_font = None
small_font = None
def validate_resolution():
"""Valide la résolution de l'écran par rapport aux capacités de l'écran."""
display_info = pygame.display.Info()
if SCREEN_WIDTH > display_info.current_w or SCREEN_HEIGHT > display_info.current_h:
logger.warning(f"Résolution {SCREEN_WIDTH}x{SCREEN_HEIGHT} dépasse les limites de l'écran")
return display_info.current_w, display_info.current_h
return SCREEN_WIDTH, SCREEN_HEIGHT

1143
ports/RGSX/controls.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,601 @@
import pygame # type: ignore
import json
import os
import logging
import config
from config import CONTROLS_CONFIG_PATH
from display import draw_gradient
import xml.etree.ElementTree as ET
logger = logging.getLogger(__name__)
# Chemin du fichier de configuration des contrôles
CONTROLS_CONFIG_PATH = os.path.join(config.SAVE_FOLDER, "controls.json")
# Actions internes de RGSX à mapper
ACTIONS = [
{"name": "confirm", "display": "Confirmer", "description": "Valider (Recommandé: Entrée, A/Croix)"},
{"name": "cancel", "display": "Annuler", "description": "Annuler/Retour (Recommandé: Retour Arrière, B/Rond)"},
{"name": "up", "display": "Haut", "description": "Naviguer vers le haut"},
{"name": "down", "display": "Bas", "description": "Naviguer vers le bas"},
{"name": "left", "display": "Gauche", "description": "Naviguer à gauche"},
{"name": "right", "display": "Droite", "description": "Naviguer à droite"},
{"name": "start", "display": "Start", "description": "Menu pause / Paramètres (Recommandé: Start, AltGr)"},
{"name": "filter", "display": "Filtrer", "description": "Ouvrir filtre (Recommandé: F, Select)"},
{"name": "page_up", "display": "Page Précédente", "description": "Page précédente/Défilement Rapide Haut (Recommandé: PageUp, LB/L1)"},
{"name": "page_down", "display": "Page Suivante", "description": "Page suivante/Défilement Rapide Bas (Recommandé: PageDown, RB/R1)"},
{"name": "history", "display": "Historique", "description": "Ouvrir l'historique (Recommandé: H, Y/Carré)"},
{"name": "progress", "display": "Progression", "description": "Historique : Effacer la liste (Recommandé: X/Triangle)"},
{"name": "delete", "display": "Supprimer", "description": "Mode Fitre : Supprimer caractère en mode recherche (Recommandé: DEL, LT/L2)"},
{"name": "space", "display": "Espace", "description": "Mode Filtre : Ajouter espace (Recommandé: Espace, RT/R2)"},
]
# Mappage des valeurs SDL vers les constantes Pygame
SDL_TO_PYGAME_KEY = {
1073741906: pygame.K_UP, # Flèche Haut
1073741905: pygame.K_DOWN, # Flèche Bas
1073741904: pygame.K_LEFT, # Flèche Gauche
1073741903: pygame.K_RIGHT, # Flèche Droite
1073742050: pygame.K_LALT, # Alt gauche
1073742054: pygame.K_RALT, # Alt droit (AltGr)
1073742049: pygame.K_LCTRL, # Ctrl gauche
1073742053: pygame.K_RCTRL, # Ctrl droit
1073742048: pygame.K_LSHIFT, # Shift gauche
1073742052: pygame.K_RSHIFT, # Shift droit
}
# Noms lisibles pour les touches clavier
KEY_NAMES = {
pygame.K_RETURN: "Entrée",
pygame.K_ESCAPE: "Échap",
pygame.K_SPACE: "Espace",
pygame.K_UP: "Flèche Haut",
pygame.K_DOWN: "Flèche Bas",
pygame.K_LEFT: "Flèche Gauche",
pygame.K_RIGHT: "Flèche Droite",
pygame.K_BACKSPACE: "Retour Arrière",
pygame.K_TAB: "Tab",
pygame.K_LALT: "Alt",
pygame.K_RALT: "AltGR",
pygame.K_LCTRL: "LCtrl",
pygame.K_RCTRL: "RCtrl",
pygame.K_LSHIFT: "LShift",
pygame.K_RSHIFT: "RShift",
pygame.K_LMETA: "LMeta",
pygame.K_RMETA: "RMeta",
pygame.K_CAPSLOCK: "Verr Maj",
pygame.K_NUMLOCK: "Verr Num",
pygame.K_SCROLLOCK: "Verr Déf",
pygame.K_a: "A",
pygame.K_b: "B",
pygame.K_c: "C",
pygame.K_d: "D",
pygame.K_e: "E",
pygame.K_f: "F",
pygame.K_g: "G",
pygame.K_h: "H",
pygame.K_i: "I",
pygame.K_j: "J",
pygame.K_k: "K",
pygame.K_l: "L",
pygame.K_m: "M",
pygame.K_n: "N",
pygame.K_o: "O",
pygame.K_p: "P",
pygame.K_q: "Q",
pygame.K_r: "R",
pygame.K_s: "S",
pygame.K_t: "T",
pygame.K_u: "U",
pygame.K_v: "V",
pygame.K_w: "W",
pygame.K_x: "X",
pygame.K_y: "Y",
pygame.K_z: "Z",
pygame.K_0: "0",
pygame.K_1: "1",
pygame.K_2: "2",
pygame.K_3: "3",
pygame.K_4: "4",
pygame.K_5: "5",
pygame.K_6: "6",
pygame.K_7: "7",
pygame.K_8: "8",
pygame.K_9: "9",
pygame.K_KP0: "Pavé 0",
pygame.K_KP1: "Pavé 1",
pygame.K_KP2: "Pavé 2",
pygame.K_KP3: "Pavé 3",
pygame.K_KP4: "Pavé 4",
pygame.K_KP5: "Pavé 5",
pygame.K_KP6: "Pavé 6",
pygame.K_KP7: "Pavé 7",
pygame.K_KP8: "Pavé 8",
pygame.K_KP9: "Pavé 9",
pygame.K_KP_PERIOD: "Pavé .",
pygame.K_KP_DIVIDE: "Pavé /",
pygame.K_KP_MULTIPLY: "Pavé *",
pygame.K_KP_MINUS: "Pavé -",
pygame.K_KP_PLUS: "Pavé +",
pygame.K_KP_ENTER: "Pavé Entrée",
pygame.K_KP_EQUALS: "Pavé =",
pygame.K_F1: "F1",
pygame.K_F2: "F2",
pygame.K_F3: "F3",
pygame.K_F4: "F4",
pygame.K_F5: "F5",
pygame.K_F6: "F6",
pygame.K_F7: "F7",
pygame.K_F8: "F8",
pygame.K_F9: "F9",
pygame.K_F10: "F10",
pygame.K_F11: "F11",
pygame.K_F12: "F12",
pygame.K_F13: "F13",
pygame.K_F14: "F14",
pygame.K_F15: "F15",
pygame.K_INSERT: "Inser",
pygame.K_DELETE: "Suppr",
pygame.K_HOME: "Début",
pygame.K_END: "Fin",
pygame.K_PAGEUP: "Page Haut",
pygame.K_PAGEDOWN: "Page Bas",
pygame.K_PRINT: "Impr Écran",
pygame.K_SYSREQ: "SysReq",
pygame.K_BREAK: "Pause",
pygame.K_PAUSE: "Pause",
pygame.K_BACKQUOTE: "`",
pygame.K_MINUS: "-",
pygame.K_EQUALS: "=",
pygame.K_LEFTBRACKET: "[",
pygame.K_RIGHTBRACKET: "]",
pygame.K_BACKSLASH: "\\",
pygame.K_SEMICOLON: ";",
pygame.K_QUOTE: "'",
pygame.K_COMMA: ",",
pygame.K_PERIOD: ".",
pygame.K_SLASH: "/",
}
def get_controller_button_names():
"""Récupère les noms des boutons depuis es_input.cfg"""
es_input_path = "/usr/share/emulationstation/es_input.cfg"
button_names = {}
if not os.path.exists(es_input_path):
return {i: f"Bouton {i}" for i in range(16)}
try:
tree = ET.parse(es_input_path)
root = tree.getroot()
# Mapping des noms ES vers des noms lisibles
es_button_names = {
"a": "A", "b": "B", "x": "X", "y": "Y",
"leftshoulder": "LB", "rightshoulder": "RB",
"lefttrigger": "LT", "righttrigger": "RT",
"select": "Select", "start": "Start",
"leftstick": "L3", "rightstick": "R3"
}
for inputConfig in root.findall("inputConfig"):
if inputConfig.get("type") == "joystick":
for input_tag in inputConfig.findall("input"):
if input_tag.get("type") == "button":
es_name = input_tag.get("name")
button_id = int(input_tag.get("id"))
readable_name = es_button_names.get(es_name, es_name.upper())
button_names[button_id] = readable_name
break
except Exception as e:
logger.error(f"Erreur parsing es_input.cfg: {e}")
# Compléter avec des noms génériques
for i in range(16):
if i not in button_names:
button_names[i] = f"Bouton {i}"
return button_names
def get_controller_axis_names():
"""Récupère les noms des axes depuis es_input.cfg"""
es_input_path = "/usr/share/emulationstation/es_input.cfg"
axis_names = {}
if not os.path.exists(es_input_path):
return {(i, d): f"Axe {i}{'+' if d > 0 else '-'}" for i in range(8) for d in [-1, 1]}
try:
tree = ET.parse(es_input_path)
root = tree.getroot()
# Mapping des noms ES vers des noms lisibles
es_axis_names = {
"leftx": "Joy G", "lefty": "Joy G",
"rightx": "Joy D", "righty": "Joy D",
"lefttrigger": "LT", "righttrigger": "RT"
}
for inputConfig in root.findall("inputConfig"):
if inputConfig.get("type") == "joystick":
for input_tag in inputConfig.findall("input"):
if input_tag.get("type") == "axis":
es_name = input_tag.get("name")
axis_id = int(input_tag.get("id"))
value = int(input_tag.get("value", "1"))
direction = 1 if value > 0 else -1
if es_name in es_axis_names:
base_name = es_axis_names[es_name]
if "Joy" in base_name:
if "leftx" in es_name or "rightx" in es_name:
axis_names[(axis_id, direction)] = f"{base_name} {'Droite' if direction > 0 else 'Gauche'}"
else:
axis_names[(axis_id, direction)] = f"{base_name} {'Bas' if direction > 0 else 'Haut'}"
else:
axis_names[(axis_id, direction)] = base_name
break
except Exception as e:
logger.error(f"Erreur parsing es_input.cfg: {e}")
# Compléter avec des noms génériques
for i in range(8):
for d in [-1, 1]:
if (i, d) not in axis_names:
axis_names[(i, d)] = f"Axe {i}{'+' if d > 0 else '-'}"
return axis_names
# Charger les noms depuis es_input.cfg
BUTTON_NAMES = get_controller_button_names()
AXIS_NAMES = get_controller_axis_names()
# Noms pour la croix directionnelle
HAT_NAMES = {
(0, 1): "D-Pad Haut",
(0, -1): "D-Pad Bas",
(-1, 0): "D-Pad Gauche",
(1, 0): "D-Pad Droite",
}
# Noms pour les boutons de souris
MOUSE_BUTTON_NAMES = {
1: "Clic Gauche",
2: "Clic Milieu",
3: "Clic Droit",
}
# Durée de maintien pour valider une entrée (en millisecondes)
HOLD_DURATION = 1000
JOYHAT_DEBOUNCE = 200 # Délai anti-rebond pour JOYHATMOTION (ms)
def load_controls_config():
"""Charge la configuration des contrôles depuis controls.json ou EmulationStation"""
try:
if os.path.exists(CONTROLS_CONFIG_PATH):
with open(CONTROLS_CONFIG_PATH, "r") as f:
config = json.load(f)
logger.debug(f"Configuration des contrôles chargée : {config}")
return config
else:
logger.debug("Aucun fichier controls.json trouvé, tentative d'importation depuis EmulationStation")
# Essayer d'importer depuis EmulationStation
from es_input_parser import parse_es_input_config
es_config = parse_es_input_config()
if es_config:
logger.info("Configuration importée depuis EmulationStation")
save_controls_config(es_config)
return es_config
else:
logger.debug("Importation depuis EmulationStation échouée, configuration par défaut")
return {}
except Exception as e:
logger.error(f"Erreur lors du chargement de controls.json : {e}")
return {}
def save_controls_config(controls_config):
"""Enregistre la configuration des contrôles dans controls.json"""
try:
os.makedirs(os.path.dirname(CONTROLS_CONFIG_PATH), exist_ok=True)
with open(CONTROLS_CONFIG_PATH, "w") as f:
json.dump(controls_config, f, indent=4)
logger.debug(f"Configuration des contrôles enregistrée : {controls_config}")
except Exception as e:
logger.error(f"Erreur lors de l'enregistrement de controls.json : {e}")
def get_readable_input_name(event):
"""Retourne un nom lisible pour une entrée (touche, bouton, axe, hat, ou souris)"""
if event.type == pygame.KEYDOWN:
key_value = SDL_TO_PYGAME_KEY.get(event.key, event.key)
return KEY_NAMES.get(key_value, pygame.key.name(key_value) or f"Touche {key_value}")
elif event.type == pygame.JOYBUTTONDOWN:
return BUTTON_NAMES.get(event.button, f"Bouton {event.button}")
elif event.type == pygame.JOYAXISMOTION:
if abs(event.value) > 0.5: # Seuil pour détecter un mouvement significatif
return AXIS_NAMES.get((event.axis, 1 if event.value > 0 else -1), f"Axe {event.axis} {'Positif' if event.value > 0 else 'Négatif'}")
elif event.type == pygame.JOYHATMOTION:
if event.value != (0, 0): # Ignorer la position neutre
return HAT_NAMES.get(event.value, f"D-Pad {event.value}")
elif event.type == pygame.MOUSEBUTTONDOWN:
return MOUSE_BUTTON_NAMES.get(event.button, f"Souris Bouton {event.button}")
return "Inconnu"
def map_controls(screen):
"""Interface de mappage des contrôles avec maintien de 3 secondes"""
controls_config = load_controls_config()
current_action_index = 0
current_input = None
input_held_time = 0
last_input_name = None
last_frame_time = pygame.time.get_ticks()
config.needs_redraw = True
last_joyhat_time = 0
# État des entrées maintenues
held_keys = set()
held_buttons = set()
held_axes = {}
held_hats = {}
held_mouse_buttons = set()
while current_action_index < len(ACTIONS):
if config.needs_redraw:
progress = min(input_held_time / HOLD_DURATION, 1.0) if current_input else 0.0
draw_controls_mapping(screen, ACTIONS[current_action_index], last_input_name, current_input is not None, progress)
pygame.display.flip()
config.needs_redraw = False
current_time = pygame.time.get_ticks()
delta_time = current_time - last_frame_time
last_frame_time = current_time
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
# Gestion des relâchements
if event.type == pygame.KEYUP and event.key in held_keys:
held_keys.remove(event.key)
if current_input and current_input["type"] == "key" and current_input["value"] == event.key:
current_input = None
input_held_time = 0
last_input_name = None
config.needs_redraw = True
elif event.type == pygame.JOYBUTTONUP and event.button in held_buttons:
held_buttons.remove(event.button)
if current_input and current_input["type"] == "button" and current_input["value"] == event.button:
current_input = None
input_held_time = 0
last_input_name = None
config.needs_redraw = True
elif event.type == pygame.JOYAXISMOTION and abs(event.value) < 0.5:
if event.axis in held_axes:
held_direction = held_axes[event.axis]
if current_input and current_input["type"] == "axis" and current_input["value"][0] == event.axis and current_input["value"][1] == held_direction:
current_input = None
input_held_time = 0
last_input_name = None
config.needs_redraw = True
del held_axes[event.axis]
elif event.type == pygame.JOYHATMOTION and event.value == (0, 0):
if event.hat in held_hats:
del held_hats[event.hat]
if current_input and current_input["type"] == "hat":
current_input = None
input_held_time = 0
last_input_name = None
config.needs_redraw = True
continue
elif event.type == pygame.MOUSEBUTTONUP and event.button in held_mouse_buttons:
held_mouse_buttons.remove(event.button)
if current_input and current_input["type"] == "mouse" and current_input["value"] == event.button:
current_input = None
input_held_time = 0
last_input_name = None
config.needs_redraw = True
# Détection des nouvelles entrées
if event.type in (pygame.KEYDOWN, pygame.JOYBUTTONDOWN, pygame.JOYAXISMOTION, pygame.JOYHATMOTION, pygame.MOUSEBUTTONDOWN):
if event.type == pygame.JOYHATMOTION:
if (current_time - last_joyhat_time) < JOYHAT_DEBOUNCE:
continue
last_joyhat_time = current_time
input_name = get_readable_input_name(event)
if input_name == "Inconnu":
continue
# Déterminer le type et la valeur
if event.type == pygame.KEYDOWN:
input_type = "key"
input_value = SDL_TO_PYGAME_KEY.get(event.key, event.key)
elif event.type == pygame.JOYBUTTONDOWN:
input_type = "button"
input_value = event.button
elif event.type == pygame.JOYAXISMOTION and abs(event.value) > 0.5:
input_type = "axis"
direction = 1 if event.value > 0 else -1
input_value = (event.axis, direction)
# Ignorer si c'est juste un changement de direction du même axe
if event.axis in held_axes and held_axes[event.axis] != direction:
continue
elif event.type == pygame.JOYHATMOTION:
input_type = "hat"
input_value = event.value
elif event.type == pygame.MOUSEBUTTONDOWN:
input_type = "mouse"
input_value = event.button
else:
continue
# Nouvelle entrée détectée
if (current_input is None or
current_input["type"] != input_type or
current_input["value"] != input_value):
current_input = {"type": input_type, "value": input_value}
input_held_time = 0
last_input_name = input_name
config.needs_redraw = True
# Mettre à jour les entrées maintenues
if input_type == "key":
held_keys.add(input_value)
elif input_type == "button":
held_buttons.add(input_value)
elif input_type == "axis":
held_axes[input_value[0]] = input_value[1]
elif input_type == "hat":
held_hats[event.hat] = input_value
elif input_type == "mouse":
held_mouse_buttons.add(input_value)
# Mise à jour du temps de maintien
if current_input:
input_held_time += delta_time
if input_held_time >= HOLD_DURATION:
action_name = ACTIONS[current_action_index]["name"]
# Sauvegarder avec la structure attendue par controls.py
if current_input["type"] == "key":
controls_config[action_name] = {
"type": "key",
"key": current_input["value"],
"display": last_input_name
}
elif current_input["type"] == "button":
controls_config[action_name] = {
"type": "button",
"button": current_input["value"],
"display": last_input_name
}
elif current_input["type"] == "axis":
axis, direction = current_input["value"]
controls_config[action_name] = {
"type": "axis",
"axis": axis,
"direction": direction,
"display": last_input_name
}
elif current_input["type"] == "hat":
controls_config[action_name] = {
"type": "hat",
"value": current_input["value"],
"display": last_input_name
}
elif current_input["type"] == "mouse":
controls_config[action_name] = {
"type": "mouse",
"button": current_input["value"],
"display": last_input_name
}
logger.debug(f"Contrôle mappé: {action_name} -> {controls_config[action_name]}")
current_action_index += 1
current_input = None
input_held_time = 0
last_input_name = None
config.needs_redraw = True
# Réinitialiser les entrées maintenues
held_keys.clear()
held_buttons.clear()
held_axes.clear()
held_hats.clear()
held_mouse_buttons.clear()
config.needs_redraw = True
pygame.time.wait(10)
save_controls_config(controls_config)
config.controls_config = controls_config
return True
def draw_controls_mapping(screen, action, last_input, waiting_for_input, hold_progress):
#Affiche l'interface de mappage des contrôles avec une barre de progression pour le maintien
draw_gradient(screen, (28, 37, 38), (47, 59, 61))
# Paramètres de l'interface
padding_horizontal = 40
padding_vertical = 30
padding_between = 15
border_radius = 24
border_width = 4
shadow_offset = 8
# Titre principal
title_text = "Configuration des contrôles"
title_surface = config.title_font.render(title_text, True, (255, 255, 255))
title_rect = title_surface.get_rect(center=(config.screen_width // 2, 80))
screen.blit(title_surface, title_rect)
# Instructions
instruction_text = "Maintenez pendant 3s pour configurer :"
description_text = action['description']
instruction_surface = config.small_font.render(instruction_text, True, (255, 255, 255))
description_surface = config.font.render(description_text, True, (200, 200, 200))
instruction_width, instruction_height = instruction_surface.get_size()
description_width, description_height = description_surface.get_size()
# Input détecté
input_text = last_input or (f"En attente d'une touche ou bouton..." if waiting_for_input else "Appuyez sur une touche ou un bouton")
input_surface = config.small_font.render(input_text, True, (0, 255, 0) if last_input else (255, 255, 255))
input_width, input_height = input_surface.get_size()
# Dimensions de la popup
text_width = max(instruction_width, description_width, input_width)
text_height = instruction_height + description_height + input_height + 2 * padding_between
popup_width = text_width + 2 * padding_horizontal
popup_height = text_height + 40 + 2 * padding_vertical # +40 pour la barre de progression
popup_x = (config.screen_width - popup_width) // 2
popup_y = (config.screen_height - popup_height) // 2
# Ombre portée
shadow_rect = pygame.Rect(popup_x + shadow_offset, popup_y + shadow_offset, popup_width, popup_height)
shadow_surface = pygame.Surface((popup_width, popup_height), pygame.SRCALPHA)
pygame.draw.rect(shadow_surface, (0, 0, 0, 100), shadow_surface.get_rect(), border_radius=border_radius)
screen.blit(shadow_surface, shadow_rect.topleft)
# Fond semi-transparent
popup_rect = pygame.Rect(popup_x, popup_y, popup_width, popup_height)
popup_surface = pygame.Surface((popup_width, popup_height), pygame.SRCALPHA)
pygame.draw.rect(popup_surface, (30, 30, 30, 220), popup_surface.get_rect(), border_radius=border_radius)
screen.blit(popup_surface, popup_rect.topleft)
# Bordure blanche
pygame.draw.rect(screen, (255, 255, 255), popup_rect, border_width, border_radius=border_radius)
# Afficher les textes
start_y = popup_y + padding_vertical
instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, start_y + instruction_height // 2))
screen.blit(instruction_surface, instruction_rect)
start_y += instruction_height + padding_between
description_rect = description_surface.get_rect(center=(config.screen_width // 2, start_y + description_height // 2))
screen.blit(description_surface, description_rect)
start_y += description_height + padding_between
input_rect = input_surface.get_rect(center=(config.screen_width // 2, start_y + input_height // 2))
screen.blit(input_surface, input_rect)
start_y += input_height + padding_between
# Barre de progression pour le maintien
bar_width = 300
bar_height = 25
bar_x = (config.screen_width - bar_width) // 2
bar_y = start_y + 20
pygame.draw.rect(screen, (50, 50, 50), (bar_x, bar_y, bar_width, bar_height))
progress_width = bar_width * hold_progress
pygame.draw.rect(screen, (0, 255, 0), (bar_x, bar_y, progress_width, bar_height))
pygame.draw.rect(screen, (255, 255, 255), (bar_x, bar_y, bar_width, bar_height), 2)
# Afficher le pourcentage de progression
if hold_progress > 0:
progress_text = f"{int(hold_progress * 100)}%"
progress_surface = config.small_font.render(progress_text, True, (255, 255, 255))
progress_rect = progress_surface.get_rect(center=(config.screen_width // 2, bar_y + bar_height + 30))
screen.blit(progress_surface, progress_rect)

1465
ports/RGSX/display.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,166 @@
import xml.etree.ElementTree as ET
import os
import logging
import pygame #type: ignore
logger = logging.getLogger(__name__)
def parse_es_input_config():
"""Parse le fichier es_input.cfg d'EmulationStation et retourne la configuration des contrôles"""
es_input_path = "/usr/share/emulationstation/es_input.cfg"
if not os.path.exists(es_input_path):
logger.debug(f"Fichier {es_input_path} non trouvé")
return None
try:
tree = ET.parse(es_input_path)
root = tree.getroot()
# Mapping des boutons EmulationStation vers les actions RGSX
# Priorité: D-pad > Joystick pour la navigation
es_to_rgsx_mapping = {
"b": "confirm",
"a": "cancel",
"up": "up",
"down": "down",
"left": "left",
"right": "right",
"pageup": "page_up",
"pagedown": "page_down",
"y": "progress",
"x": "history",
"select": "filter",
"leftshoulder": "delete",
"rightshoulder": "space",
"start": "start"
}
# Priorité pour les entrées directionnelles (hat > axis)
direction_priority = {"up": [], "down": [], "left": [], "right": []}
controls_config = {}
# Chercher la première configuration de joystick
for inputConfig in root.findall("inputConfig"):
if inputConfig.get("type") == "joystick":
logger.debug(f"Configuration trouvée pour: {inputConfig.get('deviceName', 'Manette inconnue')}")
# Première passe: collecter toutes les entrées par action
for input_tag in inputConfig.findall("input"):
es_name = input_tag.get("name")
es_type = input_tag.get("type")
es_id = input_tag.get("id")
es_value = input_tag.get("value", "1")
logger.debug(f"Entrée trouvée: {es_name} = {es_type}:{es_id} (value={es_value})")
if es_name in es_to_rgsx_mapping:
rgsx_action = es_to_rgsx_mapping[es_name]
if es_type == "hat" and rgsx_action in direction_priority:
# Priorité maximale pour le D-pad
hat_mapping = {
"1": (0, 1), # Haut
"2": (1, 0), # Droite
"4": (0, -1), # Bas
"8": (-1, 0) # Gauche
}
if es_value in hat_mapping:
logger.debug(f"D-pad trouvé pour {rgsx_action}: hat {es_id}, value {es_value}")
direction_priority[rgsx_action].append(("hat", {
"type": "hat",
"joy": 0,
"hat": int(es_id),
"value": hat_mapping[es_value]
}))
elif es_type == "axis" and rgsx_action in direction_priority:
# Priorité secondaire pour les axes
direction = 1 if int(es_value) > 0 else -1
logger.debug(f"Axe trouvé pour {rgsx_action}: axis {es_id}, direction {direction}")
direction_priority[rgsx_action].append(("axis", {
"type": "axis",
"joy": 0,
"axis": int(es_id),
"direction": direction
}))
elif es_type == "button":
controls_config[rgsx_action] = {
"type": "button",
"joy": 0,
"button": int(es_id)
}
elif es_type == "key":
controls_config[rgsx_action] = {
"type": "key",
"key": int(es_id)
}
# Deuxième passe: assigner les directions avec priorité
for action, entries in direction_priority.items():
if entries:
logger.debug(f"Priorité pour {action}: {[(e[0], e[1]['type']) for e in entries]}")
# Trier par priorité: hat d'abord, puis axis
entries.sort(key=lambda x: 0 if x[0] == "hat" else 1)
controls_config[action] = entries[0][1]
logger.debug(f"Sélectionné pour {action}: {entries[0][1]['type']}")
logger.debug(f"Configuration finale: {controls_config}")
# Forcer l'utilisation du D-pad pour les directions si disponible, sinon clavier
if any(controls_config.get(action, {}).get("type") == "axis" for action in ["up", "down", "left", "right"]):
# Vérifier si une manette est connectée
pygame.joystick.init()
if pygame.joystick.get_count() > 0:
logger.debug("Remplacement des axes par le D-pad pour la navigation")
controls_config["up"] = {"type": "hat", "joy": 0, "hat": 0, "value": (0, 1)}
controls_config["down"] = {"type": "hat", "joy": 0, "hat": 0, "value": (0, -1)}
controls_config["left"] = {"type": "hat", "joy": 0, "hat": 0, "value": (-1, 0)}
controls_config["right"] = {"type": "hat", "joy": 0, "hat": 0, "value": (1, 0)}
else:
logger.debug("Aucune manette détectée, utilisation du clavier pour toutes les actions")
controls_config["up"] = {"type": "key", "key": pygame.K_UP}
controls_config["down"] = {"type": "key", "key": pygame.K_DOWN}
controls_config["left"] = {"type": "key", "key": pygame.K_LEFT}
controls_config["right"] = {"type": "key", "key": pygame.K_RIGHT}
controls_config["confirm"] = {"type": "key", "key": pygame.K_RETURN}
controls_config["cancel"] = {"type": "key", "key": pygame.K_BACKSPACE}
controls_config["start"] = {"type": "key", "key": pygame.K_p}
controls_config["filter"] = {"type": "key", "key": pygame.K_f}
controls_config["history"] = {"type": "key", "key": pygame.K_h}
controls_config["progress"] = {"type": "key", "key": pygame.K_x}
controls_config["page_up"] = {"type": "key", "key": pygame.K_PAGEUP}
controls_config["page_down"] = {"type": "key", "key": pygame.K_PAGEDOWN}
# Ajouter les actions manquantes avec des valeurs par défaut
default_actions = {
"confirm": {"type": "key", "key": pygame.K_RETURN},
"cancel": {"type": "key", "key": pygame.K_BACKSPACE},
"up": {"type": "key", "key": pygame.K_UP},
"down": {"type": "key", "key": pygame.K_DOWN},
"left": {"type": "key", "key": pygame.K_LEFT},
"right": {"type": "key", "key": pygame.K_RIGHT},
"page_up": {"type": "key", "key": pygame.K_PAGEUP},
"page_down": {"type": "key", "key": pygame.K_PAGEDOWN},
"progress": {"type": "key", "key": pygame.K_x},
"history": {"type": "key", "key": pygame.K_h},
"filter": {"type": "key", "key": pygame.K_f},
"delete": {"type": "key", "key": pygame.K_DELETE},
"space": {"type": "key", "key": pygame.K_SPACE},
"start": {"type": "key", "key": pygame.K_p}
}
for action, default_config in default_actions.items():
if action not in controls_config:
controls_config[action] = default_config
logger.info(f"Configuration importée depuis EmulationStation pour {len(controls_config)} actions")
return controls_config
logger.debug("Aucune configuration de joystick trouvée dans es_input.cfg")
return None
except Exception as e:
logger.error(f"Erreur lors du parsing de es_input.cfg: {str(e)}")
return None

85
ports/RGSX/history.py Normal file
View File

@@ -0,0 +1,85 @@
import json
import os
import logging
import config
from datetime import datetime
logger = logging.getLogger(__name__)
# Chemin par défaut pour history.json
DEFAULT_HISTORY_PATH = os.path.join(config.SAVE_FOLDER, "history.json")
def init_history():
"""Initialise le fichier history.json s'il n'existe pas."""
history_path = getattr(config, 'HISTORY_PATH', DEFAULT_HISTORY_PATH)
# Vérifie si le fichier history.json existe, sinon le crée
if not os.path.exists(history_path):
try:
os.makedirs(os.path.dirname(history_path), exist_ok=True)
with open(history_path, "w", encoding='utf-8') as f:
json.dump([], f) # Initialise avec une liste vide
logger.info(f"Fichier d'historique créé : {history_path}")
except OSError as e:
logger.error(f"Erreur lors de la création du fichier d'historique : {e}")
else:
logger.info(f"Fichier d'historique trouvé : {history_path}")
return history_path
def load_history():
"""Charge l'historique depuis history.json."""
history_path = getattr(config, 'HISTORY_PATH', DEFAULT_HISTORY_PATH)
try:
if not os.path.exists(history_path):
logger.debug(f"Aucun fichier d'historique trouvé à {history_path}")
return []
with open(history_path, "r", encoding='utf-8') as f:
history = json.load(f)
# Valider la structure : liste de dictionnaires avec 'platform', 'game_name', 'status'
for entry in history:
if not all(key in entry for key in ['platform', 'game_name', 'status']):
logger.warning(f"Entrée d'historique invalide : {entry}")
return []
#logger.debug(f"Historique chargé depuis {history_path}, {len(history)} entrées")
return history
except (FileNotFoundError, json.JSONDecodeError) as e:
logger.error(f"Erreur lors de la lecture de {history_path} : {e}")
return []
def save_history(history):
"""Sauvegarde l'historique dans history.json."""
history_path = getattr(config, 'HISTORY_PATH', DEFAULT_HISTORY_PATH)
try:
os.makedirs(os.path.dirname(history_path), exist_ok=True)
with open(history_path, "w", encoding='utf-8') as f:
json.dump(history, f, indent=2, ensure_ascii=False)
logger.debug(f"Historique sauvegardé dans {history_path}")
except Exception as e:
logger.error(f"Erreur lors de l'écriture de {history_path} : {e}")
def add_to_history(platform, game_name, status, url=None, progress=0, message=None, timestamp=None):
"""Ajoute une entrée à l'historique."""
history = load_history()
entry = {
"platform": platform,
"game_name": game_name,
"status": status,
"url": url,
"progress": progress,
"timestamp": timestamp or datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
if message:
entry["message"] = message
history.append(entry)
save_history(history)
logger.info(f"Ajout à l'historique : platform={platform}, game_name={game_name}, status={status}, progress={progress}")
return entry
def clear_history():
"""Vide l'historique."""
history_path = getattr(config, 'HISTORY_PATH', DEFAULT_HISTORY_PATH)
try:
with open(history_path, "w", encoding='utf-8') as f:
json.dump([], f)
logger.info(f"Historique vidé : {history_path}")
except Exception as e:
logger.error(f"Erreur lors du vidage de {history_path} : {e}")

348
ports/RGSX/language.py Normal file
View File

@@ -0,0 +1,348 @@
import os
import json
import pygame #type: ignore
import logging
import config
logger = logging.getLogger(__name__)
# Langue par défaut et variables globales
DEFAULT_LANGUAGE = "fr"
current_language = DEFAULT_LANGUAGE
translations = {}
show_language_selector_on_startup = False
def load_language(lang_code=None):
"""Charge les traductions pour la langue spécifiée ou la langue par défaut."""
global current_language, translations
if lang_code is None:
lang_code = DEFAULT_LANGUAGE
lang_file = os.path.join(config.APP_FOLDER, "languages", f"{lang_code}.json")
try:
if not os.path.exists(lang_file):
if lang_code != DEFAULT_LANGUAGE:
logger.warning(f"Fichier de langue {lang_code} non trouvé, utilisation de la langue par défaut")
return load_language(DEFAULT_LANGUAGE)
else:
logger.error(f"Fichier de langue par défaut {lang_file} non trouvé")
return False
with open(lang_file, 'r', encoding='utf-8') as f:
translations = json.load(f)
current_language = lang_code
#logger.debug(f"Langue {lang_code} chargée avec succès ({len(translations)} traductions)")
return True
except Exception as e:
logger.error(f"Erreur lors du chargement de la langue {lang_code}: {str(e)}")
if lang_code != DEFAULT_LANGUAGE:
logger.warning(f"Tentative de chargement de la langue par défaut")
return load_language(DEFAULT_LANGUAGE)
return False
def get_text(key, default=None):
"""Récupère la traduction correspondant à la clé."""
if not translations:
load_language()
if key in translations:
return translations[key]
# Si la clé n'existe pas, retourner la valeur par défaut ou la clé elle-même
if default is not None:
return default
logger.warning(f"Clé de traduction '{key}' non trouvée dans la langue {current_language}")
return key
def get_available_languages():
"""Récupère la liste des langues disponibles."""
languages_dir = os.path.join(config.APP_FOLDER, "languages")
if not os.path.exists(languages_dir):
logger.warning(f"Dossier des langues {languages_dir} non trouvé")
return []
languages = []
for file in os.listdir(languages_dir):
if file.endswith(".json"):
lang_code = os.path.splitext(file)[0]
languages.append(lang_code)
return languages
def set_language(lang_code):
"""Change la langue courante et sauvegarde la préférence."""
if load_language(lang_code):
config.current_language = lang_code
save_language_preference(lang_code)
return True
return False
def save_language_preference(lang_code):
"""Sauvegarde la préférence de langue dans un fichier."""
try:
# S'assurer que le dossier existe
os.makedirs(os.path.dirname(config.LANGUAGE_CONFIG_PATH), exist_ok=True)
# Sauvegarder la préférence
with open(config.LANGUAGE_CONFIG_PATH, 'w', encoding='utf-8') as f:
json.dump({"language": lang_code}, f)
logger.debug(f"Préférence de langue sauvegardée: {lang_code}")
return True
except Exception as e:
logger.error(f"Erreur lors de la sauvegarde de la préférence de langue: {str(e)}")
return False
def load_language_preference():
"""Charge la préférence de langue depuis le fichier."""
global show_language_selector_on_startup
try:
if not os.path.exists(config.LANGUAGE_CONFIG_PATH):
logger.info("Aucune préférence de langue trouvée, utilisation du français par défaut")
# Créer le fichier avec le français par défaut
save_language_preference(DEFAULT_LANGUAGE)
return DEFAULT_LANGUAGE
with open(config.LANGUAGE_CONFIG_PATH, 'r', encoding='utf-8') as f:
data = json.load(f)
lang_code = data.get("language", DEFAULT_LANGUAGE)
return lang_code
except json.JSONDecodeError:
logger.warning("Fichier de préférence de langue corrompu, utilisation du français par défaut")
# Recréer le fichier avec le français par défaut
save_language_preference(DEFAULT_LANGUAGE)
return DEFAULT_LANGUAGE
except Exception as e:
logger.error(f"Erreur lors du chargement de la préférence de langue: {str(e)}")
# Recréer le fichier avec le français par défaut
save_language_preference(DEFAULT_LANGUAGE)
return DEFAULT_LANGUAGE
def get_language_name(lang_code):
"""Retourne le nom de la langue à partir du code."""
language_names = {
"fr": "Français",
"en": "English",
"es": "Español",
"de": "Deutsch",
"it": "Italiano",
"pt": "Português",
"ja": "日本語",
"zh": "中文",
"ru": "Русский"
}
return language_names.get(lang_code, lang_code)
def draw_language_selector(screen, selected_language_index):
"""Affiche le sélecteur de langue."""
from display import THEME_COLORS, OVERLAY
# Obtenir les langues disponibles
available_languages = get_available_languages()
if not available_languages:
logger.error("Aucune langue disponible")
return
# Afficher l'overlay
screen.blit(OVERLAY, (0, 0))
# Titre
title_text = _("language_select_title")
title_surface = config.font.render(title_text, True, THEME_COLORS["text"])
title_rect = title_surface.get_rect(center=(config.screen_width // 2, config.screen_height // 4))
# Fond du titre
title_bg_rect = title_rect.inflate(40, 20)
pygame.draw.rect(screen, THEME_COLORS["button_idle"], title_bg_rect, border_radius=10)
pygame.draw.rect(screen, THEME_COLORS["border"], title_bg_rect, 2, border_radius=10)
screen.blit(title_surface, title_rect)
# Options de langue
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
for i, lang_code in enumerate(available_languages):
# Obtenir le nom de la langue
lang_name = get_language_name(lang_code)
# Position du bouton
button_x = (config.screen_width - button_width) // 2
button_y = start_y + i * (button_height + button_spacing)
# Dessiner le bouton
button_color = THEME_COLORS["button_hover"] if i == selected_language_index else THEME_COLORS["button_idle"]
pygame.draw.rect(screen, button_color, (button_x, button_y, button_width, button_height), border_radius=10)
pygame.draw.rect(screen, THEME_COLORS["border"], (button_x, button_y, button_width, button_height), 2, border_radius=10)
# Texte du bouton
text_surface = config.font.render(lang_name, True, THEME_COLORS["text"])
text_rect = text_surface.get_rect(center=(button_x + button_width // 2, button_y + button_height // 2))
screen.blit(text_surface, text_rect)
# Instructions
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 handle_language_menu_events(event, screen):
"""Gère les événements du menu de sélection de langue avec support clavier et manette."""
available_languages = get_available_languages()
if not available_languages:
logger.error("Aucune langue disponible")
config.menu_state = "platform" # Toujours revenir à platform en cas d'erreur
config.needs_redraw = True
return
# Navigation avec les touches du clavier
if event.type == pygame.KEYDOWN:
# Navigation vers le haut
if event.key == pygame.K_UP:
config.selected_language_index = (config.selected_language_index - 1) % len(available_languages)
config.needs_redraw = True
logger.debug(f"Navigation vers le haut dans le sélecteur de langue: {config.selected_language_index}")
# Navigation vers le bas
elif event.key == pygame.K_DOWN:
config.selected_language_index = (config.selected_language_index + 1) % len(available_languages)
config.needs_redraw = True
logger.debug(f"Navigation vers le bas dans le sélecteur de langue: {config.selected_language_index}")
# Sélection de la langue
elif event.key == pygame.K_RETURN:
lang_code = available_languages[config.selected_language_index]
if set_language(lang_code):
logger.info(f"Langue changée pour {lang_code}")
config.current_language = lang_code
# Déterminer l'état suivant en fonction du contexte
if config.previous_menu_state is None:
# Premier démarrage - passer à l'état loading pour charger les plateformes
config.menu_state = "loading"
logger.debug("Premier démarrage: passage à l'état loading après sélection de la langue")
elif config.previous_menu_state == "pause_menu":
# Si on vient du menu pause, retourner au menu pause avec un message
config.menu_state = "restart_popup"
config.popup_message = _("language_changed").format(lang_code)
config.popup_timer = 2000 # 2 secondes
config.previous_menu_state = "platform" # Pour revenir à l'écran principal après le popup
logger.debug("Message de confirmation de changement de langue affiché, retour au menu pause")
else:
# Autre cas, retourner à l'état précédent avec un message
config.menu_state = "platform" # Toujours revenir à platform pour éviter les problèmes
logger.debug(f"Retour à l'écran principal après sélection de la langue")
else:
# Retour au menu pause en cas d'erreur
config.menu_state = "platform" # Toujours revenir à platform en cas d'erreur
config.needs_redraw = True
logger.debug(f"Sélection de la langue: {lang_code}")
# Annulation (seulement si on n'est pas au démarrage)
elif event.key == pygame.K_ESCAPE and config.previous_menu_state is not None:
config.menu_state = "pause_menu"
config.needs_redraw = True
logger.debug("Annulation de la sélection de langue, retour au menu pause")
# Support de la manette
elif event.type == pygame.JOYBUTTONDOWN:
# Sélection avec le bouton A (généralement 0)
if event.button == 0: # Bouton A
lang_code = available_languages[config.selected_language_index]
if set_language(lang_code):
logger.info(f"Langue changée pour {lang_code} (manette)")
config.current_language = lang_code
# Déterminer l'état suivant en fonction du contexte
if config.previous_menu_state is None:
# Premier démarrage - passer à l'état loading pour charger les plateformes
config.menu_state = "loading"
logger.debug("Premier démarrage: passage à l'état loading après sélection de la langue (manette)")
else:
config.menu_state = "platform"
else:
config.menu_state = "platform"
config.needs_redraw = True
# Annulation avec le bouton B (généralement 1)
elif event.button == 1 and config.previous_menu_state is not None: # Bouton B
config.menu_state = "pause_menu"
config.needs_redraw = True
logger.debug("Annulation de la sélection de langue (manette), retour au menu pause")
# Navigation avec le D-pad
elif event.type == pygame.JOYHATMOTION:
if event.value == (0, 1): # Haut
config.selected_language_index = (config.selected_language_index - 1) % len(available_languages)
config.needs_redraw = True
logger.debug(f"Navigation vers le haut dans le sélecteur de langue (D-pad): {config.selected_language_index}")
elif event.value == (0, -1): # Bas
config.selected_language_index = (config.selected_language_index + 1) % len(available_languages)
config.needs_redraw = True
logger.debug(f"Navigation vers le bas dans le sélecteur de langue (D-pad): {config.selected_language_index}")
# Navigation avec les joysticks analogiques
elif event.type == pygame.JOYAXISMOTION:
# Joystick gauche vertical (généralement axe 1)
if event.axis == 1 and abs(event.value) > 0.5:
if event.value < -0.5: # Haut
config.selected_language_index = (config.selected_language_index - 1) % len(available_languages)
config.needs_redraw = True
logger.debug(f"Navigation vers le haut dans le sélecteur de langue (joystick): {config.selected_language_index}")
elif event.value > 0.5: # Bas
config.selected_language_index = (config.selected_language_index + 1) % len(available_languages)
config.needs_redraw = True
logger.debug(f"Navigation vers le bas dans le sélecteur de langue (joystick): {config.selected_language_index}")
def update_valid_states():
"""Ajoute l'état language_select à la liste des états valides."""
from controls import VALID_STATES
if "language_select" not in VALID_STATES:
VALID_STATES.append("language_select")
logger.debug("État language_select ajouté aux états valides")
def initialize_language():
"""Initialise la langue au démarrage de l'application."""
global show_language_selector_on_startup
# Vérifier si le fichier de préférence de langue existe
language_file_exists = os.path.exists(config.LANGUAGE_CONFIG_PATH)
# Si le fichier n'existe pas, créer un fichier avec le français par défaut
if not language_file_exists:
logger.info("Aucun fichier de préférence de langue trouvé, création avec le français par défaut")
save_language_preference(DEFAULT_LANGUAGE)
show_language_selector_on_startup = False # Ne pas afficher le sélecteur au démarrage
else:
# Le fichier existe, charger normalement
show_language_selector_on_startup = False # Ne jamais afficher le sélecteur au démarrage
# Charger la préférence de langue
lang_code = load_language_preference()
# Charger la langue par défaut ou préférée
if load_language(lang_code):
logger.info(f"Langue chargée au démarrage: {lang_code}")
else:
logger.warning(f"Impossible de charger la langue {lang_code}, utilisation de la langue par défaut")
load_language(DEFAULT_LANGUAGE)
return True
# Alias pour faciliter l'utilisation
_ = get_text

View File

@@ -0,0 +1,176 @@
{
"welcome_message": "Willkommen bei RGSX",
"disclaimer_line1": "Es ist gefährlich, allein zu gehen, nimm alles, was du brauchst!",
"disclaimer_line2": "Aber lade nur Spiele herunter,",
"disclaimer_line3": "von denen du die Originale besitzt!",
"disclaimer_line4": "RGSX ist nicht verantwortlich für heruntergeladene Inhalte,",
"disclaimer_line5": "und hostet keine ROMs.",
"loading_test_connection": "Verbindung wird getestet...",
"loading_update_check": "Prüfung auf Updates läuft... Bitte warten...",
"loading_download_data": "Spiele und Bilder werden heruntergeladen...",
"loading_download_initial": "Download des initialen Datenordners...",
"loading_extract_initial": "Extrahieren des initialen Datenordners...",
"loading_systems": "Systeme werden geladen...",
"loading_progress": "Fortschritt: {0}%",
"error_no_internet": "Keine Internetverbindung. Überprüfe dein Netzwerk.",
"error_load_sources": "Fehler beim Laden von sources.json",
"error_controls_mapping": "Fehler beim Zuordnen der Steuerung",
"error_download_data": "Fehler beim Herunterladen/Extrahieren des Datenordners: {0}",
"error_api_key": "Achtung, du musst deinen API-Schlüssel (nur Premium) in der Datei {0} eingeben",
"error_api_key_extended": "Achtung, du musst deinen API-Schlüssel (nur Premium) in der Datei /userdata/saves/ports/rgsx/1fichierAPI.txt einfügen. Öffne die Datei in einem Texteditor und füge den API-Schlüssel ein",
"error_invalid_download_data": "Ungültige Downloaddaten",
"error_delete_sources": "Fehler beim Löschen der Datei sources.json oder Ordner",
"error_extension": "Nicht unterstützte Erweiterung oder Downloadfehler",
"error_no_download": "Keine Downloads ausstehend.",
"platform_no_platform": "Keine Plattform",
"platform_page": "Seite {0}/{1}",
"game_no_games": "Keine Spiele verfügbar",
"game_count": "{0} ({1} Spiele)",
"game_filter": "Aktiver Filter: {0}",
"game_search": "Filtern: {0}",
"history_title": "Downloads ({0})",
"history_empty": "Keine Downloads im Verlauf",
"history_column_system": "System",
"history_column_game": "Spielname",
"history_column_status": "Status",
"history_status_downloading": "Download: {0}%",
"history_status_extracting": "Extrahieren: {0}%",
"history_status_completed": "Abgeschlossen",
"history_status_error": "Fehler: {0}",
"history_status_canceled": "Abgebrochen",
"download_status": "{0}: {1}",
"download_progress": "{0}% {1} MB / {2} MB",
"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?",
"confirm_exit": "Anwendung beenden?",
"confirm_clear_history": "Verlauf löschen?",
"confirm_redownload_cache": "Spiele-Cache erneut herunterladen?",
"popup_redownload_success": "Download der Spiele abgeschlossen.\nBitte starte die Anwendung neu, um die Änderungen zu sehen.",
"popup_no_cache": "Kein Cache gefunden.\nBitte starte die Anwendung neu, um die Spiele zu laden.",
"popup_countdown": "Diese Nachricht schließt in {0} Sekunde{1}",
"language_select_title": "Sprachauswahl",
"language_select_instruction": "Verwende die Pfeiltasten zum Navigieren und Enter zum Auswählen",
"language_changed": "Sprache geändert zu {0}",
"menu_controls": "Steuerung",
"menu_remap_controls": "Steuerung neu zuordnen",
"menu_history": "Verlauf",
"menu_language": "Sprache",
"menu_accessibility": "Barrierefreiheit",
"menu_redownload_cache": "Spiele-Cache erneut herunterladen",
"menu_quit": "Beenden",
"button_yes": "Ja",
"button_no": "Nein",
"button_validate": "Bestätigen",
"controls_hold_message": "3 Sekunden halten für: '{0}'",
"controls_skip_message": "Drücke Esc, um zu überspringen (nur PC)",
"controls_waiting": "Warten...",
"controls_hold": "3 Sekunden halten",
"controls_action_confirm": "Bestätigen",
"controls_action_cancel": "Abbrechen",
"controls_action_up": "Hoch",
"controls_action_down": "Runter",
"controls_action_left": "Links",
"controls_action_right": "Rechts",
"controls_action_page_up": "Vorherige Seite",
"controls_action_page_down": "Nächste Seite",
"controls_action_progress": "Fortschritt",
"controls_action_history": "Verlauf",
"controls_action_filter": "Filtern",
"controls_action_delete": "Löschen",
"controls_action_space": "Leerzeichen",
"controls_action_start": "Hilfe / Einstellungen",
"controls_desc_confirm": "Bestätigen (z.B.: A, Enter)",
"controls_desc_cancel": "Abbrechen/Zurück (z.B.: B, Rücktaste)",
"controls_desc_up": "Nach oben navigieren",
"controls_desc_down": "Nach unten navigieren",
"controls_desc_left": "Nach links navigieren",
"controls_desc_right": "Nach rechts navigieren",
"controls_desc_page_up": "Vorherige Seite/Schnelles Scrollen nach oben (z.B.: BildAuf, LB)",
"controls_desc_page_down": "Nächste Seite/Schnelles Scrollen nach unten (z.B.: BildAb, RB)",
"controls_desc_progress": "Fortschritt anzeigen (z.B.: X)",
"controls_desc_history": "Verlauf öffnen (z.B.: H, Y)",
"controls_desc_filter": "Filter öffnen (z.B.: F, Select)",
"controls_desc_delete": "Zeichen löschen (z.B.: LT, Entf)",
"controls_desc_space": "Leerzeichen hinzufügen (z.B.: RT, Leertaste)",
"controls_desc_start": "Pausenmenü öffnen (z.B.: Start, AltGr)",
"footer_version": "RGSX v{0} - {1}: Optionen - {2}: Verlauf - {3}: Filtern",
"action_retry": "Wiederholen",
"action_quit": "Beenden",
"action_select": "Auswählen",
"action_history": "Verlauf",
"action_progress": "Fortschritt",
"action_download": "Herunterladen",
"action_filter": "Filtern",
"action_cancel": "Abbrechen",
"action_back": "Zurück",
"action_navigate": "Navigieren",
"action_page": "Seite",
"action_cancel_download": "Download abbrechen",
"action_background": "Hintergrund",
"action_confirm": "Bestätigen",
"action_redownload": "Erneut herunterladen",
"action_clear_history": "Verlauf löschen",
"network_checking_updates": "Updates werden geprüft...",
"network_update_available": "Update verfügbar: {0}",
"network_extracting_update": "Update wird extrahiert...",
"network_update_completed": "Update abgeschlossen",
"network_update_success": "Update auf {0} erfolgreich abgeschlossen. Bitte starte die Anwendung neu.",
"network_update_success_message": "Update erfolgreich abgeschlossen",
"network_no_update_available": "Keine Updates verfügbar",
"network_update_error": "Fehler während des Updates: {0}",
"network_download_extract_ok": "Download und Extraktion von {0} erfolgreich",
"network_check_update_error": "Fehler bei der Überprüfung von Updates: {0}",
"network_extraction_failed": "Fehler beim Extrahieren des Updates: {0}",
"network_extraction_partial": "Extraktion erfolgreich, aber einige Dateien wurden aufgrund von Fehlern übersprungen: {0}",
"network_extraction_success": "Extraktion erfolgreich",
"network_zip_extraction_error": "Fehler beim Extrahieren des ZIP {0}: {1}",
"network_permission_error": "Keine Schreibberechtigung für {0}",
"network_file_not_found": "Die Datei {0} existiert nicht",
"network_cannot_get_filename": "Dateiname konnte nicht abgerufen werden",
"network_cannot_get_download_url": "Download-URL konnte nicht abgerufen werden",
"download_initializing": "Initialisierung läuft...",
"accessibility_font_size": "Schriftgröße: {0}",
"confirm_cancel_download": "Laufenden Download abbrechen?",
"controls_help_title": "Hilfe zu Steuerung",
"controls_category_navigation": "Navigation",
"controls_category_main_actions": "Hauptaktionen",
"controls_category_downloads": "Downloads",
"controls_category_search": "Suche",
"controls_navigation": "Navigation",
"controls_pages": "Seiten",
"controls_confirm_select": "Bestätigen/Auswählen",
"controls_cancel_back": "Abbrechen/Zurück",
"controls_history": "Verlauf",
"controls_clear_history": "Verlauf löschen",
"controls_filter_search": "Filtern/Suchen",
"network_download_failed": "Download nach {0} Versuchen fehlgeschlagen",
"network_api_error": "Fehler bei der API-Anfrage, der Schlüssel könnte falsch sein: {0}",
"network_download_error": "Downloadfehler {0}: {1}",
"network_download_ok": "Download erfolgreich: {0}",
"utils_extracted": "Extrahiert: {0}",
"utils_corrupt_zip": "Beschädigtes ZIP-Archiv: {0}",
"utils_permission_denied": "Berechtigung während der Extraktion verweigert: {0}",
"utils_extraction_failed": "Extraktion fehlgeschlagen: {0}",
"utils_unrar_unavailable": "Befehl unrar nicht verfügbar",
"utils_rar_list_failed": "Fehler beim Auflisten der RAR-Dateien: {0}"
}

View File

@@ -0,0 +1,176 @@
{
"welcome_message": "Welcome to RGSX",
"disclaimer_line1": "It's dangerous to go alone, take all you need!",
"disclaimer_line2": "But only download games",
"disclaimer_line3": "that you already own!",
"disclaimer_line4": "RGSX is not responsible for downloaded content,",
"disclaimer_line5": "and does not host ROMs.",
"loading_test_connection": "Testing connection...",
"loading_update_check": "Checking for updates... Please wait...",
"loading_download_data": "Downloading games and images...",
"loading_download_initial": "Downloading initial Data Folder...",
"loading_extract_initial": "Extracting initial Data Folder...",
"loading_systems": "Loading systems...",
"loading_progress": "Progress: {0}%",
"error_no_internet": "No internet connection. Please check your network.",
"error_load_sources": "Failed to load sources.json",
"error_controls_mapping": "Failed to map controls",
"error_download_data": "Failed to download/extract Data Folder: {0}",
"error_api_key": "Please enter your API key (premium only) in the file {0}",
"error_api_key_extended": "Please enter your API key (premium only) in the file /userdata/saves/ports/rgsx/1fichierAPI.txt by opening it in a text editor and pasting your API key",
"error_invalid_download_data": "Invalid download data",
"error_delete_sources": "Error deleting sources.json file or folders",
"error_extension": "Unsupported extension or download error",
"error_no_download": "No pending download.",
"platform_no_platform": "No platform",
"platform_page": "Page {0}/{1}",
"game_no_games": "No games available",
"game_count": "{0} ({1} games)",
"game_filter": "Active filter: {0}",
"game_search": "Filter: {0}",
"history_title": "Downloads ({0})",
"history_empty": "No downloads in history",
"history_column_system": "System",
"history_column_game": "Game name",
"history_column_status": "Status",
"history_status_downloading": "Downloading: {0}%",
"history_status_extracting": "Extracting: {0}%",
"history_status_completed": "Completed",
"history_status_error": "Error: {0}",
"history_status_canceled": "Canceled",
"download_status": "{0}: {1}",
"download_progress": "{0}% {1} MB / {2} MB",
"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?",
"confirm_exit": "Exit application?",
"confirm_clear_history": "Clear history?",
"confirm_redownload_cache": "Redownload games cache?",
"popup_redownload_success": "Games redownloaded successfully.\nPlease restart the application to see the changes.",
"popup_no_cache": "No cache found.\nPlease restart the application to load games.",
"popup_countdown": "This message will close in {0} second{1}",
"language_select_title": "Language Selection",
"language_select_instruction": "Use arrow keys to navigate and Enter to select",
"language_changed": "Language changed to {0}",
"menu_controls": "Controls",
"menu_remap_controls": "Remap controls",
"menu_history": "History",
"menu_language": "Language",
"menu_accessibility": "Accessibility",
"menu_redownload_cache": "Redownload Games cache",
"menu_quit": "Quit",
"button_yes": "Yes",
"button_no": "No",
"button_validate": "Validate",
"controls_hold_message": "Hold for 3s for: '{0}'",
"controls_skip_message": "Press Esc to skip (PC only)",
"controls_waiting": "Waiting...",
"controls_hold": "Hold 3s",
"controls_action_confirm": "Confirm",
"controls_action_cancel": "Cancel",
"controls_action_up": "Up",
"controls_action_down": "Down",
"controls_action_left": "Left",
"controls_action_right": "Right",
"controls_action_page_up": "Previous Page",
"controls_action_page_down": "Next Page",
"controls_action_progress": "Progress",
"controls_action_history": "History",
"controls_action_filter": "Filter",
"controls_action_delete": "Delete",
"controls_action_space": "Space",
"controls_action_start": "Help / Settings",
"controls_desc_confirm": "Validate (e.g. A, Enter)",
"controls_desc_cancel": "Cancel/Back (e.g. B, Backspace)",
"controls_desc_up": "Navigate up",
"controls_desc_down": "Navigate down",
"controls_desc_left": "Navigate left",
"controls_desc_right": "Navigate right",
"controls_desc_page_up": "Previous page/Fast scroll up (e.g. PageUp, LB)",
"controls_desc_page_down": "Next page/Fast scroll down (e.g. PageDown, RB)",
"controls_desc_progress": "View progress (e.g. X)",
"controls_desc_history": "Open history (e.g. H, Y)",
"controls_desc_filter": "Open filter (e.g. F, Select)",
"controls_desc_delete": "Delete character (e.g. LT, Delete)",
"controls_desc_space": "Add space (e.g. RT, Space)",
"controls_desc_start": "Open pause menu (e.g. Start, AltGr)",
"footer_version": "RGSX v{0} - {1}: Options - {2}: History - {3}: Filter",
"action_retry": "Retry",
"action_quit": "Quit",
"action_select": "Select",
"action_history": "History",
"action_progress": "Progress",
"action_download": "Download",
"action_filter": "Filter",
"action_cancel": "Cancel",
"action_back": "Back",
"action_navigate": "Navigate",
"action_page": "Page",
"action_cancel_download": "Cancel download",
"action_background": "Background",
"action_confirm": "Confirm",
"action_redownload": "Redownload",
"action_clear_history": "Clear history",
"network_checking_updates": "Checking for updates...",
"network_update_available": "Update available: {0}",
"network_extracting_update": "Extracting update...",
"network_update_completed": "Update completed",
"network_update_success": "Update to {0} completed successfully. Please restart the application.",
"network_update_success_message": "Update completed successfully",
"network_no_update_available": "No update available",
"network_update_error": "Error during update: {0}",
"network_check_update_error": "Error checking for updates: {0}",
"network_extraction_failed": "Failed to extract update: {0}",
"network_extraction_partial": "Extraction successful, but some files were skipped due to errors: {0}",
"network_extraction_success": "Extraction successful",
"network_download_extract_ok": "Download and extraction successful of {0}",
"network_zip_extraction_error": "Error extracting ZIP {0}: {1}",
"network_permission_error": "No write permission in {0}",
"network_file_not_found": "File {0} does not exist",
"network_cannot_get_filename": "Cannot retrieve filename",
"network_cannot_get_download_url": "Cannot retrieve download URL",
"network_download_failed": "Download failed after {0} attempts",
"network_api_error": "API request error, the key may be incorrect: {0}",
"network_download_error": "Download error {0}: {1}",
"network_download_ok": "Download OK: {0}",
"utils_extracted": "Extracted: {0}",
"utils_corrupt_zip": "Corrupted ZIP archive: {0}",
"utils_permission_denied": "Permission denied during extraction: {0}",
"utils_extraction_failed": "Extraction failed: {0}",
"utils_unrar_unavailable": "Unrar command not available",
"utils_rar_list_failed": "Failed to list RAR files: {0}",
"download_initializing": "Initializing...",
"accessibility_font_size": "Font size: {0}",
"confirm_cancel_download": "Cancel current download?",
"controls_help_title": "Controls Help",
"controls_category_navigation": "Navigation",
"controls_category_main_actions": "Main Actions",
"controls_category_downloads": "Downloads",
"controls_category_search": "Search",
"controls_navigation": "Navigation",
"controls_pages": "Pages",
"controls_confirm_select": "Confirm/Select",
"controls_cancel_back": "Cancel/Back",
"controls_history": "History",
"controls_clear_history": "Clear History",
"controls_filter_search": "Filter/Search"
}

View File

@@ -0,0 +1,176 @@
{
"welcome_message": "Bienvenido a RGSX",
"disclaimer_line1": "¡Es peligroso ir solo, toma todo lo que necesites!",
"disclaimer_line2": "Pero solo descarga juegos",
"disclaimer_line3": "de los que poseas los originales.",
"disclaimer_line4": "RGSX no es responsable del contenido descargado,",
"disclaimer_line5": "y no aloja ROMs.",
"loading_test_connection": "Probando conexión...",
"loading_update_check": "Verificando actualización en curso... Por favor, espera...",
"loading_download_data": "Descargando juegos e imágenes...",
"loading_download_initial": "Descargando la carpeta de datos inicial...",
"loading_extract_initial": "Extrayendo la carpeta de datos inicial...",
"loading_systems": "Cargando sistemas...",
"loading_progress": "Progreso: {0}%",
"error_no_internet": "Sin conexión a Internet. Verifica tu red.",
"error_load_sources": "Error al cargar sources.json",
"error_controls_mapping": "Error al mapear los controles",
"error_download_data": "Error al descargar/extraer la carpeta de datos: {0}",
"error_api_key": "Atención, debes ingresar tu clave API (solo premium) en el archivo {0}",
"error_api_key_extended": "Atención, debes ingresar tu clave API (solo premium) en el archivo /userdata/saves/ports/rgsx/1fichierAPI.txt, abrirlo en un editor de texto y pegar la clave API",
"error_invalid_download_data": "Datos de descarga no válidos",
"error_delete_sources": "Error al eliminar el archivo sources.json o carpetas",
"error_extension": "Extensión no soportada o error de descarga",
"error_no_download": "No hay descargas pendientes.",
"platform_no_platform": "Ninguna plataforma",
"platform_page": "Página {0}/{1}",
"game_no_games": "No hay juegos disponibles",
"game_count": "{0} ({1} juegos)",
"game_filter": "Filtro activo: {0}",
"game_search": "Filtrar: {0}",
"history_title": "Descargas ({0})",
"history_empty": "No hay descargas en el historial",
"history_column_system": "Sistema",
"history_column_game": "Nombre del juego",
"history_column_status": "Estado",
"history_status_downloading": "Descargando: {0}%",
"history_status_extracting": "Extrayendo: {0}%",
"history_status_completed": "Completado",
"history_status_error": "Error: {0}",
"history_status_canceled": "Cancelado",
"download_status": "{0}: {1}",
"download_progress": "{0}% {1} MB / {2} MB",
"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?",
"confirm_exit": "¿Salir de la aplicación?",
"confirm_clear_history": "¿Vaciar el historial?",
"confirm_redownload_cache": "¿Volver a descargar la caché de juegos?",
"popup_redownload_success": "Descarga de juegos completada.\nPor favor, reinicia la aplicación para ver los cambios.",
"popup_no_cache": "No se encontró caché.\nPor favor, reinicia la aplicación para cargar los juegos.",
"popup_countdown": "Este mensaje se cerrará en {0} segundo{1}",
"language_select_title": "Selección de idioma",
"language_select_instruction": "Usa las flechas para navegar y Enter para seleccionar",
"language_changed": "Idioma cambiado a {0}",
"menu_controls": "Controles",
"menu_remap_controls": "Remapear controles",
"menu_history": "Historial",
"menu_language": "Idioma",
"menu_accessibility": "Accesibilidad",
"menu_redownload_cache": "Volver a descargar la caché de juegos",
"menu_quit": "Salir",
"button_yes": "Sí",
"button_no": "No",
"button_validate": "Validar",
"controls_hold_message": "Mantén presionado durante 3s para: '{0}'",
"controls_skip_message": "Presiona Esc para omitir (solo PC)",
"controls_waiting": "Esperando...",
"controls_hold": "Mantener 3s",
"controls_action_confirm": "Confirmar",
"controls_action_cancel": "Cancelar",
"controls_action_up": "Arriba",
"controls_action_down": "Abajo",
"controls_action_left": "Izquierda",
"controls_action_right": "Derecha",
"controls_action_page_up": "Página anterior",
"controls_action_page_down": "Página siguiente",
"controls_action_progress": "Progreso",
"controls_action_history": "Historial",
"controls_action_filter": "Filtrar",
"controls_action_delete": "Eliminar",
"controls_action_space": "Espacio",
"controls_action_start": "Ayuda / Configuración",
"controls_desc_confirm": "Validar (ej: A, Enter)",
"controls_desc_cancel": "Cancelar/Volver (ej: B, Retroceso)",
"controls_desc_up": "Navegar hacia arriba",
"controls_desc_down": "Navegar hacia abajo",
"controls_desc_left": "Navegar a izquierda",
"controls_desc_right": "Navegar a derecha",
"controls_desc_page_up": "Página anterior/Desplazamiento rápido arriba (ej: RePág, LB)",
"controls_desc_page_down": "Página siguiente/Desplazamiento rápido abajo (ej: AvPág, RB)",
"controls_desc_progress": "Ver progreso (ej: X)",
"controls_desc_history": "Abrir historial (ej: H, Y)",
"controls_desc_filter": "Abrir filtro (ej: F, Select)",
"controls_desc_delete": "Eliminar carácter (ej: LT, Supr)",
"controls_desc_space": "Añadir espacio (ej: RT, Espacio)",
"controls_desc_start": "Abrir el menú de pausa (ej: Start, AltGr)",
"footer_version": "RGSX v{0} - {1} : Opciones - {2} : Historial - {3} : Filtrar",
"action_retry": "Reintentar",
"action_quit": "Salir",
"action_select": "Seleccionar",
"action_history": "Historial",
"action_progress": "Progreso",
"action_download": "Descargar",
"action_filter": "Filtrar",
"action_cancel": "Cancelar",
"action_back": "Volver",
"action_navigate": "Navegar",
"action_page": "Página",
"action_cancel_download": "Cancelar la descarga",
"action_background": "Fondo",
"action_confirm": "Confirmar",
"action_redownload": "Volver a descargar",
"action_clear_history": "Vaciar el historial",
"network_checking_updates": "Verificando actualizaciones...",
"network_update_available": "Actualización disponible: {0}",
"network_extracting_update": "Extrayendo la actualización...",
"network_update_completed": "Actualización completada",
"network_update_success": "Actualización a {0} completada con éxito. Por favor, reinicia la aplicación.",
"network_update_success_message": "Actualización completada con éxito",
"network_no_update_available": "No hay actualizaciones disponibles",
"network_update_error": "Error durante la actualización: {0}",
"network_download_extract_ok": "Descarga y extracción exitosa de {0}",
"network_check_update_error": "Error al verificar actualizaciones: {0}",
"network_extraction_failed": "Error al extraer la actualización: {0}",
"network_extraction_partial": "Extracción exitosa, pero algunos archivos fueron omitidos debido a errores: {0}",
"network_extraction_success": "Extracción exitosa",
"network_zip_extraction_error": "Error al extraer el ZIP {0}: {1}",
"network_permission_error": "Sin permiso de escritura en {0}",
"network_file_not_found": "El archivo {0} no existe",
"network_cannot_get_filename": "No se pudo obtener el nombre del archivo",
"network_cannot_get_download_url": "No se pudo obtener la URL de descarga",
"download_initializing": "Inicializando...",
"accessibility_font_size": "Tamaño de fuente: {0}",
"confirm_cancel_download": "¿Cancelar la descarga en curso?",
"controls_help_title": "Ayuda de controles",
"controls_category_navigation": "Navegación",
"controls_category_main_actions": "Acciones principales",
"controls_category_downloads": "Descargas",
"controls_category_search": "Búsqueda",
"controls_navigation": "Navegación",
"controls_pages": "Páginas",
"controls_confirm_select": "Confirmar/Seleccionar",
"controls_cancel_back": "Cancelar/Volver",
"controls_history": "Historial",
"controls_clear_history": "Vaciar historial",
"controls_filter_search": "Filtrar/Buscar",
"network_download_failed": "Error en la descarga tras {0} intentos",
"network_api_error": "Error en la solicitud de API, la clave puede ser incorrecta: {0}",
"network_download_error": "Error en la descarga {0}: {1}",
"network_download_ok": "Descarga exitosa: {0}",
"utils_extracted": "Extraído: {0}",
"utils_corrupt_zip": "Archivo ZIP corrupto: {0}",
"utils_permission_denied": "Permiso denegado durante la extracción: {0}",
"utils_extraction_failed": "Error en la extracción: {0}",
"utils_unrar_unavailable": "Comando unrar no disponible",
"utils_rar_list_failed": "Error al listar los archivos RAR: {0}"
}

View File

@@ -0,0 +1,181 @@
{
"welcome_message": "Bienvenue dans RGSX",
"disclaimer_line1": "It's dangerous to go alone, take all you need!",
"disclaimer_line2": "Mais ne téléchargez que des jeux",
"disclaimer_line3": "dont vous possédez les originaux !",
"disclaimer_line4": "RGSX n'est pas responsable des contenus téléchargés,",
"disclaimer_line5": "et n'heberge pas de ROMs.",
"loading_test_connection": "Test de connexion...",
"loading_update_check": "Verification Mise à jour en cours... Patientez...",
"loading_download_data": "Téléchargement des jeux et images ...",
"loading_download_initial": "Téléchargement du Dossier Data initial...",
"loading_extract_initial": "Extraction du Dossier Data initial...",
"loading_systems": "Chargement des systèmes...",
"loading_progress": "Progression : {0}%",
"error_no_internet": "Pas de connexion Internet. Vérifiez votre réseau.",
"error_load_sources": "Échec du chargement de sources.json",
"error_controls_mapping": "Échec du mappage des contrôles",
"error_download_data": "Échec du téléchargement/extraction du Dossier Data : {0}",
"error_api_key": "Attention il faut renseigner sa clé API (premium only) dans le fichier {0}",
"error_api_key_extended": "Attention il faut renseigner sa clé API (premium only) dans le fichier /userdata/saves/ports/rgsx/1fichierAPI.txt à ouvrir dans un éditeur de texte et coller la clé API",
"error_invalid_download_data": "Données de téléchargement invalides",
"error_delete_sources": "Erreur lors de la suppression du fichier sources.json ou dossiers",
"error_extension": "Extension non supportée ou erreur de téléchargement",
"error_no_download": "Aucun téléchargement en attente.",
"platform_no_platform": "Aucune plateforme",
"platform_page": "Page {0}/{1}",
"game_no_games": "Aucun jeu disponible",
"game_count": "{0} ({1} jeux)",
"game_filter": "Filtre actif : {0}",
"game_search": "Filtrer : {0}",
"history_title": "Téléchargements ({0})",
"history_empty": "Aucun téléchargement dans l'historique",
"history_column_system": "Système",
"history_column_game": "Nom du jeu",
"history_column_status": "État",
"history_status_downloading": "Téléchargement : {0}%",
"history_status_extracting": "Extraction : {0}%",
"history_status_completed": "Terminé",
"history_status_error": "Erreur : {0}",
"history_status_canceled": "Annulé",
"download_status": "{0} : {1}",
"download_progress": "{0}% {1} Mo / {2} Mo",
"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 ?",
"confirm_exit": "Quitter l'application ?",
"confirm_clear_history": "Vider l'historique ?",
"confirm_redownload_cache": "Retélécharger le cache des jeux ?",
"popup_redownload_success": "Redownload des jeux effectué.\nVeuillez redémarrer l'application pour voir les changements.",
"popup_no_cache": "Aucun cache trouvé.\nVeuillez redémarrer l'application pour charger les jeux.",
"popup_countdown": "Ce message se fermera dans {0} seconde{1}",
"language_select_title": "Sélection de la langue",
"language_select_instruction": "Utilisez les flèches pour naviguer et Entrée pour sélectionner",
"language_changed": "Langue changée pour {0}",
"menu_controls": "Contrôles",
"menu_remap_controls": "Remapper les contrôles",
"menu_history": "Historique",
"menu_language": "Langue",
"menu_accessibility": "Accessibilité",
"menu_redownload_cache": "Retélécharger le cache 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",
"button_yes": "Oui",
"button_no": "Non",
"button_validate": "Valider",
"controls_hold_message": "Maintenez pendant 3s pour : '{0}'",
"controls_skip_message": "Appuyez sur Échap pour passer(Pc only)",
"controls_waiting": "Attente...",
"controls_hold": "Maintenez 3s",
"controls_action_confirm": "Confirmer",
"controls_action_cancel": "Annuler",
"controls_action_up": "Haut",
"controls_action_down": "Bas",
"controls_action_left": "Gauche",
"controls_action_right": "Droite",
"controls_action_page_up": "Page Précédente",
"controls_action_page_down": "Page Suivante",
"controls_action_progress": "Progression",
"controls_action_history": "Historique",
"controls_action_filter": "Filtrer",
"controls_action_delete": "Supprimer",
"controls_action_space": "Espace",
"controls_action_start": "Aide / Réglages",
"controls_action_music_toggle": "Musique On/Off",
"controls_desc_confirm": "Valider (ex: A, Entrée)",
"controls_desc_cancel": "Annuler/Retour (ex: B, RetourArrière)",
"controls_desc_up": "Naviguer vers le haut",
"controls_desc_down": "Naviguer vers le bas",
"controls_desc_left": "Naviguer à gauche",
"controls_desc_right": "Naviguer à droite",
"controls_desc_page_up": "Page précédente/Défilement Rapide Haut (ex: PageUp, LB)",
"controls_desc_page_down": "Page suivante/Défilement Rapide Bas (ex: PageDown, RB)",
"controls_desc_progress": "Voir progression (ex: X)",
"controls_desc_history": "Ouvrir l'historique (ex: H, Y)",
"controls_desc_filter": "Ouvrir filtre (ex: F, Select)",
"controls_desc_delete": "Supprimer caractère (ex: LT, Suppr)",
"controls_desc_space": "Ajouter espace (ex: RT, Espace)",
"controls_desc_start": "Ouvrir le menu pause (ex: Start, AltGr)",
"controls_desc_music_toggle": "Active ou désactive la musique de fond",
"footer_version": "RGSX v{0} - {1} : Options - {2}: Historique - {3} : Filtrer",
"action_retry": "Retenter",
"action_quit": "Quitter",
"action_select": "Sélectionner",
"action_history": "Historique",
"action_progress": "Progression",
"action_download": "Télécharger",
"action_filter": "Filtrer",
"action_cancel": "Annuler",
"action_back": "Retour",
"action_navigate": "Naviguer",
"action_page": "Page",
"action_cancel_download": "Annuler le téléchargement",
"action_background": "Arrière plan",
"action_confirm": "Confirmer",
"action_redownload": "Retélécharger",
"action_clear_history": "Vider l'historique",
"network_checking_updates": "Vérification des mises à jour...",
"network_update_available": "Mise à jour disponible : {0}",
"network_extracting_update": "Extraction de la mise à jour...",
"network_update_completed": "Mise à jour terminée",
"network_update_success": "Mise à jour vers {0} terminée avec succès. Veuillez redémarrer l'application.",
"network_update_success_message": "Mise à jour terminée avec succès",
"network_no_update_available": "Aucune mise à jour disponible",
"network_update_error": "Erreur lors de la mise à jour : {0}",
"network_download_extract_ok": "Téléchargement et extraction réussi de {0}",
"network_check_update_error": "Erreur lors de la vérification des mises à jour : {0}",
"network_extraction_failed": "Échec de l'extraction de la mise à jour : {0}",
"network_extraction_partial": "Extraction réussie, mais certains fichiers ont été ignorés en raison d'erreurs : {0}",
"network_extraction_success": "Extraction réussie",
"network_zip_extraction_error": "Erreur lors de l'extraction du ZIP {0}: {1}",
"network_permission_error": "Pas de permission d'écriture dans {0}",
"network_file_not_found": "Le fichier {0} n'existe pas",
"network_cannot_get_filename": "Impossible de récupérer le nom du fichier",
"network_cannot_get_download_url": "Impossible de récupérer l'URL de téléchargement",
"download_initializing": "Initialisation en cours...",
"accessibility_font_size": "Taille de police : {0}",
"confirm_cancel_download": "Annuler le téléchargement en cours ?",
"controls_help_title": "Aide des contrôles",
"controls_category_navigation": "Navigation",
"controls_category_main_actions": "Actions principales",
"controls_category_downloads": "Téléchargements",
"controls_category_search": "Recherche",
"controls_navigation": "Navigation",
"controls_pages": "Pages",
"controls_confirm_select": "Confirmer/Sélectionner",
"controls_cancel_back": "Annuler/Retour",
"controls_history": "Historique",
"controls_clear_history": "Effacer historique",
"controls_filter_search": "Filtrer/Rechercher",
"network_download_failed": "Échec du téléchargement après {0} tentatives",
"network_api_error": "Erreur lors de la requête API, la clé est peut-être incorrecte: {0}",
"network_download_error": "Erreur téléchargement {0}: {1}",
"network_download_ok": "Téléchargement ok : {0}",
"utils_extracted": "Extracted: {0}",
"utils_corrupt_zip": "Archive ZIP corrompue: {0}",
"utils_permission_denied": "Permission refusée lors de l'extraction: {0}",
"utils_extraction_failed": "Échec de l'extraction: {0}",
"utils_unrar_unavailable": "Commande unrar non disponible",
"utils_rar_list_failed": "Échec de la liste des fichiers RAR: {0}"
}

600
ports/RGSX/network.py Normal file
View File

@@ -0,0 +1,600 @@
import requests
import subprocess
import os
import sys
import threading
import pygame # type: ignore
import zipfile
import asyncio
import config
from config import OTA_VERSION_ENDPOINT,APP_FOLDER, UPDATE_FOLDER, OTA_UPDATE_ZIP
from utils import sanitize_filename, extract_zip, extract_rar, load_api_key_1fichier
from history import save_history
import logging
import datetime
import queue
import time
import os
from language import _ # Import de la fonction de traduction
logger = logging.getLogger(__name__)
cache = {}
CACHE_TTL = 3600 # 1 heure
def test_internet():
"""Teste la connexion Internet de manière portable pour Windows et Linux/Batocera."""
logger.debug("Test de connexion Internet")
# Choisir l'option ping en fonction de la plateforme
ping_option = '-n' if sys.platform.startswith("win") else '-c'
logger.debug(f"Utilisation de ping avec option {ping_option}")
try:
result = subprocess.run(
['ping', ping_option, '4', '8.8.8.8'],
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
logger.debug("Connexion Internet OK (ping)")
return True
else:
logger.debug(f"Échec ping 8.8.8.8, code retour: {result.returncode}")
return False
except Exception as e:
logger.debug(f"Erreur test Internet (ping): {str(e)}")
return False
async def check_for_updates():
try:
logger.debug("Vérification de la version disponible sur le serveur")
config.current_loading_system = _("network_checking_updates")
config.loading_progress = 5.0
config.needs_redraw = True
response = requests.get(OTA_VERSION_ENDPOINT, timeout=5)
response.raise_for_status()
if response.headers.get("content-type") != "application/json":
raise ValueError(f"Le fichier version.json n'est pas un JSON valide (type de contenu : {response.headers.get('content-type')})")
version_data = response.json()
latest_version = version_data.get("version")
logger.debug(f"Version distante : {latest_version}, version locale : {config.app_version}")
UPDATE_ZIP = OTA_UPDATE_ZIP.replace("RGSX.zip", f"RGSX_v{latest_version}.zip")
logger.debug(f"URL de mise à jour : {UPDATE_ZIP}")
if latest_version != config.app_version:
config.current_loading_system = _("network_update_available").format(latest_version)
config.loading_progress = 10.0
config.needs_redraw = True
logger.debug(f"Téléchargement du ZIP de mise à jour : {UPDATE_ZIP}")
# Créer le dossier UPDATE_FOLDER s'il n'existe pas
os.makedirs(UPDATE_FOLDER, exist_ok=True)
update_zip_path = os.path.join(UPDATE_FOLDER, f"RGSX_v{latest_version}.zip")
logger.debug(f"Téléchargement de {UPDATE_ZIP} vers {update_zip_path}")
# Télécharger le ZIP
with requests.get(UPDATE_ZIP, stream=True, timeout=10) as r:
r.raise_for_status()
total_size = int(r.headers.get('content-length', 0))
downloaded = 0
with open(update_zip_path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
config.loading_progress = 10.0 + (40.0 * downloaded / total_size) if total_size > 0 else 10.0
config.needs_redraw = True
await asyncio.sleep(0)
logger.debug(f"ZIP téléchargé : {update_zip_path}")
# Extraire le contenu du ZIP dans APP_FOLDER
config.current_loading_system = _("network_extracting_update")
config.loading_progress = 60.0
config.needs_redraw = True
success, message = extract_update(update_zip_path, APP_FOLDER, UPDATE_ZIP)
if not success:
logger.error(f"Échec de l'extraction : {message}")
return False, _("network_extraction_failed").format(message)
# Supprimer le fichier ZIP après extraction
if os.path.exists(update_zip_path):
os.remove(update_zip_path)
logger.debug(f"Fichier ZIP {update_zip_path} supprimé")
config.current_loading_system = _("network_update_completed")
config.loading_progress = 100.0
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"
config.update_result_message = _("network_update_success").format(latest_version)
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")
return True, _("network_update_success_message")
else:
logger.debug("Aucune mise à jour disponible")
return True, _("network_no_update_available")
except Exception as e:
logger.error(f"Erreur OTA : {str(e)}")
config.menu_state = "update_result"
config.update_result_message = _("network_update_error").format(str(e))
config.update_result_error = True
config.update_result_start_time = pygame.time.get_ticks()
config.needs_redraw = True
return False, _("network_check_update_error").format(str(e))
def extract_update(zip_path, dest_dir, source_url):
try:
os.makedirs(dest_dir, exist_ok=True)
logger.debug(f"Tentative d'ouverture du ZIP : {zip_path}")
# Extraire le ZIP
skipped_files = []
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
for file_info in zip_ref.infolist():
try:
zip_ref.extract(file_info, dest_dir)
except PermissionError as e:
logger.warning(f"Impossible d'extraire {file_info.filename}: {str(e)}")
skipped_files.append(file_info.filename)
except Exception as e:
logger.warning(f"Erreur lors de l'extraction de {file_info.filename}: {str(e)}")
skipped_files.append(file_info.filename)
if skipped_files:
message = _("network_extraction_partial").format(', '.join(skipped_files))
logger.warning(message)
return True, message # Considérer comme succès si certains fichiers sont extraits
return True, _("network_extraction_success")
except Exception as e:
logger.error(f"Erreur critique lors de l'extraction du ZIP {source_url}: {str(e)}")
return False, _("network_zip_extraction_error").format(source_url, str(e))
# File d'attente pour la progression - une par tâche
progress_queues = {}
async def download_rom(url, platform, game_name, is_zip_non_supported=False, task_id=None):
logger.debug(f"Début téléchargement: {game_name} depuis {url}, is_zip_non_supported={is_zip_non_supported}, task_id={task_id}")
result = [None, None]
# Créer une queue spécifique pour cette tâche
if task_id not in progress_queues:
progress_queues[task_id] = queue.Queue()
def download_thread():
logger.debug(f"Thread téléchargement démarré pour {url}, task_id={task_id}")
try:
dest_dir = None
for platform_dict in config.platform_dicts:
if platform_dict["platform"] == platform:
dest_dir = os.path.join(config.ROMS_FOLDER, platform_dict.get("folder", platform.lower().replace(" ", "")))
logger.debug(f"Répertoire de destination trouvé pour {platform}: {dest_dir}")
break
if not dest_dir:
dest_dir = os.path.join(os.path.dirname(os.path.dirname(config.APP_FOLDER)), platform.lower().replace(" ", ""))
os.makedirs(dest_dir, exist_ok=True)
if not os.access(dest_dir, os.W_OK):
raise PermissionError(f"Pas de permission d'écriture dans {dest_dir}")
sanitized_name = sanitize_filename(game_name)
dest_path = os.path.join(dest_dir, f"{sanitized_name}")
logger.debug(f"Chemin destination: {dest_path}")
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'DNT': '1',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
}
session = requests.Session()
session.headers.update(headers)
download_headers = headers.copy()
download_headers['Accept'] = 'application/octet-stream, */*'
download_headers['Referer'] = 'https://myrient.erista.me/'
response = session.get(url, stream=True, timeout=30, allow_redirects=True, headers=download_headers)
logger.debug(f"Status code: {response.status_code}")
response.raise_for_status()
total_size = int(response.headers.get('content-length', 0))
logger.debug(f"Taille totale: {total_size} octets")
# Initialiser la progression avec task_id
progress_queues[task_id].put((task_id, 0, total_size))
logger.debug(f"Progression initiale envoyée: 0% pour {game_name}, task_id={task_id}")
downloaded = 0
chunk_size = 4096
last_update_time = time.time()
update_interval = 0.1 # Mettre à jour toutes les 0,1 secondes
with open(dest_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=chunk_size):
if chunk:
size_received = len(chunk)
f.write(chunk)
downloaded += size_received
current_time = time.time()
if current_time - last_update_time >= update_interval:
progress_queues[task_id].put((task_id, downloaded, total_size))
last_update_time = current_time
os.chmod(dest_path, 0o644)
logger.debug(f"Téléchargement terminé: {dest_path}")
if is_zip_non_supported:
logger.debug(f"Extraction automatique nécessaire pour {dest_path}")
extension = os.path.splitext(dest_path)[1].lower()
if extension == ".zip":
try:
if isinstance(config.history, list):
for entry in config.history:
if "url" in entry and entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]:
entry["status"] = "Extracting"
entry["progress"] = 0
entry["message"] = "Préparation de l'extraction..."
save_history(config.history)
config.needs_redraw = True
break
success, msg = extract_zip(dest_path, dest_dir, url)
if success:
logger.debug(f"Extraction ZIP réussie: {msg}")
result[0] = True
result[1] = _("network_download_extract_ok").format(game_name)
else:
logger.error(f"Erreur extraction ZIP: {msg}")
result[0] = False
result[1] = _("network_extraction_failed").format(msg)
except Exception as e:
logger.error(f"Exception lors de l'extraction: {str(e)}")
result[0] = False
result[1] = f"Erreur téléchargement {game_name}: {str(e)}"
elif extension == ".rar":
try:
success, msg = extract_rar(dest_path, dest_dir, url)
if success:
logger.debug(f"Extraction RAR réussie: {msg}")
result[0] = True
result[1] = _("network_download_extract_ok").format(game_name)
else:
logger.error(f"Erreur extraction RAR: {msg}")
result[0] = False
result[1] = _("network_extraction_failed").format(msg)
except Exception as e:
logger.error(f"Exception lors de l'extraction RAR: {str(e)}")
result[0] = False
result[1] = f"Erreur extraction RAR {game_name}: {str(e)}"
else:
logger.warning(f"Type d'archive non supporté: {extension}")
result[0] = True
result[1] = _("network_download_ok").format(game_name)
else:
result[0] = True
result[1] = _("network_download_ok").format(game_name)
except Exception as e:
logger.error(f"Erreur téléchargement {url}: {str(e)}")
result[0] = False
result[1] = _("network_download_error").format(game_name, str(e))
finally:
logger.debug(f"Thread téléchargement terminé pour {url}, task_id={task_id}")
progress_queues[task_id].put((task_id, result[0], result[1]))
logger.debug(f"Final result sent to queue: success={result[0]}, message={result[1]}, task_id={task_id}")
thread = threading.Thread(target=download_thread)
thread.start()
# Boucle principale pour mettre à jour la progression
while thread.is_alive():
try:
task_queue = progress_queues.get(task_id)
if task_queue:
while not task_queue.empty():
data = task_queue.get()
#logger.debug(f"Progress queue data received: {data}")
if isinstance(data[1], bool): # Fin du téléchargement
success, message = data[1], data[2]
if isinstance(config.history, list):
for entry in config.history:
if "url" in entry and entry["url"] == url and entry["status"] in ["downloading", "Téléchargement", "Extracting"]:
entry["status"] = "Download_OK" if success else "Erreur"
entry["progress"] = 100 if success else 0
entry["message"] = message
save_history(config.history)
config.needs_redraw = True
logger.debug(f"Final update in history: status={entry['status']}, progress={entry['progress']}%, message={message}, task_id={task_id}")
break
else:
downloaded, total_size = data[1], data[2]
progress_percent = int(downloaded / total_size * 100) if total_size > 0 else 0
progress_percent = max(0, min(100, progress_percent))
if isinstance(config.history, list):
for entry in config.history:
if "url" in entry and entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]:
entry["progress"] = progress_percent
entry["status"] = "Téléchargement"
entry["downloaded_size"] = downloaded
entry["total_size"] = total_size
config.needs_redraw = True
break
await asyncio.sleep(0.1)
except Exception as e:
logger.error(f"Erreur mise à jour progression: {str(e)}")
thread.join()
# Nettoyer la queue
if task_id in progress_queues:
del progress_queues[task_id]
return result[0], result[1]
async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=False, task_id=None):
config.API_KEY_1FICHIER = load_api_key_1fichier()
logger.debug(f"Début téléchargement 1fichier: {game_name} depuis {url}, is_zip_non_supported={is_zip_non_supported}, task_id={task_id}")
logger.debug(f"Clé API 1fichier: {'présente' if config.API_KEY_1FICHIER else 'absente'}")
result = [None, None]
# Créer une queue spécifique pour cette tâche
logger.debug(f"Création queue pour task_id={task_id}")
if task_id not in progress_queues:
progress_queues[task_id] = queue.Queue()
def download_thread():
logger.debug(f"Thread téléchargement 1fichier démarré pour {url}, task_id={task_id}")
try:
link = url.split('&af=')[0]
logger.debug(f"URL nettoyée: {link}")
dest_dir = None
for platform_dict in config.platform_dicts:
if platform_dict["platform"] == platform:
dest_dir = os.path.join(config.ROMS_FOLDER, platform_dict.get("folder", platform.lower().replace(" ", "")))
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.debug(f"Répertoire destination déterminé: {dest_dir}")
logger.debug(f"Vérification répertoire destination: {dest_dir}")
os.makedirs(dest_dir, exist_ok=True)
logger.debug(f"Répertoire créé ou existant: {dest_dir}")
if not os.access(dest_dir, os.W_OK):
logger.error(f"Pas de permission d'écriture dans {dest_dir}")
raise PermissionError(f"Pas de permission d'écriture dans {dest_dir}")
headers = {
"Authorization": f"Bearer {config.API_KEY_1FICHIER}",
"Content-Type": "application/json"
}
payload = {
"url": link,
"pretty": 1
}
logger.debug(f"Préparation requête file/info pour {link}")
response = requests.post("https://api.1fichier.com/v1/file/info.cgi", headers=headers, json=payload, timeout=30)
logger.debug(f"Réponse file/info reçue, code: {response.status_code}")
response.raise_for_status()
file_info = response.json()
if "error" in file_info and file_info["error"] == "Resource not found":
logger.error(f"Le fichier {game_name} n'existe pas sur 1fichier")
result[0] = False
result[1] = _("network_file_not_found").format(game_name)
return
filename = file_info.get("filename", "").strip()
if not filename:
logger.error(f"Impossible de récupérer le nom du fichier")
result[0] = False
result[1] = _("network_cannot_get_filename")
return
sanitized_filename = sanitize_filename(filename)
dest_path = os.path.join(dest_dir, sanitized_filename)
logger.debug(f"Chemin destination: {dest_path}")
logger.debug(f"Envoi requête get_token pour {link}")
response = requests.post("https://api.1fichier.com/v1/download/get_token.cgi", headers=headers, json=payload, timeout=30)
logger.debug(f"Réponse get_token reçue, code: {response.status_code}")
response.raise_for_status()
download_info = response.json()
final_url = download_info.get("url")
if not final_url:
logger.error(f"Impossible de récupérer l'URL de téléchargement")
result[0] = False
result[1] = _("network_cannot_get_download_url")
return
logger.debug(f"URL de téléchargement obtenue: {final_url}")
lock = threading.Lock()
retries = 10
retry_delay = 10
logger.debug(f"Initialisation progression avec taille inconnue pour task_id={task_id}")
progress_queues[task_id].put((task_id, 0, 0)) # Taille initiale inconnue
for attempt in range(retries):
logger.debug(f"Début tentative {attempt + 1} pour télécharger {final_url}")
try:
with requests.get(final_url, stream=True, headers={'User-Agent': 'Mozilla/5.0'}, timeout=30) as response:
logger.debug(f"Réponse GET reçue, code: {response.status_code}")
response.raise_for_status()
total_size = int(response.headers.get('content-length', 0))
logger.debug(f"Taille totale: {total_size} octets")
with lock:
if isinstance(config.history, list):
for entry in config.history:
if "url" in entry and entry["url"] == url and entry["status"] == "downloading":
entry["total_size"] = total_size
config.needs_redraw = True
break
progress_queues[task_id].put((task_id, 0, total_size)) # Mettre à jour la taille totale
downloaded = 0
chunk_size = 8192
last_update_time = time.time()
update_interval = 0.1 # Mettre à jour toutes les 0,1 secondes
logger.debug(f"Ouverture fichier: {dest_path}")
with open(dest_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=chunk_size):
if chunk:
f.write(chunk)
downloaded += len(chunk)
current_time = time.time()
if current_time - last_update_time >= update_interval:
with lock:
if isinstance(config.history, list):
for entry in config.history:
if "url" in entry and entry["url"] == url and entry["status"] == "downloading":
progress_percent = int(downloaded / total_size * 100) if total_size > 0 else 0
progress_percent = max(0, min(100, progress_percent))
entry["progress"] = progress_percent
entry["status"] = "Téléchargement"
entry["downloaded_size"] = downloaded
entry["total_size"] = total_size
config.needs_redraw = True
break
progress_queues[task_id].put((task_id, downloaded, total_size))
last_update_time = current_time
if is_zip_non_supported:
with lock:
if isinstance(config.history, list):
for entry in config.history:
if "url" in entry and entry["url"] == url and entry["status"] == "Téléchargement":
entry["progress"] = 0
entry["status"] = "Extracting"
config.needs_redraw = True
break
extension = os.path.splitext(dest_path)[1].lower()
logger.debug(f"Début extraction, type d'archive: {extension}")
if extension == ".zip":
try:
success, msg = extract_zip(dest_path, dest_dir, url)
logger.debug(f"Extraction ZIP terminée: {msg}")
if success:
result[0] = True
result[1] = _("network_download_extract_ok").format(game_name)
else:
logger.error(f"Erreur extraction ZIP: {msg}")
result[0] = False
result[1] = _("network_extraction_failed").format(msg)
except Exception as e:
logger.error(f"Exception lors de l'extraction ZIP: {str(e)}")
result[0] = False
result[1] = f"Erreur téléchargement {game_name}: {str(e)}"
elif extension == ".rar":
try:
success, msg = extract_rar(dest_path, dest_dir, url)
logger.debug(f"Extraction RAR terminée: {msg}")
if success:
result[0] = True
result[1] = _("network_download_extract_ok").format(game_name)
else:
logger.error(f"Erreur extraction RAR: {msg}")
result[0] = False
result[1] = _("network_extraction_failed").format(msg)
except Exception as e:
logger.error(f"Exception lors de l'extraction RAR: {str(e)}")
result[0] = False
result[1] = f"Erreur extraction RAR {game_name}: {str(e)}"
else:
logger.warning(f"Type d'archive non supporté: {extension}")
result[0] = True
result[1] = _("network_download_ok").format(game_name)
else:
logger.debug(f"Application des permissions sur {dest_path}")
os.chmod(dest_path, 0o644)
logger.debug(f"Téléchargement terminé: {dest_path}")
result[0] = True
result[1] = _("network_download_ok").format(game_name)
return
except requests.exceptions.RequestException as e:
logger.error(f"Tentative {attempt + 1} échouée: {e}")
if attempt < retries - 1:
logger.debug(f"Attente de {retry_delay} secondes avant nouvelle tentative")
time.sleep(retry_delay)
else:
logger.error(f"Nombre maximum de tentatives atteint")
result[0] = False
result[1] = _("network_download_failed").format(retries)
return
except requests.exceptions.RequestException as e:
logger.error(f"Erreur API 1fichier: {e}")
result[0] = False
result[1] = _("network_api_error").format(str(e))
finally:
logger.debug(f"Thread téléchargement 1fichier terminé pour {url}, task_id={task_id}")
progress_queues[task_id].put((task_id, result[0], result[1]))
logger.debug(f"Résultat final envoyé à la queue: success={result[0]}, message={result[1]}, task_id={task_id}")
logger.debug(f"Démarrage thread pour {url}, task_id={task_id}")
thread = threading.Thread(target=download_thread)
thread.start()
# Boucle principale pour mettre à jour la progression
logger.debug(f"Début boucle de progression pour task_id={task_id}")
while thread.is_alive():
try:
task_queue = progress_queues.get(task_id)
if task_queue:
while not task_queue.empty():
data = task_queue.get()
logger.debug(f"Données queue progression reçues: {data}")
if isinstance(data[1], bool): # Fin du téléchargement
success, message = data[1], data[2]
if isinstance(config.history, list):
for entry in config.history:
if "url" in entry and entry["url"] == url and entry["status"] in ["downloading", "Téléchargement", "Extracting"]:
entry["status"] = "Download_OK" if success else "Erreur"
entry["progress"] = 100 if success else 0
entry["message"] = message
save_history(config.history)
config.needs_redraw = True
logger.debug(f"Mise à jour finale historique: status={entry['status']}, progress={entry['progress']}%, message={message}, task_id={task_id}")
break
else:
downloaded, total_size = data[1], data[2]
progress_percent = int(downloaded / total_size * 100) if total_size > 0 else 0
progress_percent = max(0, min(100, progress_percent))
if isinstance(config.history, list):
for entry in config.history:
if "url" in entry and entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]:
entry["progress"] = progress_percent
entry["status"] = "Téléchargement"
entry["downloaded_size"] = downloaded
entry["total_size"] = total_size
config.needs_redraw = True
break
await asyncio.sleep(0.1)
except Exception as e:
logger.error(f"Erreur mise à jour progression: {str(e)}")
logger.debug(f"Fin boucle de progression, attente fin thread pour task_id={task_id}")
thread.join()
logger.debug(f"Thread terminé, nettoyage queue pour task_id={task_id}")
# Nettoyer la queue
if task_id in progress_queues:
del progress_queues[task_id]
logger.debug(f"Fin download_from_1fichier, résultat: success={result[0]}, message={result[1]}")
return result[0], result[1]
def is_1fichier_url(url):
"""Détecte si l'URL est un lien 1fichier."""
return "1fichier.com" in url

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,85 @@
import os
import xml.dom.minidom
import xml.etree.ElementTree as ET
import logging
import config
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
RGSX_ENTRY = {
"path": "./RGSX/RGSX.sh",
"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",
"rating": "1",
"releasedate": "20250620T165718",
"developer": "RetroGameSets.fr",
"genre": "Various / Utilities"
}
def update_gamelist():
try:
# Si le fichier n'existe pas, est vide ou non valide, créer une nouvelle structure
if not os.path.exists(config.GAMELISTXML) or os.path.getsize(config.GAMELISTXML) == 0:
logger.info(f"Création de {config.GAMELISTXML}")
root = ET.Element("gameList")
else:
try:
logger.info(f"Lecture de {config.GAMELISTXML}")
tree = ET.parse(config.GAMELISTXML)
root = tree.getroot()
if root.tag != "gameList":
logger.info(f"{config.GAMELISTXML} n'a pas de balise <gameList>, création d'une nouvelle structure")
root = ET.Element("gameList")
except ET.ParseError:
logger.info(f"{config.GAMELISTXML} est invalide, création d'une nouvelle structure")
root = ET.Element("gameList")
# Supprimer l'ancienne entrée RGSX
for game in root.findall("game"):
path = game.find("path")
if path is not None and path.text == "./RGSX/RGSX.sh":
root.remove(game)
logger.info("Ancienne entrée RGSX supprimée")
# Ajouter la nouvelle entrée
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")
# 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(config.GAMELISTXML, 'w', encoding='utf-8') as f:
f.write(pretty_xml)
logger.info(f"{config.GAMELISTXML} mis à jour avec succès")
# Définir les permissions
os.chmod(config.GAMELISTXML, 0o644)
except Exception as e:
logger.error(f"Erreur lors de la mise à jour de {config.GAMELISTXML}: {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()

695
ports/RGSX/utils.py Normal file
View File

@@ -0,0 +1,695 @@
import shutil
import pygame # type: ignore
import re
import json
import os
import logging
import platform
import subprocess
import config
import threading
import zipfile
import time
import random
import random
from config import JSON_EXTENSIONS, SAVE_FOLDER
def load_accessibility_settings():
"""Charge les paramètres d'accessibilité depuis accessibility.json."""
accessibility_path = os.path.join(SAVE_FOLDER, "accessibility.json")
try:
if os.path.exists(accessibility_path):
with open(accessibility_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
logger.error(f"Erreur lors du chargement de accessibility.json: {str(e)}")
return {"font_scale": 1.0}
def save_accessibility_settings(settings):
"""Sauvegarde les paramètres d'accessibilité dans accessibility.json."""
accessibility_path = os.path.join(SAVE_FOLDER, "accessibility.json")
try:
os.makedirs(SAVE_FOLDER, exist_ok=True)
with open(accessibility_path, 'w', encoding='utf-8') as f:
json.dump(settings, f, indent=2)
logger.debug(f"Paramètres d'accessibilité sauvegardés: {settings}")
except Exception as e:
logger.error(f"Erreur lors de la sauvegarde de accessibility.json: {str(e)}")
from history import save_history
from language import _ # Import de la fonction de traduction
from datetime import datetime
from datetime import datetime
logger = logging.getLogger(__name__)
# Désactiver les logs DEBUG de urllib3 e requests pour supprimer les messages de connexion HTTP
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("requests").setLevel(logging.WARNING)
# Liste globale pour stocker les systèmes avec une erreur 404
unavailable_systems = []
# Détection système non-PC
def detect_non_pc():
arch = platform.machine()
try:
result = subprocess.run(["batocera-es-swissknife", "--arch"], capture_output=True, text=True, timeout=2)
if result.returncode == 0:
arch = result.stdout.strip()
#logger.debug(f"Architecture via batocera-es-swissknife: {arch}")
except (subprocess.SubprocessError, FileNotFoundError):
logger.debug(f"batocera-es-swissknife non disponible, utilisation de platform.machine(): {arch}")
is_non_pc = arch not in ["x86_64", "amd64"]
logger.debug(f"Système détecté: {platform.system()}, architecture: {arch}, is_non_pc={is_non_pc}")
return is_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."""
try:
with open(JSON_EXTENSIONS, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
logger.error(f"Erreur lors de la lecture de {JSON_EXTENSIONS}: {e}")
return []
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."""
try:
sanitized_name = sanitize_filename(game_name)
extensions_data = load_extensions_json()
if not extensions_data:
logger.error(f"Fichier {JSON_EXTENSIONS} vide ou introuvable")
return None
is_supported = is_extension_supported(sanitized_name, platform, extensions_data)
extension = os.path.splitext(sanitized_name)[1].lower()
is_archive = extension in (".zip", ".rar")
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:
logger.debug(f"Archive {extension.upper()} détectée pour {sanitized_name}, extraction automatique prévue")
return (url, platform, game_name, True)
else:
logger.debug(f"Extension non supportée ({extension}) pour {sanitized_name}, avertissement affiché")
return (url, platform, game_name, False)
except Exception as e:
logger.error(f"Erreur vérification extension {url}: {str(e)}")
return None
# Fonction pour vérifier si l'extension est supportée pour une plateforme donnée
def is_extension_supported(filename, platform, extensions_data):
"""Vérifie si l'extension du fichier est supportée pour la plateforme donnée."""
extension = os.path.splitext(filename)[1].lower()
dest_dir = None
for platform_dict in config.platform_dicts:
if platform_dict["platform"] == platform:
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)
dest_folder_name = os.path.basename(dest_dir)
for i, system in enumerate(extensions_data):
if system["folder"] == dest_folder_name:
result = extension in system["extensions"]
return result
logger.warning(f"Aucun système trouvé pour le dossier {dest_dir}")
return False
# Fonction pour charger sources.json
def load_sources():
"""Charge les sources depuis sources.json et initialise les plateformes."""
sources_path = os.path.join(config.APP_FOLDER, "sources.json")
logger.debug(f"Chargement de {sources_path}")
try:
with open(sources_path, 'r', encoding='utf-8') as f:
sources = json.load(f)
sources = sorted(sources, key=lambda x: x.get("nom", x.get("platform", "")).lower())
config.platforms = [source["platform"] for source in sources]
config.platform_dicts = sources
config.platform_names = {source["platform"]: source["nom"] for source in sources}
config.games_count = {platform: 0 for platform in config.platforms} # Initialiser à 0
# Charger les jeux pour chaque plateforme
loaded_platforms = set() # Pour suivre les plateformes déjà loguées
for platform in config.platforms:
games = load_games(platform)
config.games_count[platform] = len(games)
if platform not in loaded_platforms:
loaded_platforms.add(platform)
# Appeler write_unavailable_systems une seule fois après la boucle
write_unavailable_systems() # Assurez-vous que cette fonction est définie
return sources
except Exception as e:
logger.error(f"Erreur lors du chargement de sources.json : {str(e)}")
return []
def load_games(platform_id):
"""Charge les jeux pour une plateforme donnée en utilisant platform_id et teste la première URL."""
games_path = os.path.join(config.APP_FOLDER, "games", f"{platform_id}.json")
#logger.debug(f"Chargement des jeux pour {platform_id} depuis {games_path}")
try:
with open(games_path, 'r', encoding='utf-8') as f:
games = json.load(f)
# Tester la première URL si la liste n'est pas vide
# if games and len(games) > 0 and len(games[0]) > 1:
# first_url = games[0][1]
# try:
# response = requests.head(first_url, timeout=5, allow_redirects=True)
# if response.status_code not in (200, 303): # Ne logger que les codes autres que 200 et 303
# logger.debug(f"https://{first_url} \"HEAD {first_url} HTTP/1.1\" {response.status_code} 0")
# if response.status_code == 404:
# logger.error(f"URL non accessible pour {platform_id} : {first_url} (code 404)")
# unavailable_systems.append(platform_id) # Assurez-vous que unavailable_systems est défini
# except requests.RequestException as e:
# logger.error(f"Erreur lors du test de l'URL pour {platform_id} : {first_url} ({str(e)})")
# else:
# logger.debug(f"Aucune URL à tester pour {platform_id} (liste vide ou mal formée)")
logger.debug(f"Jeux chargés pour {platform_id}: {len(games)} jeux")
return games
except Exception as e:
logger.error(f"Erreur lors du chargement des jeux pour {platform_id} : {str(e)}")
return []
def write_unavailable_systems():
"""Écrit la liste des systèmes avec une erreur 404 dans un fichier texte."""
if not unavailable_systems:
logger.debug("Aucun système avec des liens HS, rien à écrire dans le fichier.")
return
# Formater la date et l'heure pour le nom du fichier
current_time = datetime.now()
timestamp = current_time.strftime("%d-%m-%Y-%H-%M")
log_dir = os.path.join(os.path.dirname(config.APP_FOLDER), "logs", "RGSX")
log_file = os.path.join(log_dir, f"systemes_unavailable_{timestamp}.txt")
try:
# Créer le répertoire s'il n'existe pas
os.makedirs(log_dir, exist_ok=True)
# Écrire les systèmes dans le fichier
with open(log_file, 'w', encoding='utf-8') as f:
f.write("Systèmes avec une erreur 404 :\n")
for system in unavailable_systems:
f.write(f"{system}\n")
logger.debug(f"Fichier écrit : {log_file} avec {len(unavailable_systems)} systèmes")
except Exception as e:
logger.error(f"Erreur lors de l'écriture du fichier {log_file} : {str(e)}")
def truncate_text_middle(text, font, max_width, is_filename=True):
"""Tronque le texte en insérant '...' au milieu, en préservant le début et la fin.
Si is_filename=False, ne supprime pas l'extension."""
# Supprimer l'extension uniquement si is_filename est True
if is_filename:
text = text.rsplit('.', 1)[0] if '.' in text else text
text_width = font.size(text)[0]
if text_width <= max_width:
return text
ellipsis = "..."
ellipsis_width = font.size(ellipsis)[0]
max_text_width = max_width - ellipsis_width
if max_text_width <= 0:
return ellipsis
# Diviser la largeur disponible entre début et fin, en priorisant la fin
chars = list(text)
left = []
right = []
left_width = 0
right_width = 0
left_idx = 0
right_idx = len(chars) - 1
# Préserver plus de caractères à droite pour garder le '%'
while left_idx <= right_idx and (left_width + right_width) < max_text_width:
# Ajouter à droite en priorité
if left_idx <= right_idx:
right.insert(0, chars[right_idx])
right_width = font.size(''.join(right))[0]
if left_width + right_width > max_text_width:
right.pop(0)
break
right_idx -= 1
# Ajouter à gauche seulement si nécessaire
if left_idx < right_idx:
left.append(chars[left_idx])
left_width = font.size(''.join(left))[0]
if left_width + right_width > max_text_width:
left.pop()
break
left_idx += 1
# Reculer jusqu'à un espace pour éviter de couper un mot
while left and left[-1] != ' ' and left_width + right_width > max_text_width:
left.pop()
left_width = font.size(''.join(left))[0] if left else 0
while right and right[0] != ' ' and left_width + right_width > max_text_width:
right.pop(0)
right_width = font.size(''.join(right))[0] if right else 0
return ''.join(left).rstrip() + ellipsis + ''.join(right).lstrip()
def truncate_text_end(text, font, max_width):
"""Tronque le texte à la fin pour qu'il tienne dans max_width avec la police donnée."""
if not isinstance(text, str):
logger.error(f"Texte non valide: {text}")
return ""
if not isinstance(font, pygame.font.Font):
logger.error("Police non valide dans truncate_text_end")
return text # Retourne le texte brut si la police est invalide
try:
if font.size(text)[0] <= max_width:
return text
truncated = text
while len(truncated) > 0 and font.size(truncated + "...")[0] > max_width:
truncated = truncated[:-1]
return truncated + "..." if len(truncated) < len(text) else text
except Exception as e:
logger.error(f"Erreur lors du rendu du texte '{text}': {str(e)}")
return text # Retourne le texte brut en cas d'erreur
def sanitize_filename(name):
"""Sanitise les noms de fichiers en remplaçant les caractères interdits."""
return re.sub(r'[<>:"/\/\\|?*]', '_', name).strip()
def wrap_text(text, font, max_width):
"""Divise le texte en lignes pour respecter la largeur maximale, en coupant les mots longs si nécessaire."""
if not isinstance(text, str):
text = str(text) if text is not None else ""
words = text.split(' ')
lines = []
current_line = ''
for word in words:
# Si le mot seul dépasse max_width, le couper caractère par caractère
if font.render(word, True, (255, 255, 255)).get_width() > max_width:
temp_line = current_line
for char in word:
test_line = temp_line + (' ' if temp_line else '') + char
test_surface = font.render(test_line, True, (255, 255, 255))
if test_surface.get_width() <= max_width:
temp_line = test_line
else:
if temp_line:
lines.append(temp_line)
temp_line = char
current_line = temp_line
else:
# Comportement standard pour les mots normaux
test_line = current_line + (' ' if current_line else '') + word
test_surface = font.render(test_line, True, (255, 255, 255))
if test_surface.get_width() <= max_width:
current_line = test_line
else:
if current_line:
lines.append(current_line)
current_line = word
if current_line:
lines.append(current_line)
return lines
def load_system_image(platform_dict):
"""Charge une image système depuis le chemin spécifié dans system_image."""
image_path = os.path.join(config.IMAGES_FOLDER, platform_dict.get("system_image", "default.png"))
platform_name = platform_dict.get("platform", "unknown")
#logger.debug(f"Chargement de l'image système pour {platform_name} depuis {image_path}")
try:
if not os.path.exists(image_path):
logger.error(f"Image introuvable pour {platform_name} à {image_path}")
return None
return pygame.image.load(image_path).convert_alpha()
except Exception as e:
logger.error(f"Erreur lors du chargement de l'image pour {platform_name} : {str(e)}")
return None
def extract_zip_data(zip_path, dest_dir, url):
"""Extrait le contenu du fichier ZIP dans le dossier config.APP_FOLDER sans progression a l'ecran"""
logger.debug(f"Extraction de {zip_path} dans {dest_dir}")
try:
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.testzip() # Vérifier l'intégrité de l'archive
for info in zip_ref.infolist():
if info.is_dir():
continue
file_path = os.path.join(dest_dir, info.filename)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with zip_ref.open(info) as source, open(file_path, 'wb') as dest:
shutil.copyfileobj(source, dest)
logger.info(f"Extraction terminée de {zip_path}")
return True, "Extraction terminée avec succès"
except zipfile.BadZipFile as e:
logger.error(f"Erreur: Archive ZIP corrompue: {str(e)}")
return False, _("utils_corrupt_zip").format(str(e))
def extract_zip(zip_path, dest_dir, url):
"""Extrait le contenu du fichier ZIP dans le dossier cible avec un suivi progressif de la progression."""
logger.debug(f"Extraction de {zip_path} dans {dest_dir}")
try:
lock = threading.Lock()
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.testzip() # Vérifier l'intégrité de l'archive
total_size = sum(info.file_size for info in zip_ref.infolist() if not info.is_dir())
logger.info(f"Taille totale à extraire: {total_size} octets")
if total_size == 0:
logger.warning("ZIP vide ou ne contenant que des dossiers")
return True, "ZIP vide extrait avec succès"
extracted_size = 0
os.makedirs(dest_dir, exist_ok=True)
chunk_size = 2048 # Réduire pour plus de mises à jour
last_save_time = time.time()
save_interval = 0.5 # Sauvegarder toutes les 0.5 secondes
for info in zip_ref.infolist():
if info.is_dir():
continue
file_path = os.path.join(dest_dir, info.filename)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with zip_ref.open(info) as source, open(file_path, 'wb') as dest:
file_size = info.file_size
file_extracted = 0
while True:
chunk = source.read(chunk_size)
if not chunk:
break
dest.write(chunk)
file_extracted += len(chunk)
extracted_size += len(chunk)
current_time = time.time()
with lock:
# Vérifier si config.history est une liste avant d'itérer
if isinstance(config.history, list):
for entry in config.history:
# Vérifier si l'entrée a les clés nécessaires et correspond à notre téléchargement
if "status" in entry and entry["status"] in ["Téléchargement", "Extracting", "downloading"]:
# Chercher par URL si disponible
if "url" in entry and entry["url"] == url:
# Calculer le pourcentage correctement et le limiter entre 0 et 100
progress_percent = int(extracted_size / total_size * 100) if total_size > 0 else 0
progress_percent = max(0, min(100, progress_percent))
entry["status"] = "Extracting"
entry["progress"] = progress_percent
entry["message"] = "Extraction en cours"
if current_time - last_save_time >= save_interval:
save_history(config.history)
last_save_time = current_time
# logger.debug(f"Extraction en cours: {info.filename}, file_extracted={file_extracted}/{file_size}, total_extracted={extracted_size}/{total_size}, progression={progress_percent:.1f}%")
config.needs_redraw = True
break
os.chmod(file_path, 0o644)
for root, dirs, files in os.walk(dest_dir):
for dir_name in dirs:
os.chmod(os.path.join(root, dir_name), 0o755)
try:
os.remove(zip_path)
logger.info(f"Fichier ZIP {zip_path} extrait dans {dest_dir} et supprimé")
# Mettre à jour le statut final dans l'historique
if isinstance(config.history, list):
for entry in config.history:
if "status" in entry and entry["status"] == "Extracting":
entry["status"] = "Download_OK"
entry["progress"] = 100
# Utiliser une variable intermédiaire pour stocker le message
message_text = _("utils_extracted").format(os.path.basename(zip_path))
entry["message"] = message_text
save_history(config.history)
config.needs_redraw = True
break
return True, _("utils_extracted").format(os.path.basename(zip_path))
except Exception as e:
logger.error(f"Erreur lors de la finalisation de l'extraction: {str(e)}")
return True, _("utils_extracted").format(os.path.basename(zip_path))
except zipfile.BadZipFile as e:
logger.error(f"Erreur: Archive ZIP corrompue: {str(e)}")
return False, _("utils_corrupt_zip").format(str(e))
except PermissionError as e:
logger.error(f"Erreur: Permission refusée lors de l'extraction: {str(e)}")
return False, _("utils_permission_denied").format(str(e))
except Exception as e:
logger.error(f"Erreur lors de l'extraction de {zip_path}: {str(e)}")
return False, _("utils_extraction_failed").format(str(e))
# Fonction pour extraire le contenu d'un fichier RAR
def extract_rar(rar_path, dest_dir, url):
"""Extrait le contenu du fichier RAR dans le dossier cible, préservant la structure des dossiers."""
try:
lock = threading.Lock()
os.makedirs(dest_dir, exist_ok=True)
result = subprocess.run(['unrar'], capture_output=True, text=True)
if result.returncode not in [0, 1]:
logger.error("Commande unrar non disponible")
return False, _("utils_unrar_unavailable")
result = subprocess.run(['unrar', 'l', '-v', rar_path], capture_output=True, text=True)
if result.returncode != 0:
error_msg = result.stderr.strip()
logger.error(f"Erreur lors de la liste des fichiers RAR: {error_msg}")
return False, _("utils_rar_list_failed").format(error_msg)
logger.debug(f"Sortie brute de 'unrar l -v {rar_path}':\n{result.stdout}")
total_size = 0
files_to_extract = []
root_dirs = set()
lines = result.stdout.splitlines()
in_file_list = False
for line in lines:
if line.startswith("----"):
in_file_list = not in_file_list
continue
if in_file_list:
match = re.match(r'^\s*(\S+)\s+(\d+)\s+\d*\s*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})\s+(.+)$', line)
if match:
attrs = match.group(1)
file_size = int(match.group(2))
file_date = match.group(3)
file_name = match.group(4).strip()
if 'D' not in attrs:
files_to_extract.append((file_name, file_size))
total_size += file_size
root_dir = file_name.split('/')[0] if '/' in file_name else ''
if root_dir:
root_dirs.add(root_dir)
logger.debug(f"Ligne parsée: {file_name}, taille: {file_size}, date: {file_date}")
else:
logger.debug(f"Dossier ignoré: {file_name}")
else:
logger.debug(f"Ligne ignorée (format inattendu): {line}")
logger.info(f"Taille totale à extraire (RAR): {total_size} octets")
logger.debug(f"Fichiers à extraire: {files_to_extract}")
logger.debug(f"Dossiers racines détectés: {root_dirs}")
if total_size == 0:
logger.warning("RAR vide, ne contenant que des dossiers, ou erreur de parsing")
return False, "RAR vide ou erreur lors de la liste des fichiers"
try:
with lock:
# Vérifier si l'URL existe dans config.download_progress
if url not in config.download_progress:
config.download_progress[url] = {}
config.download_progress[url]["downloaded_size"] = 0
config.download_progress[url]["total_size"] = total_size
config.download_progress[url]["status"] = "Extracting"
config.download_progress[url]["progress_percent"] = 0
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur lors de la mise à jour de la progression: {str(e)}")
# Continuer l'extraction même en cas d'erreur de mise à jour de la progression
escaped_rar_path = rar_path.replace(" ", "\\ ")
escaped_dest_dir = dest_dir.replace(" ", "\\ ")
process = subprocess.Popen(['unrar', 'x', '-y', escaped_rar_path, escaped_dest_dir],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
stdout, stderr = process.communicate()
if process.returncode != 0:
logger.error(f"Erreur lors de l'extraction de {rar_path}: {stderr}")
return False, f"Erreur lors de l'extraction: {stderr}"
extracted_size = 0
extracted_files = []
total_files = len(files_to_extract)
for i, (expected_file, file_size) in enumerate(files_to_extract):
file_path = os.path.join(dest_dir, expected_file)
if os.path.exists(file_path):
extracted_size += file_size
extracted_files.append(expected_file)
os.chmod(file_path, 0o644)
logger.debug(f"Fichier extrait: {expected_file}, taille: {file_size}, chemin: {file_path}")
try:
with lock:
if url in config.download_progress:
config.download_progress[url]["downloaded_size"] = extracted_size
config.download_progress[url]["status"] = "Extracting"
config.download_progress[url]["progress_percent"] = ((i + 1) / total_files * 100) if total_files > 0 else 0
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur lors de la mise à jour de la progression d'extraction: {str(e)}")
# Continuer l'extraction même en cas d'erreur de mise à jour de la progression
else:
logger.warning(f"Fichier non trouvé après extraction: {expected_file}")
missing_files = [f for f, _ in files_to_extract if f not in extracted_files]
if missing_files:
logger.warning(f"Fichiers non extraits: {', '.join(missing_files)}")
return False, f"Fichiers non extraits: {', '.join(missing_files)}"
ps3_dir = os.path.join(os.path.dirname(os.path.dirname(config.APP_FOLDER)), "ps3")
if dest_dir == ps3_dir and len(root_dirs) == 1:
root_dir = root_dirs.pop()
old_path = os.path.join(dest_dir, root_dir)
new_path = os.path.join(dest_dir, f"{root_dir}.ps3")
if os.path.isdir(old_path):
try:
os.rename(old_path, new_path)
logger.info(f"Dossier renommé: {old_path} -> {new_path}")
except Exception as e:
logger.error(f"Erreur lors du renommage de {old_path} en {new_path}: {str(e)}")
return False, f"Erreur lors du renommage du dossier: {str(e)}"
else:
logger.warning(f"Dossier racine {old_path} non trouvé après extraction")
elif dest_dir == ps3_dir and len(root_dirs) > 1:
logger.warning(f"Plusieurs dossiers racines détectés dans l'archive: {root_dirs}. Aucun renommage effectué.")
for root, dirs, files in os.walk(dest_dir):
for dir_name in dirs:
os.chmod(os.path.join(root, dir_name), 0o755)
os.remove(rar_path)
logger.info(f"Fichier RAR {rar_path} extrait dans {dest_dir} et supprimé")
return True, "RAR extrait avec succès"
except Exception as e:
logger.error(f"Erreur lors de l'extraction de {rar_path}: {str(e)}")
# Ne pas renvoyer l'URL comme message d'erreur
return False, f"Erreur lors de l'extraction: {str(e)}"
finally:
if os.path.exists(rar_path):
try:
os.remove(rar_path)
logger.info(f"Fichier RAR {rar_path} supprimé après échec de l'extraction")
except Exception as e:
logger.error(f"Erreur lors de la suppression de {rar_path}: {str(e)}")
def play_random_music(music_files, music_folder, current_music=None):
if not getattr(config, "music_enabled", True):
pygame.mixer.music.stop()
return current_music
if music_files:
# Éviter de rejouer la même musique consécutivement
available_music = [f for f in music_files if f != current_music]
if not available_music: # Si une seule musique, on la reprend
available_music = music_files
music_file = random.choice(available_music)
music_path = os.path.join(music_folder, music_file)
logger.debug(f"Lecture de la musique : {music_path}")
def load_and_play_music():
try:
pygame.mixer.music.load(music_path)
pygame.mixer.music.set_volume(0.5)
pygame.mixer.music.play(loops=0) # Jouer une seule fois
pygame.mixer.music.set_endevent(pygame.USEREVENT + 1) # Événement de fin
set_music_popup(music_file) # Afficher le nom de la musique dans la popup
except Exception as e:
logger.error(f"Erreur lors du chargement de la musique {music_path}: {str(e)}")
# Charger et jouer la musique dans un thread séparé pour éviter le blocage
music_thread = threading.Thread(target=load_and_play_music, daemon=True)
music_thread.start()
return music_file # Retourner la nouvelle musique pour mise à jour
else:
logger.debug("Aucune musique trouvée dans /RGSX/assets/music")
return current_music
def set_music_popup(music_name):
"""Définit le nom de la musique à afficher dans la popup."""
config.current_music_name = f"{os.path.splitext(music_name)[0]}" # Utilise l'emoji ♬ directement
config.music_popup_start_time = pygame.time.get_ticks() / 1000 # Temps actuel en secondes
config.needs_redraw = True # Forcer le redraw pour afficher le nom de la musique
def load_api_key_1fichier():
"""Charge la clé API 1fichier depuis le dossier de sauvegarde, crée le fichier si absent."""
api_path = os.path.join(SAVE_FOLDER, "1fichierAPI.txt")
logger.debug(f"Tentative de chargement de la clé API depuis: {api_path}")
try:
# Vérifie si le fichier existe déjà
if not os.path.exists(api_path):
# Crée le dossier parent si nécessaire
os.makedirs(SAVE_FOLDER, exist_ok=True)
# Crée le fichier vide si absent
with open(api_path, "w") as f:
f.write("")
logger.info(f"Fichier de clé API créé : {api_path}")
return ""
except OSError as e:
logger.error(f"Erreur lors de la création du fichier de clé API : {e}")
return ""
# Lit la clé API depuis le fichier
try:
with open(api_path, "r", encoding="utf-8") as f:
api_key = f.read().strip()
logger.debug(f"Clé API 1fichier lue: '{api_key}' (longueur: {len(api_key)})")
if not api_key:
logger.warning("Clé API 1fichier vide, veuillez la renseigner dans le fichier pour pouvoir utiliser les fonctionnalités de téléchargement sur 1fichier.")
config.API_KEY_1FICHIER = api_key
return api_key
except OSError as e:
logger.error(f"Erreur lors de la lecture de la clé API : {e}")
return ""
def load_music_config():
"""Charge la configuration musique depuis music_config.json."""
path = config.MUSIC_CONFIG_PATH
try:
if os.path.exists(path):
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
config.music_enabled = data.get("music_enabled", True)
return config.music_enabled
except Exception as e:
logger.error(f"Erreur lors du chargement de music_config.json: {str(e)}")
config.music_enabled = True
return True
def save_music_config():
"""Sauvegarde la configuration musique dans music_config.json."""
path = config.MUSIC_CONFIG_PATH
try:
os.makedirs(config.SAVE_FOLDER, exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump({"music_enabled": config.music_enabled}, f, indent=2)
logger.debug(f"Configuration musique sauvegardée: {config.music_enabled}")
except Exception as e:
logger.error(f"Erreur lors de la sauvegarde de music_config.json: {str(e)}")

20
ports/gamelist.xml Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0"?>
<gameList>
<game>
<path>./RGSX/RGSX.sh</path>
<name>RGSX</name>
<desc>Retro Games Sets X - Games Downloader</desc>
<image>./images/RGSX.png</image>
<marquee>./images/RGSX.png</marquee>
<thumbnail>./images/RGSX.png</thumbnail>
<fanart>./images/RGSX.png</fanart>
<rating>1</rating>
<releasedate>20250620T165718</releasedate>
<developer>RetroGameSets.fr</developer>
<genre>Compilation, Various / Utilities</genre>
<playcount>352</playcount>
<lastplayed>20250725T171934</lastplayed>
<gametime>48965</gametime>
<lang>fr</lang>
</game>
</gameList>

BIN
ports/images/RGSX.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
ports/videos/RGSX.mp4 Normal file

Binary file not shown.