From 3ee7fc8b3ffcdaf67e1a107bd35ffad0590e9b08 Mon Sep 17 00:00:00 2001 From: skymike03 Date: Wed, 10 Sep 2025 21:08:46 +0200 Subject: [PATCH] v2.2.0.6 - feat: implement download cancellation (real cancel) and cancel all downloads when exit feature and improve exit confirmation messages --- ports/RGSX/__main__.py | 94 ++++++++++++----------- ports/RGSX/config.py | 2 +- ports/RGSX/controls.py | 17 ++++- ports/RGSX/display.py | 141 ++++++++++++++++++++++++++++------- ports/RGSX/languages/de.json | 1 + ports/RGSX/languages/en.json | 1 + ports/RGSX/languages/es.json | 1 + ports/RGSX/languages/fr.json | 1 + ports/RGSX/languages/it.json | 1 + ports/RGSX/languages/pt.json | 1 + ports/RGSX/network.py | 84 ++++++++++++++++++++- ports/RGSX/utils.py | 26 ------- 12 files changed, 270 insertions(+), 100 deletions(-) diff --git a/ports/RGSX/__main__.py b/ports/RGSX/__main__.py index e431c19..bb9a6a2 100644 --- a/ports/RGSX/__main__.py +++ b/ports/RGSX/__main__.py @@ -28,7 +28,7 @@ from display import ( THEME_COLORS ) from language import _ -from network import test_internet, download_rom, is_1fichier_url, download_from_1fichier, check_for_updates +from network import test_internet, download_rom, is_1fichier_url, download_from_1fichier, check_for_updates, cancel_all_downloads from controls import handle_controls, validate_menu_state, process_key_repeats, get_emergency_controls from controls_mapper import map_controls, draw_controls_mapping, get_actions from controls import load_controls_config @@ -167,8 +167,9 @@ logger.debug(f"Résolution d'écran : {config.screen_width}x{config.screen_heigh try: if config.menu_state not in ("loading", "error", "pause_menu"): config.menu_state = "loading" - config.current_loading_system = _("loading_startup") if _ else "Chargement..." - config.loading_progress = 1.0 + # Afficher directement le même statut que la première étape pour éviter un écran furtif différent + config.current_loading_system = _("loading_test_connection") + config.loading_progress = 0.0 draw_loading_screen(screen) pygame.display.flip() pygame.event.pump() @@ -510,11 +511,11 @@ async def main(): config.needs_redraw = True if action == "confirm": if config.pending_download and config.extension_confirm_selection == 0: # Oui - url, platform, game_name, is_zip_non_supported = config.pending_download - logger.debug(f"Téléchargement confirmé après avertissement: {game_name} pour {platform} depuis {url}") + url, platform_name, game_name, is_zip_non_supported = config.pending_download + logger.debug(f"Téléchargement confirmé après avertissement: {game_name} pour {platform_name} depuis {url}") task_id = str(pygame.time.get_ticks()) config.history.append({ - "platform": platform, + "platform": platform_name, "game_name": game_name, "status": "downloading", "progress": 0, @@ -524,8 +525,8 @@ async def main(): config.current_history_item = len(config.history) - 1 save_history(config.history) config.download_tasks[task_id] = ( - asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported, task_id)), - url, game_name, platform + asyncio.create_task(download_rom(url, platform_name, game_name, is_zip_non_supported, task_id)), + url, game_name, platform_name ) config.menu_state = "history" config.pending_download = None @@ -553,12 +554,12 @@ async def main(): game_name = str(game) url = None # Nouveau schéma: config.platforms contient déjà platform_name (string) - platform = config.platforms[config.current_platform] + platform_name = config.platforms[config.current_platform] if url: logger.debug(f"Vérification pour {game_name}, URL: {url}") # Ajouter une entrée temporaire à l'historique config.history.append({ - "platform": platform, + "platform": platform_name, "game_name": game_name, "status": "downloading", "progress": 0, @@ -583,7 +584,7 @@ async def main(): logger.error("Clé API 1fichier absente") config.pending_download = None continue - pending = check_extension_before_download(url, platform, game_name) + pending = check_extension_before_download(url, platform_name, game_name) if not pending: config.menu_state = "error" config.error_message = _("error_invalid_download_data") if _ else "Invalid download data" @@ -593,7 +594,7 @@ async def main(): else: from utils import is_extension_supported, load_extensions_json, sanitize_filename from rgsx_settings import get_allow_unknown_extensions - is_supported = is_extension_supported(sanitize_filename(game_name), platform, load_extensions_json()) + is_supported = is_extension_supported(sanitize_filename(game_name), platform_name, load_extensions_json()) zip_ok = bool(pending[3]) allow_unknown = False try: @@ -613,14 +614,14 @@ async def main(): # Lancer le téléchargement dans une tâche asynchrone task_id = str(pygame.time.get_ticks()) config.download_tasks[task_id] = ( - asyncio.create_task(download_from_1fichier(url, platform, game_name, zip_ok)), - url, game_name, platform + asyncio.create_task(download_from_1fichier(url, platform_name, game_name, zip_ok)), + url, game_name, platform_name ) config.menu_state = "history" # Passer à l'historique config.needs_redraw = True logger.debug(f"Téléchargement 1fichier démarré pour {game_name}, passage à l'historique") else: - pending = check_extension_before_download(url, platform, game_name) + pending = check_extension_before_download(url, platform_name, game_name) if not pending: config.menu_state = "error" config.error_message = _("error_invalid_download_data") if _ else "Invalid download data" @@ -630,7 +631,7 @@ async def main(): else: from utils import is_extension_supported, load_extensions_json, sanitize_filename from rgsx_settings import get_allow_unknown_extensions - is_supported = is_extension_supported(sanitize_filename(game_name), platform, load_extensions_json()) + is_supported = is_extension_supported(sanitize_filename(game_name), platform_name, load_extensions_json()) zip_ok = bool(pending[3]) allow_unknown = False try: @@ -650,18 +651,18 @@ async def main(): # Lancer le téléchargement dans une tâche asynchrone task_id = str(pygame.time.get_ticks()) config.download_tasks[task_id] = ( - asyncio.create_task(download_rom(url, platform, game_name, zip_ok)), - url, game_name, platform + asyncio.create_task(download_rom(url, platform_name, game_name, zip_ok)), + url, game_name, platform_name ) config.menu_state = "history" # Passer à l'historique config.needs_redraw = True logger.debug(f"Téléchargement démarré pour {game_name}, passage à l'historique") elif action == "redownload" and config.menu_state == "history" and config.history: entry = config.history[config.current_history_item] - platform = entry["platform"] + platform_name = entry["platform"] game_name = entry["game_name"] for game in config.games: - if isinstance(game, (list, tuple)) and game and game[0] == game_name and config.platforms[config.current_platform] == platform: + if isinstance(game, (list, tuple)) and game and game[0] == game_name and config.platforms[config.current_platform] == platform_name: url = game[1] if len(game) > 1 else None else: continue @@ -678,7 +679,7 @@ async def main(): logger.error("Clé API 1fichier absente") config.pending_download = None continue - pending = check_extension_before_download(url, platform, game_name) + pending = check_extension_before_download(url, platform_name, game_name) if not pending: config.menu_state = "error" config.error_message = _("error_invalid_download_data") if _ else "Invalid download data" @@ -687,7 +688,7 @@ async def main(): else: from utils import is_extension_supported, load_extensions_json, sanitize_filename from rgsx_settings import get_allow_unknown_extensions - is_supported = is_extension_supported(sanitize_filename(game_name), platform, load_extensions_json()) + is_supported = is_extension_supported(sanitize_filename(game_name), platform_name, load_extensions_json()) zip_ok = bool(pending[3]) allow_unknown = False try: @@ -703,7 +704,7 @@ async def main(): else: config.previous_menu_state = config.menu_state logger.debug(f"Previous menu state défini: {config.previous_menu_state}") - success, message = download_from_1fichier(url, platform, game_name, zip_ok) + success, message = download_from_1fichier(url, platform_name, game_name, zip_ok) # Ancien popup download_result supprimé : retour direct à l'historique config.download_result_message = message config.download_result_error = not success @@ -713,7 +714,7 @@ async def main(): config.needs_redraw = True logger.debug(f"Retéléchargement 1fichier terminé pour {game_name}, succès={success}, message={message}, retour direct history") else: - pending = check_extension_before_download(url, platform, game_name) + pending = check_extension_before_download(url, platform_name, game_name) if not pending: config.menu_state = "error" config.error_message = _("error_invalid_download_data") if _ else "Invalid download data" @@ -722,7 +723,7 @@ async def main(): else: from utils import is_extension_supported, load_extensions_json, sanitize_filename from rgsx_settings import get_allow_unknown_extensions - is_supported = is_extension_supported(sanitize_filename(game_name), platform, load_extensions_json()) + is_supported = is_extension_supported(sanitize_filename(game_name), platform_name, load_extensions_json()) zip_ok = bool(pending[3]) allow_unknown = False try: @@ -738,7 +739,7 @@ async def main(): else: config.previous_menu_state = config.menu_state logger.debug(f"Previous menu state défini: {config.previous_menu_state}") - success, message = download_rom(url, platform, game_name, zip_ok) + success, message = download_rom(url, platform_name, game_name, zip_ok) config.download_result_message = message config.download_result_error = not success config.download_progress.clear() @@ -759,7 +760,7 @@ async def main(): # Gestion des téléchargements if config.download_tasks: - for task_id, (task, url, game_name, platform) in list(config.download_tasks.items()): + for task_id, (task, url, game_name, platform_name) in list(config.download_tasks.items()): if task.done(): try: success, message = await task @@ -1121,26 +1122,33 @@ async def main(): await asyncio.sleep(0.01) pygame.mixer.music.stop() - result = subprocess.run(["taskkill", "/f", "/im", "emulatorLauncher.exe"]) - if result == 0: - logger.debug(f"Quitté avec succès: emulatorLauncher.exe") + # Cancel any ongoing downloads to prevent lingering background threads + try: + cancel_all_downloads() + except Exception as e: + logger.debug(f"Erreur lors de l'annulation globale des téléchargements: {e}") + + if platform.system() == "Windows": + try: + result = subprocess.run(["taskkill", "/f", "/im", "emulatorLauncher.exe"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + if getattr(result, "returncode", 1) == 0: + logger.debug("Quitté avec succès: emulatorLauncher.exe") + else: + logger.debug("Erreur lors de la tentative d'arrêt d'emulatorLauncher.exe") + except FileNotFoundError: + logger.debug("taskkill introuvable, saut de l'étape d'arrêt d'emulatorLauncher.exe") else: - logger.debug("Error en essayant de quitter emulatorlauncher.") + try: + result2 = subprocess.run(["batocera-es-swissknife", "--emukill"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + if getattr(result2, "returncode", 1) == 0: + logger.debug("Arrêt demandé via batocera-es-swissknife --emukill") + else: + logger.debug("Erreur lors de la tentative d'arrêt via batocera-es-swissknife") + except FileNotFoundError: + logger.debug("batocera-es-swissknife introuvable, saut de l'étape d'arrêt (environnement non Batocera)") pygame.quit() logger.debug("Application terminée") - try: - if platform.system() != "Windows": - result2 = subprocess.run(["batocera-es-swissknife", "--emukill"]) - if result2 == 0: - logger.debug(f"Quitté avec succès") - else: - logger.debug("Error en essayant de quitter batocera-es-swissknife.") - except FileNotFoundError: - logger.debug("batocera-es-swissknife introuvable, saut de l'étape d'arrêt (environnement non Batocera)") - - - if platform.system() == "Emscripten": asyncio.ensure_future(main()) else: diff --git a/ports/RGSX/config.py b/ports/RGSX/config.py index 1d60f00..2a2b008 100644 --- a/ports/RGSX/config.py +++ b/ports/RGSX/config.py @@ -13,7 +13,7 @@ except Exception: pygame = None # type: ignore # Version actuelle de l'application -app_version = "2.2.0.5" +app_version = "2.2.0.6" def get_operating_system(): """Renvoie le nom du système d'exploitation.""" diff --git a/ports/RGSX/controls.py b/ports/RGSX/controls.py index 25127e2..13fc862 100644 --- a/ports/RGSX/controls.py +++ b/ports/RGSX/controls.py @@ -10,6 +10,7 @@ import os import sys from display import draw_validation_transition from network import download_rom, download_from_1fichier, is_1fichier_url +from network import download_rom, download_from_1fichier, is_1fichier_url, request_cancel from utils import ( load_games, check_extension_before_download, is_extension_supported, load_extensions_json, play_random_music, sanitize_filename, @@ -1016,11 +1017,15 @@ def handle_controls(event, sources, joystick, screen): # Annuler la tâche correspondante for task_id, (task, task_url, game_name, platform) in list(config.download_tasks.items()): if task_url == url: + try: + request_cancel(task_id) + except Exception: + pass task.cancel() del config.download_tasks[task_id] entry["status"] = "Canceled" entry["progress"] = 0 - entry["message"] = "Téléchargement annulé" + entry["message"] = _("download_canceled") if _ else "Download canceled" save_history(config.history) logger.debug(f"Téléchargement annulé: {game_name}") break @@ -1066,6 +1071,16 @@ def handle_controls(event, sources, joystick, screen): elif config.menu_state == "confirm_exit": if is_input_matched(event, "confirm"): if config.confirm_selection == 1: + # Mark all in-progress downloads as canceled in history + try: + for entry in getattr(config, 'history', []) or []: + if entry.get("status") in ["downloading", "Téléchargement", "Extracting"]: + entry["status"] = "Canceled" + entry["progress"] = 0 + entry["message"] = _("download_canceled") if _ else "Download canceled" + save_history(config.history) + except Exception: + pass return "quit" else: config.menu_state = validate_menu_state(config.previous_menu_state) diff --git a/ports/RGSX/display.py b/ports/RGSX/display.py index d252724..763e59b 100644 --- a/ports/RGSX/display.py +++ b/ports/RGSX/display.py @@ -1128,7 +1128,8 @@ def draw_extension_warning(screen): logger.warning("game_name vide, utilisation de 'Inconnu'") if is_zip: - message = _("extension_warning_zip").format(game_name) + core = _("extension_warning_zip").format(game_name) + hint = "" else: # Ajout d'un indice pour activer le téléchargement des extensions inconnues try: @@ -1136,18 +1137,25 @@ def draw_extension_warning(screen): except Exception: hint = "" core = _("extension_warning_unsupported").format(game_name) - message = core if not hint else f"{core}{hint}" + # Nettoyer et préparer les lignes max_width = config.screen_width - 80 - lines = wrap_text(message, config.font, max_width) + core_lines = wrap_text(core, config.font, max_width) + hint_text = (hint or "").replace("\n", " ").strip() + hint_lines = wrap_text(hint_text, config.small_font, max_width) if hint_text else [] try: - line_height = config.font.get_height() + 5 - text_height = len(lines) * line_height + line_height_core = config.font.get_height() + 5 + line_height_hint = config.small_font.get_height() + 4 + spacing_between = 6 if hint_lines else 0 + text_height = len(core_lines) * line_height_core + (spacing_between) + len(hint_lines) * line_height_hint 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 lines], default=300) + max_text_width = max( + [config.font.size(l)[0] for l in core_lines] + ([config.small_font.size(l)[0] for l in hint_lines] if hint_lines else []), + default=300, + ) rect_width = max_text_width + 80 rect_x = (config.screen_width - rect_width) // 2 rect_y = (config.screen_height - rect_height) // 2 @@ -1156,11 +1164,26 @@ def draw_extension_warning(screen): 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(lines): + # Lignes du cœur du message (orange) + for i, line in enumerate(core_lines): text_surface = config.font.render(line, True, THEME_COLORS["warning_text"]) - text_rect = text_surface.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + i * line_height + line_height // 2)) + text_rect = text_surface.get_rect(center=( + config.screen_width // 2, + rect_y + margin_top_bottom + i * line_height_core + line_height_core // 2, + )) screen.blit(text_surface, text_rect) + # Lignes d'indice (blanc/gris) si présentes + if hint_lines: + hint_start_y = rect_y + margin_top_bottom + len(core_lines) * line_height_core + spacing_between + for j, hline in enumerate(hint_lines): + hsurf = config.small_font.render(hline, True, THEME_COLORS["text"]) + hrect = hsurf.get_rect(center=( + config.screen_width // 2, + hint_start_y + j * line_height_hint + line_height_hint // 2, + )) + screen.blit(hsurf, hrect) + draw_stylized_button(screen, _("button_yes"), rect_x + rect_width // 2 - 180, rect_y + text_height + margin_top_bottom, 160, button_height, selected=config.extension_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.extension_confirm_selection == 0) @@ -1213,7 +1236,13 @@ def draw_controls(screen, menu_state, current_music_name=None, music_popup_start # Menu pause def draw_language_menu(screen): - """Dessine le menu de sélection de langue avec un style moderne.""" + """Dessine le menu de sélection de langue avec un style moderne. + + Améliorations: + - Hauteur des boutons réduite et responsive selon la taille d'écran. + - Bloc (titre + liste de langues) centré verticalement. + - Gestion d'overflow: réduit légèrement la hauteur/espacement si nécessaire. + """ from language import get_available_languages, get_language_name screen.blit(OVERLAY, (0, 0)) @@ -1225,21 +1254,54 @@ def draw_language_menu(screen): logger.error("Aucune langue disponible") return - # Titre + # Titre (mesuré d'abord pour connaître la hauteur réelle du fond) title_text = _("language_select_title") title_surface = config.font.render(title_text, True, THEME_COLORS["text"]) - title_rect = title_surface.get_rect(center=(config.screen_width // 2, config.screen_height // 4)) - - # Fond du titre - title_bg_rect = title_rect.inflate(40, 20) + # On calcule un rect neutre, on positionnera ensuite pour centrer le bloc + title_rect = title_surface.get_rect() + # Padding responsive plus léger pour réduire la hauteur + hpad = max(24, min(36, int(config.screen_width * 0.04))) + vpad = max(8, min(14, int(title_surface.get_height() * 0.4))) + title_bg_rect = title_rect.inflate(hpad, vpad) + + # Dimensions responsives des boutons + # Largeur bornée entre 260 et 380px (~40% de la largeur écran) + button_width = max(260, min(380, int(config.screen_width * 0.4))) + # Hauteur réduite et responsive (env. 5.5% de la hauteur écran), bornée 28..56 + button_height = max(28, min(56, int(config.screen_height * 0.055))) + # Espacement vertical proportionnel et borné + button_spacing = max(8, int(button_height * 0.35)) + + # Calcul des dimensions globales pour centrer verticalement (titre + boutons) + n = len(available_languages) + total_buttons_height = n * button_height + (n - 1) * button_spacing + content_height = title_bg_rect.height + button_spacing + total_buttons_height + + # Si le contenu dépasse, on réduit légèrement la hauteur/espacement jusqu'à rentrer + available_h = config.screen_height - 80 # marges haut/bas de confort + safety_counter = 0 + while content_height > available_h and safety_counter < 20: + if button_height > 28: + button_height -= 2 + elif button_spacing > 6: + button_spacing -= 1 + else: + break + total_buttons_height = n * button_height + (n - 1) * button_spacing + content_height = title_bg_rect.height + button_spacing + total_buttons_height + safety_counter += 1 + + # Positionner le bloc au centre verticalement + content_top = max(10, (config.screen_height - content_height) // 2) + # Positionner le titre + title_bg_rect.centerx = config.screen_width // 2 + title_bg_rect.y = content_top + title_rect.center = (title_bg_rect.centerx, title_bg_rect.y + title_bg_rect.height // 2) + + # Dessiner le titre pygame.draw.rect(screen, THEME_COLORS["button_idle"], title_bg_rect, border_radius=10) pygame.draw.rect(screen, THEME_COLORS["border"], title_bg_rect, 2, border_radius=10) screen.blit(title_surface, title_rect) - - # Options de langue - button_height = 60 - button_width = 300 - button_spacing = 20 # Démarrer la liste juste sous le titre avec le même écart que les boutons start_y = title_bg_rect.bottom + button_spacing @@ -1247,16 +1309,16 @@ def draw_language_menu(screen): for i, lang_code in enumerate(available_languages): # Obtenir le nom de la langue lang_name = get_language_name(lang_code) - + # Position du bouton button_x = (config.screen_width - button_width) // 2 button_y = start_y + i * (button_height + button_spacing) - + # Dessiner le bouton button_color = THEME_COLORS["button_hover"] if i == config.selected_language_index else THEME_COLORS["button_idle"] pygame.draw.rect(screen, button_color, (button_x, button_y, button_width, button_height), border_radius=10) pygame.draw.rect(screen, THEME_COLORS["border"], (button_x, button_y, button_width, button_height), 2, border_radius=10) - + # Texte du bouton text_surface = config.font.render(lang_name, True, THEME_COLORS["text"]) text_rect = text_surface.get_rect(center=(button_x + button_width // 2, button_y + button_height // 2)) @@ -1279,11 +1341,20 @@ def draw_display_menu(screen): show_unsupported = get_show_unsupported_platforms() allow_unknown = get_allow_unknown_extensions() + # Compter les systèmes non supportés actuellement masqués + unsupported_list = getattr(config, "unsupported_platforms", []) or [] + try: + hidden_count = 0 if show_unsupported else len(list(unsupported_list)) + except Exception: + hidden_count = 0 + unsupported_label = ((_("menu_show_unsupported_on") if show_unsupported else _("menu_show_unsupported_off")) + + f" ({hidden_count})") + # Libellés options = [ f"{_('display_layout')}: {layout_str}", _("accessibility_font_size").format(f"{font_scale:.1f}"), - _("menu_show_unsupported_on") if show_unsupported else _("menu_show_unsupported_off"), + unsupported_label, _("menu_allow_unknown_ext_on") if allow_unknown else _("menu_allow_unknown_ext_off"), _("menu_filter_platforms"), ] @@ -1396,8 +1467,11 @@ def draw_filter_platforms_menu(screen): title_text = _("filter_platforms_title") title_surface = config.title_font.render(title_text, True, THEME_COLORS["text"]) - title_rect = title_surface.get_rect(center=(config.screen_width // 2, title_surface.get_height() // 2 + 20)) - title_rect_inflated = title_rect.inflate(80, 40) + title_rect = title_surface.get_rect(center=(config.screen_width // 2, title_surface.get_height() // 2 + 14)) + # Padding responsive réduit + hpad = max(36, min(64, int(config.screen_width * 0.06))) + vpad = max(10, min(20, int(title_surface.get_height() * 0.45))) + title_rect_inflated = title_rect.inflate(hpad, vpad) title_rect_inflated.topleft = ((config.screen_width - title_rect_inflated.width) // 2, 10) pygame.draw.rect(screen, THEME_COLORS["button_idle"], title_rect_inflated, border_radius=12) pygame.draw.rect(screen, THEME_COLORS["border"], title_rect_inflated, 2, border_radius=12) @@ -1659,7 +1733,22 @@ def draw_confirm_dialog(screen): logger.debug("OVERLAY recréé dans draw_confirm_dialog") screen.blit(OVERLAY, (0, 0)) - message = _("confirm_exit") + # Dynamic message: warn when downloads are active + active_downloads = 0 + try: + active_downloads = len(getattr(config, 'download_tasks', {}) or {}) + except Exception: + active_downloads = 0 + if active_downloads > 0: + # Try translated key if it exists; otherwise fallback to generic message + try: + warn_tpl = _("confirm_exit_with_downloads") # optional key + # If untranslated key returns the same string, still format + message = warn_tpl.format(active_downloads) + except Exception: + message = f"Attention: {active_downloads} téléchargement(s) en cours. Quitter quand même ?" + else: + message = _("confirm_exit") 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 diff --git a/ports/RGSX/languages/de.json b/ports/RGSX/languages/de.json index 46f60b5..ad742ee 100644 --- a/ports/RGSX/languages/de.json +++ b/ports/RGSX/languages/de.json @@ -61,6 +61,7 @@ "extension_warning_enable_unknown_hint": "\nUm diese Meldung auszublenden: \"Warnung bei unbekannter Erweiterung ausblenden\" in Pausenmenü > Anzeige aktivieren", "confirm_exit": "Anwendung beenden?", + "confirm_exit_with_downloads": "Achtung: {0} Download(s) laufen. Trotzdem beenden?", "confirm_clear_history": "Verlauf löschen?", "confirm_redownload_cache": "Spieleliste aktualisieren?", diff --git a/ports/RGSX/languages/en.json b/ports/RGSX/languages/en.json index c4a9b26..0cacbab 100644 --- a/ports/RGSX/languages/en.json +++ b/ports/RGSX/languages/en.json @@ -61,6 +61,7 @@ "extension_warning_enable_unknown_hint": "\nTo hide this message: enable \"Hide unknown extension warning\" in Pause Menu > Display", "confirm_exit": "Exit application?", + "confirm_exit_with_downloads": "Attention: {0} download(s) in progress. Quit anyway?", "confirm_clear_history": "Clear history?", "confirm_redownload_cache": "Update games list?", diff --git a/ports/RGSX/languages/es.json b/ports/RGSX/languages/es.json index 8eea8da..d4c5ef5 100644 --- a/ports/RGSX/languages/es.json +++ b/ports/RGSX/languages/es.json @@ -62,6 +62,7 @@ "extension_warning_enable_unknown_hint": "\nPara no mostrar este mensaje: activa \"Ocultar aviso de extensión desconocida\" en Menú de pausa > Pantalla", "confirm_exit": "¿Salir de la aplicación?", + "confirm_exit_with_downloads": "Atención: {0} descarga(s) en curso. ¿Salir de todas formas?", "confirm_clear_history": "¿Vaciar el historial?", "confirm_redownload_cache": "¿Actualizar la lista de juegos?", diff --git a/ports/RGSX/languages/fr.json b/ports/RGSX/languages/fr.json index 895d0fb..6a5b045 100644 --- a/ports/RGSX/languages/fr.json +++ b/ports/RGSX/languages/fr.json @@ -58,6 +58,7 @@ "extension_warning_enable_unknown_hint": "\nPour ne plus afficher ce messager : Activer l'option \"Masquer avertissement\" dans le Menu Pause>Display", "confirm_exit": "Quitter l'application ?", + "confirm_exit_with_downloads": "Attention : {0} téléchargement(s) en cours. Quitter quand même ?", "confirm_clear_history": "Vider l'historique ?", "confirm_redownload_cache": "Mettre à jour la liste des jeux ?", diff --git a/ports/RGSX/languages/it.json b/ports/RGSX/languages/it.json index e5111e9..804df9e 100644 --- a/ports/RGSX/languages/it.json +++ b/ports/RGSX/languages/it.json @@ -61,6 +61,7 @@ "extension_warning_enable_unknown_hint": "\nPer non visualizzare questo messaggio: abilita \"Nascondi avviso estensione sconosciuta\" in Menu Pausa > Schermo", "confirm_exit": "Uscire dall'applicazione?", + "confirm_exit_with_downloads": "Attenzione: {0} download in corso. Uscire comunque?", "confirm_clear_history": "Cancellare la cronologia?", "confirm_redownload_cache": "Aggiornare l'elenco dei giochi?", diff --git a/ports/RGSX/languages/pt.json b/ports/RGSX/languages/pt.json index e6170bb..ca7c672 100644 --- a/ports/RGSX/languages/pt.json +++ b/ports/RGSX/languages/pt.json @@ -61,6 +61,7 @@ "extension_warning_enable_unknown_hint": "\nPara não ver esta mensagem: ative \"Ocultar aviso de extensão desconhecida\" em Menu de Pausa > Exibição", "confirm_exit": "Sair da aplicação?", + "confirm_exit_with_downloads": "Atenção: {0} download(s) em andamento. Sair mesmo assim?", "confirm_clear_history": "Limpar histórico?", "confirm_redownload_cache": "Atualizar lista de jogos?", diff --git a/ports/RGSX/network.py b/ports/RGSX/network.py index a0c4ede..3f21f2b 100644 --- a/ports/RGSX/network.py +++ b/ports/RGSX/network.py @@ -252,6 +252,38 @@ def extract_update(zip_path, dest_dir, source_url): # File d'attente pour la progression - une par tâche progress_queues = {} +# Cancellation and thread tracking per download task +cancel_events = {} +download_threads = {} + +def request_cancel(task_id: str) -> bool: + """Request cancellation for a running download task by its task_id.""" + ev = cancel_events.get(task_id) + if ev is not None: + try: + ev.set() + logger.debug(f"Cancel requested for task_id={task_id}") + return True + except Exception as e: + logger.debug(f"Failed to set cancel for task_id={task_id}: {e}") + return False + logger.debug(f"No cancel event found for task_id={task_id}") + return False + +def cancel_all_downloads(): + """Cancel all active downloads and attempt to stop threads quickly.""" + for tid, ev in list(cancel_events.items()): + try: + ev.set() + except Exception: + pass + # Optionally join threads briefly + for tid, th in list(download_threads.items()): + try: + if th.is_alive(): + th.join(timeout=0.2) + except Exception: + pass @@ -259,13 +291,16 @@ 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] - # Créer une queue spécifique pour cette tâche + # Créer une queue/cancel spécifique pour cette tâche if task_id not in progress_queues: progress_queues[task_id] = queue.Queue() + if task_id not in cancel_events: + cancel_events[task_id] = threading.Event() def download_thread(): logger.debug(f"Thread téléchargement démarré pour {url}, task_id={task_id}") try: + cancel_ev = cancel_events.get(task_id) # Use symlink path if enabled from rgsx_settings import apply_symlink_path @@ -407,6 +442,20 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas 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 cancel_ev is not None and cancel_ev.is_set(): + logger.debug(f"Annulation détectée, arrêt du téléchargement pour task_id={task_id}") + result[0] = False + result[1] = _("download_canceled") if _ else "Download canceled" + try: + f.close() + except Exception: + pass + try: + if os.path.exists(dest_path): + os.remove(dest_path) + except Exception: + pass + break if chunk: size_received = len(chunk) f.write(chunk) @@ -494,7 +543,8 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas 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) + thread = threading.Thread(target=download_thread, daemon=True) + download_threads[task_id] = thread thread.start() # Boucle principale pour mettre à jour la progression @@ -541,6 +591,10 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas logger.error(f"Erreur mise à jour progression: {str(e)}") thread.join() + try: + download_threads.pop(task_id, None) + except Exception: + pass # Drain any remaining final message to ensure history is saved try: task_queue = progress_queues.get(task_id) @@ -562,6 +616,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas # Nettoyer la queue if task_id in progress_queues: del progress_queues[task_id] + cancel_events.pop(task_id, None) return result[0], result[1] async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=False, task_id=None): @@ -574,10 +629,13 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported= logger.debug(f"Création queue pour task_id={task_id}") if task_id not in progress_queues: progress_queues[task_id] = queue.Queue() + if task_id not in cancel_events: + cancel_events[task_id] = threading.Event() def download_thread(): logger.debug(f"Thread téléchargement 1fichier démarré pour {url}, task_id={task_id}") try: + cancel_ev = cancel_events.get(task_id) link = url.split('&af=')[0] logger.debug(f"URL nettoyée: {link}") # Use symlink path if enabled @@ -686,6 +744,20 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported= logger.debug(f"Ouverture fichier: {dest_path}") with open(dest_path, 'wb') as f: for chunk in response.iter_content(chunk_size=chunk_size): + if cancel_ev is not None and cancel_ev.is_set(): + logger.debug(f"Annulation détectée, arrêt du téléchargement 1fichier pour task_id={task_id}") + result[0] = False + result[1] = _("download_canceled") if _ else "Download canceled" + try: + f.close() + except Exception: + pass + try: + if os.path.exists(dest_path): + os.remove(dest_path) + except Exception: + pass + break if chunk: f.write(chunk) downloaded += len(chunk) @@ -781,7 +853,8 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported= logger.debug(f"Résultat final envoyé à la queue: success={result[0]}, message={result[1]}, task_id={task_id}") logger.debug(f"Démarrage thread pour {url}, task_id={task_id}") - thread = threading.Thread(target=download_thread) + thread = threading.Thread(target=download_thread, daemon=True) + download_threads[task_id] = thread thread.start() # Boucle principale pour mettre à jour la progression @@ -825,6 +898,10 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported= logger.debug(f"Fin boucle de progression, attente fin thread pour task_id={task_id}") thread.join() + try: + download_threads.pop(task_id, None) + except Exception: + pass logger.debug(f"Thread terminé, nettoyage queue pour task_id={task_id}") # Drain any remaining final message to ensure history is saved try: @@ -847,6 +924,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported= # Nettoyer la queue if task_id in progress_queues: del progress_queues[task_id] + cancel_events.pop(task_id, None) 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): diff --git a/ports/RGSX/utils.py b/ports/RGSX/utils.py index 9299811..f98df30 100644 --- a/ports/RGSX/utils.py +++ b/ports/RGSX/utils.py @@ -493,8 +493,6 @@ def load_sources(): for platform_name in config.platforms: games = load_games(platform_name) config.games_count[platform_name] = len(games) - - write_unavailable_systems() return sources except Exception as e: logger.error(f"Erreur fusion systèmes + détection jeux: {e}") @@ -573,30 +571,6 @@ def load_games(platform_id): logger.error(f"Erreur lors du chargement des jeux pour {platform_id}: {e}") return [] -def write_unavailable_systems(): - """Écrit la liste des systèmes avec une erreur 404 dans un fichier texte.""" - if not unavailable_systems: - logger.debug("Aucun système avec des liens HS, rien à écrire dans le fichier.") - return - - # Formater la date et l'heure pour le nom du fichier - current_time = datetime.now() - timestamp = current_time.strftime("%d-%m-%Y-%H-%M") - log_file = os.path.join(config.log_dir, f"systemes_unavailable_{timestamp}.txt") - - try: - # Créer le répertoire s'il n'existe pas - os.makedirs(config.log_dir, exist_ok=True) - - # Écrire les systèmes dans le fichier - with open(log_file, 'w', encoding='utf-8') as f: - f.write("Systèmes avec une erreur 404 :\n") - for system in unavailable_systems: - f.write(f"{system}\n") - logger.debug(f"Fichier écrit : {log_file} avec {len(unavailable_systems)} systèmes") - except Exception as e: - logger.error(f"Erreur lors de l'écriture du fichier {log_file} : {str(e)}") - def truncate_text_middle(text, font, max_width, is_filename=True): """Tronque le texte en insérant '...' au milieu, en préservant le début et la fin. Si is_filename=False, ne supprime pas l'extension."""