diff --git a/__main__.py b/__main__.py index 4f455bb..402a42f 100644 --- a/__main__.py +++ b/__main__.py @@ -7,7 +7,7 @@ import logging import requests import queue import datetime -from display import init_display, draw_loading_screen, draw_error_screen, draw_platform_grid, draw_progress_screen, draw_controls, draw_virtual_keyboard, draw_popup_result_download, draw_extension_warning, draw_pause_menu, draw_controls_help, draw_game_list, draw_history_list, draw_clear_history_dialog, draw_confirm_dialog, draw_redownload_game_cache_dialog, draw_popup, draw_gradient, draw_language_menu, THEME_COLORS +from display import init_display, draw_loading_screen, draw_error_screen, draw_platform_grid, draw_progress_screen, draw_controls, draw_virtual_keyboard, draw_popup_result_download, draw_extension_warning, draw_pause_menu, draw_controls_help, draw_game_list, draw_history_list, draw_clear_history_dialog, draw_cancel_download_dialog, draw_confirm_dialog, draw_redownload_game_cache_dialog, draw_popup, draw_gradient, THEME_COLORS from language import handle_language_menu_events, _ from network import test_internet, download_rom, is_1fichier_url, download_from_1fichier, check_for_updates from controls import handle_controls, validate_menu_state, process_key_repeats @@ -38,8 +38,19 @@ logger = logging.getLogger(__name__) pygame.init() config.init_font() pygame.joystick.init() +logger.debug("--------------------------------------------------------------------") +logger.debug("---------------------------DEBUT LOG--------------------------------") +logger.debug("--------------------------------------------------------------------") +# Chargement des paramètres d'accessibilité +from utils import load_accessibility_settings +config.accessibility_settings = load_accessibility_settings() +for i, scale in enumerate(config.font_scale_options): + if scale == config.accessibility_settings.get("font_scale", 1.0): + config.current_font_scale_index = i + break + # Chargement et initialisation de la langue from language import initialize_language initialize_language() @@ -53,22 +64,8 @@ clock = pygame.time.Clock() pygame.display.set_caption("RGSX") -# Initialisation des polices -try: - font_path = os.path.join(config.APP_FOLDER, "assets", "Pixel-UniCode.ttf") - config.font = pygame.font.Font(font_path, 36) # Police principale - config.title_font = pygame.font.Font(font_path, 48) # Police pour les titres - config.search_font = pygame.font.Font(font_path, 48) # Police pour la recherche - config.progress_font = pygame.font.Font(font_path, 36) # Police pour l'affichage de la progression - config.small_font = pygame.font.Font(font_path, 28) # Police pour les petits textes - #logger.debug("Police Pixel-UniCode chargée") -except: - config.font = pygame.font.SysFont("arial", 48) # Police fallback - config.title_font = pygame.font.SysFont("arial", 60) # Police fallback pour les titres - config.search_font = pygame.font.SysFont("arial", 60) # Police fallback pour la recherche - config.progress_font = pygame.font.SysFont("arial", 36) # Police fallback pour l'affichage de la progression - config.small_font = pygame.font.SysFont("arial", 28) # Police fallback pour les petits textes - #logger.debug("Police Arial chargée") +# Initialisation des polices via config +config.init_font() # Mise à jour de la résolution dans config config.screen_width, config.screen_height = pygame.display.get_surface().get_size() @@ -190,8 +187,10 @@ async def main(): events = pygame.event.get() for event in events: # Gestion directe des événements pour le menu de langue - if config.menu_state == "language_select" and event.type == pygame.KEYDOWN: - handle_language_menu_events(event, screen) + if config.menu_state == "language_select": + from language import handle_language_menu_events + if handle_language_menu_events(event, screen): + config.needs_redraw = True continue if event.type == pygame.USEREVENT + 1: # Événement de fin de musique @@ -228,6 +227,12 @@ async def main(): #logger.debug(f"Événement transmis à handle_controls dans pause_menu: {event.type}") continue + if config.menu_state == "accessibility_menu": + from accessibility import handle_accessibility_events + if handle_accessibility_events(event): + config.needs_redraw = True + continue + if config.menu_state == "controls_help": action = handle_controls(event, sources, joystick, screen) config.needs_redraw = True @@ -237,7 +242,11 @@ async def main(): 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 == "confirm_cancel_download": + action = handle_controls(event, sources, joystick, screen) + config.needs_redraw = True continue if config.menu_state == "redownload_game_cache": @@ -298,6 +307,7 @@ async def main(): "game_name": game_name, "status": "downloading", "progress": 0, + "message": _("download_initializing"), "url": url, "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") }) @@ -559,10 +569,18 @@ async def main(): # logger.debug("Screen updated with draw_history_list") elif config.menu_state == "confirm_clear_history": draw_clear_history_dialog(screen) + elif config.menu_state == "confirm_cancel_download": + draw_cancel_download_dialog(screen) elif config.menu_state == "redownload_game_cache": draw_redownload_game_cache_dialog(screen) elif config.menu_state == "restart_popup": draw_popup(screen) + elif config.menu_state == "accessibility_menu": + from accessibility import draw_accessibility_menu + draw_accessibility_menu(screen) + elif config.menu_state == "language_select": + from display import draw_language_menu + draw_language_menu(screen) else: config.menu_state = "platform" draw_platform_grid(screen) diff --git a/accessibility.py b/accessibility.py new file mode 100644 index 0000000..2b7f9e1 --- /dev/null +++ b/accessibility.py @@ -0,0 +1,129 @@ +import pygame #type:ignore +import config +from utils import save_accessibility_settings +from language import _ + +def draw_accessibility_menu(screen): + """Affiche le menu d'accessibilité avec curseur pour la taille de police.""" + from display import OVERLAY, THEME_COLORS, draw_stylized_button + + screen.blit(OVERLAY, (0, 0)) + + # Titre + title_text = _("menu_accessibility") + title_surface = config.title_font.render(title_text, True, THEME_COLORS["text"]) + title_rect = title_surface.get_rect(center=(config.screen_width // 2, config.screen_height // 4)) + + # Fond du titre + title_bg_rect = title_rect.inflate(40, 20) + pygame.draw.rect(screen, THEME_COLORS["button_idle"], title_bg_rect, border_radius=10) + pygame.draw.rect(screen, THEME_COLORS["border"], title_bg_rect, 2, border_radius=10) + screen.blit(title_surface, title_rect) + + # Curseur de taille de police + current_scale = config.font_scale_options[config.current_font_scale_index] + font_text = _("accessibility_font_size").format(f"{current_scale:.1f}") + + # Position du curseur + cursor_y = config.screen_height // 2 + cursor_width = 400 + cursor_height = 60 + cursor_x = (config.screen_width - cursor_width) // 2 + + # Fond du curseur + pygame.draw.rect(screen, THEME_COLORS["button_idle"], (cursor_x, cursor_y, cursor_width, cursor_height), border_radius=10) + pygame.draw.rect(screen, THEME_COLORS["border"], (cursor_x, cursor_y, cursor_width, cursor_height), 2, border_radius=10) + + # Flèches gauche/droite + arrow_size = 30 + left_arrow_x = cursor_x + 20 + right_arrow_x = cursor_x + cursor_width - arrow_size - 20 + arrow_y = cursor_y + (cursor_height - arrow_size) // 2 + + # Flèche gauche + left_color = THEME_COLORS["fond_lignes"] if config.current_font_scale_index > 0 else THEME_COLORS["border"] + pygame.draw.polygon(screen, left_color, [ + (left_arrow_x + arrow_size, arrow_y), + (left_arrow_x, arrow_y + arrow_size // 2), + (left_arrow_x + arrow_size, arrow_y + arrow_size) + ]) + + # Flèche droite + right_color = THEME_COLORS["fond_lignes"] if config.current_font_scale_index < len(config.font_scale_options) - 1 else THEME_COLORS["border"] + pygame.draw.polygon(screen, right_color, [ + (right_arrow_x, arrow_y), + (right_arrow_x + arrow_size, arrow_y + arrow_size // 2), + (right_arrow_x, arrow_y + arrow_size) + ]) + + # Texte au centre + text_surface = config.font.render(font_text, True, THEME_COLORS["text"]) + text_rect = text_surface.get_rect(center=(cursor_x + cursor_width // 2, cursor_y + cursor_height // 2)) + screen.blit(text_surface, text_rect) + + # Instructions + instruction_text = _("language_select_instruction") + instruction_surface = config.small_font.render(instruction_text, True, THEME_COLORS["text"]) + instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, config.screen_height - 100)) + screen.blit(instruction_surface, instruction_rect) + +def handle_accessibility_events(event): + """Gère les événements du menu d'accessibilité avec support clavier et manette.""" + # Gestion des touches du clavier + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_LEFT and config.current_font_scale_index > 0: + config.current_font_scale_index -= 1 + update_font_scale() + return True + elif event.key == pygame.K_RIGHT and config.current_font_scale_index < len(config.font_scale_options) - 1: + config.current_font_scale_index += 1 + update_font_scale() + return True + elif event.key == pygame.K_RETURN or event.key == pygame.K_ESCAPE: + config.menu_state = "pause_menu" + return True + + # Gestion des boutons de la manette + elif event.type == pygame.JOYBUTTONDOWN: + if event.button == 0: # Bouton A (valider) + config.menu_state = "pause_menu" + return True + elif event.button == 1: # Bouton B (annuler) + config.menu_state = "pause_menu" + return True + + # Gestion du D-pad + elif event.type == pygame.JOYHATMOTION: + if event.value == (-1, 0): # Gauche + if config.current_font_scale_index > 0: + config.current_font_scale_index -= 1 + update_font_scale() + return True + elif event.value == (1, 0): # Droite + if config.current_font_scale_index < len(config.font_scale_options) - 1: + config.current_font_scale_index += 1 + update_font_scale() + return True + + # Gestion du joystick analogique (axe horizontal) + elif event.type == pygame.JOYAXISMOTION: + if event.axis == 0 and abs(event.value) > 0.5: # Joystick gauche horizontal + if event.value < -0.5 and config.current_font_scale_index > 0: # Gauche + config.current_font_scale_index -= 1 + update_font_scale() + return True + elif event.value > 0.5 and config.current_font_scale_index < len(config.font_scale_options) - 1: # Droite + config.current_font_scale_index += 1 + update_font_scale() + return True + + return False +def update_font_scale(): + """Met à jour l'échelle de police et sauvegarde.""" + new_scale = config.font_scale_options[config.current_font_scale_index] + config.accessibility_settings["font_scale"] = new_scale + save_accessibility_settings(config.accessibility_settings) + + # Réinitialiser les polices + config.init_font() + config.needs_redraw = True \ No newline at end of file diff --git a/config.py b/config.py index e6a2d07..a782c11 100644 --- a/config.py +++ b/config.py @@ -3,7 +3,7 @@ import os import logging # Version actuelle de l'application -app_version = "1.9.7.6" +app_version = "1.9.7.7" @@ -43,6 +43,9 @@ REPEAT_ACTION_DEBOUNCE = 150 # Délai anti-rebond pour répétitions (ms) - aug platforms = [] current_platform = 0 accessibility_mode = False # Mode accessibilité pour les polices agrandies +accessibility_settings = {"font_scale": 1.0} +font_scale_options = [0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0] +current_font_scale_index = 3 # Index pour 1.0 platform_names = {} # {platform_id: platform_name} games = [] current_game = 0 @@ -89,6 +92,7 @@ current_history_item = 0 history_scroll_offset = 0 # Offset pour le défilement de l'historique visible_history_items = 15 # Nombre d'éléments d'historique visibles (ajusté dynamiquement) confirm_clear_selection = 0 # confirmation clear historique +confirm_cancel_selection = 0 # confirmation annulation téléchargement last_state_change_time = 0 # Temps du dernier changement d'état pour debounce debounce_delay = 200 # Délai de debounce en millisecondes platform_dicts = [] # Liste des dictionnaires de plateformes @@ -126,27 +130,32 @@ small_font = None def init_font(): """Initialise les polices après pygame.init().""" - logger.debug("--------------------------------------------------------------------") - logger.debug("---------------------------DEBUT LOG--------------------------------") - logger.debug("--------------------------------------------------------------------") - global FONT, progress_font, title_font, search_font, small_font - multiplier = 1.5 if accessibility_mode else 1.0 + global font, progress_font, title_font, search_font, small_font + font_scale = accessibility_settings.get("font_scale", 1.0) try: - FONT = pygame.font.Font(None, int(36 * multiplier)) - progress_font = pygame.font.Font(None, int(28 * multiplier)) - title_font = pygame.font.Font(None, int(48 * multiplier)) - search_font = pygame.font.Font(None, int(36 * multiplier)) - small_font = pygame.font.Font(None, int(24 * multiplier)) - logger.debug(f"Polices initialisées avec succès (mode accessibilité: {accessibility_mode})") - # amazonq-ignore-next-line - except pygame.error as e: - logger.error(f"Erreur lors de l'initialisation des polices : {e}") - FONT = None - progress_font = None - title_font = None - search_font = None - small_font = None + font_path = os.path.join(APP_FOLDER, "assets", "Pixel-UniCode.ttf") + font = pygame.font.Font(font_path, int(36 * font_scale)) + title_font = pygame.font.Font(font_path, int(48 * font_scale)) + search_font = pygame.font.Font(font_path, int(48 * font_scale)) + progress_font = pygame.font.Font(font_path, int(36 * font_scale)) + small_font = pygame.font.Font(font_path, int(28 * font_scale)) + logger.debug(f"Polices Pixel-UniCode initialisées (font_scale: {font_scale})") + except Exception as e: + try: + font = pygame.font.SysFont("arial", int(48 * font_scale)) + title_font = pygame.font.SysFont("arial", int(60 * font_scale)) + search_font = pygame.font.SysFont("arial", int(60 * font_scale)) + progress_font = pygame.font.SysFont("arial", int(36 * font_scale)) + small_font = pygame.font.SysFont("arial", int(28 * font_scale)) + logger.debug(f"Polices Arial initialisées (font_scale: {font_scale})") + except Exception as e2: + logger.error(f"Erreur lors de l'initialisation des polices : {e2}") + font = None + progress_font = None + title_font = None + search_font = None + small_font = None def validate_resolution(): """Valide la résolution de l'écran par rapport aux capacités de l'écran.""" diff --git a/controls.py b/controls.py index 14d3993..f5a02d9 100644 --- a/controls.py +++ b/controls.py @@ -9,7 +9,7 @@ import json import os from display import draw_validation_transition from network import download_rom, download_from_1fichier, is_1fichier_url -from utils import load_games, check_extension_before_download, is_extension_supported, load_extensions_json, sanitize_filename +from utils import load_games, check_extension_before_download, is_extension_supported, load_extensions_json, sanitize_filename, load_api_key_1fichier from history import load_history, clear_history, add_to_history, save_history import logging from language import _ # Import de la fonction de traduction @@ -504,6 +504,7 @@ def handle_controls(event, sources, joystick, screen): config.current_history_item = len(config.history) - 1 # Vérifier d'abord si c'est un lien 1fichier if is_1fichier_url(url): + config.API_KEY_1FICHIER = load_api_key_1fichier() if not config.API_KEY_1FICHIER: config.previous_menu_state = config.menu_state config.menu_state = "error" @@ -739,13 +740,50 @@ def handle_controls(event, sources, joystick, screen): config.needs_redraw = True logger.error(f"config.pending_download est None pour {game_name}") break - elif is_input_matched(event, "cancel"): + elif is_input_matched(event, "cancel") or is_input_matched(event, "history"): + if config.history and config.current_history_item < len(config.history): + entry = config.history[config.current_history_item] + if entry.get("status") in ["downloading", "Téléchargement", "Extracting"] and is_input_matched(event, "cancel"): + config.menu_state = "confirm_cancel_download" + config.confirm_cancel_selection = 0 + config.needs_redraw = True + logger.debug("Demande d'annulation de téléchargement") + return action 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") + # Confirmation annulation téléchargement + elif config.menu_state == "confirm_cancel_download": + if is_input_matched(event, "confirm"): + if config.confirm_cancel_selection == 1: # Oui + entry = config.history[config.current_history_item] + url = entry.get("url") + # Annuler la tâche correspondante + for task_id, (task, task_url, game_name, platform) in list(config.download_tasks.items()): + if task_url == url: + task.cancel() + del config.download_tasks[task_id] + entry["status"] = "Canceled" + entry["progress"] = 0 + entry["message"] = "Téléchargement annulé" + save_history(config.history) + logger.debug(f"Téléchargement annulé: {game_name}") + break + config.menu_state = "history" + config.needs_redraw = True + else: # Non + config.menu_state = "history" + config.needs_redraw = True + elif is_input_matched(event, "left") or is_input_matched(event, "right"): + config.confirm_cancel_selection = 1 - config.confirm_cancel_selection + config.needs_redraw = True + elif is_input_matched(event, "cancel"): + config.menu_state = "history" + config.needs_redraw = True + # Confirmation vider l'historique" 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)}") @@ -866,26 +904,10 @@ def handle_controls(event, sources, joystick, screen): config.needs_redraw = True logger.debug(f"Passage à language_select depuis pause_menu") elif config.selected_option == 4: # Accessibility - config.accessibility_mode = not config.accessibility_mode - config.init_font() - # Reinitialiser les polices dans le main - font_path = os.path.join(config.APP_FOLDER, "assets", "Pixel-UniCode.ttf") - multiplier = 1.5 if config.accessibility_mode else 1.0 - try: - config.font = pygame.font.Font(font_path, int(36 * multiplier)) - config.title_font = pygame.font.Font(font_path, int(48 * multiplier)) - config.search_font = pygame.font.Font(font_path, int(48 * multiplier)) - config.progress_font = pygame.font.Font(font_path, int(36 * multiplier)) - config.small_font = pygame.font.Font(font_path, int(28 * multiplier)) - except: - config.font = pygame.font.SysFont("arial", int(48 * multiplier)) - config.title_font = pygame.font.SysFont("arial", int(60 * multiplier)) - config.search_font = pygame.font.SysFont("arial", int(60 * multiplier)) - config.progress_font = pygame.font.SysFont("arial", int(36 * multiplier)) - config.small_font = pygame.font.SysFont("arial", int(28 * multiplier)) - config.menu_state = config.previous_menu_state + config.previous_menu_state = validate_menu_state(config.previous_menu_state) + config.menu_state = "accessibility_menu" config.needs_redraw = True - logger.debug(f"Mode accessibilité {'activé' if config.accessibility_mode else 'désactivé'}") + logger.debug("Passage au menu accessibilité") elif config.selected_option == 5: # Redownload game cache config.previous_menu_state = validate_menu_state(config.previous_menu_state) config.menu_state = "redownload_game_cache" diff --git a/display.py b/display.py index f5acbe6..bf8214e 100644 --- a/display.py +++ b/display.py @@ -530,7 +530,7 @@ def draw_game_list(screen): for i in range(config.scroll_offset, min(config.scroll_offset + items_per_page, len(games))): game_name = games[i][0] if isinstance(games[i], (list, tuple)) else games[i] color = THEME_COLORS["fond_lignes"] if i == config.current_game else THEME_COLORS["text"] - game_text = truncate_text_middle(game_name, config.small_font, rect_width - 40) + game_text = truncate_text_middle(game_name, config.small_font, rect_width - 40, is_filename=False) text_surface = config.small_font.render(game_text, True, color) text_rect = text_surface.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + (i - config.scroll_offset) * line_height + line_height // 2)) if i == config.current_game: @@ -680,7 +680,8 @@ def draw_history_list(screen): # logger.debug(f"Affichage terminé: {game_name}, status={status_text}") elif status == "Erreur": status_text = _("history_status_error").format(entry.get('message', 'Échec')) - #logger.debug(f"Affichage erreur: {game_name}, status={status_text}") + elif status == "Canceled": + status_text = _("history_status_canceled") else: status_text = status #logger.debug(f"Affichage statut inconnu: {game_name}, status={status_text}") @@ -757,8 +758,37 @@ def draw_clear_history_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) - draw_stylized_button(screen, _("button_yes"), rect_x + rect_width // 2 - 180, rect_y + text_height + margin_top_bottom, 160, button_height, selected=config.confirm_clear_selection == 1) - draw_stylized_button(screen, _("button_no"), rect_x + rect_width // 2 + 20, rect_y + text_height + margin_top_bottom, 160, button_height, selected=config.confirm_clear_selection == 0) + button_width = min(160, (rect_width - 60) // 2) + draw_stylized_button(screen, _("button_yes"), rect_x + rect_width // 2 - button_width - 10, rect_y + text_height + margin_top_bottom, button_width, button_height, selected=config.confirm_clear_selection == 1) + draw_stylized_button(screen, _("button_no"), rect_x + rect_width // 2 + 10, rect_y + text_height + margin_top_bottom, button_width, button_height, selected=config.confirm_clear_selection == 0) + +def draw_cancel_download_dialog(screen): + """Affiche la boîte de dialogue de confirmation pour annuler un téléchargement.""" + screen.blit(OVERLAY, (0, 0)) + + message = _("confirm_cancel_download") + 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 = int(config.screen_height * 0.0463) + 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 + 150 + rect_x = (config.screen_width - rect_width) // 2 + rect_y = (config.screen_height - rect_height) // 2 + + pygame.draw.rect(screen, THEME_COLORS["button_idle"], (rect_x, rect_y, rect_width, rect_height), border_radius=12) + pygame.draw.rect(screen, THEME_COLORS["border"], (rect_x, rect_y, rect_width, rect_height), 2, border_radius=12) + + for i, line in enumerate(wrapped_message): + text = config.font.render(line, True, THEME_COLORS["text"]) + 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) + + button_width = min(160, (rect_width - 60) // 2) + draw_stylized_button(screen, _("button_yes"), rect_x + rect_width // 2 - button_width - 10, rect_y + text_height + margin_top_bottom, button_width, button_height, selected=config.confirm_cancel_selection == 1) + draw_stylized_button(screen, _("button_no"), rect_x + rect_width // 2 + 10, rect_y + text_height + margin_top_bottom, button_width, button_height, selected=config.confirm_cancel_selection == 0) # Affichage du clavier virtuel sur non-PC def draw_virtual_keyboard(screen): @@ -960,7 +990,6 @@ def draw_controls(screen, menu_state, current_music_name=None, music_popup_start current_time = pygame.time.get_ticks() / 1000 if current_time - config.music_popup_start_time < 3.0: # Afficher pendant 3 secondes control_text += f" | {config.current_music_name}" - logger.debug(f"config.current_music_name") 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 @@ -1124,22 +1153,30 @@ def draw_controls_help(screen, previous_state): } # Catégories de contrôles + nav_text = _("controls_navigation") + pages_text = _("controls_pages") + confirm_select_text = _("controls_confirm_select") + cancel_back_text = _("controls_cancel_back") + history_text = _("controls_history") + clear_history_text = _("controls_clear_history") + filter_search_text = _("controls_filter_search") + control_categories = { - "Navigation": [ - f"{get_control_display('up', '↑')} {get_control_display('down', '↓')} {get_control_display('left', '←')} {get_control_display('right', '→')} : Navigation", - f"{get_control_display('page_up', 'LB')} {get_control_display('page_down', 'RB')} : Pages" + _("controls_category_navigation"): [ + f"{get_control_display('up', '↑')} {get_control_display('down', '↓')} {get_control_display('left', '←')} {get_control_display('right', '→')} : {nav_text}", + f"{get_control_display('page_up', 'LB')} {get_control_display('page_down', 'RB')} : {pages_text}" ], - "Actions principales": [ - f"{get_control_display('confirm', 'A')} : Confirmer/Sélectionner", - f"{get_control_display('cancel', 'B')} : Annuler/Retour", + _("controls_category_main_actions"): [ + f"{get_control_display('confirm', 'A')} : {confirm_select_text}", + f"{get_control_display('cancel', 'B')} : {cancel_back_text}", f"{get_control_display('start', 'Start')} : {start_text}" ], - "Téléchargements": [ - f"{get_control_display('history', 'Y')} : Historique", - f"{get_control_display('progress', 'X')} : Effacer historique" + _("controls_category_downloads"): [ + f"{get_control_display('history', 'Y')} : {history_text}", + f"{get_control_display('progress', 'X')} : {clear_history_text}" ], - "Recherche": [ - f"{get_control_display('filter', 'Select')} : Filtrer/Rechercher", + _("controls_category_search"): [ + f"{get_control_display('filter', 'Select')} : {filter_search_text}", f"{get_control_display('delete', 'Suppr')} : {delete_text}", f"{get_control_display('space', 'Espace')} : {space_text}" ] @@ -1185,7 +1222,7 @@ def draw_controls_help(screen, previous_state): pygame.draw.rect(screen, THEME_COLORS["border"], (popup_x, popup_y, popup_width, popup_height), 2, border_radius=12) # Titre - title_text = "Aide des contrôles" + title_text = _("controls_help_title") title_surface = config.title_font.render(title_text, True, THEME_COLORS["text"]) title_rect = title_surface.get_rect(center=(config.screen_width // 2, popup_y + 25)) screen.blit(title_surface, title_rect) @@ -1257,8 +1294,9 @@ 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) - draw_stylized_button(screen, _("button_yes"), rect_x + rect_width // 2 - 180, rect_y + text_height + margin_top_bottom, 160, button_height, selected=config.confirm_selection == 1) - draw_stylized_button(screen, _("button_no"), rect_x + rect_width // 2 + 20, rect_y + text_height + margin_top_bottom, 160, button_height, selected=config.confirm_selection == 0) + button_width = min(160, (rect_width - 60) // 2) + draw_stylized_button(screen, _("button_yes"), rect_x + rect_width // 2 - button_width - 10, rect_y + text_height + margin_top_bottom, button_width, button_height, selected=config.confirm_selection == 1) + draw_stylized_button(screen, _("button_no"), rect_x + rect_width // 2 + 10, rect_y + text_height + margin_top_bottom, button_width, button_height, selected=config.confirm_selection == 0) # draw_redownload_game_cache_dialog def draw_redownload_game_cache_dialog(screen): diff --git a/languages/en.json b/languages/en.json index 5fb8cd0..5f21ea0 100644 --- a/languages/en.json +++ b/languages/en.json @@ -42,6 +42,7 @@ "history_status_extracting": "Extracting: {0}%", "history_status_completed": "Completed", "history_status_error": "Error: {0}", + "history_status_canceled": "Canceled", "download_status": "{0}: {1}", "download_progress": "{0}% {1} MB / {2} MB", @@ -66,6 +67,7 @@ "menu_remap_controls": "Remap controls", "menu_history": "History", "menu_language": "Language", + "menu_accessibility": "Accessibility", "menu_redownload_cache": "Redownload Games cache", "menu_quit": "Quit", @@ -155,5 +157,20 @@ "utils_permission_denied": "Permission denied during extraction: {0}", "utils_extraction_failed": "Extraction failed: {0}", "utils_unrar_unavailable": "Unrar command not available", - "utils_rar_list_failed": "Failed to list RAR files: {0}" + "utils_rar_list_failed": "Failed to list RAR files: {0}", + "download_initializing": "Initializing...", + "accessibility_font_size": "Font size: {0}", + "confirm_cancel_download": "Cancel current download?", + "controls_help_title": "Controls Help", + "controls_category_navigation": "Navigation", + "controls_category_main_actions": "Main Actions", + "controls_category_downloads": "Downloads", + "controls_category_search": "Search", + "controls_navigation": "Navigation", + "controls_pages": "Pages", + "controls_confirm_select": "Confirm/Select", + "controls_cancel_back": "Cancel/Back", + "controls_history": "History", + "controls_clear_history": "Clear History", + "controls_filter_search": "Filter/Search" } \ No newline at end of file diff --git a/languages/fr.json b/languages/fr.json index 9d98258..79d5f84 100644 --- a/languages/fr.json +++ b/languages/fr.json @@ -42,6 +42,7 @@ "history_status_extracting": "Extraction : {0}%", "history_status_completed": "Terminé", "history_status_error": "Erreur : {0}", + "history_status_canceled": "Annulé", "download_status": "{0} : {1}", "download_progress": "{0}% {1} Mo / {2} Mo", @@ -66,6 +67,7 @@ "menu_remap_controls": "Remapper les contrôles", "menu_history": "Historique", "menu_language": "Langue", + "menu_accessibility": "Accessibilité", "menu_redownload_cache": "Retélécharger le cache des jeux", "menu_quit": "Quitter", @@ -144,7 +146,22 @@ "network_permission_error": "Pas de permission d'écriture dans {0}", "network_file_not_found": "Le fichier {0} n'existe pas", "network_cannot_get_filename": "Impossible de récupérer le nom du fichier", - "network_cannot_get_download_url": "Impossible de récupérer l'URL de téléchargement", + "network_cannot_get_download_url": "Impossible de récupérer l'URL de téléchargement", + "download_initializing": "Initialisation en cours...", + "accessibility_font_size": "Taille de police : {0}", + "confirm_cancel_download": "Annuler le téléchargement en cours ?", + "controls_help_title": "Aide des contrôles", + "controls_category_navigation": "Navigation", + "controls_category_main_actions": "Actions principales", + "controls_category_downloads": "Téléchargements", + "controls_category_search": "Recherche", + "controls_navigation": "Navigation", + "controls_pages": "Pages", + "controls_confirm_select": "Confirmer/Sélectionner", + "controls_cancel_back": "Annuler/Retour", + "controls_history": "Historique", + "controls_clear_history": "Effacer historique", + "controls_filter_search": "Filtrer/Rechercher", "network_download_failed": "Échec du téléchargement après {0} tentatives", "network_api_error": "Erreur lors de la requête API, la clé est peut-être incorrecte: {0}", "network_download_error": "Erreur téléchargement {0}: {1}", diff --git a/network.py b/network.py index 179d3cf..fd508e9 100644 --- a/network.py +++ b/network.py @@ -10,6 +10,7 @@ from config import OTA_VERSION_ENDPOINT,APP_FOLDER, UPDATE_FOLDER, OTA_UPDATE_ZI from utils import sanitize_filename, extract_zip, extract_rar, load_api_key_1fichier from history import save_history import logging +import datetime import queue import time import os @@ -152,9 +153,8 @@ def extract_update(zip_path, dest_dir, source_url): logger.error(f"Erreur critique lors de l'extraction du ZIP {source_url}: {str(e)}") return False, _("network_zip_extraction_error").format(source_url, str(e)) -# File d'attente pour la progression -import queue -progress_queue = queue.Queue() +# File d'attente pour la progression - une par tâche +progress_queues = {} @@ -162,13 +162,9 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas logger.debug(f"Début téléchargement: {game_name} depuis {url}, is_zip_non_supported={is_zip_non_supported}, task_id={task_id}") result = [None, None] - # Vider la file d'attente avant de commencer - while not progress_queue.empty(): - try: - progress_queue.get_nowait() - logger.debug(f"File progress_queue vidée pour {game_name}") - except queue.Empty: - break + # Créer une queue spécifique pour cette tâche + if task_id not in progress_queues: + progress_queues[task_id] = queue.Queue() def download_thread(): logger.debug(f"Thread téléchargement démarré pour {url}, task_id={task_id}") @@ -178,7 +174,6 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas if platform_dict["platform"] == platform: dest_dir = os.path.join(config.ROMS_FOLDER, platform_dict.get("folder", platform.lower().replace(" ", ""))) logger.debug(f"Répertoire de destination trouvé pour {platform}: {dest_dir}") - #dest_dir = platform_dict.get("folder") break if not dest_dir: dest_dir = os.path.join(os.path.dirname(os.path.dirname(config.APP_FOLDER)), platform.lower().replace(" ", "")) @@ -201,47 +196,27 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas 'Upgrade-Insecure-Requests': '1' } - # Utiliser une session pour gérer les cookies session = requests.Session() session.headers.update(headers) - # Première requête HEAD pour obtenir la vraie URL - #logger.debug(f"Première requête HEAD vers {url}") - head_response = session.head(url, timeout=30, allow_redirects=False) - #logger.debug(f"HEAD Status: {head_response.status_code}, Headers: {dict(head_response.headers)}") - - # Suivre la redirection manuellement si nécessaire - final_url = url - if head_response.status_code in [301, 302, 303, 307, 308]: - final_url = head_response.headers.get('Location', url) - #logger.debug(f"Redirection détectée vers: {final_url}") - - # Requête GET vers l'URL finale avec en-têtes spécifiques download_headers = headers.copy() download_headers['Accept'] = 'application/octet-stream, */*' download_headers['Referer'] = 'https://myrient.erista.me/' - response = session.get(final_url, stream=True, timeout=30, allow_redirects=False, headers=download_headers) + response = session.get(url, stream=True, timeout=30, allow_redirects=True, headers=download_headers) logger.debug(f"Status code: {response.status_code}") - logger.debug(f"Headers: {dict(response.headers)}") response.raise_for_status() total_size = int(response.headers.get('content-length', 0)) logger.debug(f"Taille totale: {total_size} octets") - if total_size == 0: - logger.warning(f"Taille de fichier 0, possible redirection ou erreur. URL finale: {response.url}") - # Vérifier si c'est une redirection - if response.url != url: - logger.debug(f"Redirection détectée: {url} -> {response.url}") - # Initialiser la progression avec task_id - progress_queue.put((task_id, 0, total_size)) + progress_queues[task_id].put((task_id, 0, total_size)) logger.debug(f"Progression initiale envoyée: 0% pour {game_name}, task_id={task_id}") downloaded = 0 chunk_size = 4096 last_update_time = time.time() - update_interval = 0.5 # Mettre à jour toutes les 0,5 secondes + update_interval = 0.1 # Mettre à jour toutes les 0,1 secondes with open(dest_path, 'wb') as f: for chunk in response.iter_content(chunk_size=chunk_size): if chunk: @@ -250,24 +225,17 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas downloaded += size_received current_time = time.time() if current_time - last_update_time >= update_interval: - # Calculer le pourcentage correctement et le limiter entre 0 et 100 - progress_percent = int(downloaded / total_size * 100) if total_size > 0 else 0 - progress_percent = max(0, min(100, progress_percent)) - progress_queue.put((task_id, downloaded, total_size)) + progress_queues[task_id].put((task_id, downloaded, total_size)) last_update_time = current_time - else: - logger.debug("Chunk vide reçu") os.chmod(dest_path, 0o644) logger.debug(f"Téléchargement terminé: {dest_path}") - # Vérifier si l'extraction est nécessaire pour les archives non supportées if is_zip_non_supported: logger.debug(f"Extraction automatique nécessaire pour {dest_path}") extension = os.path.splitext(dest_path)[1].lower() if extension == ".zip": try: - # Mettre à jour le statut avant l'extraction if isinstance(config.history, list): for entry in config.history: if "url" in entry and entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]: @@ -319,7 +287,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas result[1] = _("network_download_error").format(game_name, str(e)) finally: logger.debug(f"Thread téléchargement terminé pour {url}, task_id={task_id}") - progress_queue.put((task_id, result[0], result[1])) + progress_queues[task_id].put((task_id, result[0], result[1])) logger.debug(f"Final result sent to queue: success={result[0]}, message={result[1]}, task_id={task_id}") thread = threading.Thread(target=download_thread) @@ -328,68 +296,63 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas # Boucle principale pour mettre à jour la progression while thread.is_alive(): try: - while not progress_queue.empty(): - data = progress_queue.get() - logger.debug(f"Progress queue data received: {data}") - if len(data) != 3 or data[0] != task_id: # Ignorer les données d'une autre tâche - logger.debug(f"Ignoring queue data for task_id={data[0]}, expected={task_id}") - continue - if isinstance(data[1], bool): # Fin du téléchargement - success, message = data[1], data[2] - # Vérifier si config.history est une liste avant d'itérer - if isinstance(config.history, list): - for entry in config.history: - if "url" in entry and entry["url"] == url and entry["status"] in ["downloading", "Téléchargement", "Extracting"]: - entry["status"] = "Download_OK" if success else "Erreur" - entry["progress"] = 100 if success else 0 - # Utiliser une variable intermédiaire pour stocker le message - message_text = message - entry["message"] = message_text - save_history(config.history) - config.needs_redraw = True - logger.debug(f"Final update in history: status={entry['status']}, progress={entry['progress']}%, message={message}, task_id={task_id}") - break - else: - downloaded, total_size = data[1], data[2] - # Calculer le pourcentage correctement et le limiter entre 0 et 100 - progress_percent = int(downloaded / total_size * 100) if total_size > 0 else 0 - progress_percent = max(0, min(100, progress_percent)) - - # Vérifier si config.history est une liste avant d'itérer - if isinstance(config.history, list): - for entry in config.history: - if "url" in entry and entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]: - entry["progress"] = progress_percent - entry["status"] = "Téléchargement" - entry["downloaded_size"] = downloaded - entry["total_size"] = total_size - config.needs_redraw = True - break - await asyncio.sleep(0.2) + task_queue = progress_queues.get(task_id) + if task_queue: + while not task_queue.empty(): + data = task_queue.get() + #logger.debug(f"Progress queue data received: {data}") + if isinstance(data[1], bool): # Fin du téléchargement + success, message = data[1], data[2] + if isinstance(config.history, list): + for entry in config.history: + if "url" in entry and entry["url"] == url and entry["status"] in ["downloading", "Téléchargement", "Extracting"]: + entry["status"] = "Download_OK" if success else "Erreur" + entry["progress"] = 100 if success else 0 + entry["message"] = message + save_history(config.history) + config.needs_redraw = True + logger.debug(f"Final update in history: status={entry['status']}, progress={entry['progress']}%, message={message}, task_id={task_id}") + break + else: + downloaded, total_size = data[1], data[2] + progress_percent = int(downloaded / total_size * 100) if total_size > 0 else 0 + progress_percent = max(0, min(100, progress_percent)) + + if isinstance(config.history, list): + for entry in config.history: + if "url" in entry and entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]: + entry["progress"] = progress_percent + entry["status"] = "Téléchargement" + entry["downloaded_size"] = downloaded + entry["total_size"] = total_size + config.needs_redraw = True + break + await asyncio.sleep(0.1) except Exception as e: logger.error(f"Erreur mise à jour progression: {str(e)}") thread.join() - #logger.debug(f"Thread joined for {url}, task_id={task_id}") + # Nettoyer la queue + if task_id in progress_queues: + del progress_queues[task_id] return result[0], result[1] async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=False, task_id=None): - load_api_key_1fichier() + config.API_KEY_1FICHIER = load_api_key_1fichier() logger.debug(f"Début téléchargement 1fichier: {game_name} depuis {url}, is_zip_non_supported={is_zip_non_supported}, task_id={task_id}") + logger.debug(f"Clé API 1fichier: {'présente' if config.API_KEY_1FICHIER else 'absente'}") result = [None, None] - # Vider la file d'attente avant de commencer - while not progress_queue.empty(): - try: - progress_queue.get_nowait() - logger.debug(f"File progress_queue vidée pour {game_name}") - except queue.Empty: - break + # Créer une queue spécifique pour cette tâche + logger.debug(f"Création queue pour task_id={task_id}") + if task_id not in progress_queues: + progress_queues[task_id] = queue.Queue() def download_thread(): logger.debug(f"Thread téléchargement 1fichier démarré pour {url}, task_id={task_id}") try: link = url.split('&af=')[0] + logger.debug(f"URL nettoyée: {link}") dest_dir = None for platform_dict in config.platform_dicts: if platform_dict["platform"] == platform: @@ -398,10 +361,13 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported= if not dest_dir: logger.warning(f"Aucun dossier 'folder' trouvé pour la plateforme {platform}") dest_dir = os.path.join(os.path.dirname(os.path.dirname(config.APP_FOLDER)), platform) + logger.debug(f"Répertoire destination déterminé: {dest_dir}") logger.debug(f"Vérification répertoire destination: {dest_dir}") os.makedirs(dest_dir, exist_ok=True) + logger.debug(f"Répertoire créé ou existant: {dest_dir}") if not os.access(dest_dir, os.W_OK): + logger.error(f"Pas de permission d'écriture dans {dest_dir}") raise PermissionError(f"Pas de permission d'écriture dans {dest_dir}") headers = { @@ -412,10 +378,9 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported= "url": link, "pretty": 1 } - - #logger.debug(f"Envoi requête POST à https://api.1fichier.com/v1/file/info.cgi pour {url}") + logger.debug(f"Préparation requête file/info pour {link}") response = requests.post("https://api.1fichier.com/v1/file/info.cgi", headers=headers, json=payload, timeout=30) - #logger.debug(f"Réponse reçue, status: {response.status_code}") + logger.debug(f"Réponse file/info reçue, code: {response.status_code}") response.raise_for_status() file_info = response.json() @@ -427,7 +392,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported= filename = file_info.get("filename", "").strip() if not filename: - logger.error("Impossible de récupérer le nom du fichier") + logger.error(f"Impossible de récupérer le nom du fichier") result[0] = False result[1] = _("network_cannot_get_filename") return @@ -435,50 +400,48 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported= sanitized_filename = sanitize_filename(filename) dest_path = os.path.join(dest_dir, sanitized_filename) logger.debug(f"Chemin destination: {dest_path}") - - #logger.debug(f"Envoi requête POST à https://api.1fichier.com/v1/download/get_token.cgi pour {url}") + logger.debug(f"Envoi requête get_token pour {link}") response = requests.post("https://api.1fichier.com/v1/download/get_token.cgi", headers=headers, json=payload, timeout=30) - #logger.debug(f"Réponse reçue, status: {response.status_code}") + logger.debug(f"Réponse get_token reçue, code: {response.status_code}") response.raise_for_status() download_info = response.json() final_url = download_info.get("url") if not final_url: - logger.error("Impossible de récupérer l'URL de téléchargement") + logger.error(f"Impossible de récupérer l'URL de téléchargement") result[0] = False result[1] = _("network_cannot_get_download_url") return + logger.debug(f"URL de téléchargement obtenue: {final_url}") lock = threading.Lock() retries = 10 retry_delay = 10 - # Initialiser la progression avec task_id - progress_queue.put((task_id, 0, 0)) # Taille initiale inconnue - #logger.debug(f"Progression initiale envoyée: 0% pour {game_name}, task_id={task_id}") + logger.debug(f"Initialisation progression avec taille inconnue pour task_id={task_id}") + progress_queues[task_id].put((task_id, 0, 0)) # Taille initiale inconnue for attempt in range(retries): + logger.debug(f"Début tentative {attempt + 1} pour télécharger {final_url}") try: - #logger.debug(f"Tentative {attempt + 1} : Envoi requête GET à {final_url}") with requests.get(final_url, stream=True, headers={'User-Agent': 'Mozilla/5.0'}, timeout=30) as response: - #logger.debug(f"Réponse reçue, status: {response.status_code}") + logger.debug(f"Réponse GET reçue, code: {response.status_code}") response.raise_for_status() total_size = int(response.headers.get('content-length', 0)) logger.debug(f"Taille totale: {total_size} octets") with lock: - # Vérifier si config.history est une liste avant d'itérer if isinstance(config.history, list): for entry in config.history: if "url" in entry and entry["url"] == url and entry["status"] == "downloading": entry["total_size"] = total_size config.needs_redraw = True break - progress_queue.put((task_id, 0, total_size)) # Mettre à jour la taille totale + progress_queues[task_id].put((task_id, 0, total_size)) # Mettre à jour la taille totale downloaded = 0 chunk_size = 8192 last_update_time = time.time() - update_interval = 0.5 # Mettre à jour toutes les 0,5 secondes + update_interval = 0.1 # Mettre à jour toutes les 0,1 secondes + logger.debug(f"Ouverture fichier: {dest_path}") with open(dest_path, 'wb') as f: - logger.debug(f"Ouverture fichier: {dest_path}") for chunk in response.iter_content(chunk_size=chunk_size): if chunk: f.write(chunk) @@ -486,11 +449,9 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported= current_time = time.time() if current_time - last_update_time >= update_interval: with lock: - # Vérifier si config.history est une liste avant d'itérer if isinstance(config.history, list): for entry in config.history: if "url" in entry and entry["url"] == url and entry["status"] == "downloading": - # Calculer le pourcentage correctement et le limiter entre 0 et 100 progress_percent = int(downloaded / total_size * 100) if total_size > 0 else 0 progress_percent = max(0, min(100, progress_percent)) entry["progress"] = progress_percent @@ -498,14 +459,12 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported= entry["downloaded_size"] = downloaded entry["total_size"] = total_size config.needs_redraw = True - #logger.debug(f"Progression mise à jour: {entry['progress']:.1f}% pour {game_name}") break - progress_queue.put((task_id, downloaded, total_size)) + progress_queues[task_id].put((task_id, downloaded, total_size)) last_update_time = current_time if is_zip_non_supported: with lock: - # Vérifier si config.history est une liste avant d'itérer if isinstance(config.history, list): for entry in config.history: if "url" in entry and entry["url"] == url and entry["status"] == "Téléchargement": @@ -514,11 +473,12 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported= config.needs_redraw = True break extension = os.path.splitext(dest_path)[1].lower() + logger.debug(f"Début extraction, type d'archive: {extension}") if extension == ".zip": try: success, msg = extract_zip(dest_path, dest_dir, url) + logger.debug(f"Extraction ZIP terminée: {msg}") if success: - logger.debug(f"Extraction ZIP réussie: {msg}") result[0] = True result[1] = _("network_download_extract_ok").format(game_name) else: @@ -526,14 +486,14 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported= result[0] = False result[1] = _("network_extraction_failed").format(msg) except Exception as e: - logger.error(f"Exception lors de l'extraction: {str(e)}") + logger.error(f"Exception lors de l'extraction ZIP: {str(e)}") result[0] = False result[1] = f"Erreur téléchargement {game_name}: {str(e)}" elif extension == ".rar": try: success, msg = extract_rar(dest_path, dest_dir, url) + logger.debug(f"Extraction RAR terminée: {msg}") if success: - logger.debug(f"Extraction RAR réussie: {msg}") result[0] = True result[1] = _("network_download_extract_ok").format(game_name) else: @@ -549,6 +509,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported= result[0] = True result[1] = _("network_download_ok").format(game_name) else: + logger.debug(f"Application des permissions sur {dest_path}") os.chmod(dest_path, 0o644) logger.debug(f"Téléchargement terminé: {dest_path}") result[0] = True @@ -556,78 +517,77 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported= return except requests.exceptions.RequestException as e: - logger.error(f"Tentative {attempt + 1} échouée : {e}") + logger.error(f"Tentative {attempt + 1} échouée: {e}") if attempt < retries - 1: + logger.debug(f"Attente de {retry_delay} secondes avant nouvelle tentative") time.sleep(retry_delay) else: - logger.error("Nombre maximum de tentatives atteint") + logger.error(f"Nombre maximum de tentatives atteint") result[0] = False result[1] = _("network_download_failed").format(retries) return except requests.exceptions.RequestException as e: - logger.error(f"Erreur API 1fichier : {e}") + logger.error(f"Erreur API 1fichier: {e}") result[0] = False result[1] = _("network_api_error").format(str(e)) finally: logger.debug(f"Thread téléchargement 1fichier terminé pour {url}, task_id={task_id}") - progress_queue.put((task_id, result[0], result[1])) - logger.debug(f"Final result sent to queue: success={result[0]}, message={result[1]}, task_id={task_id}") + progress_queues[task_id].put((task_id, result[0], result[1])) + logger.debug(f"Résultat final envoyé à la queue: success={result[0]}, message={result[1]}, task_id={task_id}") - thread = threading.Thread(target=download_thread) logger.debug(f"Démarrage thread pour {url}, task_id={task_id}") + thread = threading.Thread(target=download_thread) thread.start() # Boucle principale pour mettre à jour la progression + logger.debug(f"Début boucle de progression pour task_id={task_id}") while thread.is_alive(): try: - while not progress_queue.empty(): - data = progress_queue.get() - logger.debug(f"Progress queue data received: {data}") - if len(data) != 3 or data[0] != task_id: # Ignorer les données d'une autre tâche - logger.debug(f"Ignoring queue data for task_id={data[0]}, expected={task_id}") - continue - if isinstance(data[1], bool): # Fin du téléchargement - success, message = data[1], data[2] - # Vérifier si config.history est une liste avant d'itérer - if isinstance(config.history, list): - for entry in config.history: - if "url" in entry and entry["url"] == url and entry["status"] in ["downloading", "Téléchargement", "Extracting"]: - entry["status"] = "Download_OK" if success else "Erreur" - entry["progress"] = 100 if success else 0 - # Utiliser une variable intermédiaire pour stocker le message - message_text = message - entry["message"] = message_text - save_history(config.history) - config.needs_redraw = True - logger.debug(f"Final update in history: status={entry['status']}, progress={entry['progress']}%, message={message}, task_id={task_id}") - break - else: - downloaded, total_size = data[1], data[2] - # Calculer le pourcentage correctement et le limiter entre 0 et 100 - progress_percent = int(downloaded / total_size * 100) if total_size > 0 else 0 - progress_percent = max(0, min(100, progress_percent)) - - # Vérifier si config.history est une liste avant d'itérer - if isinstance(config.history, list): - for entry in config.history: - if "url" in entry and entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]: - entry["progress"] = progress_percent - entry["status"] = "Téléchargement" - entry["downloaded_size"] = downloaded - entry["total_size"] = total_size - config.needs_redraw = True - break - await asyncio.sleep(0.2) + task_queue = progress_queues.get(task_id) + if task_queue: + while not task_queue.empty(): + data = task_queue.get() + logger.debug(f"Données queue progression reçues: {data}") + if isinstance(data[1], bool): # Fin du téléchargement + success, message = data[1], data[2] + if isinstance(config.history, list): + for entry in config.history: + if "url" in entry and entry["url"] == url and entry["status"] in ["downloading", "Téléchargement", "Extracting"]: + entry["status"] = "Download_OK" if success else "Erreur" + entry["progress"] = 100 if success else 0 + entry["message"] = message + save_history(config.history) + config.needs_redraw = True + logger.debug(f"Mise à jour finale historique: status={entry['status']}, progress={entry['progress']}%, message={message}, task_id={task_id}") + break + else: + downloaded, total_size = data[1], data[2] + progress_percent = int(downloaded / total_size * 100) if total_size > 0 else 0 + progress_percent = max(0, min(100, progress_percent)) + + if isinstance(config.history, list): + for entry in config.history: + if "url" in entry and entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]: + entry["progress"] = progress_percent + entry["status"] = "Téléchargement" + entry["downloaded_size"] = downloaded + entry["total_size"] = total_size + config.needs_redraw = True + break + await asyncio.sleep(0.1) except Exception as e: logger.error(f"Erreur mise à jour progression: {str(e)}") + logger.debug(f"Fin boucle de progression, attente fin thread pour task_id={task_id}") thread.join() - logger.debug(f"Thread joined for {url}, task_id={task_id}") + logger.debug(f"Thread terminé, nettoyage queue pour task_id={task_id}") + # Nettoyer la queue + if task_id in progress_queues: + del progress_queues[task_id] + logger.debug(f"Fin download_from_1fichier, résultat: success={result[0]}, message={result[1]}") return result[0], result[1] - - def is_1fichier_url(url): """Détecte si l'URL est un lien 1fichier.""" return "1fichier.com" in url \ No newline at end of file diff --git a/utils.py b/utils.py index 229bb55..bccf36e 100644 --- a/utils.py +++ b/utils.py @@ -13,6 +13,28 @@ import time import random import random from config import JSON_EXTENSIONS, SAVE_FOLDER + +def load_accessibility_settings(): + """Charge les paramètres d'accessibilité depuis accessibility.json.""" + accessibility_path = os.path.join(SAVE_FOLDER, "accessibility.json") + try: + if os.path.exists(accessibility_path): + with open(accessibility_path, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + logger.error(f"Erreur lors du chargement de accessibility.json: {str(e)}") + return {"font_scale": 1.0} + +def save_accessibility_settings(settings): + """Sauvegarde les paramètres d'accessibilité dans accessibility.json.""" + accessibility_path = os.path.join(SAVE_FOLDER, "accessibility.json") + try: + os.makedirs(SAVE_FOLDER, exist_ok=True) + with open(accessibility_path, 'w', encoding='utf-8') as f: + json.dump(settings, f, indent=2) + logger.debug(f"Paramètres d'accessibilité sauvegardés: {settings}") + except Exception as e: + logger.error(f"Erreur lors de la sauvegarde de accessibility.json: {str(e)}") from history import save_history from language import _ # Import de la fonction de traduction from datetime import datetime @@ -85,17 +107,23 @@ def check_extension_before_download(url, platform, game_name): def is_extension_supported(filename, platform, extensions_data): """Vérifie si l'extension du fichier est supportée pour la plateforme donnée.""" extension = os.path.splitext(filename)[1].lower() + dest_dir = None for platform_dict in config.platform_dicts: if platform_dict["platform"] == platform: - dest_dir = os.path.join(config.ROMS_FOLDER, platform_dict.get("folder", platform.lower().replace(" ", ""))) + dest_dir = os.path.join(config.ROMS_FOLDER, platform_dict.get("folder")) break + if not dest_dir: logger.warning(f"Aucun dossier 'folder' trouvé pour la plateforme {platform}") dest_dir = os.path.join(os.path.dirname(os.path.dirname(config.APP_FOLDER)), platform) - for system in extensions_data: - if system["folder"] == dest_dir: - return extension in system["extensions"] + + dest_folder_name = os.path.basename(dest_dir) + for i, system in enumerate(extensions_data): + if system["folder"] == dest_folder_name: + result = extension in system["extensions"] + return result + logger.warning(f"Aucun système trouvé pour le dossier {dest_dir}") return False @@ -388,7 +416,7 @@ def extract_zip(zip_path, dest_dir, url): if current_time - last_save_time >= save_interval: save_history(config.history) last_save_time = current_time - logger.debug(f"Extraction en cours: {info.filename}, file_extracted={file_extracted}/{file_size}, total_extracted={extracted_size}/{total_size}, progression={progress_percent:.1f}%") + # logger.debug(f"Extraction en cours: {info.filename}, file_extracted={file_extracted}/{file_size}, total_extracted={extracted_size}/{total_size}, progression={progress_percent:.1f}%") config.needs_redraw = True break @@ -574,7 +602,7 @@ def extract_rar(rar_path, dest_dir, url): logger.error(f"Erreur lors de la suppression de {rar_path}: {str(e)}") def play_random_music(music_files, music_folder, current_music=None): - """Joue une musique aléatoire et configure l'événement de fin.""" + """Joue une musique aléatoire et configure l'événement de fin de manière non-bloquante.""" if music_files: # Éviter de rejouer la même musique consécutivement available_music = [f for f in music_files if f != current_music] @@ -583,11 +611,21 @@ def play_random_music(music_files, music_folder, current_music=None): music_file = random.choice(available_music) music_path = os.path.join(music_folder, music_file) logger.debug(f"Lecture de la musique : {music_path}") - pygame.mixer.music.load(music_path) - pygame.mixer.music.set_volume(0.5) - pygame.mixer.music.play(loops=0) # Jouer une seule fois - pygame.mixer.music.set_endevent(pygame.USEREVENT + 1) # Événement de fin - set_music_popup(music_file) # Afficher le nom de la musique dans la popup + + def load_and_play_music(): + try: + pygame.mixer.music.load(music_path) + pygame.mixer.music.set_volume(0.5) + pygame.mixer.music.play(loops=0) # Jouer une seule fois + pygame.mixer.music.set_endevent(pygame.USEREVENT + 1) # Événement de fin + set_music_popup(music_file) # Afficher le nom de la musique dans la popup + except Exception as e: + logger.error(f"Erreur lors du chargement de la musique {music_path}: {str(e)}") + + # Charger et jouer la musique dans un thread séparé pour éviter le blocage + music_thread = threading.Thread(target=load_and_play_music, daemon=True) + music_thread.start() + return music_file # Retourner la nouvelle musique pour mise à jour else: logger.debug("Aucune musique trouvée dans /RGSX/assets/music") @@ -602,13 +640,17 @@ def set_music_popup(music_name): def load_api_key_1fichier(): """Charge la clé API 1fichier depuis le dossier de sauvegarde, crée le fichier si absent.""" api_path = os.path.join(SAVE_FOLDER, "1fichierAPI.txt") + logger.debug(f"Tentative de chargement de la clé API depuis: {api_path}") try: # Vérifie si le fichier existe déjà if not os.path.exists(api_path): + # Crée le dossier parent si nécessaire + os.makedirs(SAVE_FOLDER, exist_ok=True) # Crée le fichier vide si absent with open(api_path, "w") as f: f.write("") logger.info(f"Fichier de clé API créé : {api_path}") + return "" except OSError as e: logger.error(f"Erreur lors de la création du fichier de clé API : {e}") return "" @@ -616,9 +658,10 @@ def load_api_key_1fichier(): try: with open(api_path, "r", encoding="utf-8") as f: api_key = f.read().strip() - logger.debug(f"Clé API 1fichier chargée : {api_key}") + logger.debug(f"Clé API 1fichier lue: '{api_key}' (longueur: {len(api_key)})") if not api_key: logger.warning("Clé API 1fichier vide, veuillez la renseigner dans le fichier pour pouvoir utiliser les fonctionnalités de téléchargement sur 1fichier.") + config.API_KEY_1FICHIER = api_key return api_key except OSError as e: logger.error(f"Erreur lors de la lecture de la clé API : {e}")