v1.9.4 correction de quelques bugs d'affichage

This commit is contained in:
skymike03
2025-07-12 18:40:58 +02:00
parent fb19ef9b60
commit 0bc8fdc154
7 changed files with 470 additions and 379 deletions

View File

@@ -1,21 +1,19 @@
import pygame# type: ignore
import os import os
os.environ["SDL_FBDEV"] = "/dev/fb0" os.environ["SDL_FBDEV"] = "/dev/fb0"
import pygame # type: ignore
import asyncio import asyncio
import platform import platform
import subprocess
import logging import logging
import requests import requests
import sys import config
import json from config import logger
import random from display import init_display, draw_loading_screen, draw_error_screen, draw_platform_grid, draw_progress_screen, draw_controls, draw_gradient, 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, THEME_COLORS, draw_music_popup
from display import init_display, draw_loading_screen, draw_error_screen, draw_platform_grid, draw_progress_screen, draw_controls, draw_gradient, 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, THEME_COLORS, set_music_popup, draw_music_popup from network import test_internet, download_rom, check_extension_before_download, extract_zip, check_for_updates
from network import test_internet, download_rom, check_extension_before_download, extract_zip
from controls import handle_controls, validate_menu_state from controls import handle_controls, validate_menu_state
from controls_mapper import load_controls_config, map_controls, draw_controls_mapping, ACTIONS from controls_mapper import load_controls_config, map_controls, draw_controls_mapping, ACTIONS
from utils import load_games, write_unavailable_systems from utils import play_random_music, load_sources, detect_non_pc
from history import load_history from history import load_history
import config from config import OTA_data_ZIP
# Configuration du logging # Configuration du logging
log_dir = "/userdata/roms/ports/RGSX/logs" log_dir = "/userdata/roms/ports/RGSX/logs"
@@ -36,61 +34,45 @@ except Exception as e:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# URL du serveur OTA # Initialisation de Pygame
OTA_SERVER_URL = "https://retrogamesets.fr/softs"
OTA_VERSION_ENDPOINT = f"{OTA_SERVER_URL}/version.json"
OTA_UPDATE_SCRIPT = f"{OTA_SERVER_URL}/rgsx-update.sh"
OTA_data_ZIP = f"{OTA_SERVER_URL}/rgsx-data.zip"
# Constantes pour la répétition automatique dans pause_menu
REPEAT_DELAY = 300 # Délai initial avant répétition (ms)
REPEAT_INTERVAL = 150 # Intervalle entre répétitions (ms), augmenté pour réduire la fréquence
REPEAT_ACTION_DEBOUNCE = 100 # Délai anti-rebond pour répétitions (ms), augmenté pour éviter les répétitions excessives
# Initialisation de Pygame et des polices
pygame.init() pygame.init()
config.init_font() config.init_font()
pygame.joystick.init() pygame.joystick.init()
pygame.mouse.set_visible(True) pygame.mouse.set_visible(True)
# Détection système non-PC # Détection du système
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
config.is_non_pc = detect_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 # Initialisation des polices
try: try:
config.font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 36) config.font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 36)
config.title_font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 48) config.title_font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 48)
config.search_font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 48) config.search_font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 48)
config.progress_font = pygame.font.SysFont("arial", 36) # Police pour l'affichage de la progression config.progress_font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 36)
config.small_font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 28) # Police pour les petits textes config.small_font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 28)
logger.debug("Police Pixel-UniCode chargée") logger.debug("Police Pixel-UniCode chargée")
except: except:
config.font = pygame.font.SysFont("arial", 48) # Police fallback config.font = pygame.font.SysFont("arial", 48)
config.title_font = pygame.font.SysFont("arial", 60) # Police fallback pour les titres config.title_font = pygame.font.SysFont("arial", 60)
config.search_font = pygame.font.SysFont("arial", 60) # Police fallback pour la recherche config.search_font = pygame.font.SysFont("arial", 60)
config.progress_font = pygame.font.SysFont("arial", 36) # Police fallback pour l'affichage de la progression config.progress_font = pygame.font.SysFont("arial", 36)
config.small_font = pygame.font.SysFont("arial", 28) # Police fallback pour les petits textes config.small_font = pygame.font.SysFont("arial", 28)
logger.debug("Police Arial chargée") logger.debug("Police Arial chargée")
# Initialisation de lécran
screen = init_display()
pygame.display.set_caption("RGSX")
# Afficher un écran de chargement initial
draw_gradient(screen, THEME_COLORS["background_top"], THEME_COLORS["background_bottom"])
loading_text = config.font.render("Initialisation...", True, (255, 255, 255))
text_rect = loading_text.get_rect(center=(config.screen_width // 2, config.screen_height // 2))
screen.blit(loading_text, text_rect)
pygame.display.flip()
logger.debug("Écran de chargement initial affiché")
# Mise à jour de la résolution dans config # Mise à jour de la résolution dans config
config.screen_width, config.screen_height = pygame.display.get_surface().get_size() 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}") logger.debug(f"Résolution réelle : {config.screen_width}x{config.screen_height}")
@@ -101,12 +83,6 @@ config.selected_platform = 0
config.selected_key = (0, 0) config.selected_key = (0, 0)
config.transition_state = "none" 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
# Chargement de l'historique # Chargement de l'historique
config.history = load_history() config.history = load_history()
logger.debug(f"Historique chargé: {len(config.history)} entrées") logger.debug(f"Historique chargé: {len(config.history)} entrées")
@@ -128,144 +104,9 @@ if pygame.joystick.get_count() > 0:
# Initialisation de pygame.mixer # Initialisation de pygame.mixer
pygame.mixer.init() pygame.mixer.init()
# Dossier musique Batocera
music_folder = "/userdata/roms/ports/RGSX/assets/music"
music_files = [f for f in os.listdir(music_folder) if f.lower().endswith(('.ogg', '.mp3'))]
current_music = None # Suivre la musique en cours
def play_random_music():
"""Joue une musique aléatoire et configure l'événement de fin."""
global current_music
if music_files:
# Éviter de rejouer la même musique consécutivement
available_music = [f for f in music_files if f != current_music]
if not available_music: # Si une seule musique, on la reprend
available_music = music_files
music_file = random.choice(available_music)
music_path = os.path.join(music_folder, music_file)
logger.debug(f"Lecture de la musique : {music_path}")
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
current_music = music_file # Mettre à jour la musique en cours
set_music_popup(music_file) # Afficher le nom de la musique dans la popup
else:
logger.debug("Aucune musique trouvée dans /userdata/roms/ports/RGSX/assets/music")
# Jouer la première musique au démarrage # Jouer la première musique au démarrage
play_random_music() play_random_music()
# Fonction pour charger sources.json
def load_sources():
sources_path = "/userdata/roms/ports/RGSX/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
for platform in config.platforms:
games = load_games(platform)
config.games_count[platform] = len(games)
logger.debug(f"Jeux chargés pour {platform}: {len(games)} jeux")
write_unavailable_systems()
logger.debug(f"load_sources: platforms={config.platforms}, platform_names={config.platform_names}, games_count={config.games_count}")
return sources
except Exception as e:
logger.error(f"Erreur lors du chargement de sources.json : {str(e)}")
return []
# Fonction pour vérifier et appliquer les mises à jour OTA
async def check_for_updates():
try:
logger.debug("Vérification de la version disponible sur le serveur")
config.current_loading_system = "Mise à jour en cours... Patientez l'ecran reste figé..Puis relancer l'application"
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}")
if latest_version != config.app_version:
config.current_loading_system = f"Mise à jour disponible : {latest_version}"
config.loading_progress = 10.0
config.needs_redraw = True
logger.debug(f"Téléchargement du script de mise à jour : {OTA_UPDATE_SCRIPT}")
update_script_path = "/userdata/roms/ports/rgsx-update.sh"
logger.debug(f"Téléchargement de {OTA_UPDATE_SCRIPT} vers {update_script_path}")
with requests.get(OTA_UPDATE_SCRIPT, stream=True, timeout=10) as r:
r.raise_for_status()
with open(update_script_path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
config.loading_progress = min(50.0, config.loading_progress + 5.0)
config.needs_redraw = True
await asyncio.sleep(0)
config.current_loading_system = "Préparation de la mise à jour..."
config.loading_progress = 60.0
config.needs_redraw = True
logger.debug(f"Rendre {update_script_path} exécutable")
subprocess.run(["chmod", "+x", update_script_path], check=True)
logger.debug(f"Script {update_script_path} rendu exécutable")
logger.debug(f"Vérification de l'existence et des permissions de {update_script_path}")
if not os.path.isfile(update_script_path):
logger.error(f"Le script {update_script_path} n'existe pas")
return False, f"Erreur : le script {update_script_path} n'existe pas"
if not os.access(update_script_path, os.X_OK):
logger.error(f"Le script {update_script_path} n'est pas exécutable")
return False, f"Erreur : le script {update_script_path} n'est pas exécutable"
wrapper_script_path = "/userdata/roms/ports/RGSX/update/run.update"
logger.debug(f"Vérification de l'existence et des permissions de {wrapper_script_path}")
if not os.path.isfile(wrapper_script_path):
logger.error(f"Le script wrapper {wrapper_script_path} n'existe pas")
return False, f"Erreur : le script wrapper {wrapper_script_path} n'existe pas"
if not os.access(wrapper_script_path, os.X_OK):
logger.error(f"Le script wrapper {wrapper_script_path} n'est pas exécutable")
subprocess.run(["chmod", "+x", wrapper_script_path], check=True)
logger.debug(f"Script wrapper {wrapper_script_path} rendu exécutable")
logger.debug("Désactivation des événements Pygame QUIT")
pygame.event.set_blocked(pygame.QUIT)
config.current_loading_system = "Application de la mise à jour..."
config.loading_progress = 80.0
config.needs_redraw = True
logger.debug(f"Exécution du script wrapper : {wrapper_script_path}")
result = os.system(f"{wrapper_script_path} &")
logger.debug(f"Résultat de os.system : {result}")
if result != 0:
logger.error(f"Échec du lancement du script wrapper : code de retour {result}")
return False, f"Échec du lancement du script wrapper : code de retour {result}"
config.current_loading_system = "Mise à jour déclenchée, redémarrage..."
config.loading_progress = 100.0
config.needs_redraw = True
logger.debug("Mise à jour déclenchée, arrêt de l'application")
config.update_triggered = True
pygame.quit()
sys.exit(0)
else:
logger.debug("Aucune mise à jour logicielle disponible")
return True, "Aucune mise à jour disponible"
except Exception as e:
logger.error(f"Erreur OTA : {str(e)}")
return False, f"Erreur lors de la vérification des mises à jour : {str(e)}"
# Boucle principale # Boucle principale
async def main(): async def main():
logger.debug("Début main") logger.debug("Début main")
@@ -276,25 +117,29 @@ async def main():
config.debounce_delay = 50 config.debounce_delay = 50
config.update_triggered = False config.update_triggered = False
last_redraw_time = pygame.time.get_ticks() last_redraw_time = pygame.time.get_ticks()
screen = init_display()
clock = pygame.time.Clock() clock = pygame.time.Clock()
# Variables pour la progression simulée
check_ota_start_time = None
load_sources_start_time = None
SIMULATED_CHECK_OTA_DURATION = 5.0
SIMULATED_LOAD_SOURCES_DURATION = 3.0
while running: while running:
clock.tick(60) # Limite à 60 FPS clock.tick(60)
if config.update_triggered: if config.update_triggered:
logger.debug("Mise à jour déclenchée, arrêt de la boucle principale") logger.debug("Mise à jour déclenchée, arrêt de la boucle principale")
break break
current_time = pygame.time.get_ticks() current_time = pygame.time.get_ticks()
current_time_sec = current_time / 1000.0
# Forcer redraw toutes les 100 ms dans download_progress # Forcer redraw toutes les 100 ms dans download_progress
if config.menu_state == "download_progress" and current_time - last_redraw_time >= 100: if config.menu_state == "download_progress" and current_time - last_redraw_time >= 100:
config.needs_redraw = True config.needs_redraw = True
last_redraw_time = current_time last_redraw_time = current_time
# Dans __main__.py, dans la boucle principale # Gestion du popup timer
current_time = pygame.time.get_ticks()
delta_time = current_time - config.last_frame_time delta_time = current_time - config.last_frame_time
config.last_frame_time = current_time config.last_frame_time = current_time
if config.menu_state == "restart_popup" and config.popup_timer > 0: if config.menu_state == "restart_popup" and config.popup_timer > 0:
@@ -316,7 +161,7 @@ async def main():
config.needs_redraw = True config.needs_redraw = True
logger.debug("Événement QUIT détecté, passage à confirm_exit") logger.debug("Événement QUIT détecté, passage à confirm_exit")
continue continue
elif event.type == pygame.USEREVENT + 1: # Fin de la musique elif event.type == pygame.USEREVENT + 1:
logger.debug("Fin de la musique actuelle, passage à la suivante") logger.debug("Fin de la musique actuelle, passage à la suivante")
play_random_music() play_random_music()
start_config = config.controls_config.get("start", {}) start_config = config.controls_config.get("start", {})
@@ -336,10 +181,9 @@ async def main():
continue continue
if config.menu_state == "pause_menu": if config.menu_state == "pause_menu":
action = handle_controls(event, sources, joystick, screen) handle_controls(event, sources, joystick, screen)
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Événement transmis à handle_controls dans pause_menu: {event.type}") logger.debug(f"Événement transmis à handle_controls dans pause_menu: {event.type}")
continue continue
if config.menu_state == "controls_help": if config.menu_state == "controls_help":
@@ -356,14 +200,13 @@ async def main():
logger.debug("Controls_help: Annulation, retour à pause_menu") logger.debug("Controls_help: Annulation, retour à pause_menu")
continue continue
# Gérer confirm_clear_history explicitement
if config.menu_state == "confirm_clear_history": if config.menu_state == "confirm_clear_history":
action = handle_controls(event, sources, joystick, screen) handle_controls(event, sources, joystick, screen)
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Événement transmis à handle_controls dans confirm_clear_history: {event.type}") logger.debug(f"Événement transmis à handle_controls dans confirm_clear_history: {event.type}")
continue continue
if config.menu_state == "redownload_game_cache": if config.menu_state == "redownload_game_cache":
action = handle_controls(event, sources, joystick, screen) handle_controls(event, sources, joystick, screen)
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Événement transmis à handle_controls dans redownload_game_cache: {event.type}") logger.debug(f"Événement transmis à handle_controls dans redownload_game_cache: {event.type}")
continue continue
@@ -392,7 +235,7 @@ async def main():
task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported)) task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported))
config.download_tasks[task] = (task, url, game_name, platform) config.download_tasks[task] = (task, url, game_name, platform)
config.menu_state = "download_progress" config.menu_state = "download_progress"
config.pending_download = None # Réinitialiser après démarrage du téléchargement config.pending_download = None
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Téléchargement démarré pour {game_name}, passage à download_progress") logger.debug(f"Téléchargement démarré pour {game_name}, passage à download_progress")
elif action == "redownload" and config.menu_state == "history" and config.history: elif action == "redownload" and config.menu_state == "history" and config.history:
@@ -413,7 +256,7 @@ async def main():
task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported)) task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported))
config.download_tasks[task] = (task, url, game_name, platform) config.download_tasks[task] = (task, url, game_name, platform)
config.menu_state = "download_progress" config.menu_state = "download_progress"
config.pending_download = None # Réinitialiser après démarrage du téléchargement config.pending_download = None
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Retéléchargement démarré pour {game_name}, passage à download_progress") logger.debug(f"Retéléchargement démarré pour {game_name}, passage à download_progress")
break break
@@ -428,8 +271,8 @@ async def main():
config.download_result_error = not success config.download_result_error = not success
config.download_result_start_time = pygame.time.get_ticks() config.download_result_start_time = pygame.time.get_ticks()
config.menu_state = "download_result" config.menu_state = "download_result"
config.download_progress.clear() # Réinitialiser download_progress config.download_progress.clear()
config.pending_download = None # Réinitialiser après téléchargement config.pending_download = None
config.needs_redraw = True config.needs_redraw = True
del config.download_tasks[task_id] del config.download_tasks[task_id]
logger.debug(f"Téléchargement terminé: {game_name}, succès={success}, message={message}") logger.debug(f"Téléchargement terminé: {game_name}, succès={success}, message={message}")
@@ -438,8 +281,8 @@ async def main():
config.download_result_error = True config.download_result_error = True
config.download_result_start_time = pygame.time.get_ticks() config.download_result_start_time = pygame.time.get_ticks()
config.menu_state = "download_result" config.menu_state = "download_result"
config.download_progress.clear() # Réinitialiser download_progress config.download_progress.clear()
config.pending_download = None # Réinitialiser après téléchargement config.pending_download = None
config.needs_redraw = True config.needs_redraw = True
del config.download_tasks[task_id] del config.download_tasks[task_id]
logger.error(f"Erreur dans tâche de téléchargement: {str(e)}") logger.error(f"Erreur dans tâche de téléchargement: {str(e)}")
@@ -447,113 +290,55 @@ async def main():
# Gestion de la fin du popup download_result # Gestion de la fin du popup download_result
if config.menu_state == "download_result" and current_time - config.download_result_start_time > 3000: if config.menu_state == "download_result" and current_time - config.download_result_start_time > 3000:
config.menu_state = config.previous_menu_state if config.previous_menu_state in ["platform", "game", "history"] else "game" config.menu_state = config.previous_menu_state if config.previous_menu_state in ["platform", "game", "history"] else "game"
config.download_progress.clear() # Réinitialiser download_progress config.download_progress.clear()
config.pending_download = None # Réinitialiser après affichage du résultat config.pending_download = None
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Fin popup download_result, retour à {config.menu_state}") logger.debug(f"Fin popup download_result, retour à {config.menu_state}")
# Affichage
if config.needs_redraw:
draw_gradient(screen, THEME_COLORS["background_top"], THEME_COLORS["background_bottom"])
if config.menu_state == "controls_mapping":
draw_controls_mapping(screen, ACTIONS[0], None, False, 0.0)
# logger.debug("Rendu initial de draw_controls_mapping")
elif config.menu_state == "loading":
draw_loading_screen(screen)
# logger.debug("Rendu de draw_loading_screen")
elif config.menu_state == "error":
draw_error_screen(screen)
# logger.debug("Rendu de draw_error_screen")
elif config.menu_state == "platform":
draw_platform_grid(screen)
# logger.debug("Rendu de draw_platform_grid")
elif config.menu_state == "game":
if not config.search_mode:
draw_game_list(screen)
if config.search_mode:
draw_game_list(screen)
draw_virtual_keyboard(screen)
# logger.debug("Rendu de draw_game_list")
elif config.menu_state == "download_progress":
draw_progress_screen(screen)
# logger.debug("Rendu de draw_progress_screen")
elif config.menu_state == "download_result":
draw_popup_result_download(screen, config.download_result_message, config.download_result_error)
# logger.debug("Rendu de draw_popup_message")
elif config.menu_state == "confirm_exit":
draw_confirm_dialog(screen)
# logger.debug("Rendu de draw_confirm_dialog")
elif config.menu_state == "extension_warning":
draw_extension_warning(screen)
# logger.debug("Rendu de draw_extension_warning")
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)
# logger.debug("Rendu de draw_controls_help")
elif config.menu_state == "history":
draw_history_list(screen)
# logger.debug("Rendu de draw_history_list")
elif config.menu_state == "confirm_clear_history":
draw_clear_history_dialog(screen)
# logger.debug("Rendu de confirm_clear_history")
elif config.menu_state == "redownload_game_cache":
draw_redownload_game_cache_dialog(screen) # Fonction existante
elif config.menu_state == "restart_popup":
draw_popup(screen) # Nouvelle fonction
elif config.menu_state == "confirm_clear_history":
draw_clear_history_dialog(screen) # Fonction existante
else:
# Gestion des états non valides
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)
screen = pygame.display.get_surface()
draw_music_popup(screen) # Ajouter l'appel à la popup
pygame.display.flip()
config.needs_redraw = False
# Gestion de l'état controls_mapping
if config.menu_state == "controls_mapping":
logger.debug("Avant appel de map_controls")
try:
success = map_controls(screen)
logger.debug(f"map_controls terminé, succès={success}")
if success:
config.controls_config = load_controls_config()
config.menu_state = "loading"
config.needs_redraw = True
logger.debug("Passage à l'état loading après mappage")
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 # Gestion de l'état loading
elif config.menu_state == "loading": if config.menu_state == "loading":
logger.debug(f"Étape chargement : {loading_step}") logger.debug(f"Étape chargement : {loading_step}")
if loading_step == "none": if loading_step == "none":
loading_step = "test_internet" loading_step = "init_sources"
config.current_loading_system = "Test de connexion..." config.current_loading_system = "Chargement des sources..."
config.loading_progress = 0.0 config.loading_progress = 0.0
config.needs_redraw = True config.needs_redraw = True
load_sources_start_time = current_time_sec
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}") logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
elif loading_step == "init_sources":
if load_sources_start_time is None:
load_sources_start_time = current_time_sec
# Simuler la progression pour init_sources
elapsed = current_time_sec - load_sources_start_time
progress = min(0.0 + (5.0 * elapsed / SIMULATED_LOAD_SOURCES_DURATION), 5.0)
config.loading_progress = progress
config.needs_redraw = True
logger.debug(f"Progression simulée init_sources : {config.loading_progress}%")
# Exécuter 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:
loading_step = "test_internet"
config.current_loading_system = "Test de connexion..."
config.loading_progress = 5.0
load_sources_start_time = None
config.needs_redraw = True
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
elif loading_step == "test_internet": elif loading_step == "test_internet":
logger.debug("Exécution de test_internet()") logger.debug("Exécution de test_internet()")
if test_internet(): if test_internet():
loading_step = "check_ota" loading_step = "check_ota"
config.current_loading_system = "Mise à jour en cours... Patientez l'ecran reste figé.. Puis relancer l'application" config.current_loading_system = "Vérification des mises à jour..."
config.loading_progress = 100 config.loading_progress = 5.0
check_ota_start_time = current_time_sec
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}") logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
else: else:
@@ -561,8 +346,19 @@ async def main():
config.error_message = "Pas de connexion Internet. Vérifiez votre réseau." config.error_message = "Pas de connexion Internet. Vérifiez votre réseau."
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Erreur : {config.error_message}") logger.debug(f"Erreur : {config.error_message}")
elif loading_step == "check_ota": elif loading_step == "check_ota":
logger.debug("Exécution de check_for_updates()") if check_ota_start_time is None:
check_ota_start_time = current_time_sec
# Simuler la progression pour check_ota
elapsed = current_time_sec - check_ota_start_time
progress = min(5.0 + (25.0 * elapsed / SIMULATED_CHECK_OTA_DURATION), 30.0)
config.loading_progress = progress
config.needs_redraw = True
logger.debug(f"Progression simulée check_ota : {config.loading_progress}%")
# Exécuter check_for_updates
success, message = await check_for_updates() success, message = await check_for_updates()
logger.debug(f"Résultat de check_for_updates : success={success}, message={message}") logger.debug(f"Résultat de check_for_updates : success={success}, message={message}")
if not success: if not success:
@@ -572,18 +368,19 @@ async def main():
logger.debug(f"Erreur OTA : {message}") logger.debug(f"Erreur OTA : {message}")
else: else:
loading_step = "check_data" loading_step = "check_data"
config.current_loading_system = "Téléchargement des jeux et images ..." config.current_loading_system = "Téléchargement des jeux et images..."
config.loading_progress = 10.0 config.loading_progress = 30.0
check_ota_start_time = None
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}") logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
elif loading_step == "check_data": elif loading_step == "check_data":
games_data_dir = "/userdata/roms/ports/RGSX/games" games_data_dir = "/userdata/roms/ports/RGSX/games"
is_data_empty = not os.path.exists(games_data_dir) or not any(os.scandir(games_data_dir)) is_data_empty = not os.path.exists(games_data_dir) or not any(os.scandir(games_data_dir))
#logger.debug(f"Dossier Data directory {games_data_dir} is {'empty' if is_data_empty else 'not empty'}")
if is_data_empty: if is_data_empty:
config.current_loading_system = "Téléchargement du Dossier Data initial..." config.current_loading_system = "Téléchargement du Dossier Data initial..."
config.loading_progress = 15.0 config.loading_progress = 30.0
config.needs_redraw = True config.needs_redraw = True
logger.debug("Dossier Data vide, début du téléchargement du ZIP") logger.debug("Dossier Data vide, début du téléchargement du ZIP")
@@ -607,19 +404,19 @@ async def main():
"status": "Téléchargement", "status": "Téléchargement",
"progress_percent": (downloaded / total_size * 100) if total_size > 0 else 0 "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.loading_progress = 30.0 + (40.0 * downloaded / total_size) if total_size > 0 else 30.0
config.needs_redraw = True config.needs_redraw = True
await asyncio.sleep(0) await asyncio.sleep(0)
logger.debug(f"ZIP téléchargé : {zip_path}") logger.debug(f"ZIP téléchargé : {zip_path}")
config.current_loading_system = "Extraction du Dossier Data initial..." config.current_loading_system = "Extraction du Dossier Data initial..."
config.loading_progress = 50.0 config.loading_progress = 70.0
config.needs_redraw = True config.needs_redraw = True
dest_dir = "/userdata/roms/ports/RGSX" dest_dir = "/userdata/roms/ports/RGSX"
success, message = extract_zip(zip_path, dest_dir, OTA_data_ZIP) success, message = extract_zip(zip_path, dest_dir, OTA_data_ZIP)
if success: if success:
logger.debug(f"Extraction réussie : {message}") logger.debug(f"Extraction réussie : {message}")
config.loading_progress = 60.0 config.loading_progress = 70.0
config.needs_redraw = True config.needs_redraw = True
else: else:
raise Exception(f"Échec de l'extraction : {message}") raise Exception(f"Échec de l'extraction : {message}")
@@ -640,16 +437,31 @@ async def main():
loading_step = "load_sources" loading_step = "load_sources"
config.current_loading_system = "Chargement des systèmes..." config.current_loading_system = "Chargement des systèmes..."
config.loading_progress = 60.0 config.loading_progress = 70.0
load_sources_start_time = current_time_sec
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}") logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
else: else:
loading_step = "load_sources" loading_step = "load_sources"
config.current_loading_system = "Chargement des systèmes..." config.current_loading_system = "Chargement des systèmes..."
config.loading_progress = 60.0 config.loading_progress = 70.0
load_sources_start_time = current_time_sec
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Dossier Data non vide, passage à {loading_step}") logger.debug(f"Dossier Data non vide, passage à {loading_step}")
elif loading_step == "load_sources": elif loading_step == "load_sources":
if load_sources_start_time is None:
load_sources_start_time = current_time_sec
# Simuler la progression pour load_sources
elapsed = current_time_sec - load_sources_start_time
progress = min(70.0 + (30.0 * elapsed / SIMULATED_LOAD_SOURCES_DURATION), 100.0)
config.loading_progress = progress
config.needs_redraw = True
logger.debug(f"Progression simulée load_sources : {config.loading_progress}%")
# Exécuter load_sources
sources = load_sources() sources = load_sources()
if not sources: if not sources:
config.menu_state = "error" config.menu_state = "error"
@@ -660,6 +472,7 @@ async def main():
config.menu_state = "platform" config.menu_state = "platform"
config.loading_progress = 0.0 config.loading_progress = 0.0
config.current_loading_system = "" config.current_loading_system = ""
load_sources_start_time = None
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Fin chargement, passage à platform, progress={config.loading_progress}") logger.debug(f"Fin chargement, passage à platform, progress={config.loading_progress}")
@@ -673,6 +486,73 @@ async def main():
config.needs_redraw = True config.needs_redraw = True
logger.debug("Transition terminée, passage à game") logger.debug("Transition terminée, passage à game")
# Affichage
if config.needs_redraw:
draw_gradient(screen, THEME_COLORS["background_top"], THEME_COLORS["background_bottom"])
if config.menu_state == "controls_mapping":
draw_controls_mapping(screen, ACTIONS[0], None, False, 0.0)
elif config.menu_state == "loading":
draw_loading_screen(screen)
elif config.menu_state == "error":
draw_error_screen(screen)
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)
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)
elif config.menu_state == "controls_help":
draw_controls_help(screen, config.previous_menu_state)
elif config.menu_state == "history":
draw_history_list(screen)
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)
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)
draw_music_popup(screen)
pygame.display.flip()
config.needs_redraw = False
# Gestion de l'état controls_mapping
if config.menu_state == "controls_mapping":
try:
success = map_controls(screen)
logger.debug(f"map_controls terminé, succès={success}")
if success:
config.controls_config = load_controls_config()
config.menu_state = "loading"
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
clock.tick(60) clock.tick(60)
await asyncio.sleep(0.01) await asyncio.sleep(0.01)
@@ -680,7 +560,6 @@ async def main():
pygame.quit() pygame.quit()
logger.debug("Application terminée") logger.debug("Application terminée")
if platform.system() == "Emscripten": if platform.system() == "Emscripten":
asyncio.ensure_future(main()) asyncio.ensure_future(main())
else: else:

