v1.9.7.0 - Ajout support multilangues (beta) , correction de bugs de logique, amélioration des erreurs
This commit is contained in:
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
logs/
|
||||
images/
|
||||
games/
|
||||
__pycache__/
|
||||
sources.json
|
||||
gamelist.xml
|
||||
*.log
|
||||
*.rar
|
||||
*.zip
|
||||
175
README.md
Normal file
175
README.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# 🎮 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.
|
||||
- **Mode recherche** : Filtrez les jeux par nom pour une navigation rapide.
|
||||
- **Gestion des erreurs**
|
||||
- **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 dans `/userdata/roms/ports/RGSX` 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 `/userdata/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 `/userdata/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
|
||||
> `/userdata/saves/ports/RGSX/1FichierAPI.txt`
|
||||
> si vous souhaitez télécharger depuis des liens 1Fichier.
|
||||
---
|
||||
|
||||
- Lancez RGSX depuis ports.
|
||||
- Configurez les contrôles. Ils pourront être reconfigurés via le menu pause par la suite si erreur.
|
||||
- Supprimez le fichier `/userdata/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.
|
||||
- Appuyez sur **Échap** pour ignorer une action sans la mapper.
|
||||
|
||||
---
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
### Logs
|
||||
|
||||
Les logs sont enregistrés dans `/userdata/roms/ports/RGSX/logs/RGSX.log` pour diagnostiquer les problèmes.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Structure du projet
|
||||
```
|
||||
/userdata/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.
|
||||
├── 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.
|
||||
├── utils.py # Fonctions utilitaires (wrap du texte, troncage etc.).
|
||||
└── logs/
|
||||
└── RGSX.log # Fichier de logs.
|
||||
|
||||
/userdata/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 `/userdata/roms/ports/RGSX/logs/RGSX.log`.
|
||||
2. Ouvrez une issue sur GitHub avec une description détaillée et les logs pertinents.
|
||||
|
||||
### Proposer une fonctionnalité
|
||||
|
||||
- Soumettez une issue avec une description claire de la fonctionnalité proposée.
|
||||
- Expliquez comment elle s'intègre dans l'application.
|
||||
|
||||
### Contribuer au code
|
||||
|
||||
1. Forkez le dépôt et créez une branche pour votre fonctionnalité ou correction :
|
||||
git checkout -b feature/nom-de-votre-fonctionnalité
|
||||
2. Testez vos modifications sur Batocera.
|
||||
3. Soumettez une pull request avec une description détaillée.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Problèmes connus / À implémenter
|
||||
|
||||
- Gestion des téléchargements multiples
|
||||
|
||||
---
|
||||
|
||||
## 📝 Licence
|
||||
|
||||
Ce projet est libre. Vous êtes libre de l'utiliser, le modifier et le distribuer selon les termes de cette licence.
|
||||
|
||||
Développé avec ❤️ pour les amateurs de jeux rétro.
|
||||
4
RGSX.sh
Normal file
4
RGSX.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
# Supprimer SDL_VIDEODRIVER=fbcon pour laisser SDL choisir le pilote
|
||||
# export SDL_VIDEODRIVER=fbcon
|
||||
/usr/bin/python3 /userdata/roms/ports/RGSX
|
||||
779
__main__.py
Normal file
779
__main__.py
Normal file
@@ -0,0 +1,779 @@
|
||||
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_confirm_dialog, draw_redownload_game_cache_dialog, draw_popup, draw_gradient, draw_language_menu, THEME_COLORS
|
||||
from language import update_valid_states, 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
|
||||
log_dir = os.path.join(config.APP_FOLDER, "logs")
|
||||
log_file = os.path.join(log_dir, "RGSX.log")
|
||||
try:
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
logging.basicConfig(
|
||||
filename=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 {log_file}: {str(e)}")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Initialisation de Pygame et des polices
|
||||
pygame.init()
|
||||
config.init_font()
|
||||
pygame.joystick.init()
|
||||
pygame.mouse.set_visible(True)
|
||||
|
||||
# Initialisation du sélecteur de langue
|
||||
update_valid_states()
|
||||
|
||||
# Chargement et initialisation de la langue
|
||||
from language import initialize_language
|
||||
initialize_language()
|
||||
logger.debug(f"Langue initialisée: {config.current_language}")
|
||||
|
||||
# Détection du système non-PC
|
||||
config.is_non_pc = detect_non_pc()
|
||||
|
||||
# Initialisation de l’écran
|
||||
screen = init_display()
|
||||
pygame.display.set_caption("RGSX")
|
||||
clock = pygame.time.Clock()
|
||||
|
||||
# Initialisation des polices
|
||||
try:
|
||||
font_path = os.path.join(config.APP_FOLDER, "assets", "Pixel-UniCode.ttf")
|
||||
config.font = pygame.font.Font(font_path, 36) # Police principale
|
||||
config.title_font = pygame.font.Font(font_path, 48) # Police pour les titres
|
||||
config.search_font = pygame.font.Font(font_path, 48) # Police pour la recherche
|
||||
config.progress_font = pygame.font.Font(font_path, 36) # Police pour l'affichage de la progression
|
||||
config.small_font = pygame.font.Font(font_path, 28) # Police pour les petits textes
|
||||
logger.debug("Police Pixel-UniCode chargée")
|
||||
except:
|
||||
config.font = pygame.font.SysFont("arial", 48) # Police fallback
|
||||
config.title_font = pygame.font.SysFont("arial", 60) # Police fallback pour les titres
|
||||
config.search_font = pygame.font.SysFont("arial", 60) # Police fallback pour la recherche
|
||||
config.progress_font = pygame.font.SysFont("arial", 36) # Police fallback pour l'affichage de la progression
|
||||
config.small_font = pygame.font.SysFont("arial", 28) # Police fallback pour les petits textes
|
||||
logger.debug("Police Arial chargée")
|
||||
|
||||
# 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 réelle : {config.screen_width}x{config.screen_height}")
|
||||
|
||||
# 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
|
||||
current_music_name = None
|
||||
music_popup_start_time = 0
|
||||
# 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
|
||||
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")
|
||||
|
||||
|
||||
# Chargement de l'historique
|
||||
config.history = load_history()
|
||||
logger.debug(f"Historique chargé: {len(config.history)} entrées")
|
||||
|
||||
# Vérifier si le fichier de configuration des contrôles existe
|
||||
controls_file_exists = os.path.exists(config.CONTROLS_CONFIG_PATH)
|
||||
logger.debug(f"Fichier controls.json existe: {controls_file_exists} à {config.CONTROLS_CONFIG_PATH}")
|
||||
|
||||
# Vérification et chargement de la configuration des contrôles
|
||||
config.controls_config = load_controls_config()
|
||||
|
||||
# Déterminer l'état initial de l'application
|
||||
if not controls_file_exists:
|
||||
# Si pas de fichier de contrôles, on commence par les configurer
|
||||
config.menu_state = "controls_mapping"
|
||||
config.needs_redraw = True # Forcer le redraw immédiatement
|
||||
logger.info(f"Pas de fichier de contrôles à {config.CONTROLS_CONFIG_PATH}, configuration des contrôles")
|
||||
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()
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
screen = init_display()
|
||||
clock = pygame.time.Clock()
|
||||
|
||||
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" and event.type == pygame.KEYDOWN:
|
||||
handle_language_menu_events(event, screen)
|
||||
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("value")) or
|
||||
(event.type == pygame.JOYBUTTONDOWN and start_config.get("type") == "button" and event.button == start_config.get("value")) or
|
||||
(event.type == pygame.JOYAXISMOTION and start_config.get("type") == "axis" and event.axis == start_config.get("value")[0] and abs(event.value) > 0.5 and (1 if event.value > 0 else -1) == start_config.get("value")[1]) or
|
||||
(event.type == pygame.JOYHATMOTION and start_config.get("type") == "hat" and event.value == tuple(start_config.get("value"))) or
|
||||
(event.type == pygame.MOUSEBUTTONDOWN and start_config.get("type") == "mouse" and event.button == start_config.get("value"))
|
||||
):
|
||||
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 == "controls_help":
|
||||
cancel_config = config.controls_config.get("cancel", {})
|
||||
if (
|
||||
(event.type == pygame.KEYDOWN and cancel_config and event.key == cancel_config.get("value")) or
|
||||
(event.type == pygame.JOYBUTTONDOWN and cancel_config and cancel_config.get("type") == "button" and event.button == cancel_config.get("value")) or
|
||||
(event.type == pygame.JOYAXISMOTION and cancel_config and cancel_config.get("type") == "axis" and event.axis == cancel_config.get("value")[0] and abs(event.value) > 0.5 and (1 if event.value > 0 else -1) == cancel_config.get("value")[1]) or
|
||||
(event.type == pygame.JOYHATMOTION and cancel_config and cancel_config.get("type") == "hat" and event.value == tuple(cancel_config.get("value")))
|
||||
):
|
||||
config.previous_menu_state = validate_menu_state(config.previous_menu_state)
|
||||
config.menu_state = "pause_menu"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Controls_help: Annulation, retour à pause_menu")
|
||||
continue
|
||||
|
||||
if config.menu_state == "confirm_clear_history":
|
||||
action = handle_controls(event, sources, joystick, screen)
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Événement transmis à handle_controls dans confirm_clear_history: {event.type}")
|
||||
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,
|
||||
"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 = (
|
||||
"Attention il faut renseigner sa clé API (premium only) dans le fichier /userdata/saves/ports/rgsx/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 == "redownload_game_cache":
|
||||
draw_redownload_game_cache_dialog(screen)
|
||||
elif config.menu_state == "restart_popup":
|
||||
draw_popup(screen)
|
||||
elif config.menu_state == "language_select":
|
||||
draw_language_menu(screen)
|
||||
# Ajout de log pour déboguer
|
||||
logger.debug(f"Affichage du sélecteur de langue, index={config.selected_language_index}")
|
||||
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)
|
||||
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())
|
||||
BIN
assets/Pixel-UniCode.ttf
Normal file
BIN
assets/Pixel-UniCode.ttf
Normal file
Binary file not shown.
BIN
assets/music/8bit.mp3
Normal file
BIN
assets/music/8bit.mp3
Normal file
Binary file not shown.
BIN
assets/music/90s.mp3
Normal file
BIN
assets/music/90s.mp3
Normal file
Binary file not shown.
BIN
assets/music/aquatic_ambience.mp3
Normal file
BIN
assets/music/aquatic_ambience.mp3
Normal file
Binary file not shown.
BIN
assets/music/fantasia.mp3
Normal file
BIN
assets/music/fantasia.mp3
Normal file
Binary file not shown.
BIN
assets/music/game_mode.mp3
Normal file
BIN
assets/music/game_mode.mp3
Normal file
Binary file not shown.
BIN
assets/music/pixel_racer.mp3
Normal file
BIN
assets/music/pixel_racer.mp3
Normal file
Binary file not shown.
BIN
assets/music/return_8bit.mp3
Normal file
BIN
assets/music/return_8bit.mp3
Normal file
Binary file not shown.
BIN
assets/music/stranger.mp3
Normal file
BIN
assets/music/stranger.mp3
Normal file
Binary file not shown.
148
config.py
Normal file
148
config.py
Normal file
@@ -0,0 +1,148 @@
|
||||
import pygame # type: ignore
|
||||
import os
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Version actuelle de l'application
|
||||
app_version = "1.9.7.0"
|
||||
|
||||
# Langue par défaut
|
||||
current_language = "fr"
|
||||
|
||||
|
||||
# URL
|
||||
OTA_SERVER_URL = "https://retrogamesets.fr/softs"
|
||||
OTA_VERSION_ENDPOINT = f"{OTA_SERVER_URL}/version.json"
|
||||
OTA_UPDATE_ZIP = f"{OTA_SERVER_URL}/RGSX.zip"
|
||||
OTA_data_ZIP = f"{OTA_SERVER_URL}/rgsx-data.zip"
|
||||
|
||||
# Chemins de base
|
||||
APP_FOLDER = "/userdata/roms/ports/RGSX"
|
||||
SAVE_FOLDER = "/userdata/saves/ports/rgsx"
|
||||
UPDATE_FOLDER = f"{APP_FOLDER}/update"
|
||||
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")
|
||||
|
||||
|
||||
# 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
|
||||
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 = {}
|
||||
|
||||
# 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
|
||||
pending_download = None
|
||||
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
|
||||
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()
|
||||
|
||||
|
||||
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
|
||||
try:
|
||||
FONT = pygame.font.Font(None, 36)
|
||||
progress_font = pygame.font.Font(None, 28)
|
||||
title_font = pygame.font.Font(None, 48)
|
||||
search_font = pygame.font.Font(None, 36)
|
||||
small_font = pygame.font.Font(None, 24)
|
||||
logger.debug("Polices initialisées avec succès")
|
||||
# amazonq-ignore-next-line
|
||||
except pygame.error as e:
|
||||
logger.error(f"Erreur lors de l'initialisation des polices : {e}")
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
API_KEY_1FICHIER = "" # Initialisation de la variable globale pour la clé API
|
||||
1079
controls.py
Normal file
1079
controls.py
Normal file
File diff suppressed because it is too large
Load Diff
516
controls_mapper.py
Normal file
516
controls_mapper.py
Normal file
@@ -0,0 +1,516 @@
|
||||
import pygame # type: ignore
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
import config
|
||||
from config import CONTROLS_CONFIG_PATH
|
||||
from display import draw_gradient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Chemin du fichier de configuration des contrôles
|
||||
CONTROLS_CONFIG_PATH = "/userdata/saves/ports/rgsx/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": "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/Triangle)"},
|
||||
{"name": "progress", "display": "Progression", "description": "Historique : Effacer la liste (Recommandé: X/Carré)"},
|
||||
{"name": "filter", "display": "Filtrer", "description": "Ouvrir filtre (Recommandé: F, Select)"},
|
||||
{"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)"},
|
||||
{"name": "start", "display": "Start", "description": "Menu pause / Paramètres (Recommandé: Start, AltGr)"},
|
||||
]
|
||||
|
||||
# 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
|
||||
1073742051: pygame.K_RSHIFT, # Alt droit
|
||||
1073742049: pygame.K_LCTRL, # Ctrl gauche
|
||||
1073742053: pygame.K_RCTRL, # Ctrl droit
|
||||
1073742048: pygame.K_LSHIFT, # Shift gauche
|
||||
1073742054: pygame.K_RALT, # 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: "/",
|
||||
}
|
||||
|
||||
# Noms lisibles pour les boutons de manette
|
||||
BUTTON_NAMES = {
|
||||
0: "A",
|
||||
1: "B",
|
||||
2: "X",
|
||||
3: "Y",
|
||||
4: "LB",
|
||||
5: "RB",
|
||||
6: "LT",
|
||||
7: "RT",
|
||||
8: "Select",
|
||||
9: "Start",
|
||||
}
|
||||
|
||||
# Noms pour les axes de joystick
|
||||
AXIS_NAMES = {
|
||||
(0, 1): "Joy G Haut",
|
||||
(0, -1): "Joy G Bas",
|
||||
(1, 1): "Joy G Gauche",
|
||||
(1, -1): "Joy G Droite",
|
||||
(2, 1): "Joy D Haut",
|
||||
(2, -1): "Joy D Bas",
|
||||
(3, 1): "Joy D Gauche",
|
||||
(3, -1): "Joy D Droite",
|
||||
}
|
||||
|
||||
# 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
|
||||
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é, 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}")
|
||||
elif event.type == pygame.JOYHATMOTION:
|
||||
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):
|
||||
mapping = True
|
||||
current_action = 0
|
||||
clock = pygame.time.Clock()
|
||||
while mapping:
|
||||
clock.tick(100) # 100 FPS
|
||||
for event in pygame.event.get():
|
||||
# Initialisation des variables de contrôle
|
||||
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 # Pour le débouncing des événements JOYHATMOTION
|
||||
|
||||
# Initialiser l'état des boutons et axes pour suivre les relâchements
|
||||
held_keys = set()
|
||||
held_buttons = set()
|
||||
held_axes = {} # {axis: direction}
|
||||
held_hats = {} # {hat: value}
|
||||
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
|
||||
|
||||
events = pygame.event.get()
|
||||
for event in events:
|
||||
if event.type == pygame.QUIT:
|
||||
return False
|
||||
|
||||
# Détecter les relâchements pour réinitialiser
|
||||
if event.type == pygame.KEYUP:
|
||||
if 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
|
||||
logger.debug(f"Touche relâchée: {event.key}")
|
||||
elif event.type == pygame.JOYBUTTONUP:
|
||||
if 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
|
||||
logger.debug(f"Bouton relâché: {event.button}")
|
||||
elif event.type == pygame.MOUSEBUTTONUP:
|
||||
if 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
|
||||
logger.debug(f"Bouton souris relâché: {event.button}")
|
||||
elif event.type == pygame.JOYAXISMOTION:
|
||||
if abs(event.value) < 0.5: # Axe revenu à la position neutre
|
||||
if event.axis in held_axes:
|
||||
del held_axes[event.axis]
|
||||
if current_input and current_input["type"] == "axis" and current_input["value"][0] == event.axis:
|
||||
current_input = None
|
||||
input_held_time = 0
|
||||
last_input_name = None
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Axe relâché: {event.axis}")
|
||||
elif event.type == pygame.JOYHATMOTION:
|
||||
logger.debug(f"JOYHATMOTION détecté: hat={event.hat}, value={event.value}")
|
||||
if event.value == (0, 0): # D-Pad revenu à la position neutre
|
||||
if event.hat in held_hats:
|
||||
del held_hats[event.hat]
|
||||
if current_input and current_input["type"] == "hat" and current_input["value"] == event.value:
|
||||
current_input = None
|
||||
input_held_time = 0
|
||||
last_input_name = None
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"D-Pad relâché: {event.hat}")
|
||||
continue # Ignorer les événements (0, 0) pour la détection des nouvelles entrées
|
||||
|
||||
# Détecter les nouvelles entrées
|
||||
if event.type in (pygame.KEYDOWN, pygame.JOYBUTTONDOWN, pygame.JOYAXISMOTION, pygame.JOYHATMOTION, pygame.MOUSEBUTTONDOWN):
|
||||
# Appliquer le débouncing pour JOYHATMOTION
|
||||
if event.type == pygame.JOYHATMOTION and (current_time - last_joyhat_time) < JOYHAT_DEBOUNCE:
|
||||
logger.debug(f"Événement JOYHATMOTION ignoré (debounce): hat={event.hat}, value={event.value}")
|
||||
continue
|
||||
if event.type == pygame.JOYHATMOTION:
|
||||
last_joyhat_time = current_time
|
||||
|
||||
|
||||
input_name = get_readable_input_name(event)
|
||||
if input_name != "Inconnu":
|
||||
input_type = {
|
||||
pygame.KEYDOWN: "key",
|
||||
pygame.JOYBUTTONDOWN: "button",
|
||||
pygame.JOYAXISMOTION: "axis",
|
||||
pygame.JOYHATMOTION: "hat",
|
||||
pygame.MOUSEBUTTONDOWN: "mouse",
|
||||
}[event.type]
|
||||
input_value = (
|
||||
SDL_TO_PYGAME_KEY.get(event.key, event.key) if event.type == pygame.KEYDOWN else
|
||||
event.button if event.type == pygame.JOYBUTTONDOWN else
|
||||
(event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION and abs(event.value) > 0.5 else
|
||||
event.value if event.type == pygame.JOYHATMOTION else
|
||||
event.button
|
||||
)
|
||||
|
||||
# Vérifier si l'entrée est nouvelle ou différente
|
||||
if (current_input is None or
|
||||
(input_type == "key" and current_input["value"] != input_value) or
|
||||
(input_type == "button" and current_input["value"] != input_value) or
|
||||
(input_type == "axis" and current_input["value"] != input_value) or
|
||||
(input_type == "hat" and current_input["value"] != input_value) or
|
||||
(input_type == "mouse" and 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
|
||||
logger.debug(f"Nouvelle entrée détectée: {input_type}:{input_value} ({input_name})")
|
||||
|
||||
# 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)
|
||||
|
||||
# Désactivation du passage avec Échap
|
||||
# Aucun code ici pour empêcher de sauter les actions avec Échap
|
||||
|
||||
# Mettre à jour le temps de maintien
|
||||
if current_input:
|
||||
input_held_time += delta_time
|
||||
if input_held_time >= HOLD_DURATION:
|
||||
action_name = ACTIONS[current_action_index]["name"]
|
||||
logger.debug(f"Entrée validée pour {action_name}: {current_input['type']}:{current_input['value']} ({last_input_name})")
|
||||
controls_config[action_name] = {
|
||||
"type": current_input["type"],
|
||||
"value": current_input["value"],
|
||||
"display": last_input_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 pour éviter les interférences
|
||||
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
|
||||
pass
|
||||
|
||||
def save_controls_config(config):
|
||||
#Enregistre la configuration des contrôles dans un fichier JSON
|
||||
try:
|
||||
with open(CONTROLS_CONFIG_PATH, "w") as f:
|
||||
json.dump(config, f, indent=4)
|
||||
logger.debug("Configuration des contrôles enregistrée")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'enregistrement de controls.json : {e}")
|
||||
return False
|
||||
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)
|
||||
1244
display.py
Normal file
1244
display.py
Normal file
File diff suppressed because it is too large
Load Diff
85
history.py
Normal file
85
history.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
import config
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Chemin par défaut pour history.json
|
||||
DEFAULT_HISTORY_PATH = os.path.join(config.SAVE_FOLDER, "history.json")
|
||||
|
||||
def init_history():
|
||||
"""Initialise le fichier history.json s'il n'existe pas."""
|
||||
history_path = getattr(config, 'HISTORY_PATH', DEFAULT_HISTORY_PATH)
|
||||
# Vérifie si le fichier history.json existe, sinon le crée
|
||||
if not os.path.exists(history_path):
|
||||
try:
|
||||
os.makedirs(os.path.dirname(history_path), exist_ok=True)
|
||||
with open(history_path, "w", encoding='utf-8') as f:
|
||||
json.dump([], f) # Initialise avec une liste vide
|
||||
logger.info(f"Fichier d'historique créé : {history_path}")
|
||||
except OSError as e:
|
||||
logger.error(f"Erreur lors de la création du fichier d'historique : {e}")
|
||||
else:
|
||||
logger.info(f"Fichier d'historique trouvé : {history_path}")
|
||||
return history_path
|
||||
|
||||
def load_history():
|
||||
"""Charge l'historique depuis history.json."""
|
||||
history_path = getattr(config, 'HISTORY_PATH', DEFAULT_HISTORY_PATH)
|
||||
try:
|
||||
if not os.path.exists(history_path):
|
||||
logger.debug(f"Aucun fichier d'historique trouvé à {history_path}")
|
||||
return []
|
||||
with open(history_path, "r", encoding='utf-8') as f:
|
||||
history = json.load(f)
|
||||
# Valider la structure : liste de dictionnaires avec 'platform', 'game_name', 'status'
|
||||
for entry in history:
|
||||
if not all(key in entry for key in ['platform', 'game_name', 'status']):
|
||||
logger.warning(f"Entrée d'historique invalide : {entry}")
|
||||
return []
|
||||
logger.debug(f"Historique chargé depuis {history_path}, {len(history)} entrées")
|
||||
return history
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
logger.error(f"Erreur lors de la lecture de {history_path} : {e}")
|
||||
return []
|
||||
|
||||
def save_history(history):
|
||||
"""Sauvegarde l'historique dans history.json."""
|
||||
history_path = getattr(config, 'HISTORY_PATH', DEFAULT_HISTORY_PATH)
|
||||
try:
|
||||
os.makedirs(os.path.dirname(history_path), exist_ok=True)
|
||||
with open(history_path, "w", encoding='utf-8') as f:
|
||||
json.dump(history, f, indent=2, ensure_ascii=False)
|
||||
logger.debug(f"Historique sauvegardé dans {history_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'écriture de {history_path} : {e}")
|
||||
|
||||
def add_to_history(platform, game_name, status, url=None, progress=0, message=None, timestamp=None):
|
||||
"""Ajoute une entrée à l'historique."""
|
||||
history = load_history()
|
||||
entry = {
|
||||
"platform": platform,
|
||||
"game_name": game_name,
|
||||
"status": status,
|
||||
"url": url,
|
||||
"progress": progress,
|
||||
"timestamp": timestamp or datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
if message:
|
||||
entry["message"] = message
|
||||
history.append(entry)
|
||||
save_history(history)
|
||||
logger.info(f"Ajout à l'historique : platform={platform}, game_name={game_name}, status={status}, progress={progress}")
|
||||
return entry
|
||||
|
||||
def clear_history():
|
||||
"""Vide l'historique."""
|
||||
history_path = getattr(config, 'HISTORY_PATH', DEFAULT_HISTORY_PATH)
|
||||
try:
|
||||
with open(history_path, "w", encoding='utf-8') as f:
|
||||
json.dump([], f)
|
||||
logger.info(f"Historique vidé : {history_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du vidage de {history_path} : {e}")
|
||||
349
language.py
Normal file
349
language.py
Normal file
@@ -0,0 +1,349 @@
|
||||
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)
|
||||
|
||||
logger.debug(f"Préférence de langue chargée: {lang_code}")
|
||||
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
|
||||
159
languages/en.json
Normal file
159
languages/en.json
Normal file
@@ -0,0 +1,159 @@
|
||||
{
|
||||
"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}",
|
||||
|
||||
"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_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": "Menu",
|
||||
|
||||
"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}"
|
||||
}
|
||||
159
languages/fr.json
Normal file
159
languages/fr.json
Normal file
@@ -0,0 +1,159 @@
|
||||
{
|
||||
"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}",
|
||||
|
||||
"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_redownload_cache": "Retélécharger le cache des jeux",
|
||||
"menu_quit": "Quitter",
|
||||
|
||||
"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": "Menu",
|
||||
|
||||
"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)",
|
||||
|
||||
"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",
|
||||
"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}"
|
||||
}
|
||||
595
network.py
Normal file
595
network.py
Normal file
@@ -0,0 +1,595 @@
|
||||
import requests
|
||||
import subprocess
|
||||
import os
|
||||
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 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():
|
||||
logger.debug("Test de connexion Internet")
|
||||
try:
|
||||
result = subprocess.run(['ping', '-c', '4', '8.8.8.8'], capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
logger.debug("Connexion Internet OK")
|
||||
return True
|
||||
else:
|
||||
logger.debug("Échec ping 8.8.8.8")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.debug(f"Erreur test Internet: {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, "RGSX.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:
|
||||
# Vérifier et ajuster les permissions du répertoire de destination
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
try:
|
||||
subprocess.run(["chmod", "-R", "u+rw", dest_dir], check=True)
|
||||
logger.debug(f"Permissions ajustées pour {dest_dir}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.warning(f"Impossible d'ajuster les permissions pour {dest_dir}: {str(e)}")
|
||||
|
||||
# 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
|
||||
import queue
|
||||
progress_queue = queue.Queue()
|
||||
|
||||
|
||||
|
||||
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]
|
||||
|
||||
# Vider la file d'attente avant de commencer
|
||||
while not progress_queue.empty():
|
||||
try:
|
||||
progress_queue.get_nowait()
|
||||
logger.debug(f"File progress_queue vidée pour {game_name}")
|
||||
except queue.Empty:
|
||||
break
|
||||
|
||||
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 = platform_dict.get("folder")
|
||||
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'}
|
||||
response = requests.get(url, stream=True, headers=headers, timeout=30)
|
||||
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_queue.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.5 # Mettre à jour toutes les 0,5 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:
|
||||
# Calculer le pourcentage correctement et le limiter entre 0 et 100
|
||||
progress_percent = int(downloaded / total_size * 100) if total_size > 0 else 0
|
||||
progress_percent = max(0, min(100, progress_percent))
|
||||
progress_queue.put((task_id, downloaded, total_size))
|
||||
last_update_time = current_time
|
||||
else:
|
||||
logger.debug("Chunk vide reçu")
|
||||
|
||||
os.chmod(dest_path, 0o644)
|
||||
logger.debug(f"Téléchargement terminé: {dest_path}")
|
||||
|
||||
# Vérifier si l'extraction est nécessaire pour les archives non supportées
|
||||
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:
|
||||
# Mettre à jour le statut avant l'extraction
|
||||
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_queue.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:
|
||||
while not progress_queue.empty():
|
||||
data = progress_queue.get()
|
||||
logger.debug(f"Progress queue data received: {data}")
|
||||
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]
|
||||
# Vérifier si config.history est une liste avant d'itérer
|
||||
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
|
||||
# Utiliser une variable intermédiaire pour stocker le message
|
||||
message_text = message
|
||||
entry["message"] = message_text
|
||||
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]
|
||||
# Calculer le pourcentage correctement et le limiter entre 0 et 100
|
||||
progress_percent = int(downloaded / total_size * 100) if total_size > 0 else 0
|
||||
progress_percent = max(0, min(100, progress_percent))
|
||||
|
||||
# Vérifier si config.history est une liste avant d'itérer
|
||||
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.2)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur mise à jour progression: {str(e)}")
|
||||
|
||||
thread.join()
|
||||
logger.debug(f"Thread joined for {url}, task_id={task_id}")
|
||||
return result[0], result[1]
|
||||
|
||||
async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=False, task_id=None):
|
||||
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}")
|
||||
result = [None, None]
|
||||
|
||||
# Vider la file d'attente avant de commencer
|
||||
while not progress_queue.empty():
|
||||
try:
|
||||
progress_queue.get_nowait()
|
||||
logger.debug(f"File progress_queue vidée pour {game_name}")
|
||||
except queue.Empty:
|
||||
break
|
||||
|
||||
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]
|
||||
dest_dir = None
|
||||
for platform_dict in config.platform_dicts:
|
||||
if platform_dict["platform"] == platform:
|
||||
dest_dir = platform_dict.get("folder")
|
||||
break
|
||||
if not dest_dir:
|
||||
logger.warning(f"Aucun dossier 'folder' trouvé pour la plateforme {platform}")
|
||||
dest_dir = os.path.join(os.path.dirname(os.path.dirname(config.APP_FOLDER)), platform)
|
||||
|
||||
logger.debug(f"Vérification répertoire destination: {dest_dir}")
|
||||
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}")
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {config.API_KEY_1FICHIER}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
payload = {
|
||||
"url": link,
|
||||
"pretty": 1
|
||||
}
|
||||
|
||||
logger.debug(f"Envoi requête POST à https://api.1fichier.com/v1/file/info.cgi pour {url}")
|
||||
response = requests.post("https://api.1fichier.com/v1/file/info.cgi", headers=headers, json=payload, timeout=30)
|
||||
logger.debug(f"Réponse reçue, status: {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("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 POST à https://api.1fichier.com/v1/download/get_token.cgi pour {url}")
|
||||
response = requests.post("https://api.1fichier.com/v1/download/get_token.cgi", headers=headers, json=payload, timeout=30)
|
||||
logger.debug(f"Réponse reçue, status: {response.status_code}")
|
||||
response.raise_for_status()
|
||||
download_info = response.json()
|
||||
|
||||
final_url = download_info.get("url")
|
||||
if not final_url:
|
||||
logger.error("Impossible de récupérer l'URL de téléchargement")
|
||||
result[0] = False
|
||||
result[1] = _("network_cannot_get_download_url")
|
||||
return
|
||||
|
||||
lock = threading.Lock()
|
||||
retries = 10
|
||||
retry_delay = 10
|
||||
# Initialiser la progression avec task_id
|
||||
progress_queue.put((task_id, 0, 0)) # Taille initiale inconnue
|
||||
logger.debug(f"Progression initiale envoyée: 0% pour {game_name}, task_id={task_id}")
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
logger.debug(f"Tentative {attempt + 1} : Envoi requête GET à {final_url}")
|
||||
with requests.get(final_url, stream=True, headers={'User-Agent': 'Mozilla/5.0'}, timeout=30) as response:
|
||||
logger.debug(f"Réponse reçue, status: {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:
|
||||
# Vérifier si config.history est une liste avant d'itérer
|
||||
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_queue.put((task_id, 0, total_size)) # Mettre à jour la taille totale
|
||||
|
||||
downloaded = 0
|
||||
chunk_size = 8192
|
||||
last_update_time = time.time()
|
||||
update_interval = 0.5 # Mettre à jour toutes les 0,5 secondes
|
||||
with open(dest_path, 'wb') as f:
|
||||
logger.debug(f"Ouverture fichier: {dest_path}")
|
||||
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:
|
||||
# Vérifier si config.history est une liste avant d'itérer
|
||||
if isinstance(config.history, list):
|
||||
for entry in config.history:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] == "downloading":
|
||||
# Calculer le pourcentage correctement et le limiter entre 0 et 100
|
||||
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
|
||||
logger.debug(f"Progression mise à jour: {entry['progress']:.1f}% pour {game_name}")
|
||||
break
|
||||
progress_queue.put((task_id, downloaded, total_size))
|
||||
last_update_time = current_time
|
||||
|
||||
if is_zip_non_supported:
|
||||
with lock:
|
||||
# Vérifier si config.history est une liste avant d'itérer
|
||||
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()
|
||||
if extension == ".zip":
|
||||
try:
|
||||
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:
|
||||
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:
|
||||
time.sleep(retry_delay)
|
||||
else:
|
||||
logger.error("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_queue.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)
|
||||
logger.debug(f"Démarrage thread pour {url}, task_id={task_id}")
|
||||
thread.start()
|
||||
|
||||
# Boucle principale pour mettre à jour la progression
|
||||
while thread.is_alive():
|
||||
try:
|
||||
while not progress_queue.empty():
|
||||
data = progress_queue.get()
|
||||
logger.debug(f"Progress queue data received: {data}")
|
||||
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]
|
||||
# Vérifier si config.history est une liste avant d'itérer
|
||||
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
|
||||
# Utiliser une variable intermédiaire pour stocker le message
|
||||
message_text = message
|
||||
entry["message"] = message_text
|
||||
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]
|
||||
# Calculer le pourcentage correctement et le limiter entre 0 et 100
|
||||
progress_percent = int(downloaded / total_size * 100) if total_size > 0 else 0
|
||||
progress_percent = max(0, min(100, progress_percent))
|
||||
|
||||
# Vérifier si config.history est une liste avant d'itérer
|
||||
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.2)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur mise à jour progression: {str(e)}")
|
||||
|
||||
thread.join()
|
||||
logger.debug(f"Thread joined for {url}, task_id={task_id}")
|
||||
return result[0], result[1]
|
||||
|
||||
|
||||
def is_1fichier_url(url):
|
||||
"""Détecte si l'URL est un lien 1fichier."""
|
||||
return "1fichier.com" in url
|
||||
2317
rom_extensions.json
Normal file
2317
rom_extensions.json
Normal file
File diff suppressed because it is too large
Load Diff
87
update_gamelist.py
Normal file
87
update_gamelist.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import os
|
||||
import xml.dom.minidom
|
||||
import xml.etree.ElementTree as ET
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
GAMELIST_FILE = "/userdata/roms/ports/gamelist.xml"
|
||||
RGSX_ENTRY = {
|
||||
"path": "./RGSX/RGSX.sh",
|
||||
"name": "RGSX",
|
||||
"desc": "Retro Games Sets X - Games Downloader",
|
||||
"image": "./images/RGSX.png",
|
||||
"marquee": "./images/RGSX.png",
|
||||
"thumbnail": "./images/RGSX.png",
|
||||
"fanart": "./images/RGSX.png",
|
||||
"rating": "1",
|
||||
"releasedate": "20250620T165718",
|
||||
"developer": "RetroGameSets.fr",
|
||||
"genre": "Compilation, Various / Utilities",
|
||||
"playcount": "251",
|
||||
"lastplayed": "20250621T234656",
|
||||
"gametime": "30830",
|
||||
"lang": "fr"
|
||||
}
|
||||
|
||||
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(GAMELIST_FILE) or os.path.getsize(GAMELIST_FILE) == 0:
|
||||
logger.info(f"Création de {GAMELIST_FILE}")
|
||||
root = ET.Element("gameList")
|
||||
else:
|
||||
try:
|
||||
logger.info(f"Lecture de {GAMELIST_FILE}")
|
||||
tree = ET.parse(GAMELIST_FILE)
|
||||
root = tree.getroot()
|
||||
if root.tag != "gameList":
|
||||
logger.info(f"{GAMELIST_FILE} n'a pas de balise <gameList>, création d'une nouvelle structure")
|
||||
root = ET.Element("gameList")
|
||||
except ET.ParseError:
|
||||
logger.info(f"{GAMELIST_FILE} 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(GAMELIST_FILE, 'w', encoding='utf-8') as f:
|
||||
f.write(pretty_xml)
|
||||
logger.info(f"{GAMELIST_FILE} mis à jour avec succès")
|
||||
|
||||
# Définir les permissions
|
||||
os.chmod(GAMELIST_FILE, 0o644)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la mise à jour de {GAMELIST_FILE}: {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()
|
||||
623
utils.py
Normal file
623
utils.py
Normal file
@@ -0,0 +1,623 @@
|
||||
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
|
||||
from config import JSON_EXTENSIONS, SAVE_FOLDER
|
||||
from history import save_history
|
||||
from language import _ # Import de la fonction de traduction
|
||||
|
||||
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 = 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)
|
||||
for system in extensions_data:
|
||||
if system["folder"] == dest_dir:
|
||||
return extension in system["extensions"]
|
||||
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 = platform_dict.get("system_image")
|
||||
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):
|
||||
"""Joue une musique aléatoire et configure l'événement de fin."""
|
||||
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}")
|
||||
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
|
||||
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."""
|
||||
global current_music_name, music_popup_start_time
|
||||
current_music_name = f"♬ {os.path.splitext(music_name)[0]}" # Utilise l'emoji ♬ directement
|
||||
music_popup_start_time = pygame.time.get_ticks() / 1000 # Temps actuel en secondes
|
||||
|
||||
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")
|
||||
try:
|
||||
# Vérifie si le fichier existe déjà
|
||||
if not os.path.exists(api_path):
|
||||
# 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}")
|
||||
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 chargée : {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.")
|
||||
return api_key
|
||||
except OSError as e:
|
||||
logger.error(f"Erreur lors de la lecture de la clé API : {e}")
|
||||
return ""
|
||||
Reference in New Issue
Block a user