retructuration pour ajouter compatibilité retrobat
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -0,0 +1,129 @@
|
||||
import pygame #type:ignore
|
||||
import config
|
||||
from utils import save_accessibility_settings
|
||||
from language import _
|
||||
|
||||
def draw_accessibility_menu(screen):
|
||||
"""Affiche le menu d'accessibilité avec curseur pour la taille de police."""
|
||||
from display import OVERLAY, THEME_COLORS, draw_stylized_button
|
||||
|
||||
screen.blit(OVERLAY, (0, 0))
|
||||
|
||||
# Titre
|
||||
title_text = _("menu_accessibility")
|
||||
title_surface = config.title_font.render(title_text, True, THEME_COLORS["text"])
|
||||
title_rect = title_surface.get_rect(center=(config.screen_width // 2, config.screen_height // 4))
|
||||
|
||||
# Fond du titre
|
||||
title_bg_rect = title_rect.inflate(40, 20)
|
||||
pygame.draw.rect(screen, THEME_COLORS["button_idle"], title_bg_rect, border_radius=10)
|
||||
pygame.draw.rect(screen, THEME_COLORS["border"], title_bg_rect, 2, border_radius=10)
|
||||
screen.blit(title_surface, title_rect)
|
||||
|
||||
# Curseur de taille de police
|
||||
current_scale = config.font_scale_options[config.current_font_scale_index]
|
||||
font_text = _("accessibility_font_size").format(f"{current_scale:.1f}")
|
||||
|
||||
# Position du curseur
|
||||
cursor_y = config.screen_height // 2
|
||||
cursor_width = 400
|
||||
cursor_height = 60
|
||||
cursor_x = (config.screen_width - cursor_width) // 2
|
||||
|
||||
# Fond du curseur
|
||||
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (cursor_x, cursor_y, cursor_width, cursor_height), border_radius=10)
|
||||
pygame.draw.rect(screen, THEME_COLORS["border"], (cursor_x, cursor_y, cursor_width, cursor_height), 2, border_radius=10)
|
||||
|
||||
# Flèches gauche/droite
|
||||
arrow_size = 30
|
||||
left_arrow_x = cursor_x + 20
|
||||
right_arrow_x = cursor_x + cursor_width - arrow_size - 20
|
||||
arrow_y = cursor_y + (cursor_height - arrow_size) // 2
|
||||
|
||||
# Flèche gauche
|
||||
left_color = THEME_COLORS["fond_lignes"] if config.current_font_scale_index > 0 else THEME_COLORS["border"]
|
||||
pygame.draw.polygon(screen, left_color, [
|
||||
(left_arrow_x + arrow_size, arrow_y),
|
||||
(left_arrow_x, arrow_y + arrow_size // 2),
|
||||
(left_arrow_x + arrow_size, arrow_y + arrow_size)
|
||||
])
|
||||
|
||||
# Flèche droite
|
||||
right_color = THEME_COLORS["fond_lignes"] if config.current_font_scale_index < len(config.font_scale_options) - 1 else THEME_COLORS["border"]
|
||||
pygame.draw.polygon(screen, right_color, [
|
||||
(right_arrow_x, arrow_y),
|
||||
(right_arrow_x + arrow_size, arrow_y + arrow_size // 2),
|
||||
(right_arrow_x, arrow_y + arrow_size)
|
||||
])
|
||||
|
||||
# Texte au centre
|
||||
text_surface = config.font.render(font_text, True, THEME_COLORS["text"])
|
||||
text_rect = text_surface.get_rect(center=(cursor_x + cursor_width // 2, cursor_y + cursor_height // 2))
|
||||
screen.blit(text_surface, text_rect)
|
||||
|
||||
# Instructions
|
||||
instruction_text = _("language_select_instruction")
|
||||
instruction_surface = config.small_font.render(instruction_text, True, THEME_COLORS["text"])
|
||||
instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, config.screen_height - 100))
|
||||
screen.blit(instruction_surface, instruction_rect)
|
||||
|
||||
def handle_accessibility_events(event):
|
||||
"""Gère les événements du menu d'accessibilité avec support clavier et manette."""
|
||||
# Gestion des touches du clavier
|
||||
if event.type == pygame.KEYDOWN:
|
||||
if event.key == pygame.K_LEFT and config.current_font_scale_index > 0:
|
||||
config.current_font_scale_index -= 1
|
||||
update_font_scale()
|
||||
return True
|
||||
elif event.key == pygame.K_RIGHT and config.current_font_scale_index < len(config.font_scale_options) - 1:
|
||||
config.current_font_scale_index += 1
|
||||
update_font_scale()
|
||||
return True
|
||||
elif event.key == pygame.K_RETURN or event.key == pygame.K_ESCAPE:
|
||||
config.menu_state = "pause_menu"
|
||||
return True
|
||||
|
||||
# Gestion des boutons de la manette
|
||||
elif event.type == pygame.JOYBUTTONDOWN:
|
||||
if event.button == 0: # Bouton A (valider)
|
||||
config.menu_state = "pause_menu"
|
||||
return True
|
||||
elif event.button == 1: # Bouton B (annuler)
|
||||
config.menu_state = "pause_menu"
|
||||
return True
|
||||
|
||||
# Gestion du D-pad
|
||||
elif event.type == pygame.JOYHATMOTION:
|
||||
if event.value == (-1, 0): # Gauche
|
||||
if config.current_font_scale_index > 0:
|
||||
config.current_font_scale_index -= 1
|
||||
update_font_scale()
|
||||
return True
|
||||
elif event.value == (1, 0): # Droite
|
||||
if config.current_font_scale_index < len(config.font_scale_options) - 1:
|
||||
config.current_font_scale_index += 1
|
||||
update_font_scale()
|
||||
return True
|
||||
|
||||
# Gestion du joystick analogique (axe horizontal)
|
||||
elif event.type == pygame.JOYAXISMOTION:
|
||||
if event.axis == 0 and abs(event.value) > 0.5: # Joystick gauche horizontal
|
||||
if event.value < -0.5 and config.current_font_scale_index > 0: # Gauche
|
||||
config.current_font_scale_index -= 1
|
||||
update_font_scale()
|
||||
return True
|
||||
elif event.value > 0.5 and config.current_font_scale_index < len(config.font_scale_options) - 1: # Droite
|
||||
config.current_font_scale_index += 1
|
||||
update_font_scale()
|
||||
return True
|
||||
|
||||
return False
|
||||
def update_font_scale():
|
||||
"""Met à jour l'échelle de police et sauvegarde."""
|
||||
new_scale = config.font_scale_options[config.current_font_scale_index]
|
||||
config.accessibility_settings["font_scale"] = new_scale
|
||||
save_accessibility_settings(config.accessibility_settings)
|
||||
|
||||
# Réinitialiser les polices
|
||||
config.init_font()
|
||||
config.needs_redraw = True
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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}")
|
||||
@@ -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
|
||||
@@ -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}"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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}"
|
||||
}
|
||||
@@ -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}"
|
||||
}
|
||||
@@ -0,0 +1,600 @@
|
||||
import requests
|
||||
import subprocess
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import pygame # type: ignore
|
||||
import zipfile
|
||||
import asyncio
|
||||
import config
|
||||
from config import OTA_VERSION_ENDPOINT,APP_FOLDER, UPDATE_FOLDER, OTA_UPDATE_ZIP
|
||||
from utils import sanitize_filename, extract_zip, extract_rar, load_api_key_1fichier
|
||||
from history import save_history
|
||||
import logging
|
||||
import datetime
|
||||
import queue
|
||||
import time
|
||||
import os
|
||||
from language import _ # Import de la fonction de traduction
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
cache = {}
|
||||
CACHE_TTL = 3600 # 1 heure
|
||||
|
||||
def test_internet():
|
||||
"""Teste la connexion Internet de manière portable pour Windows et Linux/Batocera."""
|
||||
logger.debug("Test de connexion Internet")
|
||||
|
||||
# Choisir l'option ping en fonction de la plateforme
|
||||
ping_option = '-n' if sys.platform.startswith("win") else '-c'
|
||||
logger.debug(f"Utilisation de ping avec option {ping_option}")
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['ping', ping_option, '4', '8.8.8.8'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
logger.debug("Connexion Internet OK (ping)")
|
||||
return True
|
||||
else:
|
||||
logger.debug(f"Échec ping 8.8.8.8, code retour: {result.returncode}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.debug(f"Erreur test Internet (ping): {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
async def check_for_updates():
|
||||
try:
|
||||
logger.debug("Vérification de la version disponible sur le serveur")
|
||||
config.current_loading_system = _("network_checking_updates")
|
||||
config.loading_progress = 5.0
|
||||
config.needs_redraw = True
|
||||
response = requests.get(OTA_VERSION_ENDPOINT, timeout=5)
|
||||
response.raise_for_status()
|
||||
if response.headers.get("content-type") != "application/json":
|
||||
raise ValueError(f"Le fichier version.json n'est pas un JSON valide (type de contenu : {response.headers.get('content-type')})")
|
||||
version_data = response.json()
|
||||
latest_version = version_data.get("version")
|
||||
logger.debug(f"Version distante : {latest_version}, version locale : {config.app_version}")
|
||||
UPDATE_ZIP = OTA_UPDATE_ZIP.replace("RGSX.zip", f"RGSX_v{latest_version}.zip")
|
||||
logger.debug(f"URL de mise à jour : {UPDATE_ZIP}")
|
||||
if latest_version != config.app_version:
|
||||
config.current_loading_system = _("network_update_available").format(latest_version)
|
||||
config.loading_progress = 10.0
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Téléchargement du ZIP de mise à jour : {UPDATE_ZIP}")
|
||||
|
||||
# Créer le dossier UPDATE_FOLDER s'il n'existe pas
|
||||
os.makedirs(UPDATE_FOLDER, exist_ok=True)
|
||||
update_zip_path = os.path.join(UPDATE_FOLDER, f"RGSX_v{latest_version}.zip")
|
||||
logger.debug(f"Téléchargement de {UPDATE_ZIP} vers {update_zip_path}")
|
||||
|
||||
# Télécharger le ZIP
|
||||
with requests.get(UPDATE_ZIP, stream=True, timeout=10) as r:
|
||||
r.raise_for_status()
|
||||
total_size = int(r.headers.get('content-length', 0))
|
||||
downloaded = 0
|
||||
with open(update_zip_path, "wb") as f:
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
downloaded += len(chunk)
|
||||
config.loading_progress = 10.0 + (40.0 * downloaded / total_size) if total_size > 0 else 10.0
|
||||
config.needs_redraw = True
|
||||
await asyncio.sleep(0)
|
||||
logger.debug(f"ZIP téléchargé : {update_zip_path}")
|
||||
|
||||
# Extraire le contenu du ZIP dans APP_FOLDER
|
||||
config.current_loading_system = _("network_extracting_update")
|
||||
config.loading_progress = 60.0
|
||||
config.needs_redraw = True
|
||||
success, message = extract_update(update_zip_path, APP_FOLDER, UPDATE_ZIP)
|
||||
if not success:
|
||||
logger.error(f"Échec de l'extraction : {message}")
|
||||
return False, _("network_extraction_failed").format(message)
|
||||
|
||||
# Supprimer le fichier ZIP après extraction
|
||||
if os.path.exists(update_zip_path):
|
||||
os.remove(update_zip_path)
|
||||
logger.debug(f"Fichier ZIP {update_zip_path} supprimé")
|
||||
|
||||
config.current_loading_system = _("network_update_completed")
|
||||
config.loading_progress = 100.0
|
||||
config.needs_redraw = True
|
||||
logger.debug("Mise à jour terminée avec succès")
|
||||
|
||||
# Configurer la popup pour afficher le message de succès
|
||||
config.menu_state = "update_result"
|
||||
config.update_result_message = _("network_update_success").format(latest_version)
|
||||
config.update_result_error = False
|
||||
config.update_result_start_time = pygame.time.get_ticks()
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Affichage de la popup de mise à jour réussie")
|
||||
|
||||
return True, _("network_update_success_message")
|
||||
else:
|
||||
logger.debug("Aucune mise à jour disponible")
|
||||
return True, _("network_no_update_available")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur OTA : {str(e)}")
|
||||
config.menu_state = "update_result"
|
||||
config.update_result_message = _("network_update_error").format(str(e))
|
||||
config.update_result_error = True
|
||||
config.update_result_start_time = pygame.time.get_ticks()
|
||||
config.needs_redraw = True
|
||||
return False, _("network_check_update_error").format(str(e))
|
||||
|
||||
def extract_update(zip_path, dest_dir, source_url):
|
||||
|
||||
try:
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
logger.debug(f"Tentative d'ouverture du ZIP : {zip_path}")
|
||||
# Extraire le ZIP
|
||||
skipped_files = []
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||
for file_info in zip_ref.infolist():
|
||||
try:
|
||||
zip_ref.extract(file_info, dest_dir)
|
||||
except PermissionError as e:
|
||||
logger.warning(f"Impossible d'extraire {file_info.filename}: {str(e)}")
|
||||
skipped_files.append(file_info.filename)
|
||||
except Exception as e:
|
||||
logger.warning(f"Erreur lors de l'extraction de {file_info.filename}: {str(e)}")
|
||||
skipped_files.append(file_info.filename)
|
||||
|
||||
if skipped_files:
|
||||
message = _("network_extraction_partial").format(', '.join(skipped_files))
|
||||
logger.warning(message)
|
||||
return True, message # Considérer comme succès si certains fichiers sont extraits
|
||||
return True, _("network_extraction_success")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur critique lors de l'extraction du ZIP {source_url}: {str(e)}")
|
||||
return False, _("network_zip_extraction_error").format(source_url, str(e))
|
||||
|
||||
# File d'attente pour la progression - une par tâche
|
||||
progress_queues = {}
|
||||
|
||||
|
||||
|
||||
async def download_rom(url, platform, game_name, is_zip_non_supported=False, task_id=None):
|
||||
logger.debug(f"Début téléchargement: {game_name} depuis {url}, is_zip_non_supported={is_zip_non_supported}, task_id={task_id}")
|
||||
result = [None, None]
|
||||
|
||||
# Créer une queue spécifique pour cette tâche
|
||||
if task_id not in progress_queues:
|
||||
progress_queues[task_id] = queue.Queue()
|
||||
|
||||
def download_thread():
|
||||
logger.debug(f"Thread téléchargement démarré pour {url}, task_id={task_id}")
|
||||
try:
|
||||
dest_dir = None
|
||||
for platform_dict in config.platform_dicts:
|
||||
if platform_dict["platform"] == platform:
|
||||
dest_dir = os.path.join(config.ROMS_FOLDER, platform_dict.get("folder", platform.lower().replace(" ", "")))
|
||||
logger.debug(f"Répertoire de destination trouvé pour {platform}: {dest_dir}")
|
||||
break
|
||||
if not dest_dir:
|
||||
dest_dir = os.path.join(os.path.dirname(os.path.dirname(config.APP_FOLDER)), platform.lower().replace(" ", ""))
|
||||
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
if not os.access(dest_dir, os.W_OK):
|
||||
raise PermissionError(f"Pas de permission d'écriture dans {dest_dir}")
|
||||
|
||||
sanitized_name = sanitize_filename(game_name)
|
||||
dest_path = os.path.join(dest_dir, f"{sanitized_name}")
|
||||
logger.debug(f"Chemin destination: {dest_path}")
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
'Accept-Encoding': 'gzip, deflate',
|
||||
'DNT': '1',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1'
|
||||
}
|
||||
|
||||
session = requests.Session()
|
||||
session.headers.update(headers)
|
||||
|
||||
download_headers = headers.copy()
|
||||
download_headers['Accept'] = 'application/octet-stream, */*'
|
||||
download_headers['Referer'] = 'https://myrient.erista.me/'
|
||||
response = session.get(url, stream=True, timeout=30, allow_redirects=True, headers=download_headers)
|
||||
logger.debug(f"Status code: {response.status_code}")
|
||||
response.raise_for_status()
|
||||
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
logger.debug(f"Taille totale: {total_size} octets")
|
||||
|
||||
# Initialiser la progression avec task_id
|
||||
progress_queues[task_id].put((task_id, 0, total_size))
|
||||
logger.debug(f"Progression initiale envoyée: 0% pour {game_name}, task_id={task_id}")
|
||||
|
||||
downloaded = 0
|
||||
chunk_size = 4096
|
||||
last_update_time = time.time()
|
||||
update_interval = 0.1 # Mettre à jour toutes les 0,1 secondes
|
||||
with open(dest_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=chunk_size):
|
||||
if chunk:
|
||||
size_received = len(chunk)
|
||||
f.write(chunk)
|
||||
downloaded += size_received
|
||||
current_time = time.time()
|
||||
if current_time - last_update_time >= update_interval:
|
||||
progress_queues[task_id].put((task_id, downloaded, total_size))
|
||||
last_update_time = current_time
|
||||
|
||||
os.chmod(dest_path, 0o644)
|
||||
logger.debug(f"Téléchargement terminé: {dest_path}")
|
||||
|
||||
if is_zip_non_supported:
|
||||
logger.debug(f"Extraction automatique nécessaire pour {dest_path}")
|
||||
extension = os.path.splitext(dest_path)[1].lower()
|
||||
if extension == ".zip":
|
||||
try:
|
||||
if isinstance(config.history, list):
|
||||
for entry in config.history:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]:
|
||||
entry["status"] = "Extracting"
|
||||
entry["progress"] = 0
|
||||
entry["message"] = "Préparation de l'extraction..."
|
||||
save_history(config.history)
|
||||
config.needs_redraw = True
|
||||
break
|
||||
|
||||
success, msg = extract_zip(dest_path, dest_dir, url)
|
||||
if success:
|
||||
logger.debug(f"Extraction ZIP réussie: {msg}")
|
||||
result[0] = True
|
||||
result[1] = _("network_download_extract_ok").format(game_name)
|
||||
else:
|
||||
logger.error(f"Erreur extraction ZIP: {msg}")
|
||||
result[0] = False
|
||||
result[1] = _("network_extraction_failed").format(msg)
|
||||
except Exception as e:
|
||||
logger.error(f"Exception lors de l'extraction: {str(e)}")
|
||||
result[0] = False
|
||||
result[1] = f"Erreur téléchargement {game_name}: {str(e)}"
|
||||
elif extension == ".rar":
|
||||
try:
|
||||
success, msg = extract_rar(dest_path, dest_dir, url)
|
||||
if success:
|
||||
logger.debug(f"Extraction RAR réussie: {msg}")
|
||||
result[0] = True
|
||||
result[1] = _("network_download_extract_ok").format(game_name)
|
||||
else:
|
||||
logger.error(f"Erreur extraction RAR: {msg}")
|
||||
result[0] = False
|
||||
result[1] = _("network_extraction_failed").format(msg)
|
||||
except Exception as e:
|
||||
logger.error(f"Exception lors de l'extraction RAR: {str(e)}")
|
||||
result[0] = False
|
||||
result[1] = f"Erreur extraction RAR {game_name}: {str(e)}"
|
||||
else:
|
||||
logger.warning(f"Type d'archive non supporté: {extension}")
|
||||
result[0] = True
|
||||
result[1] = _("network_download_ok").format(game_name)
|
||||
else:
|
||||
result[0] = True
|
||||
result[1] = _("network_download_ok").format(game_name)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur téléchargement {url}: {str(e)}")
|
||||
result[0] = False
|
||||
result[1] = _("network_download_error").format(game_name, str(e))
|
||||
finally:
|
||||
logger.debug(f"Thread téléchargement terminé pour {url}, task_id={task_id}")
|
||||
progress_queues[task_id].put((task_id, result[0], result[1]))
|
||||
logger.debug(f"Final result sent to queue: success={result[0]}, message={result[1]}, task_id={task_id}")
|
||||
|
||||
thread = threading.Thread(target=download_thread)
|
||||
thread.start()
|
||||
|
||||
# Boucle principale pour mettre à jour la progression
|
||||
while thread.is_alive():
|
||||
try:
|
||||
task_queue = progress_queues.get(task_id)
|
||||
if task_queue:
|
||||
while not task_queue.empty():
|
||||
data = task_queue.get()
|
||||
#logger.debug(f"Progress queue data received: {data}")
|
||||
if isinstance(data[1], bool): # Fin du téléchargement
|
||||
success, message = data[1], data[2]
|
||||
if isinstance(config.history, list):
|
||||
for entry in config.history:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["downloading", "Téléchargement", "Extracting"]:
|
||||
entry["status"] = "Download_OK" if success else "Erreur"
|
||||
entry["progress"] = 100 if success else 0
|
||||
entry["message"] = message
|
||||
save_history(config.history)
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Final update in history: status={entry['status']}, progress={entry['progress']}%, message={message}, task_id={task_id}")
|
||||
break
|
||||
else:
|
||||
downloaded, total_size = data[1], data[2]
|
||||
progress_percent = int(downloaded / total_size * 100) if total_size > 0 else 0
|
||||
progress_percent = max(0, min(100, progress_percent))
|
||||
|
||||
if isinstance(config.history, list):
|
||||
for entry in config.history:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]:
|
||||
entry["progress"] = progress_percent
|
||||
entry["status"] = "Téléchargement"
|
||||
entry["downloaded_size"] = downloaded
|
||||
entry["total_size"] = total_size
|
||||
config.needs_redraw = True
|
||||
break
|
||||
await asyncio.sleep(0.1)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur mise à jour progression: {str(e)}")
|
||||
|
||||
thread.join()
|
||||
# Nettoyer la queue
|
||||
if task_id in progress_queues:
|
||||
del progress_queues[task_id]
|
||||
return result[0], result[1]
|
||||
|
||||
async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=False, task_id=None):
|
||||
config.API_KEY_1FICHIER = load_api_key_1fichier()
|
||||
logger.debug(f"Début téléchargement 1fichier: {game_name} depuis {url}, is_zip_non_supported={is_zip_non_supported}, task_id={task_id}")
|
||||
logger.debug(f"Clé API 1fichier: {'présente' if config.API_KEY_1FICHIER else 'absente'}")
|
||||
result = [None, None]
|
||||
|
||||
# Créer une queue spécifique pour cette tâche
|
||||
logger.debug(f"Création queue pour task_id={task_id}")
|
||||
if task_id not in progress_queues:
|
||||
progress_queues[task_id] = queue.Queue()
|
||||
|
||||
def download_thread():
|
||||
logger.debug(f"Thread téléchargement 1fichier démarré pour {url}, task_id={task_id}")
|
||||
try:
|
||||
link = url.split('&af=')[0]
|
||||
logger.debug(f"URL nettoyée: {link}")
|
||||
dest_dir = None
|
||||
for platform_dict in config.platform_dicts:
|
||||
if platform_dict["platform"] == platform:
|
||||
dest_dir = os.path.join(config.ROMS_FOLDER, platform_dict.get("folder", platform.lower().replace(" ", "")))
|
||||
break
|
||||
if not dest_dir:
|
||||
logger.warning(f"Aucun dossier 'folder' trouvé pour la plateforme {platform}")
|
||||
dest_dir = os.path.join(os.path.dirname(os.path.dirname(config.APP_FOLDER)), platform)
|
||||
logger.debug(f"Répertoire destination déterminé: {dest_dir}")
|
||||
|
||||
logger.debug(f"Vérification répertoire destination: {dest_dir}")
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
logger.debug(f"Répertoire créé ou existant: {dest_dir}")
|
||||
if not os.access(dest_dir, os.W_OK):
|
||||
logger.error(f"Pas de permission d'écriture dans {dest_dir}")
|
||||
raise PermissionError(f"Pas de permission d'écriture dans {dest_dir}")
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {config.API_KEY_1FICHIER}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
payload = {
|
||||
"url": link,
|
||||
"pretty": 1
|
||||
}
|
||||
logger.debug(f"Préparation requête file/info pour {link}")
|
||||
response = requests.post("https://api.1fichier.com/v1/file/info.cgi", headers=headers, json=payload, timeout=30)
|
||||
logger.debug(f"Réponse file/info reçue, code: {response.status_code}")
|
||||
response.raise_for_status()
|
||||
file_info = response.json()
|
||||
|
||||
if "error" in file_info and file_info["error"] == "Resource not found":
|
||||
logger.error(f"Le fichier {game_name} n'existe pas sur 1fichier")
|
||||
result[0] = False
|
||||
result[1] = _("network_file_not_found").format(game_name)
|
||||
return
|
||||
|
||||
filename = file_info.get("filename", "").strip()
|
||||
if not filename:
|
||||
logger.error(f"Impossible de récupérer le nom du fichier")
|
||||
result[0] = False
|
||||
result[1] = _("network_cannot_get_filename")
|
||||
return
|
||||
|
||||
sanitized_filename = sanitize_filename(filename)
|
||||
dest_path = os.path.join(dest_dir, sanitized_filename)
|
||||
logger.debug(f"Chemin destination: {dest_path}")
|
||||
logger.debug(f"Envoi requête get_token pour {link}")
|
||||
response = requests.post("https://api.1fichier.com/v1/download/get_token.cgi", headers=headers, json=payload, timeout=30)
|
||||
logger.debug(f"Réponse get_token reçue, code: {response.status_code}")
|
||||
response.raise_for_status()
|
||||
download_info = response.json()
|
||||
|
||||
final_url = download_info.get("url")
|
||||
if not final_url:
|
||||
logger.error(f"Impossible de récupérer l'URL de téléchargement")
|
||||
result[0] = False
|
||||
result[1] = _("network_cannot_get_download_url")
|
||||
return
|
||||
|
||||
logger.debug(f"URL de téléchargement obtenue: {final_url}")
|
||||
lock = threading.Lock()
|
||||
retries = 10
|
||||
retry_delay = 10
|
||||
logger.debug(f"Initialisation progression avec taille inconnue pour task_id={task_id}")
|
||||
progress_queues[task_id].put((task_id, 0, 0)) # Taille initiale inconnue
|
||||
for attempt in range(retries):
|
||||
logger.debug(f"Début tentative {attempt + 1} pour télécharger {final_url}")
|
||||
try:
|
||||
with requests.get(final_url, stream=True, headers={'User-Agent': 'Mozilla/5.0'}, timeout=30) as response:
|
||||
logger.debug(f"Réponse GET reçue, code: {response.status_code}")
|
||||
response.raise_for_status()
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
logger.debug(f"Taille totale: {total_size} octets")
|
||||
with lock:
|
||||
if isinstance(config.history, list):
|
||||
for entry in config.history:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] == "downloading":
|
||||
entry["total_size"] = total_size
|
||||
config.needs_redraw = True
|
||||
break
|
||||
progress_queues[task_id].put((task_id, 0, total_size)) # Mettre à jour la taille totale
|
||||
|
||||
downloaded = 0
|
||||
chunk_size = 8192
|
||||
last_update_time = time.time()
|
||||
update_interval = 0.1 # Mettre à jour toutes les 0,1 secondes
|
||||
logger.debug(f"Ouverture fichier: {dest_path}")
|
||||
with open(dest_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=chunk_size):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
downloaded += len(chunk)
|
||||
current_time = time.time()
|
||||
if current_time - last_update_time >= update_interval:
|
||||
with lock:
|
||||
if isinstance(config.history, list):
|
||||
for entry in config.history:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] == "downloading":
|
||||
progress_percent = int(downloaded / total_size * 100) if total_size > 0 else 0
|
||||
progress_percent = max(0, min(100, progress_percent))
|
||||
entry["progress"] = progress_percent
|
||||
entry["status"] = "Téléchargement"
|
||||
entry["downloaded_size"] = downloaded
|
||||
entry["total_size"] = total_size
|
||||
config.needs_redraw = True
|
||||
break
|
||||
progress_queues[task_id].put((task_id, downloaded, total_size))
|
||||
last_update_time = current_time
|
||||
|
||||
if is_zip_non_supported:
|
||||
with lock:
|
||||
if isinstance(config.history, list):
|
||||
for entry in config.history:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] == "Téléchargement":
|
||||
entry["progress"] = 0
|
||||
entry["status"] = "Extracting"
|
||||
config.needs_redraw = True
|
||||
break
|
||||
extension = os.path.splitext(dest_path)[1].lower()
|
||||
logger.debug(f"Début extraction, type d'archive: {extension}")
|
||||
if extension == ".zip":
|
||||
try:
|
||||
success, msg = extract_zip(dest_path, dest_dir, url)
|
||||
logger.debug(f"Extraction ZIP terminée: {msg}")
|
||||
if success:
|
||||
result[0] = True
|
||||
result[1] = _("network_download_extract_ok").format(game_name)
|
||||
else:
|
||||
logger.error(f"Erreur extraction ZIP: {msg}")
|
||||
result[0] = False
|
||||
result[1] = _("network_extraction_failed").format(msg)
|
||||
except Exception as e:
|
||||
logger.error(f"Exception lors de l'extraction ZIP: {str(e)}")
|
||||
result[0] = False
|
||||
result[1] = f"Erreur téléchargement {game_name}: {str(e)}"
|
||||
elif extension == ".rar":
|
||||
try:
|
||||
success, msg = extract_rar(dest_path, dest_dir, url)
|
||||
logger.debug(f"Extraction RAR terminée: {msg}")
|
||||
if success:
|
||||
result[0] = True
|
||||
result[1] = _("network_download_extract_ok").format(game_name)
|
||||
else:
|
||||
logger.error(f"Erreur extraction RAR: {msg}")
|
||||
result[0] = False
|
||||
result[1] = _("network_extraction_failed").format(msg)
|
||||
except Exception as e:
|
||||
logger.error(f"Exception lors de l'extraction RAR: {str(e)}")
|
||||
result[0] = False
|
||||
result[1] = f"Erreur extraction RAR {game_name}: {str(e)}"
|
||||
else:
|
||||
logger.warning(f"Type d'archive non supporté: {extension}")
|
||||
result[0] = True
|
||||
result[1] = _("network_download_ok").format(game_name)
|
||||
else:
|
||||
logger.debug(f"Application des permissions sur {dest_path}")
|
||||
os.chmod(dest_path, 0o644)
|
||||
logger.debug(f"Téléchargement terminé: {dest_path}")
|
||||
result[0] = True
|
||||
result[1] = _("network_download_ok").format(game_name)
|
||||
return
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Tentative {attempt + 1} échouée: {e}")
|
||||
if attempt < retries - 1:
|
||||
logger.debug(f"Attente de {retry_delay} secondes avant nouvelle tentative")
|
||||
time.sleep(retry_delay)
|
||||
else:
|
||||
logger.error(f"Nombre maximum de tentatives atteint")
|
||||
result[0] = False
|
||||
result[1] = _("network_download_failed").format(retries)
|
||||
return
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Erreur API 1fichier: {e}")
|
||||
result[0] = False
|
||||
result[1] = _("network_api_error").format(str(e))
|
||||
|
||||
finally:
|
||||
logger.debug(f"Thread téléchargement 1fichier terminé pour {url}, task_id={task_id}")
|
||||
progress_queues[task_id].put((task_id, result[0], result[1]))
|
||||
logger.debug(f"Résultat final envoyé à la queue: success={result[0]}, message={result[1]}, task_id={task_id}")
|
||||
|
||||
logger.debug(f"Démarrage thread pour {url}, task_id={task_id}")
|
||||
thread = threading.Thread(target=download_thread)
|
||||
thread.start()
|
||||
|
||||
# Boucle principale pour mettre à jour la progression
|
||||
logger.debug(f"Début boucle de progression pour task_id={task_id}")
|
||||
while thread.is_alive():
|
||||
try:
|
||||
task_queue = progress_queues.get(task_id)
|
||||
if task_queue:
|
||||
while not task_queue.empty():
|
||||
data = task_queue.get()
|
||||
logger.debug(f"Données queue progression reçues: {data}")
|
||||
if isinstance(data[1], bool): # Fin du téléchargement
|
||||
success, message = data[1], data[2]
|
||||
if isinstance(config.history, list):
|
||||
for entry in config.history:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["downloading", "Téléchargement", "Extracting"]:
|
||||
entry["status"] = "Download_OK" if success else "Erreur"
|
||||
entry["progress"] = 100 if success else 0
|
||||
entry["message"] = message
|
||||
save_history(config.history)
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Mise à jour finale historique: status={entry['status']}, progress={entry['progress']}%, message={message}, task_id={task_id}")
|
||||
break
|
||||
else:
|
||||
downloaded, total_size = data[1], data[2]
|
||||
progress_percent = int(downloaded / total_size * 100) if total_size > 0 else 0
|
||||
progress_percent = max(0, min(100, progress_percent))
|
||||
|
||||
if isinstance(config.history, list):
|
||||
for entry in config.history:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]:
|
||||
entry["progress"] = progress_percent
|
||||
entry["status"] = "Téléchargement"
|
||||
entry["downloaded_size"] = downloaded
|
||||
entry["total_size"] = total_size
|
||||
config.needs_redraw = True
|
||||
break
|
||||
await asyncio.sleep(0.1)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur mise à jour progression: {str(e)}")
|
||||
|
||||
logger.debug(f"Fin boucle de progression, attente fin thread pour task_id={task_id}")
|
||||
thread.join()
|
||||
logger.debug(f"Thread terminé, nettoyage queue pour task_id={task_id}")
|
||||
# Nettoyer la queue
|
||||
if task_id in progress_queues:
|
||||
del progress_queues[task_id]
|
||||
logger.debug(f"Fin download_from_1fichier, résultat: success={result[0]}, message={result[1]}")
|
||||
return result[0], result[1]
|
||||
def is_1fichier_url(url):
|
||||
"""Détecte si l'URL est un lien 1fichier."""
|
||||
return "1fichier.com" in url
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
@@ -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)}")
|
||||
@@ -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>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
Binary file not shown.
Reference in New Issue
Block a user