View File

@@ -5,15 +5,22 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Version actuelle de l'application # Version actuelle de l'application
app_version = "1.9.3" app_version = "1.9.4"
# URL du serveur OTA
OTA_SERVER_URL = "https://retrogamesets.fr/softs"
OTA_VERSION_ENDPOINT = f"{OTA_SERVER_URL}/version.json"
OTA_UPDATE_SCRIPT = f"{OTA_SERVER_URL}/rgsx-update.sh"
OTA_data_ZIP = f"{OTA_SERVER_URL}/rgsx-data.zip"
# Variables d'état # Variables d'état
platforms = [] platforms = [] # Liste des plateformes chargées depuis sources.json
current_platform = 0 current_platform = 0 # Index de la plateforme actuellement sélectionnée
platform_names = {} # {platform_id: platform_name} platform_names = {} # {platform_id: platform_name}
games = [] games = [] # Liste des jeux chargés pour la plateforme actuelle
current_game = 0 current_game = 0 # Index du jeu actuellement sélectionné
menu_state = "popup" menu_state = "" # État actuel du menu (par exemple, "main_menu", "game_list", "settings", etc.)
confirm_choice = False confirm_choice = False
scroll_offset = 0 scroll_offset = 0
visible_games = 15 visible_games = 15
@@ -32,10 +39,6 @@ download_result_start_time = 0
loading_progress = 0.0 loading_progress = 0.0
current_loading_system = "" current_loading_system = ""
error_message = "" error_message = ""
repeat_action = None
repeat_start_time = 0
repeat_last_action = 0
repeat_key = None
filtered_games = [] filtered_games = []
search_mode = False search_mode = False
search_query = "" search_query = ""

