1
0
forked from Mirrors/RGSX

v2.2.0.6 - feat: implement download cancellation (real cancel) and cancel all downloads when exit feature and improve exit confirmation messages

This commit is contained in:
skymike03
2025-09-10 21:08:46 +02:00
parent 8d1f57d13e
commit 3ee7fc8b3f
12 changed files with 270 additions and 100 deletions

View File

@@ -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:

View File

@@ -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."""

View File

@@ -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)

View File

@@ -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

View File

@@ -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?",

View File

@@ -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?",

View File

@@ -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?",

View File

@@ -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 ?",

View File

@@ -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?",

View File

@@ -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?",

View File

@@ -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):

View File

@@ -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."""