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 THEME_COLORS
) )
from language import _ 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 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_mapper import map_controls, draw_controls_mapping, get_actions
from controls import load_controls_config 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: try:
if config.menu_state not in ("loading", "error", "pause_menu"): if config.menu_state not in ("loading", "error", "pause_menu"):
config.menu_state = "loading" config.menu_state = "loading"
config.current_loading_system = _("loading_startup") if _ else "Chargement..." # Afficher directement le même statut que la première étape pour éviter un écran furtif différent
config.loading_progress = 1.0 config.current_loading_system = _("loading_test_connection")
config.loading_progress = 0.0
draw_loading_screen(screen) draw_loading_screen(screen)
pygame.display.flip() pygame.display.flip()
pygame.event.pump() pygame.event.pump()
@@ -510,11 +511,11 @@ async def main():
config.needs_redraw = True config.needs_redraw = True
if action == "confirm": if action == "confirm":
if config.pending_download and config.extension_confirm_selection == 0: # Oui if config.pending_download and config.extension_confirm_selection == 0: # Oui
url, platform, game_name, is_zip_non_supported = config.pending_download 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} depuis {url}") logger.debug(f"Téléchargement confirmé après avertissement: {game_name} pour {platform_name} depuis {url}")
task_id = str(pygame.time.get_ticks()) task_id = str(pygame.time.get_ticks())
config.history.append({ config.history.append({
"platform": platform, "platform": platform_name,
"game_name": game_name, "game_name": game_name,
"status": "downloading", "status": "downloading",
"progress": 0, "progress": 0,
@@ -524,8 +525,8 @@ async def main():
config.current_history_item = len(config.history) - 1 config.current_history_item = len(config.history) - 1
save_history(config.history) save_history(config.history)
config.download_tasks[task_id] = ( config.download_tasks[task_id] = (
asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported, task_id)), asyncio.create_task(download_rom(url, platform_name, game_name, is_zip_non_supported, task_id)),
url, game_name, platform url, game_name, platform_name
) )
config.menu_state = "history" config.menu_state = "history"
config.pending_download = None config.pending_download = None
@@ -553,12 +554,12 @@ async def main():
game_name = str(game) game_name = str(game)
url = None url = None
# Nouveau schéma: config.platforms contient déjà platform_name (string) # 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: if url:
logger.debug(f"Vérification pour {game_name}, URL: {url}") logger.debug(f"Vérification pour {game_name}, URL: {url}")
# Ajouter une entrée temporaire à l'historique # Ajouter une entrée temporaire à l'historique
config.history.append({ config.history.append({
"platform": platform, "platform": platform_name,
"game_name": game_name, "game_name": game_name,
"status": "downloading", "status": "downloading",
"progress": 0, "progress": 0,
@@ -583,7 +584,7 @@ async def main():
logger.error("Clé API 1fichier absente") logger.error("Clé API 1fichier absente")
config.pending_download = None config.pending_download = None
continue continue
pending = check_extension_before_download(url, platform, game_name) pending = check_extension_before_download(url, platform_name, game_name)
if not pending: if not pending:
config.menu_state = "error" config.menu_state = "error"
config.error_message = _("error_invalid_download_data") if _ else "Invalid download data" config.error_message = _("error_invalid_download_data") if _ else "Invalid download data"
@@ -593,7 +594,7 @@ async def main():
else: else:
from utils import is_extension_supported, load_extensions_json, sanitize_filename from utils import is_extension_supported, load_extensions_json, sanitize_filename
from rgsx_settings import get_allow_unknown_extensions 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]) zip_ok = bool(pending[3])
allow_unknown = False allow_unknown = False
try: try:
@@ -613,14 +614,14 @@ async def main():
# Lancer le téléchargement dans une tâche asynchrone # Lancer le téléchargement dans une tâche asynchrone
task_id = str(pygame.time.get_ticks()) task_id = str(pygame.time.get_ticks())
config.download_tasks[task_id] = ( config.download_tasks[task_id] = (
asyncio.create_task(download_from_1fichier(url, platform, game_name, zip_ok)), asyncio.create_task(download_from_1fichier(url, platform_name, game_name, zip_ok)),
url, game_name, platform url, game_name, platform_name
) )
config.menu_state = "history" # Passer à l'historique config.menu_state = "history" # Passer à l'historique
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Téléchargement 1fichier démarré pour {game_name}, passage à l'historique") logger.debug(f"Téléchargement 1fichier démarré pour {game_name}, passage à l'historique")
else: else:
pending = check_extension_before_download(url, platform, game_name) pending = check_extension_before_download(url, platform_name, game_name)
if not pending: if not pending:
config.menu_state = "error" config.menu_state = "error"
config.error_message = _("error_invalid_download_data") if _ else "Invalid download data" config.error_message = _("error_invalid_download_data") if _ else "Invalid download data"
@@ -630,7 +631,7 @@ async def main():
else: else:
from utils import is_extension_supported, load_extensions_json, sanitize_filename from utils import is_extension_supported, load_extensions_json, sanitize_filename
from rgsx_settings import get_allow_unknown_extensions 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]) zip_ok = bool(pending[3])
allow_unknown = False allow_unknown = False
try: try:
@@ -650,18 +651,18 @@ async def main():
# Lancer le téléchargement dans une tâche asynchrone # Lancer le téléchargement dans une tâche asynchrone
task_id = str(pygame.time.get_ticks()) task_id = str(pygame.time.get_ticks())
config.download_tasks[task_id] = ( config.download_tasks[task_id] = (
asyncio.create_task(download_rom(url, platform, game_name, zip_ok)), asyncio.create_task(download_rom(url, platform_name, game_name, zip_ok)),
url, game_name, platform url, game_name, platform_name
) )
config.menu_state = "history" # Passer à l'historique config.menu_state = "history" # Passer à l'historique
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Téléchargement démarré pour {game_name}, passage à l'historique") 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: elif action == "redownload" and config.menu_state == "history" and config.history:
entry = config.history[config.current_history_item] entry = config.history[config.current_history_item]
platform = entry["platform"] platform_name = entry["platform"]
game_name = entry["game_name"] game_name = entry["game_name"]
for game in config.games: 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 url = game[1] if len(game) > 1 else None
else: else:
continue continue
@@ -678,7 +679,7 @@ async def main():
logger.error("Clé API 1fichier absente") logger.error("Clé API 1fichier absente")
config.pending_download = None config.pending_download = None
continue continue
pending = check_extension_before_download(url, platform, game_name) pending = check_extension_before_download(url, platform_name, game_name)
if not pending: if not pending:
config.menu_state = "error" config.menu_state = "error"
config.error_message = _("error_invalid_download_data") if _ else "Invalid download data" config.error_message = _("error_invalid_download_data") if _ else "Invalid download data"
@@ -687,7 +688,7 @@ async def main():
else: else:
from utils import is_extension_supported, load_extensions_json, sanitize_filename from utils import is_extension_supported, load_extensions_json, sanitize_filename
from rgsx_settings import get_allow_unknown_extensions 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]) zip_ok = bool(pending[3])
allow_unknown = False allow_unknown = False
try: try:
@@ -703,7 +704,7 @@ async def main():
else: else:
config.previous_menu_state = config.menu_state config.previous_menu_state = config.menu_state
logger.debug(f"Previous menu state défini: {config.previous_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 # Ancien popup download_result supprimé : retour direct à l'historique
config.download_result_message = message config.download_result_message = message
config.download_result_error = not success config.download_result_error = not success
@@ -713,7 +714,7 @@ async def main():
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Retéléchargement 1fichier terminé pour {game_name}, succès={success}, message={message}, retour direct history") logger.debug(f"Retéléchargement 1fichier terminé pour {game_name}, succès={success}, message={message}, retour direct history")
else: else:
pending = check_extension_before_download(url, platform, game_name) pending = check_extension_before_download(url, platform_name, game_name)
if not pending: if not pending:
config.menu_state = "error" config.menu_state = "error"
config.error_message = _("error_invalid_download_data") if _ else "Invalid download data" config.error_message = _("error_invalid_download_data") if _ else "Invalid download data"
@@ -722,7 +723,7 @@ async def main():
else: else:
from utils import is_extension_supported, load_extensions_json, sanitize_filename from utils import is_extension_supported, load_extensions_json, sanitize_filename
from rgsx_settings import get_allow_unknown_extensions 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]) zip_ok = bool(pending[3])
allow_unknown = False allow_unknown = False
try: try:
@@ -738,7 +739,7 @@ async def main():
else: else:
config.previous_menu_state = config.menu_state config.previous_menu_state = config.menu_state
logger.debug(f"Previous menu state défini: {config.previous_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_message = message
config.download_result_error = not success config.download_result_error = not success
config.download_progress.clear() config.download_progress.clear()
@@ -759,7 +760,7 @@ async def main():
# Gestion des téléchargements # Gestion des téléchargements
if config.download_tasks: 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(): if task.done():
try: try:
success, message = await task success, message = await task
@@ -1121,26 +1122,33 @@ async def main():
await asyncio.sleep(0.01) await asyncio.sleep(0.01)
pygame.mixer.music.stop() pygame.mixer.music.stop()
result = subprocess.run(["taskkill", "/f", "/im", "emulatorLauncher.exe"]) # Cancel any ongoing downloads to prevent lingering background threads
if result == 0: try:
logger.debug(f"Quitté avec succès: emulatorLauncher.exe") 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: 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() pygame.quit()
logger.debug("Application terminée") 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": if platform.system() == "Emscripten":
asyncio.ensure_future(main()) asyncio.ensure_future(main())
else: else:

View File

@@ -13,7 +13,7 @@ except Exception:
pygame = None # type: ignore pygame = None # type: ignore
# Version actuelle de l'application # Version actuelle de l'application
app_version = "2.2.0.5" app_version = "2.2.0.6"
def get_operating_system(): def get_operating_system():
"""Renvoie le nom du système d'exploitation.""" """Renvoie le nom du système d'exploitation."""

View File

@@ -10,6 +10,7 @@ import os
import sys import sys
from display import draw_validation_transition 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
from network import download_rom, download_from_1fichier, is_1fichier_url, request_cancel
from utils import ( from utils import (
load_games, check_extension_before_download, is_extension_supported, load_games, check_extension_before_download, is_extension_supported,
load_extensions_json, play_random_music, sanitize_filename, load_extensions_json, play_random_music, sanitize_filename,
@@ -1016,11 +1017,15 @@ def handle_controls(event, sources, joystick, screen):
# Annuler la tâche correspondante # Annuler la tâche correspondante
for task_id, (task, task_url, game_name, platform) in list(config.download_tasks.items()): for task_id, (task, task_url, game_name, platform) in list(config.download_tasks.items()):
if task_url == url: if task_url == url:
try:
request_cancel(task_id)
except Exception:
pass
task.cancel() task.cancel()
del config.download_tasks[task_id] del config.download_tasks[task_id]
entry["status"] = "Canceled" entry["status"] = "Canceled"
entry["progress"] = 0 entry["progress"] = 0
entry["message"] = "Téléchargement annulé" entry["message"] = _("download_canceled") if _ else "Download canceled"
save_history(config.history) save_history(config.history)
logger.debug(f"Téléchargement annulé: {game_name}") logger.debug(f"Téléchargement annulé: {game_name}")
break break
@@ -1066,6 +1071,16 @@ def handle_controls(event, sources, joystick, screen):
elif config.menu_state == "confirm_exit": elif config.menu_state == "confirm_exit":
if is_input_matched(event, "confirm"): if is_input_matched(event, "confirm"):
if config.confirm_selection == 1: 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" return "quit"
else: else:
config.menu_state = validate_menu_state(config.previous_menu_state) 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'") logger.warning("game_name vide, utilisation de 'Inconnu'")
if is_zip: if is_zip:
message = _("extension_warning_zip").format(game_name) core = _("extension_warning_zip").format(game_name)
hint = ""
else: else:
# Ajout d'un indice pour activer le téléchargement des extensions inconnues # Ajout d'un indice pour activer le téléchargement des extensions inconnues
try: try:
@@ -1136,18 +1137,25 @@ def draw_extension_warning(screen):
except Exception: except Exception:
hint = "" hint = ""
core = _("extension_warning_unsupported").format(game_name) 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 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: try:
line_height = config.font.get_height() + 5 line_height_core = config.font.get_height() + 5
text_height = len(lines) * line_height 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) button_height = int(config.screen_height * 0.0463)
margin_top_bottom = 20 margin_top_bottom = 20
rect_height = text_height + button_height + 2 * margin_top_bottom 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_width = max_text_width + 80
rect_x = (config.screen_width - rect_width) // 2 rect_x = (config.screen_width - rect_width) // 2
rect_y = (config.screen_height - rect_height) // 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["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) 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_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) 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_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) 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 # Menu pause
def draw_language_menu(screen): 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 from language import get_available_languages, get_language_name
screen.blit(OVERLAY, (0, 0)) screen.blit(OVERLAY, (0, 0))
@@ -1225,21 +1254,54 @@ def draw_language_menu(screen):
logger.error("Aucune langue disponible") logger.error("Aucune langue disponible")
return return
# Titre # Titre (mesuré d'abord pour connaître la hauteur réelle du fond)
title_text = _("language_select_title") title_text = _("language_select_title")
title_surface = config.font.render(title_text, True, THEME_COLORS["text"]) 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)) # On calcule un rect neutre, on positionnera ensuite pour centrer le bloc
title_rect = title_surface.get_rect()
# Fond du titre # Padding responsive plus léger pour réduire la hauteur
title_bg_rect = title_rect.inflate(40, 20) 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["button_idle"], title_bg_rect, border_radius=10)
pygame.draw.rect(screen, THEME_COLORS["border"], title_bg_rect, 2, border_radius=10) pygame.draw.rect(screen, THEME_COLORS["border"], title_bg_rect, 2, border_radius=10)
screen.blit(title_surface, title_rect) 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 # Démarrer la liste juste sous le titre avec le même écart que les boutons
start_y = title_bg_rect.bottom + button_spacing 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): for i, lang_code in enumerate(available_languages):
# Obtenir le nom de la langue # Obtenir le nom de la langue
lang_name = get_language_name(lang_code) lang_name = get_language_name(lang_code)
# Position du bouton # Position du bouton
button_x = (config.screen_width - button_width) // 2 button_x = (config.screen_width - button_width) // 2
button_y = start_y + i * (button_height + button_spacing) button_y = start_y + i * (button_height + button_spacing)
# Dessiner le bouton # Dessiner le bouton
button_color = THEME_COLORS["button_hover"] if i == config.selected_language_index else THEME_COLORS["button_idle"] 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, 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) pygame.draw.rect(screen, THEME_COLORS["border"], (button_x, button_y, button_width, button_height), 2, border_radius=10)
# Texte du bouton # Texte du bouton
text_surface = config.font.render(lang_name, True, THEME_COLORS["text"]) 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)) 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() show_unsupported = get_show_unsupported_platforms()
allow_unknown = get_allow_unknown_extensions() 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 # Libellés
options = [ options = [
f"{_('display_layout')}: {layout_str}", f"{_('display_layout')}: {layout_str}",
_("accessibility_font_size").format(f"{font_scale:.1f}"), _("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_allow_unknown_ext_on") if allow_unknown else _("menu_allow_unknown_ext_off"),
_("menu_filter_platforms"), _("menu_filter_platforms"),
] ]
@@ -1396,8 +1467,11 @@ def draw_filter_platforms_menu(screen):
title_text = _("filter_platforms_title") title_text = _("filter_platforms_title")
title_surface = config.title_font.render(title_text, True, THEME_COLORS["text"]) 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 = title_surface.get_rect(center=(config.screen_width // 2, title_surface.get_height() // 2 + 14))
title_rect_inflated = title_rect.inflate(80, 40) # 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) 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["button_idle"], title_rect_inflated, border_radius=12)
pygame.draw.rect(screen, THEME_COLORS["border"], title_rect_inflated, 2, 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") logger.debug("OVERLAY recréé dans draw_confirm_dialog")
screen.blit(OVERLAY, (0, 0)) 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) wrapped_message = wrap_text(message, config.font, config.screen_width - 80)
line_height = config.font.get_height() + 5 line_height = config.font.get_height() + 5
text_height = len(wrapped_message) * line_height 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", "extension_warning_enable_unknown_hint": "\nUm diese Meldung auszublenden: \"Warnung bei unbekannter Erweiterung ausblenden\" in Pausenmenü > Anzeige aktivieren",
"confirm_exit": "Anwendung beenden?", "confirm_exit": "Anwendung beenden?",
"confirm_exit_with_downloads": "Achtung: {0} Download(s) laufen. Trotzdem beenden?",
"confirm_clear_history": "Verlauf löschen?", "confirm_clear_history": "Verlauf löschen?",
"confirm_redownload_cache": "Spieleliste aktualisieren?", "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", "extension_warning_enable_unknown_hint": "\nTo hide this message: enable \"Hide unknown extension warning\" in Pause Menu > Display",
"confirm_exit": "Exit application?", "confirm_exit": "Exit application?",
"confirm_exit_with_downloads": "Attention: {0} download(s) in progress. Quit anyway?",
"confirm_clear_history": "Clear history?", "confirm_clear_history": "Clear history?",
"confirm_redownload_cache": "Update games list?", "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", "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": "¿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_clear_history": "¿Vaciar el historial?",
"confirm_redownload_cache": "¿Actualizar la lista de juegos?", "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", "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": "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_clear_history": "Vider l'historique ?",
"confirm_redownload_cache": "Mettre à jour la liste des jeux ?", "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", "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": "Uscire dall'applicazione?",
"confirm_exit_with_downloads": "Attenzione: {0} download in corso. Uscire comunque?",
"confirm_clear_history": "Cancellare la cronologia?", "confirm_clear_history": "Cancellare la cronologia?",
"confirm_redownload_cache": "Aggiornare l'elenco dei giochi?", "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", "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": "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_clear_history": "Limpar histórico?",
"confirm_redownload_cache": "Atualizar lista de jogos?", "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 # File d'attente pour la progression - une par tâche
progress_queues = {} 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}") 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] 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: if task_id not in progress_queues:
progress_queues[task_id] = queue.Queue() progress_queues[task_id] = queue.Queue()
if task_id not in cancel_events:
cancel_events[task_id] = threading.Event()
def download_thread(): def download_thread():
logger.debug(f"Thread téléchargement démarré pour {url}, task_id={task_id}") logger.debug(f"Thread téléchargement démarré pour {url}, task_id={task_id}")
try: try:
cancel_ev = cancel_events.get(task_id)
# Use symlink path if enabled # Use symlink path if enabled
from rgsx_settings import apply_symlink_path 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 update_interval = 0.1 # Mettre à jour toutes les 0,1 secondes
with open(dest_path, 'wb') as f: with open(dest_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=chunk_size): 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: if chunk:
size_received = len(chunk) size_received = len(chunk)
f.write(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])) 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}") 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() thread.start()
# Boucle principale pour mettre à jour la progression # 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)}") logger.error(f"Erreur mise à jour progression: {str(e)}")
thread.join() thread.join()
try:
download_threads.pop(task_id, None)
except Exception:
pass
# Drain any remaining final message to ensure history is saved # Drain any remaining final message to ensure history is saved
try: try:
task_queue = progress_queues.get(task_id) 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 # Nettoyer la queue
if task_id in progress_queues: if task_id in progress_queues:
del progress_queues[task_id] del progress_queues[task_id]
cancel_events.pop(task_id, None)
return result[0], result[1] return result[0], result[1]
async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=False, task_id=None): 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}") logger.debug(f"Création queue pour task_id={task_id}")
if task_id not in progress_queues: if task_id not in progress_queues:
progress_queues[task_id] = queue.Queue() progress_queues[task_id] = queue.Queue()
if task_id not in cancel_events:
cancel_events[task_id] = threading.Event()
def download_thread(): def download_thread():
logger.debug(f"Thread téléchargement 1fichier démarré pour {url}, task_id={task_id}") logger.debug(f"Thread téléchargement 1fichier démarré pour {url}, task_id={task_id}")
try: try:
cancel_ev = cancel_events.get(task_id)
link = url.split('&af=')[0] link = url.split('&af=')[0]
logger.debug(f"URL nettoyée: {link}") logger.debug(f"URL nettoyée: {link}")
# Use symlink path if enabled # 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}") logger.debug(f"Ouverture fichier: {dest_path}")
with open(dest_path, 'wb') as f: with open(dest_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=chunk_size): 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: if chunk:
f.write(chunk) f.write(chunk)
downloaded += len(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"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}") 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() thread.start()
# Boucle principale pour mettre à jour la progression # 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}") logger.debug(f"Fin boucle de progression, attente fin thread pour task_id={task_id}")
thread.join() thread.join()
try:
download_threads.pop(task_id, None)
except Exception:
pass
logger.debug(f"Thread terminé, nettoyage queue pour task_id={task_id}") logger.debug(f"Thread terminé, nettoyage queue pour task_id={task_id}")
# Drain any remaining final message to ensure history is saved # Drain any remaining final message to ensure history is saved
try: try:
@@ -847,6 +924,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
# Nettoyer la queue # Nettoyer la queue
if task_id in progress_queues: if task_id in progress_queues:
del progress_queues[task_id] 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]}") logger.debug(f"Fin download_from_1fichier, résultat: success={result[0]}, message={result[1]}")
return result[0], result[1] return result[0], result[1]
def is_1fichier_url(url): def is_1fichier_url(url):

View File

@@ -493,8 +493,6 @@ def load_sources():
for platform_name in config.platforms: for platform_name in config.platforms:
games = load_games(platform_name) games = load_games(platform_name)
config.games_count[platform_name] = len(games) config.games_count[platform_name] = len(games)
write_unavailable_systems()
return sources return sources
except Exception as e: except Exception as e:
logger.error(f"Erreur fusion systèmes + détection jeux: {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}") logger.error(f"Erreur lors du chargement des jeux pour {platform_id}: {e}")
return [] 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): 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. """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.""" Si is_filename=False, ne supprime pas l'extension."""