View File

@@ -3,7 +3,6 @@ import pygame # type: ignore
import config import config
from config import CONTROLS_CONFIG_PATH , GRID_COLS, GRID_ROWS from config import CONTROLS_CONFIG_PATH , GRID_COLS, GRID_ROWS
import asyncio import asyncio
import math
import json import json
import os import os
from display import draw_validation_transition from display import draw_validation_transition

View File

@@ -849,18 +849,18 @@ def draw_pause_menu(screen, selected_option):
def draw_controls_help(screen, previous_state): def draw_controls_help(screen, previous_state):
"""Affiche la liste des contrôles avec un style moderne.""" """Affiche la liste des contrôles avec un style moderne."""
common_controls = { common_controls = {
"confirm": lambda action: f"{get_control_display('confirm', 'Entrée/A')} : {action}", "confirm": lambda action: f"{get_control_display('confirm', 'Entrée/A/Croix')} : {action}",
"cancel": lambda action: f"{get_control_display('cancel', 'Échap/B')} : {action}", "cancel": lambda action: f"{get_control_display('cancel', 'Échap/B/Rond')} : {action}",
"start": lambda: f"{get_control_display('start', 'Start')} : Menu", "start": lambda: f"{get_control_display('start', 'Start/')} : Menu",
"progress": lambda action: f"{get_control_display('progress', 'X')} : {action}", "progress": lambda action: f"{get_control_display('progress', 'X/Carré')} : {action}",
"up": lambda action: f"{get_control_display('up', 'Flèche Haut')} : {action}", "up": lambda action: f"{get_control_display('up', 'Flèche Haut')} : {action}",
"down": lambda action: f"{get_control_display('down', 'Flèche Bas')} : {action}", "down": lambda action: f"{get_control_display('down', 'Flèche Bas')} : {action}",
"page_up": lambda action: f"{get_control_display('page_up', 'Q/LB')} : {action}", "page_up": lambda action: f"{get_control_display('page_up', 'Q/LB/L1')} : {action}",
"page_down": lambda action: f"{get_control_display('page_down', 'E/RB')} : {action}", "page_down": lambda action: f"{get_control_display('page_down', 'E/RB/R1')} : {action}",
"filter": lambda action: f"{get_control_display('filter', 'Select')} : {action}", "filter": lambda action: f"{get_control_display('filter', 'Select')} : {action}",
"history": lambda action: f"{get_control_display('history', 'H')} : {action}", "history": lambda action: f"{get_control_display('history', 'H/Y/Triangle')} : {action}",
"delete": lambda: f"{get_control_display('delete', 'Retour Arrière')} : Supprimer", "delete": lambda: f"{get_control_display('delete', 'Backspace/LT/L2')} : Supprimer",
"space": lambda: f"{get_control_display('space', 'Espace')} : Espace" "space": lambda: f"{get_control_display('space', 'Espace/RT/R2')} : Espace"
} }
state_controls = { state_controls = {
@@ -1085,8 +1085,3 @@ def draw_music_popup(screen):
# Afficher la popup # Afficher la popup
screen.blit(popup_surface, (rect_x, rect_y)) screen.blit(popup_surface, (rect_x, rect_y))
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

View File

@@ -2,7 +2,6 @@ import json
import os import os
import logging import logging
import config import config
from config import HISTORY_PATH
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -12,16 +11,18 @@ DEFAULT_HISTORY_PATH = "/userdata/saves/ports/rgsx/history.json"
def init_history(): def init_history():
"""Initialise le fichier history.json s'il n'existe pas.""" """Initialise le fichier history.json s'il n'existe pas."""
history_path = getattr(config, 'HISTORY_PATH', DEFAULT_HISTORY_PATH) 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): if not os.path.exists(history_path):
try: try:
os.makedirs(os.path.dirname(history_path), exist_ok=True) os.makedirs(os.path.dirname(history_path), exist_ok=True)
with open(history_path, "w") as f: with open(history_path, "w") as f:
json.dump([], f) json.dump([], f) # Initialise avec une liste vide
logger.info(f"Fichier history.json créé à {history_path}") logger.info(f"Fichier d'historique créé : {history_path}")
except Exception as e: except OSError as e:
logger.error(f"Erreur lors de la création de history.json : {e}") logger.error(f"Erreur lors de la création du fichier d'historique : {e}")
return False else:
return True logger.info(f"Fichier d'historique trouvé : {history_path}")
return history_path
def load_history(): def load_history():
"""Charge l'historique depuis history.json.""" """Charge l'historique depuis history.json."""

View File

@@ -6,9 +6,10 @@ import threading
import pygame # type: ignore import pygame # type: ignore
import zipfile import zipfile
import json import json
import time
import asyncio import asyncio
import config import config
import sys
from config import OTA_VERSION_ENDPOINT, OTA_UPDATE_SCRIPT
from utils import sanitize_filename from utils import sanitize_filename
from history import add_to_history, load_history from history import add_to_history, load_history
import logging import logging
@@ -20,18 +21,125 @@ cache = {}
CACHE_TTL = 3600 # 1 heure CACHE_TTL = 3600 # 1 heure
def test_internet(): def test_internet():
"""Teste la connexion Internet dans un thread séparé."""
logger.debug("Test de connexion Internet") logger.debug("Test de connexion Internet")
try: result = [False]
result = subprocess.run(['ping', '-c', '4', '8.8.8.8'], capture_output=True, text=True, timeout=5)
if result.returncode == 0: def ping_thread():
logger.debug("Connexion Internet OK") try:
return True proc = subprocess.run(['ping', '-c', '4', '8.8.8.8'], capture_output=True, text=True, timeout=5)
else: result[0] = proc.returncode == 0
logger.debug("Échec ping 8.8.8.8") logger.debug("Connexion Internet OK" if result[0] else "Échec ping 8.8.8.8")
return False except Exception as e:
except Exception as e: logger.debug(f"Erreur test Internet: {str(e)}")
logger.debug(f"Erreur test Internet: {str(e)}") result[0] = False
return False
thread = threading.Thread(target=ping_thread)
thread.start()
thread.join()
return result[0]
# Fonction pour vérifier et appliquer les mises à jour OTA
async def check_for_updates():
"""Vérifie et applique les mises à jour OTA dans un thread séparé."""
result = [None, None]
def update_thread():
try:
logger.debug("Vérification de la version disponible sur le serveur")
config.current_loading_system = "Mise à jour en cours... Patientez l'écran reste figé..Puis relancer l'application"
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}")
if latest_version != config.app_version:
config.current_loading_system = f"Mise à jour disponible : {latest_version}"
config.loading_progress = 10.0
config.needs_redraw = True
logger.debug(f"Téléchargement du script de mise à jour : {OTA_UPDATE_SCRIPT}")
update_script_path = "/userdata/roms/ports/rgsx-update.sh"
logger.debug(f"Téléchargement de {OTA_UPDATE_SCRIPT} vers {update_script_path}")
with requests.get(OTA_UPDATE_SCRIPT, stream=True, timeout=10) as r:
r.raise_for_status()
with open(update_script_path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
config.loading_progress = min(50.0, config.loading_progress + 5.0)
config.needs_redraw = True
# Pas de sleep ici, car on est dans un thread
config.current_loading_system = "Préparation de la mise à jour..."
config.loading_progress = 60.0
config.needs_redraw = True
logger.debug(f"Rendre {update_script_path} exécutable")
subprocess.run(["chmod", "+x", update_script_path], check=True)
logger.debug(f"Script {update_script_path} rendu exécutable")
logger.debug(f"Vérification de l'existence et des permissions de {update_script_path}")
if not os.path.isfile(update_script_path):
logger.error(f"Le script {update_script_path} n'existe pas")
result[0], result[1] = False, f"Erreur : le script {update_script_path} n'existe pas"
return
if not os.access(update_script_path, os.X_OK):
logger.error(f"Le script {update_script_path} n'est pas exécutable")
result[0], result[1] = False, f"Erreur : le script {update_script_path} n'est pas exécutable"
return
wrapper_script_path = "/userdata/roms/ports/RGSX/update/run.update"
logger.debug(f"Vérification de l'existence et des permissions de {wrapper_script_path}")
if not os.path.isfile(wrapper_script_path):
logger.error(f"Le script wrapper {wrapper_script_path} n'existe pas")
result[0], result[1] = False, f"Erreur : le script wrapper {wrapper_script_path} n'existe pas"
return
if not os.access(wrapper_script_path, os.X_OK):
logger.error(f"Le script wrapper {wrapper_script_path} n'est pas exécutable")
subprocess.run(["chmod", "+x", wrapper_script_path], check=True)
logger.debug(f"Script wrapper {wrapper_script_path} rendu exécutable")
logger.debug("Désactivation des événements Pygame QUIT")
pygame.event.set_blocked(pygame.QUIT)
config.current_loading_system = "Application de la mise à jour..."
config.loading_progress = 80.0
config.needs_redraw = True
logger.debug(f"Exécution du script wrapper : {wrapper_script_path}")
os_result = os.system(f"{wrapper_script_path} &")
logger.debug(f"Résultat de os.system : {os_result}")
if os_result != 0:
logger.error(f"Échec du lancement du script wrapper : code de retour {os_result}")
result[0], result[1] = False, f"Échec du lancement du script wrapper : code de retour {os_result}"
return
config.current_loading_system = "Mise à jour déclenchée, redémarrage..."
config.loading_progress = 100.0
config.needs_redraw = True
logger.debug("Mise à jour déclenchée, arrêt de l'application")
config.update_triggered = True
pygame.quit()
sys.exit(0)
else:
logger.debug("Aucune mise à jour logicielle disponible")
result[0], result[1] = True, "Aucune mise à jour disponible"
except Exception as e:
logger.error(f"Erreur OTA : {str(e)}")
result[0], result[1] = False, f"Erreur lors de la vérification des mises à jour : {str(e)}"
thread = threading.Thread(target=update_thread)
thread.start()
while thread.is_alive():
pygame.event.pump()
await asyncio.sleep(0.1)
thread.join()
return result[0], result[1]
def load_extensions_json(): def load_extensions_json():
"""Charge le fichier JSON contenant les extensions supportées.""" """Charge le fichier JSON contenant les extensions supportées."""

142
utils.py
View File

@@ -3,17 +3,39 @@ import re
import json import json
import os import os
import logging import logging
import threading
import requests import requests
import config
from datetime import datetime import random
import platform
import subprocess
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Liste globale pour stocker les systèmes avec une erreur 404
unavailable_systems = []
def load_games(platform_id): unavailable_systems = [] # Liste globale pour stocker les systèmes avec une erreur 404
def check_url(url, platform_id, unavailable_systems_lock=None, unavailable_systems=None):
"""Vérifie si une URL est accessible via une requête HEAD."""
try:
response = requests.head(url, timeout=5, allow_redirects=True)
if response.status_code == 404:
logger.error(f"URL non accessible pour {platform_id}: {url} (code 404)")
if unavailable_systems_lock and unavailable_systems is not None:
with unavailable_systems_lock:
unavailable_systems.append(platform_id)
elif unavailable_systems is not None:
unavailable_systems.append(platform_id)
except requests.RequestException as e:
logger.error(f"Erreur lors du test de l'URL pour {platform_id}: {url} ({str(e)})")
if unavailable_systems_lock and unavailable_systems is not None:
with unavailable_systems_lock:
unavailable_systems.append(platform_id)
elif unavailable_systems is not None:
unavailable_systems.append(platform_id)
def load_games(platform_id, unavailable_systems_lock=None, unavailable_systems=None):
"""Charge les jeux pour une plateforme donnée en utilisant platform_id et teste la première URL.""" """Charge les jeux pour une plateforme donnée en utilisant platform_id et teste la première URL."""
games_path = f"/userdata/roms/ports/RGSX/games/{platform_id}.json" games_path = f"/userdata/roms/ports/RGSX/games/{platform_id}.json"
try: try:
@@ -23,20 +45,14 @@ def load_games(platform_id):
# Tester la première URL si la liste n'est pas vide # Tester la première URL si la liste n'est pas vide
if games and len(games) > 0 and len(games[0]) > 1: if games and len(games) > 0 and len(games[0]) > 1:
first_url = games[0][1] first_url = games[0][1]
try: check_url(first_url, platform_id, unavailable_systems_lock, unavailable_systems)
response = requests.head(first_url, timeout=5, allow_redirects=True)
if response.status_code == 404:
logger.error(f"URL non accessible pour {platform_id} : {first_url} (code 404)")
unavailable_systems.append(platform_id) # Ajouter à la liste globale
except requests.RequestException as e:
logger.error(f"Erreur lors du test de l'URL pour {platform_id} : {first_url} ({str(e)})")
else: else:
logger.debug(f"Aucune URL à tester pour {platform_id} (liste vide ou mal formée)") 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") logger.debug(f"Jeux chargés pour {platform_id}: {len(games)} jeux")
return games return games
except Exception as e: except Exception as e:
logger.error(f"Erreur lors du chargement des jeux pour {platform_id} : {str(e)}") logger.error(f"Erreur lors du chargement des jeux pour {platform_id}: {str(e)}")
return [] return []
def write_unavailable_systems(): def write_unavailable_systems():
@@ -45,24 +61,81 @@ def write_unavailable_systems():
logger.debug("Aucun système avec une erreur 404, aucun fichier écrit") logger.debug("Aucun système avec une erreur 404, aucun fichier écrit")
return return
# Formater la date et l'heure pour le nom du fichier from datetime import datetime
current_time = datetime.now() current_time = datetime.now()
timestamp = current_time.strftime("%d-%m-%Y-%H-%M") timestamp = current_time.strftime("%d-%m-%Y-%H-%M")
log_dir = "/userdata/roms/ports/logs/RGSX" log_dir = "/userdata/roms/ports/logs/RGSX"
log_file = f"{log_dir}/systemes_unavailable_{timestamp}.txt" log_file = f"{log_dir}/systemes_unavailable_{timestamp}.txt"
try: try:
# Créer le répertoire s'il n'existe pas
os.makedirs(log_dir, exist_ok=True) os.makedirs(log_dir, exist_ok=True)
# Écrire les systèmes dans le fichier
with open(log_file, 'w', encoding='utf-8') as f: with open(log_file, 'w', encoding='utf-8') as f:
f.write("Systèmes avec une erreur 404 :\n") f.write("Systèmes avec une erreur 404 :\n")
for system in unavailable_systems: for system in unavailable_systems:
f.write(f"{system}\n") f.write(f"{system}\n")
logger.debug(f"Fichier écrit : {log_file} avec {len(unavailable_systems)} systèmes") logger.debug(f"Fichier écrit : {log_file} avec {len(unavailable_systems)} systèmes")
except Exception as e: except Exception as e:
logger.error(f"Erreur lors de l'écriture du fichier {log_file} : {str(e)}") logger.error(f"Erreur lors de l'écriture du fichier {log_file}: {str(e)}")
def load_sources():
"""Charge sources.json et les jeux pour toutes les plateformes en parallèle."""
sources_path = "/userdata/roms/ports/RGSX/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}
# Créer un verrou pour unavailable_systems
unavailable_systems_lock = threading.Lock()
global unavailable_systems
unavailable_systems = []
# Lancer les chargements des jeux en parallèle avec threading
threads = []
results = [None] * len(config.platforms)
for i, platform in enumerate(config.platforms):
thread = threading.Thread(target=lambda idx=i, plat=platform: results.__setitem__(idx, load_games(plat, unavailable_systems_lock, unavailable_systems)))
threads.append(thread)
thread.start()
# Attendre que tous les threads se terminent
for thread in threads:
thread.join()
# Mettre à jour games_count avec les résultats
for platform, games in zip(config.platforms, results):
if games:
config.games_count[platform] = len(games)
logger.debug(f"Jeux chargés pour {platform}: {len(games)} jeux")
else:
config.games_count[platform] = 0
logger.error(f"Échec du chargement des jeux pour {platform}")
write_unavailable_systems()
return sources
except Exception as e:
logger.error(f"Erreur lors du chargement de sources.json: {str(e)}")
return []
# 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
def truncate_text_middle(text, font, max_width): def truncate_text_middle(text, font, max_width):
@@ -190,3 +263,36 @@ def load_system_image(platform_dict):
except Exception as e: except Exception as e:
logger.error(f"Erreur lors du chargement de l'image pour {platform_name} : {str(e)}") logger.error(f"Erreur lors du chargement de l'image pour {platform_name} : {str(e)}")
return None return None
# Dossier musique Batocera
music_folder = "/userdata/roms/ports/RGSX/assets/music"
music_files = [f for f in os.listdir(music_folder) if f.lower().endswith(('.ogg', '.mp3'))]
current_music = None # Suivre la musique en cours
loading_step = "none"
def play_random_music():
"""Joue une musique aléatoire et configure l'événement de fin."""
global current_music
if music_files:
# Éviter de rejouer la même musique consécutivement
available_music = [f for f in music_files if f != current_music]
if not available_music: # Si une seule musique, on la reprend
available_music = music_files
music_file = random.choice(available_music)
music_path = os.path.join(music_folder, music_file)
logger.debug(f"Lecture de la musique : {music_path}")
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
current_music = music_file # Mettre à jour la musique en cours
set_music_popup(music_file) # Afficher le nom de la musique dans la popup
else:
logger.debug("Aucune musique trouvée dans /userdata/roms/ports/RGSX/assets/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