retructuration pour ajouter compatibilité retrobat
This commit is contained in:
181
ports/RGSX/README.md
Normal file
181
ports/RGSX/README.md
Normal 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
4
ports/RGSX/RGSX.sh
Normal 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
|
||||
119
ports/RGSX/RGSX_Retrobat.bat
Normal file
119
ports/RGSX/RGSX_Retrobat.bat
Normal 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
789
ports/RGSX/__main__.py
Normal 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
129
ports/RGSX/accessibility.py
Normal 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
|
||||
BIN
ports/RGSX/assets/Pixel-UniCode.ttf
Normal file
BIN
ports/RGSX/assets/Pixel-UniCode.ttf
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/8bit.mp3
Normal file
BIN
ports/RGSX/assets/music/8bit.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/90s.mp3
Normal file
BIN
ports/RGSX/assets/music/90s.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/aquatic_ambience.mp3
Normal file
BIN
ports/RGSX/assets/music/aquatic_ambience.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/fantasia.mp3
Normal file
BIN
ports/RGSX/assets/music/fantasia.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/game_8bit.mp3
Normal file
BIN
ports/RGSX/assets/music/game_8bit.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/game_mode.mp3
Normal file
BIN
ports/RGSX/assets/music/game_mode.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/level_IV.mp3
Normal file
BIN
ports/RGSX/assets/music/level_IV.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/pixel_racer.mp3
Normal file
BIN
ports/RGSX/assets/music/pixel_racer.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/retro_chill.mp3
Normal file
BIN
ports/RGSX/assets/music/retro_chill.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/return_8bit.mp3
Normal file
BIN
ports/RGSX/assets/music/return_8bit.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/stranger.mp3
Normal file
BIN
ports/RGSX/assets/music/stranger.mp3
Normal file
Binary file not shown.
207
ports/RGSX/config.py
Normal file
207
ports/RGSX/config.py
Normal 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
1143
ports/RGSX/controls.py
Normal file
File diff suppressed because it is too large
Load Diff
601
ports/RGSX/controls_mapper.py
Normal file
601
ports/RGSX/controls_mapper.py
Normal 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
1465
ports/RGSX/display.py
Normal file
File diff suppressed because it is too large
Load Diff
166
ports/RGSX/es_input_parser.py
Normal file
166
ports/RGSX/es_input_parser.py
Normal 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
85
ports/RGSX/history.py
Normal 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
348
ports/RGSX/language.py
Normal 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
|
||||
176
ports/RGSX/languages/de.json
Normal file
176
ports/RGSX/languages/de.json
Normal 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}"
|
||||
}
|
||||
176
ports/RGSX/languages/en.json
Normal file
176
ports/RGSX/languages/en.json
Normal 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"
|
||||
}
|
||||
176
ports/RGSX/languages/es.json
Normal file
176
ports/RGSX/languages/es.json
Normal 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}"
|
||||
}
|
||||
181
ports/RGSX/languages/fr.json
Normal file
181
ports/RGSX/languages/fr.json
Normal 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
600
ports/RGSX/network.py
Normal 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
|
||||
2317
ports/RGSX/rom_extensions.json
Normal file
2317
ports/RGSX/rom_extensions.json
Normal file
File diff suppressed because it is too large
Load Diff
85
ports/RGSX/update_gamelist.py
Normal file
85
ports/RGSX/update_gamelist.py
Normal 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
695
ports/RGSX/utils.py
Normal 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
20
ports/gamelist.xml
Normal 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
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
BIN
ports/videos/RGSX.mp4
Normal file
Binary file not shown.
Reference in New Issue
Block a user