v1.9.7.0 - Ajout support multilangues (beta) , correction de bugs de logique, amélioration des erreurs
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
logs/
|
||||||
|
images/
|
||||||
|
games/
|
||||||
|
__pycache__/
|
||||||
|
sources.json
|
||||||
|
gamelist.xml
|
||||||
|
*.log
|
||||||
|
*.rar
|
||||||
|
*.zip
|
||||||
@@ -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.
|
||||||
@@ -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
@@ -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())
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,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
File diff suppressed because it is too large
Load Diff
@@ -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
File diff suppressed because it is too large
Load Diff
+85
@@ -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
@@ -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
|
||||||
@@ -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}"
|
||||||
|
}
|
||||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||||
@@ -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