diff --git a/__main__.py b/__main__.py index 6a8eeba..1fe0755 100644 --- a/__main__.py +++ b/__main__.py @@ -9,11 +9,12 @@ import logging import requests import sys import json -from display import init_display, draw_loading_screen, draw_error_screen, draw_platform_grid, draw_progress_screen, draw_scrollbar, draw_confirm_dialog, draw_controls, draw_gradient, draw_virtual_keyboard, draw_popup_message, draw_extension_warning, draw_pause_menu, draw_controls_help, draw_game_list +from display import init_display, draw_loading_screen, draw_error_screen, draw_platform_grid, draw_progress_screen, draw_scrollbar, draw_confirm_dialog, draw_controls, draw_gradient, draw_virtual_keyboard, draw_popup_message, draw_extension_warning, draw_pause_menu, draw_controls_help, draw_game_list, draw_history, draw_clear_history_dialog from network import test_internet, download_rom, check_extension_before_download, extract_zip -from controls import handle_controls +from controls import handle_controls, validate_menu_state from controls_mapper import load_controls_config, map_controls, draw_controls_mapping, ACTIONS from utils import truncate_text_end, load_system_image, load_games +from history import load_history import config # Configuration du logging @@ -104,6 +105,10 @@ config.repeat_key = None config.repeat_start_time = 0 config.repeat_last_action = 0 +# Chargement de l'historique +config.history = load_history() +logger.debug(f"Historique chargé: {len(config.history)} entrées") + # Vérification et chargement de la configuration des contrôles config.controls_config = load_controls_config() if not config.controls_config: @@ -287,10 +292,10 @@ async def main(): (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 == start_config.get("value")) 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"]: + 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_pause_option = 0 @@ -313,7 +318,7 @@ async def main(): (event.type == pygame.KEYDOWN and up_config and event.key == up_config.get("value")) or (event.type == pygame.JOYBUTTONDOWN and up_config and up_config.get("type") == "button" and event.button == up_config.get("value")) or (event.type == pygame.JOYAXISMOTION and up_config and up_config.get("type") == "axis" and event.axis == up_config.get("value")[0] and abs(event.value) > 0.5 and (1 if event.value > 0 else -1) == up_config.get("value")[1]) or - (event.type == pygame.JOYHATMOTION and up_config and up_config.get("type") == "hat" and event.value == up_config.get("value")) + (event.type == pygame.JOYHATMOTION and up_config and up_config.get("type") == "hat" and event.value == tuple(up_config.get("value"))) ): config.selected_pause_option = max(0, config.selected_pause_option - 1) config.repeat_action = "up" @@ -326,9 +331,9 @@ async def main(): (event.type == pygame.KEYDOWN and down_config and event.key == down_config.get("value")) or (event.type == pygame.JOYBUTTONDOWN and down_config and down_config.get("type") == "button" and event.button == down_config.get("value")) or (event.type == pygame.JOYAXISMOTION and down_config and down_config.get("type") == "axis" and event.axis == down_config.get("value")[0] and abs(event.value) > 0.5 and (1 if event.value > 0 else -1) == down_config.get("value")[1]) or - (event.type == pygame.JOYHATMOTION and down_config and down_config.get("type") == "hat" and event.value == down_config.get("value")) + (event.type == pygame.JOYHATMOTION and down_config and down_config.get("type") == "hat" and event.value == tuple(down_config.get("value"))) ): - config.selected_pause_option = min(2, config.selected_pause_option + 1) + config.selected_pause_option = min(3, config.selected_pause_option + 1) config.repeat_action = "down" config.repeat_start_time = current_time + REPEAT_DELAY config.repeat_last_action = current_time @@ -339,15 +344,17 @@ async def main(): (event.type == pygame.KEYDOWN and confirm_config and event.key == confirm_config.get("value")) or (event.type == pygame.JOYBUTTONDOWN and confirm_config and confirm_config.get("type") == "button" and event.button == confirm_config.get("value")) or (event.type == pygame.JOYAXISMOTION and confirm_config and confirm_config.get("type") == "axis" and event.axis == confirm_config.get("value")[0] and abs(event.value) > 0.5 and (1 if event.value > 0 else -1) == confirm_config.get("value")[1]) or - (event.type == pygame.JOYHATMOTION and confirm_config and confirm_config.get("type") == "hat" and event.value == confirm_config.get("value")) + (event.type == pygame.JOYHATMOTION and confirm_config and confirm_config.get("type") == "hat" and event.value == tuple(confirm_config.get("value"))) ): if config.selected_pause_option == 0: + config.previous_menu_state = validate_menu_state(config.previous_menu_state) config.menu_state = "controls_help" config.needs_redraw = True logger.debug("Menu pause: Aide sélectionnée") elif config.selected_pause_option == 1: + config.previous_menu_state = validate_menu_state(config.previous_menu_state) if map_controls(screen): - config.menu_state = config.previous_menu_state if config.previous_menu_state in ["platform", "game", "download_progress", "download_result", "confirm_exit", "extension_warning"] else "platform" + config.menu_state = config.previous_menu_state if config.previous_menu_state in ["platform", "game", "download_progress", "download_result", "confirm_exit", "extension_warning", "history"] else "platform" config.controls_config = load_controls_config() logger.debug(f"Mappage des contrôles terminé, retour à {config.menu_state}") else: @@ -356,15 +363,25 @@ async def main(): config.needs_redraw = True logger.debug("Échec du mappage des contrôles") elif config.selected_pause_option == 2: - running = False + config.previous_menu_state = validate_menu_state(config.previous_menu_state) + config.menu_state = "history" + config.current_history_item = 0 + config.history_scroll_offset = 0 + config.needs_redraw = True + logger.debug("Menu pause: Historique sélectionné") + elif config.selected_pause_option == 3: + config.previous_menu_state = validate_menu_state(config.previous_menu_state) + config.menu_state = "confirm_exit" + config.confirm_selection = 0 + config.needs_redraw = True logger.debug("Menu pause: Quitter sélectionné") elif ( (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 == cancel_config.get("value")) + (event.type == pygame.JOYHATMOTION and cancel_config and cancel_config.get("type") == "hat" and event.value == tuple(cancel_config.get("value"))) ): - config.menu_state = config.previous_menu_state if config.previous_menu_state in ["platform", "game", "download_progress", "download_result", "confirm_exit", "extension_warning"] else "platform" + config.menu_state = config.previous_menu_state if config.previous_menu_state in ["platform", "game", "download_progress", "download_result", "confirm_exit", "extension_warning", "history"] else "platform" config.needs_redraw = True logger.debug(f"Menu pause: Annulation, retour à {config.menu_state}") @@ -388,7 +405,7 @@ async def main(): config.needs_redraw = True logger.debug(f"Menu pause: Répétition haut, selected_option={config.selected_pause_option}") elif config.repeat_action == "down": - config.selected_pause_option = min(2, config.selected_pause_option + 1) + config.selected_pause_option = min(3, config.selected_pause_option + 1) config.needs_redraw = True logger.debug(f"Menu pause: Répétition bas, selected_option={config.selected_pause_option}") config.repeat_start_time = current_time + REPEAT_INTERVAL @@ -399,14 +416,24 @@ async def main(): 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")) + (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 in ["platform", "game", "error", "confirm_exit", "download_progress", "download_result", "extension_warning"]: + # Gérer confirm_clear_history explicitement + 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 in ["platform", "game", "error", "confirm_exit", "download_progress", "download_result", "extension_warning", "history"]: action = handle_controls(event, sources, joystick, screen) config.needs_redraw = True if action == "quit": @@ -430,8 +457,31 @@ async def main(): task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported)) config.download_tasks[task] = (task, url, game_name, platform) config.menu_state = "download_progress" + config.pending_download = None # Réinitialiser après démarrage du téléchargement config.needs_redraw = True 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: + 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] + 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: + task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported)) + config.download_tasks[task] = (task, url, game_name, platform) + config.menu_state = "download_progress" + config.pending_download = None # Réinitialiser après démarrage du téléchargement + config.needs_redraw = True + logger.debug(f"Retéléchargement démarré pour {game_name}, passage à download_progress") + break # Gestion des téléchargements if config.download_tasks: @@ -444,6 +494,7 @@ async def main(): config.download_result_start_time = pygame.time.get_ticks() config.menu_state = "download_result" config.download_progress.clear() # Réinitialiser download_progress + config.pending_download = None # Réinitialiser après téléchargement config.needs_redraw = True del config.download_tasks[task_id] logger.debug(f"Téléchargement terminé: {game_name}, succès={success}, message={message}") @@ -453,14 +504,16 @@ async def main(): config.download_result_start_time = pygame.time.get_ticks() config.menu_state = "download_result" config.download_progress.clear() # Réinitialiser download_progress + config.pending_download = None # Réinitialiser après téléchargement config.needs_redraw = True del config.download_tasks[task_id] logger.error(f"Erreur dans tâche de téléchargement: {str(e)}") # 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 = "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.pending_download = None # Réinitialiser après affichage du résultat config.needs_redraw = True logger.debug(f"Fin popup download_result, retour à {config.menu_state}") @@ -500,7 +553,18 @@ async def main(): 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(screen) + logger.debug("Rendu de draw_history") + elif config.menu_state == "confirm_clear_history": + draw_clear_history_dialog(screen) + logger.debug("Rendu de confirm_clear_history") + 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) pygame.display.flip() config.needs_redraw = False diff --git a/config.py b/config.py index 705c32c..edba70e 100644 --- a/config.py +++ b/config.py @@ -45,6 +45,16 @@ pending_download = None controls_config = {} selected_pause_option = 0 previous_menu_state = None +history = [] # Liste des entrées de l'historique +current_history_item = 0 # Index de l'élément sélectionné dans l'historique +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) # Résolution de l'écran screen_width = 800 @@ -68,6 +78,8 @@ small_font = None CONTROLS_CONFIG_PATH = "/userdata/saves/ports/rgsx/controls.json" """Chemin du fichier de configuration des contrôles.""" +HISTORY_PATH = "/userdata/saves/ports/rgsx/history.json" +"""Chemin du fichier de l'historique des téléchargements.""" def init_font(): """Initialise les polices après pygame.init().""" diff --git a/controls.py b/controls.py index 1cdfbe4..82e0d02 100644 --- a/controls.py +++ b/controls.py @@ -3,10 +3,12 @@ import config from config import CONTROLS_CONFIG_PATH import asyncio import math +import json from display import draw_validation_transition from network import download_rom, check_extension_before_download from controls_mapper import get_readable_input_name -from utils import load_games # Ajout de l'import +from utils import load_games +from history import load_history, clear_history import logging logger = logging.getLogger(__name__) @@ -18,15 +20,57 @@ JOYHAT_DEBOUNCE = 200 # Délai anti-rebond pour JOYHATMOTION (ms) JOYAXIS_DEBOUNCE = 50 # Délai anti-rebond pour JOYAXISMOTION (ms) REPEAT_ACTION_DEBOUNCE = 50 # Délai anti-rebond pour répétitions up/down/left/right (ms) +# Liste des états valides (mise à jour) +VALID_STATES = [ + "platform", "game", "download_progress", "download_result", "confirm_exit", + "extension_warning", "pause_menu", "controls_help", "history", "remap_controls", + "error", "loading", "confirm_clear_history" # Ajout du nouvel état +] + +def validate_menu_state(state): + """Valide l'état du menu et retourne un état par défaut si non valide.""" + return state if state in VALID_STATES else "platform" + + + def load_controls_config(path=CONTROLS_CONFIG_PATH): """Charge la configuration des contrôles depuis un fichier JSON.""" try: with open(path, "r") as f: - return json.load(f) + config_data = json.load(f) + # Vérifier les actions nécessaires + required_actions = ["confirm", "cancel", "left", "right"] + for action in required_actions: + if action not in config_data: + logger.warning(f"Action {action} manquante dans {path}, utilisation de la valeur par défaut") + config_data[action] = { + "type": "key", + "value": { + "confirm": pygame.K_RETURN, + "cancel": pygame.K_ESCAPE, + "left": pygame.K_LEFT, + "right": pygame.K_RIGHT + }[action] + } + return config_data except (FileNotFoundError, json.JSONDecodeError) as e: - logging.error(f"Erreur lors de la lecture de {path} : {e}") - return {} - + logger.error(f"Erreur lors de la lecture de {path} : {e}, utilisation de la configuration par défaut") + return { + "confirm": {"type": "key", "value": pygame.K_RETURN}, + "cancel": {"type": "key", "value": pygame.K_ESCAPE}, + "left": {"type": "key", "value": pygame.K_LEFT}, + "right": {"type": "key", "value": pygame.K_RIGHT}, + "up": {"type": "key", "value": pygame.K_UP}, + "down": {"type": "key", "value": pygame.K_DOWN}, + "start": {"type": "key", "value": pygame.K_p}, + "progress": {"type": "key", "value": pygame.K_t}, + "page_up": {"type": "key", "value": pygame.K_PAGEUP}, + "page_down": {"type": "key", "value": pygame.K_PAGEDOWN}, + "filter": {"type": "key", "value": pygame.K_f}, + "delete": {"type": "key", "value": pygame.K_BACKSPACE}, + "space": {"type": "key", "value": pygame.K_SPACE} + } + def is_input_matched(event, action_name): """Vérifie si l'événement correspond à l'action configurée.""" if not config.controls_config.get(action_name): @@ -53,7 +97,6 @@ def is_input_matched(event, action_name): logger.debug(f"Vérification axis: event_axis={event_axis}, event_value={event_value}, input_value={input_value}, result={result}") return result elif input_type == "hat" and event_type == pygame.JOYHATMOTION: - # Convertir input_value en tuple pour comparaison input_value_tuple = tuple(input_value) if isinstance(input_value, list) else input_value logger.debug(f"Vérification hat: event_value={event_value}, input_value={input_value_tuple}") return event_value == input_value_tuple @@ -64,11 +107,15 @@ def is_input_matched(event, action_name): def handle_controls(event, sources, joystick, screen): """Gère un événement clavier/joystick/souris et la répétition automatique. - Retourne 'quit', 'download', ou None. + Retourne 'quit', 'download', 'redownload', ou None. """ action = None current_time = pygame.time.get_ticks() + # Valider previous_menu_state avant tout traitement + config.previous_menu_state = validate_menu_state(config.previous_menu_state) + logger.debug(f"Validation initiale: previous_menu_state={config.previous_menu_state}") + # Debounce général if current_time - config.last_state_change_time < config.debounce_delay: return action @@ -100,14 +147,25 @@ def handle_controls(event, sources, joystick, screen): if is_input_matched(event, action_name): logger.debug(f"Action mappée détectée: {action_name}, input={get_readable_input_name(event)}") + # Menu pause + if is_input_matched(event, "start") and config.menu_state not in ("pause_menu", "controls_help", "history", "remap_controls"): + config.previous_menu_state = config.menu_state + config.menu_state = "pause_menu" + config.selected_option = 0 + config.needs_redraw = True + logger.debug(f"Passage à pause_menu depuis {config.previous_menu_state}") + return action + # Erreur if config.menu_state == "error": if is_input_matched(event, "confirm"): config.menu_state = "loading" + config.needs_redraw = True logger.debug("Sortie erreur avec Confirm") elif is_input_matched(event, "cancel"): config.menu_state = "confirm_exit" config.confirm_selection = 0 + config.needs_redraw = True # Plateformes elif config.menu_state == "platform": @@ -167,17 +225,29 @@ def handle_controls(event, sources, joystick, screen): config.repeat_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 else event.value config.needs_redraw = True elif is_input_matched(event, "page_down"): - if (config.current_page + 1) * 9 < len(config.platforms): - config.current_page += 1 - config.selected_platform = config.current_page * 9 + row * 3 - if config.selected_platform >= len(config.platforms): - config.selected_platform = len(config.platforms) - 1 - config.repeat_action = None # Réinitialiser la répétition - config.repeat_key = None - config.repeat_start_time = 0 - config.repeat_last_action = current_time - config.needs_redraw = True - logger.debug("Page suivante, répétition réinitialisée") + if (config.current_page + 1) * 9 < len(config.platforms): + config.current_page += 1 + config.selected_platform = config.current_page * 9 + row * 3 + if config.selected_platform >= len(config.platforms): + config.selected_platform = len(config.platforms) - 1 + config.repeat_action = None + config.repeat_key = None + config.repeat_start_time = 0 + config.repeat_last_action = current_time + config.needs_redraw = True + logger.debug("Page suivante, répétition réinitialisée") + elif is_input_matched(event, "page_up"): + if config.current_page > 0: + config.current_page -= 1 + config.selected_platform = config.current_page * 9 + row * 3 + if config.selected_platform >= len(config.platforms): + config.selected_platform = len(config.platforms) - 1 + config.repeat_action = None + config.repeat_key = None + config.repeat_start_time = 0 + config.repeat_last_action = current_time + config.needs_redraw = True + logger.debug("Page précédente, répétition réinitialisée") elif is_input_matched(event, "progress"): if config.download_tasks: config.menu_state = "download_progress" @@ -186,352 +256,445 @@ def handle_controls(event, sources, joystick, screen): elif is_input_matched(event, "confirm"): if config.platforms: config.current_platform = config.selected_platform - config.games = load_games(config.platforms[config.current_platform]) # Appel à load_games depuis utils + config.games = load_games(config.platforms[config.current_platform]) config.filtered_games = config.games config.filter_active = False config.current_game = 0 config.scroll_offset = 0 - draw_validation_transition(screen, config.current_platform) # Animation de transition + draw_validation_transition(screen, config.current_platform) config.menu_state = "game" config.needs_redraw = True logger.debug(f"Plateforme sélectionnée: {config.platforms[config.current_platform]}, {len(config.games)} jeux chargés") elif is_input_matched(event, "cancel"): config.menu_state = "confirm_exit" config.confirm_selection = 0 + config.needs_redraw = True # Jeux elif config.menu_state == "game": - if config.search_mode: - if config.is_non_pc: - keyboard_layout = [ - ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], - ['A', 'Z', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'], - ['Q', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M'], - ['W', 'X', 'C', 'V', 'B', 'N'] - ] - row, col = config.selected_key - max_row = len(keyboard_layout) - 1 - max_col = len(keyboard_layout[row]) - 1 - if is_input_matched(event, "up"): - if row > 0: - config.selected_key = (row - 1, min(col, len(keyboard_layout[row - 1]) - 1)) - config.repeat_action = "up" - config.repeat_start_time = current_time + REPEAT_DELAY - config.repeat_last_action = current_time - config.repeat_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 else event.value - config.needs_redraw = True - elif is_input_matched(event, "down"): - if row < max_row: - config.selected_key = (row + 1, min(col, len(keyboard_layout[row + 1]) - 1)) - config.repeat_action = "down" - config.repeat_start_time = current_time + REPEAT_DELAY - config.repeat_last_action = current_time - config.repeat_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 else event.value - config.needs_redraw = True - elif is_input_matched(event, "left"): - if col > 0: - config.selected_key = (row, col - 1) - config.repeat_action = "left" - config.repeat_start_time = current_time + REPEAT_DELAY - config.repeat_last_action = current_time - config.repeat_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 else event.value - config.needs_redraw = True - elif is_input_matched(event, "right"): - if col < max_col: - config.selected_key = (row, col + 1) - config.repeat_action = "right" - config.repeat_start_time = current_time + REPEAT_DELAY - config.repeat_last_action = current_time - config.repeat_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 else event.value - config.needs_redraw = True - elif is_input_matched(event, "confirm"): - key = keyboard_layout[row][col] - if len(config.search_query) < 50: - config.search_query += key.lower() - config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()] if config.search_query else config.games - config.current_game = 0 - config.scroll_offset = 0 - config.needs_redraw = True - elif is_input_matched(event, "delete"): - config.search_query = config.search_query[:-1] - config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()] if config.search_query else config.games - config.current_game = 0 - config.scroll_offset = 0 + games = config.filtered_games if config.filter_active or config.search_mode else config.games + if config.search_mode and config.is_non_pc: + keyboard_layout = [ + ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], + ['A', 'Z', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'], + ['Q', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M'], + ['W', 'X', 'C', 'V', 'B', 'N'] + ] + row, col = config.selected_key + max_row = len(keyboard_layout) - 1 + max_col = len(keyboard_layout[row]) - 1 + if is_input_matched(event, "up"): + if row > 0: + config.selected_key = (row - 1, min(col, len(keyboard_layout[row - 1]) - 1)) + config.repeat_action = "up" + config.repeat_start_time = current_time + REPEAT_DELAY + config.repeat_last_action = current_time + config.repeat_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 else event.value config.needs_redraw = True - elif is_input_matched(event, "space"): - config.search_query += " " - config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()] if config.search_query else config.games - config.current_game = 0 - config.scroll_offset = 0 + elif is_input_matched(event, "down"): + if row < max_row: + config.selected_key = (row + 1, min(col, len(keyboard_layout[row + 1]) - 1)) + config.repeat_action = "down" + config.repeat_start_time = current_time + REPEAT_DELAY + config.repeat_last_action = current_time + config.repeat_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 else event.value config.needs_redraw = True - elif is_input_matched(event, "cancel"): - config.search_mode = False - config.search_query = "" - config.filtered_games = config.games - config.filter_active = False - config.current_game = 0 - config.scroll_offset = 0 + elif is_input_matched(event, "left"): + if col > 0: + config.selected_key = (row, col - 1) + config.repeat_action = "left" + config.repeat_start_time = current_time + REPEAT_DELAY + config.repeat_last_action = current_time + config.repeat_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 else event.value config.needs_redraw = True - logger.debug("Filtre annulé") - elif is_input_matched(event, "filter"): - config.search_mode = False - config.filter_active = bool(config.search_query) + elif is_input_matched(event, "right"): + if col < max_col: + config.selected_key = (row, col + 1) + config.repeat_action = "right" + config.repeat_start_time = current_time + REPEAT_DELAY + config.repeat_last_action = current_time + config.repeat_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 else event.value config.needs_redraw = True - else: - if is_input_matched(event, "confirm"): - config.search_mode = False - config.filter_active = bool(config.search_query) - config.needs_redraw = True - elif is_input_matched(event, "cancel"): - config.search_mode = False - config.search_query = "" - config.filtered_games = config.games - config.filter_active = False - config.current_game = 0 - config.scroll_offset = 0 - config.needs_redraw = True - logger.debug("Filtre annulé") - elif event.type == pygame.KEYDOWN: - if event.key == pygame.K_BACKSPACE: - config.search_query = config.search_query[:-1] - config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()] if config.search_query else config.games - config.current_game = 0 - config.scroll_offset = 0 - config.needs_redraw = True - elif event.key == pygame.K_SPACE: - config.search_query += " " - config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()] if config.search_query else config.games - config.current_game = 0 - config.scroll_offset = 0 - config.needs_redraw = True - elif event.unicode.isprintable() and len(config.search_query) < 50: - config.search_query += event.unicode - config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()] if config.search_query else config.games - config.current_game = 0 - config.scroll_offset = 0 - config.needs_redraw = True - else: - if is_input_matched(event, "down"): - config.current_game = min(config.current_game + 1, len(config.filtered_games) - 1) - if config.current_game >= config.scroll_offset + config.visible_games: - config.scroll_offset += 1 - config.repeat_action = "down" - config.repeat_start_time = current_time + REPEAT_DELAY - config.repeat_last_action = current_time - config.repeat_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 else event.value - config.needs_redraw = True - elif is_input_matched(event, "up"): - config.current_game = max(config.current_game - 1, 0) - if config.current_game < config.scroll_offset: - config.scroll_offset -= 1 - config.repeat_action = "up" - config.repeat_start_time = current_time + REPEAT_DELAY - config.repeat_last_action = current_time - config.repeat_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 else event.value - config.needs_redraw = True - elif is_input_matched(event, "page_up"): - config.current_game = max(config.current_game - config.visible_games, 0) - config.scroll_offset = max(config.scroll_offset - config.visible_games, 0) - config.repeat_action = "page_up" - config.repeat_start_time = current_time + REPEAT_DELAY - config.repeat_last_action = current_time - config.repeat_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 else event.value - config.needs_redraw = True - elif is_input_matched(event, "page_down"): - config.current_game = min(config.current_game + config.visible_games, len(config.filtered_games) - 1) - config.scroll_offset = min(config.scroll_offset + config.visible_games, len(config.filtered_games) - config.visible_games) - config.repeat_action = "page_down" - config.repeat_start_time = current_time + REPEAT_DELAY - config.repeat_last_action = current_time - config.repeat_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 else event.value - config.needs_redraw = True elif is_input_matched(event, "confirm"): - if config.filtered_games: - action = "download" + config.search_query += keyboard_layout[row][col] + config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()] + config.current_game = 0 + config.scroll_offset = 0 + config.needs_redraw = True + logger.debug(f"Recherche mise à jour: query={config.search_query}, jeux filtrés={len(config.filtered_games)}") + elif is_input_matched(event, "delete"): + if config.search_query: + config.search_query = config.search_query[:-1] + config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()] + config.current_game = 0 + config.scroll_offset = 0 + config.needs_redraw = True + logger.debug(f"Suppression caractère: query={config.search_query}, jeux filtrés={len(config.filtered_games)}") + elif is_input_matched(event, "space"): + config.search_query += " " + config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()] + config.current_game = 0 + config.scroll_offset = 0 + config.needs_redraw = True + logger.debug(f"Espace ajouté: query={config.search_query}, jeux filtrés={len(config.filtered_games)}") + elif is_input_matched(event, "cancel"): + config.search_mode = False + config.search_query = "" + config.selected_key = (0, 0) + config.filtered_games = config.games + config.current_game = 0 + config.scroll_offset = 0 + config.needs_redraw = True + logger.debug("Sortie du mode recherche") + else: + if is_input_matched(event, "up"): + if config.current_game > 0: + config.current_game -= 1 + config.repeat_action = "up" + config.repeat_start_time = current_time + REPEAT_DELAY + config.repeat_last_action = current_time + config.repeat_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 else event.value + config.needs_redraw = True + elif is_input_matched(event, "down"): + if config.current_game < len(games) - 1: + config.current_game += 1 + config.repeat_action = "down" + config.repeat_start_time = current_time + REPEAT_DELAY + config.repeat_last_action = current_time + config.repeat_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 else event.value + config.needs_redraw = True + elif is_input_matched(event, "page_up"): + config.current_game = max(0, config.current_game - config.visible_games) + config.repeat_action = None + config.repeat_key = None + config.repeat_start_time = 0 + config.repeat_last_action = current_time + config.needs_redraw = True + logger.debug("Page précédente dans la liste des jeux") + elif is_input_matched(event, "page_down"): + config.current_game = min(len(games) - 1, config.current_game + config.visible_games) + config.repeat_action = None + config.repeat_key = None + config.repeat_start_time = 0 + config.repeat_last_action = current_time + config.needs_redraw = True + logger.debug("Page suivante dans la liste des jeux") elif is_input_matched(event, "filter"): config.search_mode = True config.search_query = "" config.filtered_games = config.games + config.current_game = 0 + config.scroll_offset = 0 config.selected_key = (0, 0) config.needs_redraw = True logger.debug("Entrée en mode recherche") - elif is_input_matched(event, "cancel"): - config.menu_state = "platform" - config.current_game = 0 - config.scroll_offset = 0 - config.filter_active = False - config.filtered_games = config.games - config.needs_redraw = True - logger.debug("Retour à platform, filtre réinitialisé") elif is_input_matched(event, "progress"): if config.download_tasks: config.menu_state = "download_progress" config.needs_redraw = True logger.debug("Retour à download_progress depuis game") + elif is_input_matched(event, "confirm"): + if games: + config.pending_download = check_extension_before_download(games[config.current_game][0], config.platforms[config.current_platform], games[config.current_game][1]) + if config.pending_download: + url, platform, game_name, is_zip_non_supported = config.pending_download + if is_zip_non_supported: + config.menu_state = "extension_warning" + config.extension_confirm_selection = 0 + config.needs_redraw = True + logger.debug(f"Extension non supportée, passage à extension_warning pour {game_name}") + else: + task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported)) + config.download_tasks[task] = (task, url, game_name, platform) # Stocker tuple de 4 éléments + config.menu_state = "download_progress" + config.needs_redraw = True + logger.debug(f"Début du téléchargement: {game_name} pour {platform} depuis {url}") + config.pending_download = None # Réinitialiser après démarrage + action = "download" + else: + config.menu_state = "error" + config.error_message = "Extension non supportée ou erreur de téléchargement" + config.pending_download = None + config.needs_redraw = True + logger.error(f"config.pending_download est None pour {games[config.current_game][0]}") + elif is_input_matched(event, "cancel"): + config.menu_state = "platform" + config.current_game = 0 + config.scroll_offset = 0 + config.needs_redraw = True + logger.debug("Retour à platform") - # Download progress + elif config.menu_state == "history": + history = config.history + if is_input_matched(event, "up"): + if config.current_history_item > 0: + config.current_history_item -= 1 + config.repeat_action = "up" + config.repeat_start_time = current_time + REPEAT_DELAY + config.repeat_last_action = current_time + config.repeat_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 else event.value + config.needs_redraw = True + elif is_input_matched(event, "down"): + if config.current_history_item < len(history) - 1: + config.current_history_item += 1 + config.repeat_action = "down" + config.repeat_start_time = current_time + REPEAT_DELAY + config.repeat_last_action = current_time + config.repeat_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 else event.value + config.needs_redraw = True + elif is_input_matched(event, "page_up"): + config.current_history_item = max(0, config.current_history_item - config.visible_history_items) + config.repeat_action = None + config.repeat_key = None + config.repeat_start_time = 0 + config.repeat_last_action = current_time + config.needs_redraw = True + logger.debug("Page précédente dans l'historique") + elif is_input_matched(event, "page_down"): + config.current_history_item = min(len(history) - 1, config.current_history_item + config.visible_history_items) + config.repeat_action = None + config.repeat_key = None + config.repeat_start_time = 0 + config.repeat_last_action = current_time + config.needs_redraw = True + logger.debug("Page suivante dans l'historique") + elif is_input_matched(event, "progress"): + config.previous_menu_state = validate_menu_state(config.previous_menu_state) + config.menu_state = "confirm_clear_history" + config.confirm_clear_selection = 0 # 0 pour "Non", 1 pour "Oui" + config.needs_redraw = True + logger.debug("Passage à confirm_clear_history depuis history") + elif is_input_matched(event, "confirm"): + if history: + entry = history[config.current_history_item] + platform = entry["platform"] + game_name = entry["game_name"] + # Rechercher l'URL dans config.games + for game in config.games: + if game[0] == game_name and config.platforms[config.current_platform] == platform: + config.pending_download = check_extension_before_download(game_name, platform, game[1]) + if config.pending_download: + url, platform, game_name, is_zip_non_supported = config.pending_download + if is_zip_non_supported: + config.previous_menu_state = validate_menu_state(config.previous_menu_state) + config.menu_state = "extension_warning" + config.extension_confirm_selection = 0 + config.needs_redraw = True + logger.debug(f"Extension non supportée pour retéléchargement, passage à extension_warning pour {game_name}") + else: + task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported)) + config.download_tasks[task] = (task, url, game_name, platform) + config.previous_menu_state = validate_menu_state(config.previous_menu_state) + config.menu_state = "download_progress" + config.needs_redraw = True + logger.debug(f"Retéléchargement: {game_name} pour {platform} depuis {url}") + config.pending_download = None + action = "redownload" + else: + config.menu_state = "error" + config.error_message = "Extension non supportée ou erreur de retéléchargement" + config.pending_download = None + config.needs_redraw = True + logger.error(f"config.pending_download est None pour {game_name}") + break + elif is_input_matched(event, "cancel"): + config.menu_state = validate_menu_state(config.previous_menu_state) + config.current_history_item = 0 + config.history_scroll_offset = 0 + config.needs_redraw = True + logger.debug(f"Retour à {config.menu_state} depuis history") + # Ajouter un nouvel état "confirm_clear_history" après l'état "confirm_exit" + elif config.menu_state == "confirm_clear_history": + logger.debug(f"État confirm_clear_history, confirm_clear_selection={config.confirm_clear_selection}, événement={event.type}, valeur={getattr(event, 'value', None)}") + if is_input_matched(event, "confirm"): + logger.debug(f"Action confirm détectée dans confirm_clear_history") + if config.confirm_clear_selection == 1: # Oui + clear_history() + config.history = [] + config.current_history_item = 0 + config.history_scroll_offset = 0 + config.menu_state = "history" + config.needs_redraw = True + logger.info("Historique vidé après confirmation") + else: # Non + config.menu_state = "history" + config.needs_redraw = True + logger.debug("Annulation du vidage de l'historique, retour à history") + elif is_input_matched(event, "left"): + logger.debug(f"Action left détectée dans confirm_clear_history") + config.confirm_clear_selection = 1 # Sélectionner "Non" + config.needs_redraw = True + logger.debug(f"Changement sélection confirm_clear_history: {config.confirm_clear_selection}") + elif is_input_matched(event, "right"): + logger.debug(f"Action right détectée dans confirm_clear_history") + config.confirm_clear_selection = 0 # Sélectionner "Oui" + config.needs_redraw = True + logger.debug(f"Changement sélection confirm_clear_history: {config.confirm_clear_selection}") + elif is_input_matched(event, "cancel"): + logger.debug(f"Action cancel détectée dans confirm_clear_history") + config.menu_state = "history" + config.needs_redraw = True + logger.debug("Annulation du vidage de l'historique, retour à history") + # Progression téléchargement elif config.menu_state == "download_progress": if is_input_matched(event, "cancel"): - if config.download_tasks: - task = list(config.download_tasks.keys())[0] - config.download_tasks[task][0].cancel() - url = config.download_tasks[task][1] - game_name = config.download_tasks[task][2] - if url in config.download_progress: - del config.download_progress[url] - del config.download_tasks[task] - config.download_result_message = f"Téléchargement annulé : {game_name}" - config.download_result_error = True - config.download_result_start_time = pygame.time.get_ticks() - config.menu_state = "download_result" - elif is_input_matched(event, "progress"): - config.menu_state = "game" - config.needs_redraw = True - logger.debug("Retour à game depuis download_progress") - - # Confirmation de sortie - elif config.menu_state == "confirm_exit": - if is_input_matched(event, "left"): - config.confirm_selection = 1 - config.needs_redraw = True - logger.debug("Sélection Oui") - elif is_input_matched(event, "right"): - config.confirm_selection = 0 - config.needs_redraw = True - logger.debug("Sélection Non") - elif is_input_matched(event, "confirm"): - if config.confirm_selection == 1: - logger.debug("Retour de 'quit' pour fermer l'application") - return "quit" - else: - config.menu_state = "platform" - config.needs_redraw = True - logger.debug("Retour à platform depuis confirm_exit") - elif is_input_matched(event, "cancel"): - config.menu_state = "platform" - config.needs_redraw = True - logger.debug("Annulation confirm_exit") - - # Avertissement d'extension - elif config.menu_state == "extension_warning": - if is_input_matched(event, "left"): - config.extension_confirm_selection = 1 - config.needs_redraw = True - logger.debug("Sélection Oui (extension_warning)") - elif is_input_matched(event, "right"): - config.extension_confirm_selection = 0 - config.needs_redraw = True - logger.debug("Sélection Non (extension_warning)") - elif is_input_matched(event, "confirm"): - if config.extension_confirm_selection == 1: - if config.pending_download: - url, platform, game_name, is_zip_non_supported = config.pending_download - task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported=is_zip_non_supported)) - config.download_tasks[task] = (task, url, game_name, platform) - config.menu_state = "download_progress" - config.pending_download = None - config.needs_redraw = True - else: - config.menu_state = "game" - config.needs_redraw = True - else: - config.menu_state = "game" - config.pending_download = None - config.needs_redraw = True - logger.debug("Téléchargement annulé (extension_warning)") - elif is_input_matched(event, "cancel"): - config.menu_state = "game" + for task in config.download_tasks: + task.cancel() + config.download_tasks.clear() + config.download_progress.clear() config.pending_download = None + config.menu_state = validate_menu_state(config.previous_menu_state) config.needs_redraw = True - logger.debug("Annulation extension_warning") + logger.debug(f"Téléchargement annulé, retour à {config.menu_state}") + elif is_input_matched(event, "progress"): + config.menu_state = validate_menu_state(config.previous_menu_state) + config.needs_redraw = True + logger.debug(f"Retour à {config.menu_state} depuis download_progress") # Résultat téléchargement elif config.menu_state == "download_result": if is_input_matched(event, "confirm"): - config.menu_state = "game" + config.menu_state = validate_menu_state(config.previous_menu_state) + config.popup_timer = 0 + config.pending_download = None config.needs_redraw = True - logger.debug("Retour à game depuis download_result") + logger.debug(f"Retour à {config.menu_state} depuis download_result") - # Enregistrer la touche pour la répétition - if config.repeat_action in ["up", "down", "page_up", "page_down", "left", "right"]: - if event.type == pygame.KEYDOWN: - config.repeat_key = event.key - elif event.type == pygame.JOYBUTTONDOWN: - config.repeat_key = event.button - elif event.type == pygame.JOYAXISMOTION: - config.repeat_key = (event.axis, 1 if event.value > 0 else -1) - elif event.type == pygame.JOYHATMOTION: - config.repeat_key = event.value + # Confirmation quitter + elif config.menu_state == "confirm_exit": + if is_input_matched(event, "confirm"): + if config.confirm_selection == 1: + return "quit" + else: + config.menu_state = validate_menu_state(config.previous_menu_state) + config.needs_redraw = True + logger.debug(f"Retour à {config.menu_state} depuis confirm_exit") + elif is_input_matched(event, "left") or is_input_matched(event, "right"): + config.confirm_selection = 1 - config.confirm_selection + config.needs_redraw = True + logger.debug(f"Changement sélection confirm_exit: {config.confirm_selection}") + + # Avertissement extension + elif config.menu_state == "extension_warning": + if is_input_matched(event, "confirm"): + if config.extension_confirm_selection == 1: + if config.pending_download and len(config.pending_download) == 4: + url, platform, game_name, is_zip_non_supported = config.pending_download + task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported)) + config.download_tasks[task] = (task, url, game_name, platform) + config.previous_menu_state = validate_menu_state(config.previous_menu_state) + config.menu_state = "download_progress" + config.needs_redraw = True + logger.debug(f"Téléchargement confirmé après avertissement: {game_name} pour {platform} depuis {url}") + config.pending_download = None + action = "download" + else: + config.menu_state = "error" + config.error_message = "Données de téléchargement invalides" + config.pending_download = None + config.needs_redraw = True + logger.error("config.pending_download invalide") + else: + config.pending_download = None + config.menu_state = validate_menu_state(config.previous_menu_state) + config.needs_redraw = True + logger.debug(f"Retour à {config.menu_state} depuis extension_warning") + elif is_input_matched(event, "left") or is_input_matched(event, "right"): + config.extension_confirm_selection = 1 - config.extension_confirm_selection + config.needs_redraw = True + logger.debug(f"Changement sélection extension_warning: {config.extension_confirm_selection}") + elif is_input_matched(event, "cancel"): + config.pending_download = None + config.menu_state = validate_menu_state(config.previous_menu_state) + config.needs_redraw = True + logger.debug(f"Retour à {config.menu_state} depuis extension_warning") + + # Menu pause + elif config.menu_state == "pause_menu": + if is_input_matched(event, "up"): + config.selected_option = max(0, config.selected_option - 1) + config.repeat_action = "up" + config.repeat_start_time = current_time + REPEAT_DELAY config.repeat_last_action = current_time + config.repeat_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 else event.value + config.needs_redraw = True + elif is_input_matched(event, "down"): + config.selected_option = min(3, config.selected_option + 1) + config.repeat_action = "down" + config.repeat_start_time = current_time + REPEAT_DELAY + config.repeat_last_action = current_time + config.repeat_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 else event.value + config.needs_redraw = True + elif is_input_matched(event, "confirm"): + if config.selected_option == 0: # Controls + config.previous_menu_state = validate_menu_state(config.previous_menu_state) + config.menu_state = "controls_help" + config.needs_redraw = True + logger.debug(f"Passage à controls_help depuis pause_menu") + elif config.selected_option == 1: # Remap controls + config.previous_menu_state = validate_menu_state(config.previous_menu_state) + config.menu_state = "remap_controls" + config.needs_redraw = True + logger.debug(f"Passage à remap_controls depuis pause_menu") + elif config.selected_option == 2: # History + config.history = load_history() + config.current_history_item = 0 + config.history_scroll_offset = 0 + config.previous_menu_state = validate_menu_state(config.previous_menu_state) + config.menu_state = "history" + config.needs_redraw = True + logger.debug(f"Passage à history depuis pause_menu") + elif config.selected_option == 3: # Quit + config.previous_menu_state = validate_menu_state(config.previous_menu_state) + config.menu_state = "confirm_exit" + config.confirm_selection = 0 + config.needs_redraw = True + logger.debug(f"Passage à confirm_exit depuis pause_menu") + elif is_input_matched(event, "cancel"): + config.menu_state = validate_menu_state(config.previous_menu_state) + config.needs_redraw = True + logger.debug(f"Retour à {config.menu_state} depuis pause_menu") - elif event.type in (pygame.KEYUP, pygame.JOYBUTTONUP): - if config.menu_state in ("game", "platform") and is_input_matched(event, config.repeat_action): - config.repeat_action = None - config.repeat_key = None - config.repeat_start_time = 0 - config.needs_redraw = True + # Aide contrôles + elif config.menu_state == "controls_help": + if is_input_matched(event, "cancel"): + config.menu_state = validate_menu_state(config.previous_menu_state) + config.needs_redraw = True + logger.debug(f"Retour à {config.menu_state} depuis controls_help") - # Gestion de la répétition automatique - if config.menu_state in ("game", "platform") and config.repeat_action: - if current_time >= config.repeat_start_time: - if config.repeat_action in ["up", "down", "left", "right"] and current_time - config.repeat_last_action < REPEAT_ACTION_DEBOUNCE: - return action + # Remap controls + elif config.menu_state == "remap_controls": + if is_input_matched(event, "cancel"): + config.menu_state = "pause_menu" + config.needs_redraw = True + logger.debug("Retour à pause_menu depuis remap_controls") - last_repeat_time = config.repeat_start_time - REPEAT_INTERVAL + # Gestion de la répétition automatique (relâchement) + if event.type in (pygame.KEYUP, pygame.JOYBUTTONUP, pygame.JOYAXISMOTION, pygame.JOYHATMOTION): + if event.type == pygame.JOYAXISMOTION and abs(event.value) > 0.5: + return action + if event.type == pygame.JOYHATMOTION and event.value != (0, 0): + return action + config.repeat_action = None + config.repeat_key = None + config.repeat_start_time = 0 + logger.debug("Répétition arrêtée") + + return action + +async def handle_repeat_actions(): + """Gère la répétition automatique des actions.""" + current_time = pygame.time.get_ticks() + if config.repeat_action and config.repeat_key and current_time > config.repeat_start_time: + if current_time - config.repeat_last_action > REPEAT_ACTION_DEBOUNCE: + logger.debug(f"Répétition action: {config.repeat_action}") + event_dict = { + "type": pygame.KEYDOWN if isinstance(config.repeat_key, int) and config.repeat_key < 1000 else pygame.JOYBUTTONDOWN if isinstance(config.repeat_key, int) else pygame.JOYAXISMOTION if isinstance(config.repeat_key, tuple) and len(config.repeat_key) == 2 else pygame.JOYHATMOTION, + "key": config.repeat_key if isinstance(config.repeat_key, int) and config.repeat_key < 1000 else None, + "button": config.repeat_key if isinstance(config.repeat_key, int) and config.repeat_key >= 1000 else None, + "axis": config.repeat_key[0] if isinstance(config.repeat_key, tuple) and len(config.repeat_key) == 2 else None, + "value": config.repeat_key[1] if isinstance(config.repeat_key, tuple) and len(config.repeat_key) == 2 else config.repeat_key if isinstance(config.repeat_key, tuple) else None + } + handle_controls(event_dict, None, None, None) config.repeat_last_action = current_time - if config.menu_state == "game": - if config.repeat_action == "down": - config.current_game = min(config.current_game + 1, len(config.filtered_games) - 1) - if config.current_game >= config.scroll_offset + config.visible_games: - config.scroll_offset += 1 - config.needs_redraw = True - elif config.repeat_action == "up": - config.current_game = max(config.current_game - 1, 0) - if config.current_game < config.scroll_offset: - config.scroll_offset -= 1 - config.needs_redraw = True - elif config.repeat_action == "page_down": - config.current_game = min(config.current_game + config.visible_games, len(config.filtered_games) - 1) - config.scroll_offset = min(config.scroll_offset + config.visible_games, len(config.filtered_games) - config.visible_games) - config.needs_redraw = True - elif config.repeat_action == "page_up": - config.current_game = max(config.current_game - config.visible_games, 0) - config.scroll_offset = max(config.scroll_offset - config.visible_games, 0) - config.needs_redraw = True - elif config.menu_state == "platform": - max_index = min(9, len(config.platforms) - config.current_page * 9) - 1 - current_grid_index = config.selected_platform - config.current_page * 9 - row = current_grid_index // 3 - if config.repeat_action == "down": - if current_grid_index + 3 <= max_index: - config.selected_platform += 3 - config.needs_redraw = True - elif config.repeat_action == "up": - if current_grid_index - 3 >= 0: - config.selected_platform -= 3 - config.needs_redraw = True - elif config.repeat_action == "left": - if current_grid_index % 3 != 0: - config.selected_platform -= 1 - config.needs_redraw = True - elif config.current_page > 0: - config.current_page -= 1 - config.selected_platform = config.current_page * 9 + row * 3 + 2 - if config.selected_platform >= len(config.platforms): - config.selected_platform = len(config.platforms) - 1 - config.needs_redraw = True - elif config.repeat_action == "right": - if current_grid_index % 3 != 2 and current_grid_index < max_index: - config.selected_platform += 1 - config.needs_redraw = True - elif (config.current_page + 1) * 9 < len(config.platforms): - config.current_page += 1 - config.selected_platform = config.current_page * 9 + row * 3 - if config.selected_platform >= len(config.platforms): - config.selected_platform = len(config.platforms) - 1 - config.needs_redraw = True - config.repeat_start_time = last_repeat_time + REPEAT_INTERVAL - if config.repeat_start_time < current_time: - config.repeat_start_time = current_time + REPEAT_INTERVAL - - return action \ No newline at end of file + config.repeat_start_time = current_time + REPEAT_INTERVAL \ No newline at end of file diff --git a/display.py b/display.py index 0203d8d..0bcadaa 100644 --- a/display.py +++ b/display.py @@ -3,6 +3,7 @@ import config import math from utils import truncate_text_end, wrap_text, load_system_image, load_games import logging +from history import load_history # Ajout de l'import logger = logging.getLogger(__name__) @@ -223,18 +224,18 @@ def draw_game_list(screen): line_height = config.font.get_height() + 10 margin_top_bottom = 20 - extra_margin_top = 5 # Marge supplémentaire pour éviter le chevauchement avec le titre - extra_margin_bottom = 40 # Marge supplémentaire en bas pour éloigner du texte des contrôles - title_height = max(config.title_font.get_height(), config.search_font.get_height(), config.small_font.get_height()) + 20 # Hauteur du titre avec padding réduit + extra_margin_top = 5 + extra_margin_bottom = 40 + title_height = max(config.title_font.get_height(), config.search_font.get_height(), config.small_font.get_height()) + 20 available_height = config.screen_height - title_height - extra_margin_top - extra_margin_bottom - 2 * margin_top_bottom games_per_page = available_height // line_height + config.visible_games = games_per_page # Mettre à jour config.visible_games max_text_width = max([config.font.size(truncate_text_end(game[0] if isinstance(game, (list, tuple)) else game, config.font, config.screen_width - 80))[0] for game in games], default=300) rect_width = max_text_width + 40 rect_height = games_per_page * line_height + 2 * margin_top_bottom rect_x = (config.screen_width - rect_width) // 2 rect_y = title_height + extra_margin_top + (config.screen_height - title_height - extra_margin_top - extra_margin_bottom - rect_height) // 2 - # Limiter scroll_offset pour éviter l'espace vide config.scroll_offset = max(0, min(config.scroll_offset, max(0, len(games) - games_per_page))) if config.current_game < config.scroll_offset: config.scroll_offset = config.current_game @@ -243,7 +244,6 @@ def draw_game_list(screen): screen.blit(OVERLAY, (0, 0)) - # Afficher le titre ou le texte de recherche/filtre if config.search_mode: search_text = f"Filtrer : {config.search_query}_" title_surface = config.search_font.render(search_text, True, (255, 255, 255)) @@ -272,7 +272,6 @@ def draw_game_list(screen): pygame.draw.rect(screen, (255, 255, 255), title_rect_inflated, 2, border_radius=10) screen.blit(title_surface, title_rect) - # Afficher le rectangle de fond et la liste des jeux pygame.draw.rect(screen, (50, 50, 50, 200), (rect_x, rect_y, rect_width, rect_height), border_radius=10) pygame.draw.rect(screen, (255, 255, 255), (rect_x, rect_y, rect_width, rect_height), 2, border_radius=10) @@ -288,7 +287,126 @@ def draw_game_list(screen): draw_scrollbar(screen) if config.search_mode and config.is_non_pc: draw_virtual_keyboard(screen) + +def draw_history_list(screen): + """Affiche l'historique des téléchargements sous forme de tableau avec système, nom du jeu et état.""" + logger.debug("Début de draw_history_list") + + history = config.history if hasattr(config, 'history') else load_history() + history_count = len(history) + + if not history: + logger.debug("Aucun historique disponible") + message = "Aucun téléchargement dans l'historique" + lines = wrap_text(message, config.font, config.screen_width - 80) + line_height = config.font.get_height() + 5 + text_height = len(lines) * line_height + margin_top_bottom = 20 + rect_height = text_height + 2 * margin_top_bottom + max_text_width = max([config.font.size(line)[0] for line in lines], default=300) + rect_width = max_text_width + 40 + rect_x = (config.screen_width - rect_width) // 2 + rect_y = (config.screen_height - rect_height) // 2 + + screen.blit(OVERLAY, (0, 0)) + pygame.draw.rect(screen, (50, 50, 50, 200), (rect_x, rect_y, rect_width, rect_height), border_radius=10) + pygame.draw.rect(screen, (255, 255, 255), (rect_x, rect_y, rect_width, rect_height), 2, border_radius=10) + + for i, line in enumerate(lines): + text_surface = config.font.render(line, True, (255, 255, 255)) + text_rect = text_surface.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + i * line_height + line_height // 2)) + screen.blit(text_surface, text_rect) + return + + line_height = config.small_font.get_height() + 10 + margin_top_bottom = 20 + extra_margin_top = 5 + extra_margin_bottom = 40 + title_height = config.title_font.get_height() + 20 + available_height = config.screen_height - title_height - extra_margin_top - extra_margin_bottom - 2 * margin_top_bottom + items_per_page = available_height // line_height + config.visible_history_items = items_per_page # Mettre à jour config.visible_history_items + + # Calculer la largeur des colonnes + col_platform_width = config.screen_width // 4 # ~25% pour le système + col_game_width = config.screen_width // 2 # ~50% pour le nom du jeu + col_status_width = config.screen_width // 4 # ~25% pour l'état + max_text_width = col_platform_width + col_game_width + col_status_width + rect_width = max_text_width + 40 + rect_height = items_per_page * line_height + 2 * margin_top_bottom + rect_x = (config.screen_width - rect_width) // 2 + rect_y = title_height + extra_margin_top + (config.screen_height - title_height - extra_margin_top - extra_margin_bottom - rect_height) // 2 + + config.history_scroll_offset = max(0, min(config.history_scroll_offset, max(0, len(history) - items_per_page))) + if config.current_history_item < config.history_scroll_offset: + config.history_scroll_offset = config.current_history_item + elif config.current_history_item >= config.history_scroll_offset + items_per_page: + config.history_scroll_offset = config.current_history_item - items_per_page + 1 + + screen.blit(OVERLAY, (0, 0)) + + title_text = f"Historique des téléchargements ({history_count})" + title_surface = config.title_font.render(title_text, True, (255, 255, 255)) + title_rect = title_surface.get_rect(center=(config.screen_width // 2, title_surface.get_height() // 2 + 10)) + title_rect_inflated = title_rect.inflate(40, 20) + title_rect_inflated.topleft = ((config.screen_width - title_rect_inflated.width) // 2, 0) + pygame.draw.rect(screen, (50, 50, 50, 200), title_rect_inflated, border_radius=10) + pygame.draw.rect(screen, (255, 255, 255), title_rect_inflated, 2, border_radius=10) + screen.blit(title_surface, title_rect) + + pygame.draw.rect(screen, (50, 50, 50, 200), (rect_x, rect_y, rect_width, rect_height), border_radius=10) + pygame.draw.rect(screen, (255, 255, 255), (rect_x, rect_y, rect_width, rect_height), 2, border_radius=10) + + # En-têtes du tableau + headers = ["Système", "Nom du jeu", "État"] + header_y = rect_y + margin_top_bottom - line_height // 2 + header_x_positions = [ + rect_x + 20 + col_platform_width // 2, + rect_x + 20 + col_platform_width + col_game_width // 2, + rect_x + 20 + col_platform_width + col_game_width + col_status_width // 2 + ] + for header, x_pos in zip(headers, header_x_positions): + text_surface = config.small_font.render(header, True, (255, 255, 255)) + text_rect = text_surface.get_rect(center=(x_pos, header_y)) + screen.blit(text_surface, text_rect) + + # Lignes du tableau + for i in range(config.history_scroll_offset, min(config.history_scroll_offset + items_per_page, len(history))): + entry = history[i] + platform = entry["platform"] + game_name = entry["game_name"] + status = entry["status"] + color = (0, 150, 255) if i == config.current_history_item else (255, 255, 255) + platform_text = truncate_text_end(platform, config.small_font, col_platform_width - 10) + game_text = truncate_text_end(game_name, config.small_font, col_game_width - 10) + status_text = truncate_text_end(status, config.small_font, col_status_width - 10) + y_pos = rect_y + margin_top_bottom + (i - config.history_scroll_offset + 1) * line_height + line_height // 2 + platform_surface = config.small_font.render(platform_text, True, color) + game_surface = config.small_font.render(game_text, True, color) + status_surface = config.small_font.render(status_text, True, color) + + platform_rect = platform_surface.get_rect(center=(header_x_positions[0], y_pos)) + game_rect = game_surface.get_rect(center=(header_x_positions[1], y_pos)) + status_rect = status_surface.get_rect(center=(header_x_positions[2], y_pos)) + + screen.blit(platform_surface, platform_rect) + screen.blit(game_surface, game_rect) + screen.blit(status_surface, status_rect) + logger.debug(f"Entrée historique affichée : index={i}, platform={platform_text}, game={game_text}, status={status_text}, selected={i == config.current_history_item}") + + draw_history_scrollbar(screen) + +def draw_history_scrollbar(screen): + """Affiche la barre de défilement pour l'historique.""" + if len(config.history) <= config.visible_history_items: + return + + game_area_height = config.screen_height - 150 + scrollbar_height = game_area_height * (config.visible_history_items / len(config.history)) + scrollbar_y = 120 + (game_area_height - scrollbar_height) * (config.history_scroll_offset / max(1, len(config.history) - config.visible_history_items)) + pygame.draw.rect(screen, (255, 255, 255), (config.screen_width - 25, scrollbar_y, 15, scrollbar_height)) + def draw_virtual_keyboard(screen): """Affiche un clavier virtuel pour la saisie dans search_mode, centré verticalement.""" keyboard_layout = [ @@ -352,10 +470,10 @@ def draw_progress_screen(screen): text_height = len(title_lines) * line_height margin_top_bottom = 20 bar_height = int(config.screen_height * 0.0278) # ~30px pour 1080p - percent_height = line_height # Hauteur pour le texte de progression + percent_height = line_height rect_height = text_height + bar_height + percent_height + 3 * margin_top_bottom max_text_width = max([config.font.size(line)[0] for line in title_lines], default=300) - bar_width = max_text_width # Ajuster la barre à la largeur du texte + bar_width = max_text_width rect_width = max_text_width + 40 rect_x = (config.screen_width - rect_width) // 2 rect_y = (config.screen_height - rect_height) // 2 @@ -389,7 +507,7 @@ def draw_progress_screen(screen): percent_rect = percent_render.get_rect(center=(config.screen_width // 2, text_y + i * line_height + line_height // 2)) screen.blit(percent_render, percent_rect) logger.debug(f"Texte de progression affiché : texte={line}, position={percent_rect}, taille={percent_render.get_size()}") - + def draw_scrollbar(screen): """Affiche la barre de défilement à droite de l’écran.""" if len(config.filtered_games) <= config.visible_games: @@ -400,11 +518,11 @@ def draw_scrollbar(screen): scrollbar_y = 120 + (game_area_height - scrollbar_height) * (config.scroll_offset / max(1, len(config.filtered_games) - config.visible_games)) pygame.draw.rect(screen, (255, 255, 255), (config.screen_width - 25, scrollbar_y, 15, scrollbar_height)) -def draw_confirm_dialog(screen): - """Affiche la boîte de dialogue de confirmation pour quitter.""" +def draw_clear_history_dialog(screen): + """Affiche la boîte de dialogue de confirmation pour vider l'historique.""" screen.blit(OVERLAY, (0, 0)) - message = "Voulez-vous vraiment quitter ?" + message = "Vider l'historique ?" wrapped_message = wrap_text(message, config.font, config.screen_width - 80) line_height = config.font.get_height() + 5 text_height = len(wrapped_message) * line_height @@ -424,14 +542,14 @@ def draw_confirm_dialog(screen): text_rect = text.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + i * line_height + line_height // 2)) screen.blit(text, text_rect) - yes_text = config.font.render("Oui", True, (0, 150, 255) if config.confirm_selection == 1 else (255, 255, 255)) - no_text = config.font.render("Non", True, (0, 150, 255) if config.confirm_selection == 0 else (255, 255, 255)) + yes_text = config.font.render("Oui", True, (0, 150, 255) if config.confirm_clear_selection == 1 else (255, 255, 255)) + no_text = config.font.render("Non", True, (0, 150, 255) if config.confirm_clear_selection == 0 else (255, 255, 255)) yes_rect = yes_text.get_rect(center=(config.screen_width // 2 - 100, rect_y + text_height + margin_top_bottom + line_height // 2)) no_rect = no_text.get_rect(center=(config.screen_width // 2 + 100, rect_y + text_height + margin_top_bottom + line_height // 2)) screen.blit(yes_text, yes_rect) screen.blit(no_text, no_rect) - + def draw_popup_message(screen, message, is_error): """Affiche une popup avec un message de résultat.""" screen.blit(OVERLAY, (0, 0)) @@ -526,12 +644,12 @@ def draw_extension_warning(screen): def draw_controls(screen, menu_state): """Affiche les contrôles sur une seule ligne en bas de l’écran pour tous les états du menu.""" start_button = get_control_display('start', 'START') - control_text = f"Menu {menu_state} - {start_button} : Options - Controls" + control_text = f"Menu {menu_state} - {start_button} : Options - History - Controls" max_width = config.screen_width - 40 wrapped_controls = wrap_text(control_text, config.small_font, max_width) line_height = config.small_font.get_height() + 5 rect_height = len(wrapped_controls) * line_height + 20 - rect_y = config.screen_height - rect_height - 40 # Marge inférieure de 40px + rect_y = config.screen_height - rect_height - 5 for i, line in enumerate(wrapped_controls): text_surface = config.small_font.render(line, True, (255, 255, 255)) @@ -561,12 +679,13 @@ def draw_validation_transition(screen, platform_index): pygame.time.wait(10) def draw_pause_menu(screen, selected_option): - """Dessine le menu pause avec les options Aide, Configurer contrôles, Quitter.""" + """Dessine le menu pause avec les options Aide, Configurer contrôles, Historique, Quitter.""" screen.blit(OVERLAY, (0, 0)) options = [ "Controls", "Remap controls", + "History", "Quit" ] @@ -629,7 +748,6 @@ def draw_controls_help(screen, previous_state): common_controls["space"]() ] if config.search_mode and config.is_non_pc else []), *( [ - f"Saisir texte : Filtrer" if config.search_mode else f"{common_controls['up']('Naviguer')} / {common_controls['down']('Naviguer')}", f"{common_controls['page_up']('Page')} / {common_controls['page_down']('Page')}", common_controls["filter"]("Filtrer") @@ -650,6 +768,14 @@ def draw_controls_help(screen, previous_state): ], "extension_warning": [ common_controls["confirm"]("Confirmer") + ], + "history": [ + common_controls["confirm"]("Retélécharger"), + common_controls["cancel"]("Retour"), + common_controls["progress"]("Vider l'historique"), + f"{common_controls['up']('Naviguer')} / {common_controls['down']('Naviguer')}", + f"{common_controls['page_up']('Page')} / {common_controls['page_down']('Page')}", + common_controls["start"]() ] } @@ -681,4 +807,120 @@ def draw_controls_help(screen, previous_state): for i, line in enumerate(wrapped_controls): text = config.font.render(line, True, (255, 255, 255)) text_rect = text.get_rect(center=(config.screen_width // 2, popup_y + 40 + i * line_height)) - screen.blit(text, text_rect) \ No newline at end of file + screen.blit(text, text_rect) + +def draw_history(screen): + """Affiche la liste de l'historique des téléchargements.""" + if not config.history: + text = config.font.render("Aucun téléchargement dans l'historique", True, (255, 255, 255)) + screen.blit(text, (config.screen_width // 2 - text.get_width() // 2, config.screen_height // 2)) + return + + # Calculer le nombre d'éléments visibles + item_height = config.small_font.get_height() + 10 + config.visible_history_items = (config.screen_height - 200) // item_height + max_scroll = max(0, len(config.history) - config.visible_history_items) + config.history_scroll_offset = max(0, min(config.history_scroll_offset, max_scroll)) + + # Cadre semi-transparent + panel_width = config.screen_width - 100 + panel_height = config.visible_history_items * item_height + 20 + panel_x = (config.screen_width - panel_width) // 2 + panel_y = (config.screen_height - panel_height) // 2 + panel_surface = pygame.Surface((panel_width, panel_height), pygame.SRCALPHA) + panel_surface.fill((0, 0, 0, 128)) + screen.blit(panel_surface, (panel_x, panel_y)) + pygame.draw.rect(screen, (255, 255, 255), (panel_x, panel_y, panel_width, panel_height), 2) + + # Afficher les colonnes + headers = ["Système", "Nom du jeu", "État"] + col_widths = [panel_width // 4, panel_width // 2, panel_width // 4] + header_y = panel_y + 10 + for i, header in enumerate(headers): + text = config.small_font.render(header, True, (255, 255, 255)) + screen.blit(text, (panel_x + sum(col_widths[:i]) + 10, header_y)) + + # Afficher les entrées + start_index = config.history_scroll_offset + end_index = min(start_index + config.visible_history_items, len(config.history)) + for i, entry in enumerate(config.history[start_index:end_index]): + y = panel_y + 40 + i * item_height + color = (255, 255, 0) if i + start_index == config.current_history_item else (255, 255, 255) + system = config.platform_names.get(entry["platform"], entry["platform"]) + system_text = truncate_text_end(system, config.small_font, col_widths[0] - 20) + game_text = truncate_text_end(entry["game_name"], config.small_font, col_widths[1] - 20) + status_text = entry["status"] + texts = [system_text, game_text, status_text] + for j, text in enumerate(texts): + rendered = config.small_font.render(text, True, color) + screen.blit(rendered, (panel_x + sum(col_widths[:j]) + 10, y)) + if i + start_index == config.current_history_item: + pygame.draw.rect(screen, (255, 255, 0), (panel_x + 5, y - 5, panel_width - 10, item_height), 1) + + # Barre de défilement + if len(config.history) > config.visible_history_items: + draw_scrollbar(screen, config.history_scroll_offset, len(config.history), config.visible_history_items, panel_x + panel_width - 10, panel_y, panel_height) + +def draw_confirm_dialog(screen): + """Affiche la boîte de dialogue de confirmation pour quitter.""" + screen.blit(OVERLAY, (0, 0)) + + message = "Voulez-vous vraiment quitter ?" + wrapped_message = wrap_text(message, config.font, config.screen_width - 80) + line_height = config.font.get_height() + 5 + text_height = len(wrapped_message) * line_height + button_height = line_height + 20 + margin_top_bottom = 20 + rect_height = text_height + button_height + 2 * margin_top_bottom + max_text_width = max([config.font.size(line)[0] for line in wrapped_message], default=300) + rect_width = max_text_width + 40 + rect_x = (config.screen_width - rect_width) // 2 + rect_y = (config.screen_height - rect_height) // 2 + + pygame.draw.rect(screen, (50, 50, 50, 200), (rect_x, rect_y, rect_width, rect_height), border_radius=10) + pygame.draw.rect(screen, (255, 255, 255), (rect_x, rect_y, rect_width, rect_height), 2, border_radius=10) + + for i, line in enumerate(wrapped_message): + text = config.font.render(line, True, (255, 255, 255)) + text_rect = text.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + i * line_height + line_height // 2)) + screen.blit(text, text_rect) + + yes_text = config.font.render("Oui", True, (0, 150, 255) if config.confirm_selection == 1 else (255, 255, 255)) + no_text = config.font.render("Non", True, (0, 150, 255) if config.confirm_selection == 0 else (255, 255, 255)) + yes_rect = yes_text.get_rect(center=(config.screen_width // 2 - 100, rect_y + text_height + margin_top_bottom + line_height // 2)) + no_rect = no_text.get_rect(center=(config.screen_width // 2 + 100, rect_y + text_height + margin_top_bottom + line_height // 2)) + + screen.blit(yes_text, yes_rect) + screen.blit(no_text, no_rect) + +def draw_clear_history_dialog(screen): + """Affiche la boîte de dialogue de confirmation pour vider l'historique.""" + screen.blit(OVERLAY, (0, 0)) + + message = "Vider l'historique ?" + wrapped_message = wrap_text(message, config.font, config.screen_width - 80) + line_height = config.font.get_height() + 5 + text_height = len(wrapped_message) * line_height + button_height = line_height + 20 + margin_top_bottom = 20 + rect_height = text_height + button_height + 2 * margin_top_bottom + max_text_width = max([config.font.size(line)[0] for line in wrapped_message], default=300) + rect_width = max_text_width + 40 + rect_x = (config.screen_width - rect_width) // 2 + rect_y = (config.screen_height - rect_height) // 2 + + pygame.draw.rect(screen, (50, 50, 50, 200), (rect_x, rect_y, rect_width, rect_height), border_radius=10) + pygame.draw.rect(screen, (255, 255, 255), (rect_x, rect_y, rect_width, rect_height), 2, border_radius=10) + + for i, line in enumerate(wrapped_message): + text = config.font.render(line, True, (255, 255, 255)) + text_rect = text.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + i * line_height + line_height // 2)) + screen.blit(text, text_rect) + + yes_text = config.font.render("Oui", True, (0, 150, 255) if config.confirm_clear_selection == 1 else (255, 255, 255)) + no_text = config.font.render("Non", True, (0, 150, 255) if config.confirm_clear_selection == 0 else (255, 255, 255)) + yes_rect = yes_text.get_rect(center=(config.screen_width // 2 - 100, rect_y + text_height + margin_top_bottom + line_height // 2)) + no_rect = no_text.get_rect(center=(config.screen_width // 2 + 100, rect_y + text_height + margin_top_bottom + line_height // 2)) + + screen.blit(yes_text, yes_rect) + screen.blit(no_text, no_rect) \ No newline at end of file diff --git a/history.py b/history.py index a0fc5a7..17ae092 100644 --- a/history.py +++ b/history.py @@ -1,7 +1,8 @@ import json import os import logging -from config import HISTORY_FILE_PATH +import config +from config import HISTORY_PATH logger = logging.getLogger(__name__) @@ -10,7 +11,7 @@ DEFAULT_HISTORY_PATH = "/userdata/saves/ports/rgsx/history.json" def init_history(): """Initialise le fichier history.json s'il n'existe pas.""" - history_path = getattr(config, 'HISTORY_FILE_PATH', DEFAULT_HISTORY_PATH) + history_path = getattr(config, 'HISTORY_PATH', DEFAULT_HISTORY_PATH) if not os.path.exists(history_path): try: os.makedirs(os.path.dirname(history_path), exist_ok=True) @@ -24,7 +25,7 @@ def init_history(): def load_history(): """Charge l'historique depuis history.json.""" - history_path = getattr(config, 'HISTORY_FILE_PATH', DEFAULT_HISTORY_PATH) + history_path = getattr(config, 'HISTORY_PATH', DEFAULT_HISTORY_PATH) try: with open(history_path, "r") as f: history = json.load(f) @@ -40,7 +41,7 @@ def load_history(): def save_history(history): """Sauvegarde l'historique dans history.json.""" - history_path = getattr(config, 'HISTORY_FILE_PATH', DEFAULT_HISTORY_PATH) + history_path = getattr(config, 'HISTORY_PATH', DEFAULT_HISTORY_PATH) try: with open(history_path, "w") as f: json.dump(history, f, indent=2) @@ -48,7 +49,7 @@ def save_history(history): except Exception as e: logger.error(f"Erreur lors de l'écriture de {history_path} : {e}") -def add_download_to_history(platform, game_name, status): +def add_to_history(platform, game_name, status): """Ajoute une entrée à l'historique.""" history = load_history() history.append({ @@ -61,7 +62,7 @@ def add_download_to_history(platform, game_name, status): def clear_history(): """Vide l'historique.""" - history_path = getattr(config, 'HISTORY_FILE_PATH', DEFAULT_HISTORY_PATH) + history_path = getattr(config, 'HISTORY_PATH', DEFAULT_HISTORY_PATH) try: with open(history_path, "w") as f: json.dump([], f) diff --git a/network.py b/network.py index 03efd15..e6268f1 100644 --- a/network.py +++ b/network.py @@ -6,10 +6,12 @@ import threading import pygame import zipfile import json +import time from urllib.parse import urljoin, unquote import asyncio import config from utils import sanitize_filename +from history import add_to_history, load_history import logging logger = logging.getLogger(__name__) @@ -330,12 +332,12 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False): if not success: raise Exception(f"Échec de l'extraction de l'archive: {msg}") result[0] = True - result[1] = f"Téléchargé et extrait : {game_name}" + result[1] = f"Downloaded / extracted : {game_name}" else: os.chmod(dest_path, 0o644) logger.debug(f"Téléchargement terminé: {dest_path}") result[0] = True - result[1] = f"Téléchargé : {game_name}" + result[1] = f"Download_OK : {game_name}" except Exception as e: logger.error(f"Erreur téléchargement {url}: {str(e)}") if url in config.download_progress: @@ -348,7 +350,15 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False): finally: logger.debug(f"Thread téléchargement terminé pour {url}") with lock: + config.download_result_message = result[1] + config.download_result_error = not result[0] + config.download_result_start_time = pygame.time.get_ticks() + config.menu_state = "download_result" config.needs_redraw = True # Forcer le redraw + # Enregistrement dans l'historique + add_to_history(platform, game_name, "Download_OK" if result[0] else "Erreur") + config.history = load_history() # Recharger l'historique + logger.debug(f"Enregistrement dans l'historique: platform={platform}, game_name={game_name}, status={'Download_OK' if result[0] else 'Erreur'}") thread = threading.Thread(target=download_thread) logger.debug(f"Démarrage thread pour {url}") @@ -359,37 +369,30 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False): thread.join() logger.debug(f"Thread rejoint pour {url}") - with threading.Lock(): - config.download_result_message = result[1] - config.download_result_error = not result[0] - config.download_result_start_time = pygame.time.get_ticks() - config.menu_state = "download_result" - config.needs_redraw = True # Forcer le redraw - logger.debug(f"Transition vers download_result, message={result[1]}, erreur={not result[0]}") return result[0], result[1] -def check_extension_before_download(url, platform, game_name): - """Vérifie l'extension avant de lancer le téléchargement.""" +def check_extension_before_download(game_name, platform, url): + """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 False, "Fichier de configuration des extensions introuvable", False - + 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 True, "", False + return (url, platform, game_name, False) else: if is_archive: logger.debug(f"Fichier {extension.upper()} détecté pour {sanitized_name}, extraction automatique prévue") - return False, f"Fichiers {extension.upper()} non supportés par cette plateforme, extraction automatique après le téléchargement.", True + return (url, platform, game_name, True) logger.debug(f"L'extension de {sanitized_name} n'est pas supportée pour {platform}") - return False, f"L'extension de {sanitized_name} n'est pas supportée pour {platform}", False + return None except Exception as e: logger.error(f"Erreur vérification extension {url}: {str(e)}") - return False, str(e), False \ No newline at end of file + return None \ No newline at end of file