import pygame # type: ignore import os import io import config from utils import truncate_text_middle, wrap_text, load_system_image, truncate_text_end import logging import math from history import load_history # Ajout de l'import from language import _ # Import de la fonction de traduction logger = logging.getLogger(__name__) OVERLAY = None # Initialisé dans init_display() # --- Helpers: SVG icons for controls (local cache, optional cairosvg) --- _HELP_ICON_CACHE = {} def _images_base_dir(): try: base_dir = os.path.join(os.path.dirname(__file__), "assets", "images") except Exception: base_dir = "assets/images" return base_dir def _action_icon_filename(action_name: str): mapping = { "up": "dpad_up.svg", "down": "dpad_down.svg", "left": "dpad_left.svg", "right": "dpad_right.svg", "confirm": "buttons_south.svg", "cancel": "buttons_east.svg", "clear_history": "buttons_west.svg", "history": "buttons_north.svg", "start": "button_start.svg", "filter": "button_select.svg", "delete": "button_l.svg", "space": "button_r.svg", "page_up": "button_lt.svg", "page_down": "button_rt.svg", } return mapping.get(action_name) def _load_svg_icon_surface(svg_path: str, size: int): try: # Prefer cairosvg if available for crisp rasterization try: import cairosvg # type: ignore except Exception: cairosvg = None # type: ignore if cairosvg is not None: with open(svg_path, "rb") as f: svg_bytes = f.read() png_bytes = cairosvg.svg2png(bytestring=svg_bytes, output_width=size, output_height=size) return pygame.image.load(io.BytesIO(png_bytes), "icon.png").convert_alpha() # Fallback: try direct load (works if SDL_image has SVG support) surf = pygame.image.load(svg_path) w, h = surf.get_size() if w != size or h != size: scale = min(size / max(w, 1), size / max(h, 1)) new_w = max(1, int(w * scale)) new_h = max(1, int(h * scale)) surf = pygame.transform.smoothscale(surf, (new_w, new_h)) return surf.convert_alpha() except Exception as e: try: logger.debug(f"Help icon load failed for {svg_path}: {e}") except Exception: pass return None def get_help_icon_surface(action_name: str, size: int): key = (action_name, size) if key in _HELP_ICON_CACHE: return _HELP_ICON_CACHE[key] filename = _action_icon_filename(action_name) if not filename: _HELP_ICON_CACHE[key] = None return None full_path = os.path.join(_images_base_dir(), filename) if not os.path.exists(full_path): _HELP_ICON_CACHE[key] = None return None surf = _load_svg_icon_surface(full_path, size) _HELP_ICON_CACHE[key] = surf return surf def _render_icons_line(actions, text, target_col_width, font, text_color, icon_size=28, icon_gap=8, icon_text_gap=12): """Compose une ligne avec une rangée d'icônes (actions) et un texte à droite. Renvoie un pygame.Surface prêt à être blité, limité à target_col_width. """ # Charger icônes (ignorer celles manquantes) icon_surfs = [] for a in actions: surf = get_help_icon_surface(a, icon_size) if surf is not None: icon_surfs.append(surf) # Si aucune icône, rendre simplement le texte (le layout appelant ajoutera les espacements) if not icon_surfs: try: lines = wrap_text(text, font, target_col_width) except Exception: lines = [text] line_surfs = [font.render(l, True, text_color) for l in lines] width = max((s.get_width() for s in line_surfs), default=1) height = sum(s.get_height() for s in line_surfs) + max(0, (len(line_surfs) - 1)) * 4 surf = pygame.Surface((width, height), pygame.SRCALPHA) y = 0 for s in line_surfs: surf.blit(s, (0, y)) y += s.get_height() + 4 return surf # Calcul largeur totale des icônes icons_width = sum(s.get_width() for s in icon_surfs) + (len(icon_surfs) - 1) * icon_gap if icons_width + icon_text_gap > target_col_width: scale = (target_col_width - icon_text_gap) / max(1, icons_width) scale = max(0.6, min(1.0, scale)) new_icon_surfs = [] for s in icon_surfs: new_size = (max(1, int(s.get_width() * scale)), max(1, int(s.get_height() * scale))) new_icon_surfs.append(pygame.transform.smoothscale(s, new_size)) icon_surfs = new_icon_surfs icons_width = sum(s.get_width() for s in icon_surfs) + (len(icon_surfs) - 1) * icon_gap text_area_width = max(60, target_col_width - icons_width - icon_text_gap) try: lines = wrap_text(text, font, text_area_width) except Exception: lines = [text] line_surfs = [font.render(l, True, text_color) for l in lines] text_block_width = max((s.get_width() for s in line_surfs), default=1) text_block_height = sum(s.get_height() for s in line_surfs) + max(0, (len(line_surfs) - 1)) * 4 total_width = min(target_col_width, icons_width + icon_text_gap + text_block_width) total_height = max(max((s.get_height() for s in icon_surfs), default=0), text_block_height) surf = pygame.Surface((total_width, total_height), pygame.SRCALPHA) x = 0 icon_y_center = total_height // 2 for idx, s in enumerate(icon_surfs): r = s.get_rect() y = icon_y_center - r.height // 2 surf.blit(s, (x, y)) x += r.width + (icon_gap if idx < len(icon_surfs) - 1 else 0) text_x = x + icon_text_gap y = (total_height - text_block_height) // 2 for ls in line_surfs: surf.blit(ls, (text_x, y)) y += ls.get_height() + 4 return surf # Couleurs modernes pour le thème THEME_COLORS = { # Fond des lignes sélectionnées "fond_lignes": (0, 255, 0), # vert # Fond par défaut des images de grille des systèmes "fond_image": (50, 50, 70), # Bleu sombre métal # Néon image grille des systèmes "neon": (0, 134, 179), # bleu # Dégradé sombre pour le fond "background_top": (30, 40, 50), "background_bottom": (60, 80, 100), # noir vers bleu foncé # Fond des cadres "button_idle": (50, 50, 70, 150), # Bleu sombre métal # Fond des boutons sélectionnés dans les popups ou menu "button_hover": (255, 0, 255, 220), # Rose # Générique "text": (255, 255, 255), # blanc # Texte sélectionné (alias pour compatibilité) "text_selected": (0, 255, 0), # utilise le même vert que fond_lignes # Erreur "error_text": (255, 0, 0), # rouge # Avertissement "warning_text": (255, 100, 0), # orange # Titres "title_text": (200, 200, 200), # gris clair # Bordures "border": (150, 150, 150), # Bordures grises subtiles } # Général, résolution, overlay def init_display(): """Initialise l'écran et les ressources globales.""" global OVERLAY logger.debug("Initialisation de l'écran") display_info = pygame.display.Info() screen_width = display_info.current_w screen_height = display_info.current_h screen = pygame.display.set_mode((screen_width, screen_height)) config.screen_width = screen_width config.screen_height = screen_height # Initialisation de OVERLAY OVERLAY = pygame.Surface((screen_width, screen_height), pygame.SRCALPHA) OVERLAY.fill((0, 0, 0, 150)) # Transparence augmentée logger.debug(f"Écran initialisé avec résolution : {screen_width}x{screen_height}") return screen # Fond d'écran dégradé def draw_gradient(screen, top_color, bottom_color): """Dessine un fond dégradé vertical avec des couleurs vibrantes.""" height = screen.get_height() top_color = pygame.Color(*top_color) bottom_color = pygame.Color(*bottom_color) for y in range(height): ratio = y / height color = top_color.lerp(bottom_color, ratio) pygame.draw.line(screen, color, (0, y), (screen.get_width(), y)) # Nouvelle fonction pour dessiner un bouton stylisé def draw_stylized_button(screen, text, x, y, width, height, selected=False): """Dessine un bouton moderne avec effet de survol et bordure arrondie.""" button_surface = pygame.Surface((width, height), pygame.SRCALPHA) button_color = THEME_COLORS["button_hover"] if selected else THEME_COLORS["button_idle"] pygame.draw.rect(button_surface, button_color, (0, 0, width, height), border_radius=12) pygame.draw.rect(button_surface, THEME_COLORS["border"], (0, 0, width, height), 2, border_radius=12) if selected: glow_surface = pygame.Surface((width + 10, height + 10), pygame.SRCALPHA) pygame.draw.rect(glow_surface, THEME_COLORS["fond_lignes"] + (50,), (5, 5, width, height), border_radius=12) screen.blit(glow_surface, (x - 5, y - 5)) screen.blit(button_surface, (x, y)) text_surface = config.font.render(text, True, THEME_COLORS["text"]) text_rect = text_surface.get_rect(center=(x + width // 2, y + height // 2)) screen.blit(text_surface, text_rect) # Transition d'image lors de la sélection d'un système def draw_validation_transition(screen, platform_index): """Affiche une animation de transition fluide pour la sélection d’une plateforme. Utilise le mapping par nom pour éviter les décalages d'image si l'ordre d'affichage diffère de l'ordre de stockage.""" # Récupérer le nom affiché correspondant à l'index trié if platform_index < 0 or platform_index >= len(config.platforms): return platform_name = config.platforms[platform_index] platform_dict = getattr(config, 'platform_dict_by_name', {}).get(platform_name) if not platform_dict: # Fallback index direct si mapping absent try: platform_dict = config.platform_dicts[platform_index] except Exception: return image = load_system_image(platform_dict) if not image: return # Dimensions originales et calcul du ratio pour préserver les proportions orig_width, orig_height = image.get_width(), image.get_height() base_size = int(config.screen_width * 0.0781) # ~150px pour 1920p ratio = min(base_size / orig_width, base_size / orig_height) # Maintenir les proportions base_width = int(orig_width * ratio) base_height = int(orig_height * ratio) # Paramètres de l'animation start_time = pygame.time.get_ticks() duration = 1000 # Durée augmentée à 1 seconde fps = 60 frame_time = 1000 / fps # Temps par frame en ms while pygame.time.get_ticks() - start_time < duration: # Fond dégradé draw_gradient(screen, THEME_COLORS["background_top"], THEME_COLORS["background_bottom"]) # Calcul de l'échelle avec une courbe sinusoïdale pour une transition fluide elapsed = pygame.time.get_ticks() - start_time progress = elapsed / duration # Courbe sinusoïdale pour une montée/descente douce scale = 1.5 + 1.0 * math.sin(math.pi * progress) # Échelle de 1.5 à 2.5 new_width = int(base_width * scale) new_height = int(base_height * scale) # Redimensionner l'image en préservant les proportions scaled_image = pygame.transform.smoothscale(image, (new_width, new_height)) image_rect = scaled_image.get_rect(center=(config.screen_width // 2, config.screen_height // 2)) # Effet de fondu (opacité de 50% à 100% puis retour à 50%) alpha = int(128 + 127 * math.cos(math.pi * progress)) # Opacité entre 128 et 255 scaled_image.set_alpha(alpha) # Effet de glow néon pour l'image sélectionnée neon_color = THEME_COLORS["neon"] # Cyan vif padding = 24 neon_surface = pygame.Surface((new_width + 2 * padding, new_height + 2 * padding), pygame.SRCALPHA) pygame.draw.rect(neon_surface, neon_color + (40,), neon_surface.get_rect(), border_radius=24) pygame.draw.rect(neon_surface, neon_color + (100,), neon_surface.get_rect().inflate(-10, -10), border_radius=18) screen.blit(neon_surface, (image_rect.left - padding, image_rect.top - padding), special_flags=pygame.BLEND_RGBA_ADD) # Afficher l'image screen.blit(scaled_image, image_rect) pygame.display.flip() # Contrôler la fréquence de rendu pygame.time.wait(int(frame_time)) # Afficher l'image finale sans effet pour une transition propre draw_gradient(screen, THEME_COLORS["background_top"], THEME_COLORS["background_bottom"]) final_image = pygame.transform.smoothscale(image, (base_width, base_height)) final_image.set_alpha(255) # Opacité complète final_rect = final_image.get_rect(center=(config.screen_width // 2, config.screen_height // 2)) screen.blit(final_image, final_rect) pygame.display.flip() # Écran de chargement def draw_loading_screen(screen): """Affiche l’écran de chargement avec un style moderne.""" disclaimer_lines = [ _("welcome_message"), _("disclaimer_line1"), _("disclaimer_line2"), _("disclaimer_line3"), _("disclaimer_line4"), _("disclaimer_line5"), ] margin_horizontal = int(config.screen_width * 0.025) padding_vertical = int(config.screen_height * 0.0185) padding_between = int(config.screen_height * 0.0074) border_radius = 16 border_width = 3 shadow_offset = 6 line_height = config.small_font.get_height() + padding_between total_height = line_height * len(disclaimer_lines) - padding_between rect_width = config.screen_width - 2 * margin_horizontal rect_height = total_height + 2 * padding_vertical rect_x = margin_horizontal rect_y = int(config.screen_height * 0.0185) shadow_rect = pygame.Rect(rect_x + shadow_offset, rect_y + shadow_offset, rect_width, rect_height) shadow_surface = pygame.Surface((rect_width, rect_height), pygame.SRCALPHA) pygame.draw.rect(shadow_surface, (0, 0, 0, 100), shadow_surface.get_rect(), border_radius=border_radius) screen.blit(shadow_surface, shadow_rect.topleft) disclaimer_rect = pygame.Rect(rect_x, rect_y, rect_width, rect_height) disclaimer_surface = pygame.Surface((rect_width, rect_height), pygame.SRCALPHA) pygame.draw.rect(disclaimer_surface, THEME_COLORS["button_idle"], disclaimer_surface.get_rect(), border_radius=border_radius) screen.blit(disclaimer_surface, disclaimer_rect.topleft) pygame.draw.rect(screen, THEME_COLORS["border"], disclaimer_rect, border_width, border_radius=border_radius) max_text_width = rect_width - 2 * padding_vertical for i, line in enumerate(disclaimer_lines): wrapped_lines = wrap_text(line, config.small_font, max_text_width) for j, wrapped_line in enumerate(wrapped_lines): text_surface = config.small_font.render(wrapped_line, True, THEME_COLORS["title_text"]) text_rect = text_surface.get_rect(center=( config.screen_width // 2, rect_y + padding_vertical + (i * len(wrapped_lines) + j + 0.5) * line_height - padding_between // 2 )) screen.blit(text_surface, text_rect) loading_y = rect_y + rect_height + int(config.screen_height * 0.0926) text = config.small_font.render(truncate_text_middle(f"{config.current_loading_system}", config.small_font, config.screen_width - 2 * margin_horizontal), True, THEME_COLORS["text"]) text_rect = text.get_rect(center=(config.screen_width // 2, loading_y)) screen.blit(text, text_rect) progress_text = config.small_font.render(_("loading_progress").format(int(config.loading_progress)), True, THEME_COLORS["text"]) progress_rect = progress_text.get_rect(center=(config.screen_width // 2, loading_y + int(config.screen_height * 0.0463))) screen.blit(progress_text, progress_rect) bar_width = int(config.screen_width * 0.2083) bar_height = int(config.screen_height * 0.037) progress_width = (bar_width * config.loading_progress) / 100 pygame.draw.rect(screen, THEME_COLORS["button_idle"], (config.screen_width // 2 - bar_width // 2, loading_y + int(config.screen_height * 0.0926), bar_width, bar_height), border_radius=8) pygame.draw.rect(screen, THEME_COLORS["fond_lignes"], (config.screen_width // 2 - bar_width // 2, loading_y + int(config.screen_height * 0.0926), progress_width, bar_height), border_radius=8) # Écran d'erreur def draw_error_screen(screen): """Affiche l’écran d’erreur avec un style moderne.""" wrapped_message = wrap_text(config.error_message, config.small_font, config.screen_width - 80) line_height = config.small_font.get_height() + 5 text_height = len(wrapped_message) * line_height button_height = int(config.screen_height * 0.0463) margin_top_bottom = 20 rect_height = text_height + button_height + 2 * margin_top_bottom max_text_width = max([config.small_font.size(line)[0] for line in wrapped_message], default=300) rect_width = max_text_width + 80 rect_x = (config.screen_width - rect_width) // 2 rect_y = (config.screen_height - rect_height) // 2 screen.blit(OVERLAY, (0, 0)) pygame.draw.rect(screen, THEME_COLORS["button_idle"], (rect_x, rect_y, rect_width, rect_height), border_radius=12) pygame.draw.rect(screen, THEME_COLORS["border"], (rect_x, rect_y, rect_width, rect_height), 2, border_radius=12) for i, line in enumerate(wrapped_message): text = config.small_font.render(line, True, THEME_COLORS["error_text"]) text_rect = text.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + i * line_height + line_height // 2)) screen.blit(text, text_rect) draw_stylized_button(screen, _("button_OK"), rect_x + rect_width // 2 - 80, rect_y + text_height + margin_top_bottom, 160, button_height, selected=True) # Récupérer les noms d'affichage des contrôles def get_control_display(action, default): """Récupère le nom d'affichage d'une action depuis controls_config.""" if not config.controls_config: logger.warning(f"controls_config vide pour l'action {action}, utilisation de la valeur par défaut") return default control_config = config.controls_config.get(action, {}) control_type = control_config.get('type', '') # Si un libellé personnalisé est défini dans controls.json, on le privilégie custom_label = control_config.get('display') if isinstance(custom_label, str) and custom_label.strip(): return custom_label # Générer le nom d'affichage basé sur la configuration réelle if control_type == 'key': key_code = control_config.get('key') key_names = { pygame.K_RETURN: "Enter", pygame.K_ESCAPE: "Échap", pygame.K_SPACE: "Espace", pygame.K_UP: "↑", pygame.K_DOWN: "↓", pygame.K_LEFT: "←", pygame.K_RIGHT: "→", pygame.K_BACKSPACE: "Backspace", pygame.K_TAB: "Tab", pygame.K_LALT: "Alt", pygame.K_RALT: "AltGR", pygame.K_LCTRL: "LCtrl", pygame.K_RCTRL: "RCtrl", pygame.K_LSHIFT: "LShift", pygame.K_RSHIFT: "RShift", pygame.K_LMETA: "LMeta", pygame.K_RMETA: "RMeta", pygame.K_CAPSLOCK: "Verr Maj", pygame.K_NUMLOCK: "Verr Num", pygame.K_SCROLLOCK: "Verr Déf", pygame.K_a: "A", pygame.K_b: "B", pygame.K_c: "C", pygame.K_d: "D", pygame.K_e: "E", pygame.K_f: "F", pygame.K_g: "G", pygame.K_h: "H", pygame.K_i: "I", pygame.K_j: "J", pygame.K_k: "K", pygame.K_l: "L", pygame.K_m: "M", pygame.K_n: "N", pygame.K_o: "O", pygame.K_p: "P", pygame.K_q: "Q", pygame.K_r: "R", pygame.K_s: "S", pygame.K_t: "T", pygame.K_u: "U", pygame.K_v: "V", pygame.K_w: "W", pygame.K_x: "X", pygame.K_y: "Y", pygame.K_z: "Z", pygame.K_0: "0", pygame.K_1: "1", pygame.K_2: "2", pygame.K_3: "3", pygame.K_4: "4", pygame.K_5: "5", pygame.K_6: "6", pygame.K_7: "7", pygame.K_8: "8", pygame.K_9: "9", pygame.K_KP0: "Num 0", pygame.K_KP1: "Num 1", pygame.K_KP2: "Num 2", pygame.K_KP3: "Num 3", pygame.K_KP4: "Num 4", pygame.K_KP5: "Num 5", pygame.K_KP6: "Num 6", pygame.K_KP7: "Num 7", pygame.K_KP8: "Num 8", pygame.K_KP9: "Num 9", pygame.K_KP_PERIOD: "Num .", pygame.K_KP_DIVIDE: "Num /", pygame.K_KP_MULTIPLY: "Num *", pygame.K_KP_MINUS: "Num -", pygame.K_KP_PLUS: "Num +", pygame.K_KP_ENTER: "Num Enter", pygame.K_KP_EQUALS: "Num =", pygame.K_F1: "F1", pygame.K_F2: "F2", pygame.K_F3: "F3", pygame.K_F4: "F4", pygame.K_F5: "F5", pygame.K_F6: "F6", pygame.K_F7: "F7", pygame.K_F8: "F8", pygame.K_F9: "F9", pygame.K_F10: "F10", pygame.K_F11: "F11", pygame.K_F12: "F12", pygame.K_F13: "F13", pygame.K_F14: "F14", pygame.K_F15: "F15", pygame.K_INSERT: "Inser", pygame.K_DELETE: "Suppr", pygame.K_HOME: "Début", pygame.K_END: "Fin", pygame.K_PAGEUP: "Page+", pygame.K_PAGEDOWN: "Page-", pygame.K_PRINT: "Printscreen", pygame.K_SYSREQ: "SysReq", pygame.K_BREAK: "Pause", pygame.K_PAUSE: "Pause", pygame.K_BACKQUOTE: "`", pygame.K_MINUS: "-", pygame.K_EQUALS: "=", pygame.K_LEFTBRACKET: "[", pygame.K_RIGHTBRACKET: "]", pygame.K_BACKSLASH: "\\", pygame.K_SEMICOLON: ";", pygame.K_QUOTE: "'", pygame.K_COMMA: ",", pygame.K_PERIOD: ".", pygame.K_SLASH: "/", } return key_names.get(key_code, chr(key_code) if 32 <= key_code <= 126 else f"Key{key_code}") elif control_type == 'button': button_id = control_config.get('button') # Étendre le mapping pour couvrir plus de manettes (incl. Trimui) button_names = { 0: "A", 1: "B", 2: "X", 3: "Y", 4: "LB", 5: "RB", 6: "Select", 7: "Start", 8: "Select", 9: "Start", 10: "L3", 11: "R3", } return button_names.get(button_id, f"Btn{button_id}") elif control_type == 'hat': hat_value = control_config.get('value', (0, 0)) hat_names = { (0, 1): "D↑", (0, -1): "D↓", (-1, 0): "D←", (1, 0): "D→" } return hat_names.get(tuple(hat_value) if isinstance(hat_value, list) else hat_value, "D-Pad") elif control_type == 'axis': axis_id = control_config.get('axis') direction = control_config.get('direction') axis_names = { (0, -1): "J←", (0, 1): "J→", (1, -1): "J↑", (1, 1): "J↓" } return axis_names.get((axis_id, direction), f"Joy{axis_id}") # Fallback vers l'ancien système ou valeur par défaut return control_config.get('display', default) # Cache pour les images des plateformes platform_images_cache = {} # Grille des systèmes 3x3 def draw_platform_grid(screen): """Affiche la grille des plateformes avec un style moderne et fluide.""" global platform_images_cache if not config.platforms or config.selected_platform >= len(config.platforms): platform_name = _("platform_no_platform") logger.warning("Aucune plateforme ou selected_platform hors limites") else: platform = config.platforms[config.selected_platform] platform_name = config.platform_names.get(platform, platform) # Affichage du titre avec animation subtile # Afficher le nombre total de jeux disponibles (tous systèmes) pour cohérence avec l'écran jeux # Nombre de jeux pour la plateforme sélectionnée (utilise le cache pre-calculé si disponible) game_count = 0 try: if hasattr(config, 'games_count') and isinstance(config.games_count, dict): game_count = config.games_count.get(platform_name, 0) # Fallback dynamique si pas dans le cache (ex: plateformes modifiées à chaud) if game_count == 0 and hasattr(config, 'platform_dict_by_name'): from utils import load_games # import local pour éviter import circulaire global game_count = len(load_games(platform_name)) except Exception: game_count = 0 title_text = f"{platform_name} ({game_count})" if game_count > 0 else f"{platform_name}" 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(60, 30) title_rect_inflated.topleft = ((config.screen_width - title_rect_inflated.width) // 2, 10) # Effet de pulsation subtil pour le titre - calculé une seule fois par frame current_time = pygame.time.get_ticks() pulse_factor = 0.05 * (1 + math.sin(current_time / 500)) title_glow = pygame.Surface((title_rect_inflated.width + 10, title_rect_inflated.height + 10), pygame.SRCALPHA) pygame.draw.rect(title_glow, THEME_COLORS["neon"] + (int(40 * pulse_factor),), title_glow.get_rect(), border_radius=14) screen.blit(title_glow, (title_rect_inflated.left - 5, title_rect_inflated.top - 5)) 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) screen.blit(title_surface, title_rect) # Configuration de la grille - calculée une seule fois margin_left = int(config.screen_width * 0.026) margin_right = int(config.screen_width * 0.026) margin_top = int(config.screen_height * 0.140) margin_bottom = int(config.screen_height * 0.0648) num_cols = getattr(config, 'GRID_COLS', 3) num_rows = getattr(config, 'GRID_ROWS', 4) systems_per_page = num_cols * num_rows available_width = config.screen_width - margin_left - margin_right available_height = config.screen_height - margin_top - margin_bottom col_width = available_width // num_cols row_height = available_height // num_rows x_positions = [margin_left + col_width * i + col_width // 2 for i in range(num_cols)] y_positions = [margin_top + row_height * i + row_height // 2 for i in range(num_rows)] # Filtrage éventuel des systèmes premium selon réglage try: from rgsx_settings import get_hide_premium_systems hide_premium = get_hide_premium_systems() except Exception: hide_premium = False premium_markers = getattr(config, 'PREMIUM_HOST_MARKERS', []) if hide_premium and premium_markers: visible_platforms = [p for p in config.platforms if not any(m.lower() in p.lower() for m in premium_markers)] else: visible_platforms = list(config.platforms) # Ajuster selected_platform et current_platform/page si liste réduite if config.selected_platform >= len(visible_platforms): config.selected_platform = max(0, len(visible_platforms) - 1) # Recalcule la page courante en fonction de selected_platform systems_per_page = num_cols * num_rows if systems_per_page <= 0: systems_per_page = 1 config.current_page = config.selected_platform // systems_per_page if systems_per_page else 0 total_pages = (len(visible_platforms) + systems_per_page - 1) // systems_per_page if total_pages > 1: page_indicator_text = _("platform_page").format(config.current_page + 1, total_pages) page_indicator = config.small_font.render(page_indicator_text, True, THEME_COLORS["text"]) page_rect = page_indicator.get_rect(center=(config.screen_width // 2, config.screen_height - margin_bottom // 2)) screen.blit(page_indicator, page_rect) # Calculer une seule fois la pulsation pour les éléments sélectionnés pulse = 0.1 * math.sin(current_time / 300) glow_intensity = 40 + int(30 * math.sin(current_time / 300)) # Pré-calcul des images pour optimiser le rendu start_idx = config.current_page * systems_per_page for idx in range(start_idx, start_idx + systems_per_page): if idx >= len(visible_platforms): break grid_idx = idx - start_idx row = grid_idx // num_cols col = grid_idx % num_cols x = x_positions[col] y = y_positions[row] # Animation fluide pour l'item sélectionné is_selected = idx == config.selected_platform scale_base = 1.5 if is_selected else 1.0 scale = scale_base + pulse if is_selected else scale_base # Récupération robuste du dict via nom display_name = visible_platforms[idx] platform_dict = getattr(config, 'platform_dict_by_name', {}).get(display_name) if not platform_dict: # Fallback index brut # Chercher en parcourant platform_dicts pour correspondance nom for pd in config.platform_dicts: n = pd.get("platform_name") or pd.get("platform") if n == display_name: platform_dict = pd break else: continue platform_id = platform_dict.get("platform_name") or platform_dict.get("platform") or display_name # Utiliser le cache d'images pour éviter de recharger/redimensionner à chaque frame cache_key = f"{platform_id}_{scale:.2f}" if cache_key not in platform_images_cache: image = load_system_image(platform_dict) if image: orig_width, orig_height = image.get_width(), image.get_height() max_size = int(min(col_width, row_height) * scale * 1.1) # Légèrement plus grand que la cellule ratio = min(max_size / orig_width, max_size / orig_height) new_width = int(orig_width * ratio) new_height = int(orig_height * ratio) scaled_image = pygame.transform.smoothscale(image, (new_width, new_height)) platform_images_cache[cache_key] = { "image": scaled_image, "width": new_width, "height": new_height, "last_used": current_time } else: continue else: # Mettre à jour le timestamp de dernière utilisation platform_images_cache[cache_key]["last_used"] = current_time scaled_image = platform_images_cache[cache_key]["image"] new_width = platform_images_cache[cache_key]["width"] new_height = platform_images_cache[cache_key]["height"] image_rect = scaled_image.get_rect(center=(x, y)) # Effet visuel amélioré pour l'item sélectionné if is_selected: neon_color = THEME_COLORS["neon"] border_radius = 12 padding = 12 rect_width = image_rect.width + 2 * padding rect_height = image_rect.height + 2 * padding # Effet de glow dynamique neon_surface = pygame.Surface((rect_width, rect_height), pygame.SRCALPHA) pygame.draw.rect(neon_surface, neon_color + (glow_intensity,), neon_surface.get_rect(), border_radius=border_radius) pygame.draw.rect(neon_surface, neon_color + (100,), neon_surface.get_rect().inflate(-10, -10), border_radius=border_radius) pygame.draw.rect(neon_surface, neon_color + (200,), neon_surface.get_rect().inflate(-20, -20), width=1, border_radius=border_radius) screen.blit(neon_surface, (image_rect.left - padding, image_rect.top - padding), special_flags=pygame.BLEND_RGBA_ADD) # Fond pour toutes les images background_surface = pygame.Surface((image_rect.width + 10, image_rect.height + 10), pygame.SRCALPHA) bg_alpha = 220 if is_selected else 180 # Plus opaque pour l'item sélectionné pygame.draw.rect(background_surface, THEME_COLORS["fond_image"] + (bg_alpha,), background_surface.get_rect(), border_radius=12) screen.blit(background_surface, (image_rect.left - 5, image_rect.top - 5)) # Affichage de l'image avec un léger effet de transparence pour les items non sélectionnés if not is_selected: # Appliquer la transparence seulement si nécessaire temp_image = scaled_image.copy() temp_image.set_alpha(220) screen.blit(temp_image, image_rect) else: screen.blit(scaled_image, image_rect) # Nettoyer le cache périodiquement (garder seulement les images utilisées récemment) if len(platform_images_cache) > 50: # Limite arbitraire pour éviter une croissance excessive current_time = pygame.time.get_ticks() cache_timeout = 30000 # 30 secondes keys_to_remove = [k for k, v in platform_images_cache.items() if current_time - v["last_used"] > cache_timeout] for key in keys_to_remove: del platform_images_cache[key] # Liste des jeux def draw_game_list(screen): """Affiche la liste des jeux avec un style moderne.""" platform = config.platforms[config.current_platform] platform_name = config.platform_names.get(platform, platform) games = config.filtered_games if config.filter_active or config.search_mode else config.games game_count = len(games) if not games: logger.debug("Aucune liste de jeux disponible") message = _("game_no_games") lines = wrap_text(message, config.font, config.screen_width - 80) line_height = config.font.get_height() + 5 text_height = len(lines) * line_height margin_top_bottom = 20 rect_height = text_height + 2 * margin_top_bottom max_text_width = max([config.font.size(line)[0] for line in lines], default=300) rect_width = max_text_width + 80 rect_x = (config.screen_width - rect_width) // 2 rect_y = (config.screen_height - rect_height) // 2 screen.blit(OVERLAY, (0, 0)) pygame.draw.rect(screen, 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): text_surface = config.font.render(line, True, THEME_COLORS["text"]) text_rect = text_surface.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + i * line_height + line_height // 2)) screen.blit(text_surface, text_rect) return line_height = config.small_font.get_height() + 10 header_height = line_height # hauteur de l'en-tête identique à une ligne margin_top_bottom = 20 extra_margin_top = 20 extra_margin_bottom = 60 title_height = config.title_font.get_height() + 20 # Réserver de l'espace pour l'en-tête (header_height) available_height = config.screen_height - title_height - extra_margin_top - extra_margin_bottom - 2 * margin_top_bottom - header_height items_per_page = max(1, available_height // line_height) rect_height = header_height + items_per_page * line_height + 2 * margin_top_bottom rect_width = int(0.95 * config.screen_width) rect_x = (config.screen_width - rect_width) // 2 rect_y = title_height + extra_margin_top + (config.screen_height - title_height - extra_margin_top - extra_margin_bottom - rect_height) // 2 config.scroll_offset = max(0, min(config.scroll_offset, max(0, len(games) - items_per_page))) if config.current_game < config.scroll_offset: config.scroll_offset = config.current_game elif config.current_game >= config.scroll_offset + items_per_page: config.scroll_offset = config.current_game - items_per_page + 1 screen.blit(OVERLAY, (0, 0)) if config.search_mode: search_text = _("game_search").format(config.search_query + "_") title_surface = config.search_font.render(search_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(60, 30) 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) screen.blit(title_surface, title_rect) elif config.filter_active: filter_text = _("game_filter").format(config.search_query) title_surface = config.font.render(filter_text, True, THEME_COLORS["fond_lignes"]) title_rect = title_surface.get_rect(center=(config.screen_width // 2, title_surface.get_height() // 2 + 20)) title_rect_inflated = title_rect.inflate(60, 30) 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) screen.blit(title_surface, title_rect) else: title_text = _("game_count").format(platform_name, game_count) 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(60, 30) 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) screen.blit(title_surface, title_rect) 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) # Largeur colonne taille (15%) mini 120px, reste pour nom size_col_width = max(120, int(rect_width * 0.15)) name_col_width = rect_width - 40 - size_col_width # padding horizontal 40 # ---- En-tête ---- header_name = _("game_header_name") header_size = _("game_header_size") header_y_center = rect_y + margin_top_bottom + header_height // 2 # Nom aligné gauche header_name_surface = config.small_font.render(header_name, True, THEME_COLORS["text"]) header_name_rect = header_name_surface.get_rect() header_name_rect.midleft = (rect_x + 20, header_y_center) # Taille alignée droite header_size_surface = config.small_font.render(header_size, True, THEME_COLORS["text"]) header_size_rect = header_size_surface.get_rect() header_size_rect.midright = (rect_x + rect_width - 20, header_y_center) screen.blit(header_name_surface, header_name_rect) screen.blit(header_size_surface, header_size_rect) # Ligne de séparation sous l'en-tête separator_y = rect_y + margin_top_bottom + header_height pygame.draw.line(screen, THEME_COLORS["border"], (rect_x + 20, separator_y), (rect_x + rect_width - 20, separator_y), 2) # Position de départ des lignes après l'en-tête list_start_y = rect_y + margin_top_bottom + header_height for i in range(config.scroll_offset, min(config.scroll_offset + items_per_page, len(games))): item = games[i] if isinstance(item, (list, tuple)) and item: game_name = item[0] size_val = item[2] if len(item) > 2 else None else: game_name = str(item) size_val = None size_text = size_val if (isinstance(size_val, str) and size_val.strip()) else "N/A" is_marked = i in getattr(config, 'selected_games', set()) color = THEME_COLORS["fond_lignes"] if (i == config.current_game or is_marked) else THEME_COLORS["text"] prefix = "[X] " if is_marked else " " truncated_name = truncate_text_middle(prefix + game_name, config.small_font, name_col_width, is_filename=False) name_surface = config.small_font.render(truncated_name, True, color) size_surface = config.small_font.render(size_text, True, THEME_COLORS["text"]) row_center_y = list_start_y + (i - config.scroll_offset) * line_height + line_height // 2 # Position nom (aligné à gauche dans la boite) name_rect = name_surface.get_rect() name_rect.midleft = (rect_x + 20, row_center_y) size_rect = size_surface.get_rect() size_rect.midright = (rect_x + rect_width - 20, row_center_y) if i == config.current_game: glow_width = rect_width - 40 glow_height = name_rect.height + 10 glow_surface = pygame.Surface((glow_width, glow_height), pygame.SRCALPHA) pygame.draw.rect(glow_surface, THEME_COLORS["fond_lignes"] + (50,), (0, 0, glow_width, glow_height), border_radius=8) screen.blit(glow_surface, (rect_x + 20, row_center_y - glow_height // 2)) screen.blit(name_surface, name_rect) screen.blit(size_surface, size_rect) if len(games) > items_per_page: try: draw_game_scrollbar( screen, config.scroll_offset, len(games), items_per_page, rect_x + rect_width - 10, rect_y, rect_height ) except NameError as e: logger.error(f"Erreur : draw_game_scrollbar non défini: {str(e)}") # Barre de défilement des jeux def draw_game_scrollbar(screen, scroll_offset, total_items, visible_items, x, y, height): """Affiche la barre de défilement pour la liste des jeux.""" if total_items <= visible_items: return game_area_height = height scrollbar_height = game_area_height * (visible_items / total_items) scrollbar_y = y + (game_area_height - scrollbar_height) * (scroll_offset / max(1, total_items - visible_items)) pygame.draw.rect(screen, THEME_COLORS["fond_lignes"], (x, scrollbar_y, 15, scrollbar_height), border_radius=4) def format_size(size): """Convertit une taille en octets en format lisible.""" if not isinstance(size, (int, float)) or size == 0: return "N/A" for unit in ['o', 'Ko', 'Mo', 'Go', 'To']: if size < 1024.0: return f"{size:.1f} {unit}" size /= 1024.0 return f"{size:.1f} Po" def draw_history_list(screen): # logger.debug(f"Dessin historique, history={config.history}, needs_redraw={config.needs_redraw}") history = config.history if hasattr(config, 'history') else load_history() history_count = len(history) # Cherche une entrée en cours de téléchargement pour afficher la vitesse speed_str = "" for entry in history: if entry.get("status") in ["Téléchargement", "downloading"]: speed = entry.get("speed", 0.0) if speed and speed > 0: speed_str = f" - {speed:.2f} Mo/s" break screen.blit(OVERLAY, (0, 0)) title_text = _("history_title").format(history_count) + speed_str 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(60, 30) 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) # fond opaque pygame.draw.rect(screen, THEME_COLORS["border"], title_rect_inflated, 2, border_radius=12) screen.blit(title_surface, title_rect) # Define column widths as percentages of available space (give more space to status/error messages) column_width_percentages = { "platform": 0.15, # narrower platform column "game_name": 0.45, # game name column "size": 0.10, # size column remains compact "status": 0.30 # wider status column for long error codes/messages } available_width = int(0.95 * config.screen_width - 60) # Total available width for columns col_platform_width = int(available_width * column_width_percentages["platform"]) col_game_width = int(available_width * column_width_percentages["game_name"]) col_size_width = int(available_width * column_width_percentages["size"]) col_status_width = int(available_width * column_width_percentages["status"]) rect_width = int(0.95 * config.screen_width) line_height = config.small_font.get_height() + 10 header_height = line_height margin_top_bottom = 20 extra_margin_top = 40 extra_margin_bottom = 80 title_height = config.title_font.get_height() + 20 # Sécuriser current_history_item pour éviter IndexError if history: if config.current_history_item < 0 or config.current_history_item >= len(history): config.current_history_item = max(0, min(len(history) - 1, config.current_history_item)) else: config.current_history_item = 0 speed = 0.0 if history and history[config.current_history_item].get("status") in ["Téléchargement", "downloading"]: speed = history[config.current_history_item].get("speed", 0.0) if speed > 0: speed_str = f"{speed:.2f} Mo/s" title_text = _("history_title").format(history_count) + f" {speed_str}" else: title_text = _("history_title").format(history_count) title_surface = config.title_font.render(title_text, True, THEME_COLORS["text"]) if not history: logger.debug("Aucun historique disponible") message = _("history_empty") lines = wrap_text(message, config.font, config.screen_width - 80) line_height = config.font.get_height() + 5 text_height = len(lines) * line_height rect_height = text_height + 2 * margin_top_bottom max_text_width = max([config.font.size(line)[0] for line in lines], default=300) rect_width = max_text_width + 80 rect_x = (config.screen_width - rect_width) // 2 rect_y = (config.screen_height - rect_height) // 2 screen.blit(OVERLAY, (0, 0)) pygame.draw.rect(screen, 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): text_surface = config.font.render(line, True, THEME_COLORS["text"]) text_rect = text_surface.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + i * line_height + line_height // 2)) screen.blit(text_surface, text_rect) return # Espace visible garanti entre le titre et la liste, et au-dessus du footer top_gap = 20 bottom_reserved = 70 # réserve pour le footer (barre des contrôles) + marge visuelle (réduit) # Positionner la liste juste après le titre, avec un espace dédié # Utiliser le rectangle du titre déjà dessiné pour une meilleure précision title_bottom = title_rect_inflated.bottom rect_y = title_bottom + top_gap # Calculer l'espace disponible en bas en réservant une zone pour le footer available_height = max(0, config.screen_height - rect_y - bottom_reserved) # Déterminer le nombre d'éléments par page en tenant compte de l'en-tête et des marges internes items_per_page = max(1, (available_height - header_height - 2 * margin_top_bottom) // line_height) rect_height = header_height + items_per_page * line_height + 2 * margin_top_bottom rect_x = (config.screen_width - rect_width) // 2 config.history_scroll_offset = max(0, min(config.history_scroll_offset, max(0, len(history) - items_per_page))) if config.current_history_item < config.history_scroll_offset: config.history_scroll_offset = config.current_history_item elif config.current_history_item >= config.history_scroll_offset + items_per_page: config.history_scroll_offset = config.current_history_item - items_per_page + 1 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) headers = [_("history_column_system"), _("history_column_game"), _("history_column_size"), _("history_column_status")] header_y = rect_y + margin_top_bottom + header_height // 2 header_x_positions = [ rect_x + 20 + col_platform_width // 2, rect_x + 20 + col_platform_width + col_game_width // 2, rect_x + 20 + col_platform_width + col_game_width + col_size_width // 2, rect_x + 20 + col_platform_width + col_game_width + col_size_width + col_status_width // 2 ] for header, x_pos in zip(headers, header_x_positions): text_surface = config.small_font.render(header, True, THEME_COLORS["text"]) text_rect = text_surface.get_rect(center=(x_pos, header_y)) screen.blit(text_surface, text_rect) separator_y = rect_y + margin_top_bottom + header_height pygame.draw.line(screen, THEME_COLORS["border"], (rect_x + 20, separator_y), (rect_x + rect_width - 20, separator_y), 2) for idx, i in enumerate(range(config.history_scroll_offset, min(config.history_scroll_offset + items_per_page, len(history)))): entry = history[i] platform = entry.get("platform", "Inconnu") game_name = entry.get("game_name", "Inconnu") # Correction du calcul de la taille size = entry.get("total_size", 0) color = THEME_COLORS["fond_lignes"] if i == config.current_history_item else THEME_COLORS["text"] size_text = format_size(size) status = entry.get("status", "Inconnu") progress = entry.get("progress", 0) progress = max(0, min(100, progress)) # Clamp progress between 0 and 100 # Precompute provider prefix once provider_prefix = entry.get("provider_prefix") or (entry.get("provider") + ":" if entry.get("provider") else "") # Compute status text (optimized version without redundant prefix for errors) if status in ["Téléchargement", "downloading"]: status_text = _("history_status_downloading").format(progress) # Coerce to string and prefix provider when relevant status_text = str(status_text or "") if provider_prefix and not status_text.startswith(provider_prefix): status_text = f"{provider_prefix} {status_text}" elif status == "Extracting": status_text = _("history_status_extracting").format(progress) status_text = str(status_text or "") if provider_prefix and not status_text.startswith(provider_prefix): status_text = f"{provider_prefix} {status_text}" elif status == "Download_OK": # Completed: no provider prefix (per requirement) status_text = _("history_status_completed") status_text = str(status_text or "") elif status == "Erreur": # Prefer friendly mapped message now stored in 'message' status_text = entry.get('message') if not status_text: # Some legacy entries might have only raw in result[1] or auxiliary field status_text = entry.get('raw_error_realdebrid') or entry.get('error') or 'Échec' # Coerce to string early for safe operations status_text = str(status_text or "") # Strip redundant prefixes if any for prefix in ["Erreur :", "Erreur:", "Error:", "Error :"]: if status_text.startswith(prefix): status_text = status_text[len(prefix):].strip() break if provider_prefix and not status_text.startswith(provider_prefix): status_text = f"{provider_prefix} {status_text}" elif status == "Canceled": status_text = _("history_status_canceled") status_text = str(status_text or "") else: status_text = str(status or "") # Determine color dedicated to status (independent from selection for better readability) if status == "Erreur": status_color = THEME_COLORS.get("error_text", (255, 0, 0)) elif status == "Canceled": status_color = THEME_COLORS.get("warning_text", (255, 100, 0)) elif status == "Download_OK": # Use green OK color status_color = THEME_COLORS.get("fond_lignes", (0, 255, 0)) else: status_color = THEME_COLORS.get("text", (255, 255, 255)) platform_text = truncate_text_end(platform, config.small_font, col_platform_width - 10) game_text = truncate_text_end(game_name, config.small_font, col_game_width - 10) size_text = truncate_text_end(size_text, config.small_font, col_size_width - 10) status_text = truncate_text_middle(str(status_text or ""), config.small_font, col_status_width - 10, is_filename=False) y_pos = rect_y + margin_top_bottom + header_height + idx * line_height + line_height // 2 platform_surface = config.small_font.render(platform_text, True, color) game_surface = config.small_font.render(game_text, True, color) size_surface = config.small_font.render(size_text, True, color) # Correction ici status_surface = config.small_font.render(status_text, True, status_color) platform_rect = platform_surface.get_rect(center=(header_x_positions[0], y_pos)) game_rect = game_surface.get_rect(center=(header_x_positions[1], y_pos)) size_rect = size_surface.get_rect(center=(header_x_positions[2], y_pos)) status_rect = status_surface.get_rect(center=(header_x_positions[3], y_pos)) if i == config.current_history_item: glow_surface = pygame.Surface((rect_width - 40, line_height), pygame.SRCALPHA) pygame.draw.rect(glow_surface, THEME_COLORS["fond_lignes"] + (50,), (0, 0, rect_width - 40, line_height), border_radius=8) screen.blit(glow_surface, (rect_x + 20, y_pos - line_height // 2)) screen.blit(platform_surface, platform_rect) screen.blit(game_surface, game_rect) screen.blit(size_surface, size_rect) screen.blit(status_surface, status_rect) if len(history) > items_per_page: try: draw_history_scrollbar( screen, config.history_scroll_offset, len(history), items_per_page, rect_x + rect_width - 10, rect_y, rect_height ) except NameError as e: logger.error(f"Erreur : draw_history_scrollbar non défini: {str(e)}") # Barre de défilement de l'historique def draw_history_scrollbar(screen, scroll_offset, total_items, visible_items, x, y, height): """Affiche la barre de défilement avec un style moderne.""" if total_items <= visible_items: return game_area_height = height scrollbar_height = game_area_height * (visible_items / total_items) - 10 scrollbar_y = y + (game_area_height - scrollbar_height) * (scroll_offset / max(1, total_items - visible_items)) + 10 pygame.draw.rect(screen, THEME_COLORS["fond_lignes"], (x, scrollbar_y, 5, scrollbar_height), border_radius=4) # Écran confirmation vider historique def draw_clear_history_dialog(screen): """Affiche la boîte de dialogue de confirmation pour vider l'historique.""" screen.blit(OVERLAY, (0, 0)) message = _("confirm_clear_history") wrapped_message = wrap_text(message, config.font, config.screen_width - 80) line_height = config.font.get_height() + 5 text_height = len(wrapped_message) * line_height button_height = int(config.screen_height * 0.0463) margin_top_bottom = 20 rect_height = text_height + button_height + 2 * margin_top_bottom max_text_width = max([config.font.size(line)[0] for line in wrapped_message], default=300) rect_width = max_text_width + 150 rect_x = (config.screen_width - rect_width) // 2 rect_y = (config.screen_height - rect_height) // 2 pygame.draw.rect(screen, THEME_COLORS["button_idle"], (rect_x, rect_y, rect_width, rect_height), border_radius=12) pygame.draw.rect(screen, THEME_COLORS["border"], (rect_x, rect_y, rect_width, rect_height), 2, border_radius=12) for i, line in enumerate(wrapped_message): text = config.font.render(line, True, THEME_COLORS["text"]) text_rect = text.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + i * line_height + line_height // 2)) screen.blit(text, text_rect) button_width = min(160, (rect_width - 60) // 2) draw_stylized_button(screen, _("button_yes"), rect_x + rect_width // 2 - button_width - 10, rect_y + text_height + margin_top_bottom, button_width, button_height, selected=config.confirm_clear_selection == 1) draw_stylized_button(screen, _("button_no"), rect_x + rect_width // 2 + 10, rect_y + text_height + margin_top_bottom, button_width, button_height, selected=config.confirm_clear_selection == 0) def draw_cancel_download_dialog(screen): """Affiche la boîte de dialogue de confirmation pour annuler un téléchargement.""" screen.blit(OVERLAY, (0, 0)) message = _("confirm_cancel_download") wrapped_message = wrap_text(message, config.font, config.screen_width - 80) line_height = config.font.get_height() + 5 text_height = len(wrapped_message) * line_height button_height = int(config.screen_height * 0.0463) margin_top_bottom = 20 rect_height = text_height + button_height + 2 * margin_top_bottom max_text_width = max([config.font.size(line)[0] for line in wrapped_message], default=300) rect_width = max_text_width + 150 rect_x = (config.screen_width - rect_width) // 2 rect_y = (config.screen_height - rect_height) // 2 pygame.draw.rect(screen, THEME_COLORS["button_idle"], (rect_x, rect_y, rect_width, rect_height), border_radius=12) pygame.draw.rect(screen, THEME_COLORS["border"], (rect_x, rect_y, rect_width, rect_height), 2, border_radius=12) for i, line in enumerate(wrapped_message): text = config.font.render(line, True, THEME_COLORS["text"]) text_rect = text.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + i * line_height + line_height // 2)) screen.blit(text, text_rect) button_width = min(160, (rect_width - 60) // 2) draw_stylized_button(screen, _("button_yes"), rect_x + rect_width // 2 - button_width - 10, rect_y + text_height + margin_top_bottom, button_width, button_height, selected=config.confirm_cancel_selection == 1) draw_stylized_button(screen, _("button_no"), rect_x + rect_width // 2 + 10, rect_y + text_height + margin_top_bottom, button_width, button_height, selected=config.confirm_cancel_selection == 0) # Affichage du clavier virtuel sur non-PC def draw_virtual_keyboard(screen): """Affiche un clavier virtuel avec un style moderne.""" keyboard_layout = [ ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], ['A', 'Z', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'], ['Q', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M'], ['W', 'X', 'C', 'V', 'B', 'N'] ] key_width = int(config.screen_width * 0.03125) key_height = int(config.screen_height * 0.0556) key_spacing = int(config.screen_width * 0.0052) keyboard_width = len(keyboard_layout[0]) * (key_width + key_spacing) - key_spacing keyboard_height = len(keyboard_layout) * (key_height + key_spacing) - key_spacing start_x = (config.screen_width - keyboard_width) // 2 search_bottom_y = int(config.screen_height * 0.111) + (config.search_font.get_height() + 40) // 2 controls_y = config.screen_height - int(config.screen_height * 0.037) available_height = controls_y - search_bottom_y start_y = search_bottom_y + (available_height - keyboard_height - 40) // 2 keyboard_rect = pygame.Rect(start_x - 20, start_y - 20, keyboard_width + 40, keyboard_height + 40) pygame.draw.rect(screen, THEME_COLORS["button_idle"], keyboard_rect, border_radius=12) pygame.draw.rect(screen, THEME_COLORS["border"], keyboard_rect, 2, border_radius=12) for row_idx, row in enumerate(keyboard_layout): for col_idx, key in enumerate(row): x = start_x + col_idx * (key_width + key_spacing) y = start_y + row_idx * (key_height + key_spacing) key_rect = pygame.Rect(x, y, key_width, key_height) if (row_idx, col_idx) == config.selected_key: pygame.draw.rect(screen, THEME_COLORS["fond_lignes"] + (150,), key_rect, border_radius=8) else: pygame.draw.rect(screen, THEME_COLORS["button_idle"], key_rect, border_radius=8) pygame.draw.rect(screen, THEME_COLORS["border"], key_rect, 1, border_radius=8) text = config.font.render(key, True, THEME_COLORS["text"]) text_rect = text.get_rect(center=key_rect.center) screen.blit(text, text_rect) # Écran de progression de téléchargement/extraction def draw_progress_screen(screen): """Affiche l'écran de progression des téléchargements avec un style moderne.""" if not config.download_tasks: logger.debug("Aucune tâche de téléchargement active") return task = list(config.download_tasks.keys())[0] game_name = config.download_tasks[task][2] url = config.download_tasks[task][1] progress = config.download_progress.get(url, {"downloaded_size": 0, "total_size": 0, "status": "Téléchargement", "progress_percent": 0}) status = progress.get("status", "Téléchargement") downloaded_size = progress["downloaded_size"] total_size = progress["total_size"] progress_percent = progress["progress_percent"] # S'assurer que le pourcentage est entre 0 et 100 progress_percent = max(0, min(100, progress_percent)) screen.blit(OVERLAY, (0, 0)) title_text = _("download_status").format(status, truncate_text_middle(game_name, config.font, config.screen_width - 200)) title_lines = wrap_text(title_text, config.font, config.screen_width - 80) line_height = config.font.get_height() + 5 text_height = len(title_lines) * line_height margin_top_bottom = 20 bar_height = int(config.screen_height * 0.0278) percent_height = config.progress_font.get_height() + 5 rect_height = text_height + bar_height + percent_height + 3 * margin_top_bottom max_text_width = max([config.font.size(line)[0] for line in title_lines], default=300) bar_width = max_text_width rect_width = max_text_width + 80 rect_x = (config.screen_width - rect_width) // 2 rect_y = (config.screen_height - rect_height) // 2 pygame.draw.rect(screen, THEME_COLORS["button_idle"], (rect_x, rect_y, rect_width, rect_height), border_radius=12) pygame.draw.rect(screen, THEME_COLORS["border"], (rect_x, rect_y, rect_width, rect_height), 2, border_radius=12) for i, line in enumerate(title_lines): title_render = config.font.render(line, True, THEME_COLORS["text"]) title_rect = title_render.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + i * line_height + line_height // 2)) screen.blit(title_render, title_rect) bar_y = rect_y + text_height + margin_top_bottom progress_width = 0 pygame.draw.rect(screen, THEME_COLORS["button_idle"], (rect_x + 20, bar_y, bar_width, bar_height), border_radius=8) if total_size > 0: # Limiter le pourcentage entre 0 et 100 pour l'affichage de la barre progress_width = int(bar_width * (min(100, max(0, progress_percent)) / 100)) # Écran avertissement extension non supportée téléchargement def draw_extension_warning(screen): """Affiche un avertissement pour une extension non reconnue ou un fichier ZIP.""" if not config.pending_download: logger.error("config.pending_download est None ou vide dans extension_warning") message = "Erreur : Aucun téléchargement en attente." is_zip = False game_name = "Inconnu" else: url, platform, game_name, is_zip_non_supported = config.pending_download # Log réduit: pas de détail verbeux ici is_zip = is_zip_non_supported if not game_name: game_name = "Inconnu" logger.warning("game_name vide, utilisation de 'Inconnu'") if is_zip: core = _("extension_warning_zip").format(game_name) hint = "" else: # Ajout d'un indice pour activer le téléchargement des extensions inconnues try: hint = _("extension_warning_enable_unknown_hint") except Exception: hint = "" core = _("extension_warning_unsupported").format(game_name) # Nettoyer et préparer les lignes max_width = config.screen_width - 80 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_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(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 screen.blit(OVERLAY, (0, 0)) 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) # 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_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) except Exception as e: logger.error(f"Erreur lors du rendu de extension_warning : {str(e)}") error_message = "Erreur d'affichage de l'avertissement." wrapped_error = wrap_text(error_message, config.font, config.screen_width - 80) line_height = config.font.get_height() + 5 rect_height = len(wrapped_error) * line_height + 2 * 20 max_text_width = max([config.font.size(line)[0] for line in wrapped_error], default=300) rect_width = max_text_width + 80 rect_x = (config.screen_width - rect_width) // 2 rect_y = (config.screen_height - rect_height) // 2 screen.blit(OVERLAY, (0, 0)) pygame.draw.rect(screen, THEME_COLORS["button_idle"], (rect_x, rect_y, rect_width, rect_height), border_radius=12) pygame.draw.rect(screen, THEME_COLORS["border"], (rect_x, rect_y, rect_width, rect_height), 2, border_radius=12) for i, line in enumerate(wrapped_error): error_surface = config.font.render(line, True, THEME_COLORS["error_text"]) error_rect = error_surface.get_rect(center=(config.screen_width // 2, rect_y + 20 + i * line_height + line_height // 2)) screen.blit(error_surface, error_rect) # Affichage des contrôles en bas de page def draw_controls(screen, menu_state, current_music_name=None, music_popup_start_time=0): """Affiche les contrôles sur une seule ligne en bas de l’écran.""" start_button = get_control_display('start', 'START') start_text = _("controls_action_start") control_text = f"RGSX v{config.app_version} - {start_button} : {start_text}" # Afficher le nom du joystick s'il est détecté try: device_name = getattr(config, 'controller_device_name', '') or '' if device_name: # Utilise la clé i18n si disponible, sinon fallback try: joy_label = _("footer_joystick") except Exception: joy_label = "Joystick: {0}" # Formater si le placeholder {0} est présent if isinstance(joy_label, str) and "{0}" in joy_label: joy_text = joy_label.format(device_name) else: joy_text = f"{joy_label} {device_name}" if joy_label else f"Joystick: {device_name}" control_text += f" | {joy_text}" except Exception: pass # Ajouter le nom de la musique si disponible if config.current_music_name and config.music_popup_start_time > 0: current_time = pygame.time.get_ticks() / 1000 if current_time - config.music_popup_start_time < 3.0: # Afficher pendant 3 secondes control_text += f" | {config.current_music_name}" max_width = config.screen_width - 40 wrapped_controls = wrap_text(control_text, config.small_font, max_width) line_height = config.small_font.get_height() + 5 rect_height = len(wrapped_controls) * line_height + 20 rect_y = config.screen_height - rect_height - 5 rect_x = (config.screen_width - max_width) // 2 pygame.draw.rect(screen, THEME_COLORS["button_idle"], (rect_x, rect_y, max_width, rect_height), border_radius=8) pygame.draw.rect(screen, THEME_COLORS["border"], (rect_x, rect_y, max_width, rect_height), 1, border_radius=8) for i, line in enumerate(wrapped_controls): text_surface = config.small_font.render(line, True, THEME_COLORS["text"]) text_rect = text_surface.get_rect(center=(config.screen_width // 2, rect_y + 10 + i * line_height + line_height // 2)) screen.blit(text_surface, text_rect) # Menu pause def draw_language_menu(screen): """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)) # Obtenir les langues disponibles available_languages = get_available_languages() if not available_languages: logger.error("Aucune langue disponible") return # 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"]) # 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) # Démarrer la liste juste sous le titre avec le même écart que les boutons start_y = title_bg_rect.bottom + button_spacing 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)) screen.blit(text_surface, text_rect) # Instructions (placer juste au-dessus du footer sans chevauchement) instruction_text = _("language_select_instruction") instruction_surface = config.small_font.render(instruction_text, True, THEME_COLORS["text"]) footer_reserved = 72 # hauteur approximative footer (barre bas) + marge bottom_margin = 12 instruction_y = config.screen_height - footer_reserved - bottom_margin # Empêcher un chevauchement avec les derniers boutons si espace réduit last_button_bottom = start_y + (len(available_languages) - 1) * (button_height + button_spacing) + button_height min_gap = 16 if instruction_y - last_button_bottom < min_gap: instruction_y = last_button_bottom + min_gap instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, instruction_y)) screen.blit(instruction_surface, instruction_rect) def draw_menu_instruction(screen, instruction_text, last_button_bottom=None): """Dessine une ligne d'instruction centrée au-dessus du footer. - Réserve une zone footer (72px) + marge bas. - Si last_button_bottom est fourni, s'assure d'un écart minimal (16px). - Utilise la petite police et couleurs du thème. """ if not instruction_text: return try: instruction_surface = config.small_font.render(instruction_text, True, THEME_COLORS["text"]) footer_reserved = 72 bottom_margin = 12 instruction_y = config.screen_height - footer_reserved - bottom_margin min_gap = 16 if last_button_bottom is not None and instruction_y - last_button_bottom < min_gap: instruction_y = last_button_bottom + min_gap instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, instruction_y)) screen.blit(instruction_surface, instruction_rect) except Exception as e: logger.error(f"Erreur draw_menu_instruction: {e}") def draw_display_menu(screen): """Affiche le sous-menu Affichage (layout, taille de police, systèmes non supportés).""" screen.blit(OVERLAY, (0, 0)) # États actuels layout_str = f"{getattr(config, 'GRID_COLS', 3)}x{getattr(config, 'GRID_ROWS', 4)}" font_scale = config.accessibility_settings.get("font_scale", 1.0) from rgsx_settings import get_show_unsupported_platforms, get_allow_unknown_extensions 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 if hidden_count > 0: unsupported_label = _("menu_show_unsupported_and_hidden").format(hidden_count) else: unsupported_label = _("menu_show_unsupported_all_displayed") # Libellés options = [ f"{_('display_layout')}: {layout_str}", _("accessibility_font_size").format(f"{font_scale:.1f}"), unsupported_label, _("menu_allow_unknown_ext_on") if allow_unknown else _("menu_allow_unknown_ext_off"), _("menu_filter_platforms"), ] selected = getattr(config, 'display_menu_selection', 0) # Dimensions du cadre (cohérent avec le menu pause) title_text = _("menu_display") title_surface = config.title_font.render(title_text, True, THEME_COLORS["text"]) title_height = title_surface.get_height() + 10 menu_width = int(config.screen_width * 0.7) button_height = int(config.screen_height * 0.0463) margin_top_bottom = 20 vertical_spacing = 10 menu_height = title_height + len(options) * (button_height + vertical_spacing) + 2 * margin_top_bottom menu_x = (config.screen_width - menu_width) // 2 menu_y = (config.screen_height - menu_height) // 2 # Cadre pygame.draw.rect(screen, THEME_COLORS["button_idle"], (menu_x, menu_y, menu_width, menu_height), border_radius=12) pygame.draw.rect(screen, THEME_COLORS["border"], (menu_x, menu_y, menu_width, menu_height), 2, border_radius=12) # Titre centré dans le cadre title_rect = title_surface.get_rect(center=(config.screen_width // 2, menu_y + margin_top_bottom + title_surface.get_height() // 2)) screen.blit(title_surface, title_rect) # Boutons des options for i, option_text in enumerate(options): y = menu_y + margin_top_bottom + title_height + i * (button_height + vertical_spacing) draw_stylized_button( screen, option_text, menu_x + 20, y, menu_width - 40, button_height, selected=(i == selected) ) # Aide en bas de l'écran instruction_text = _("language_select_instruction") instruction_surface = config.small_font.render(instruction_text, True, THEME_COLORS["text"]) instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, config.screen_height - 50)) screen.blit(instruction_surface, instruction_rect) def draw_pause_menu(screen, selected_option): """Dessine le menu pause racine (catégories).""" screen.blit(OVERLAY, (0, 0)) # Nouvel ordre: Language / Controls / Display / Games / Settings / Restart / Quit options = [ _("menu_language") if _ else "Language", # 0 -> sélecteur de langue direct _("menu_controls"), # 1 -> sous-menu controls _("menu_display"), # 2 -> sous-menu display _("menu_games") if _ else "Games", # 3 -> sous-menu games (history + sources + update) _("menu_settings_category") if _ else "Settings", # 4 -> sous-menu settings _("menu_restart"), # 5 -> reboot _("menu_quit") # 6 -> quit ] menu_width = int(config.screen_width * 0.6) button_height = int(config.screen_height * 0.048) margin_top_bottom = 24 menu_height = len(options) * (button_height + 12) + 2 * margin_top_bottom menu_x = (config.screen_width - menu_width) // 2 menu_y = (config.screen_height - menu_height) // 2 pygame.draw.rect(screen, THEME_COLORS["button_idle"], (menu_x, menu_y, menu_width, menu_height), border_radius=12) pygame.draw.rect(screen, THEME_COLORS["border"], (menu_x, menu_y, menu_width, menu_height), 2, border_radius=12) for i, option in enumerate(options): draw_stylized_button( screen, option, menu_x + 20, menu_y + margin_top_bottom + i * (button_height + 12), menu_width - 40, button_height, selected=i == selected_option ) config.pause_menu_total_options = len(options) # Instruction contextuelle pour l'option sélectionnée # Mapping des clés i18n parallèles à la liste options (même ordre) instruction_keys = [ "instruction_pause_language", "instruction_pause_controls", "instruction_pause_display", "instruction_pause_games", "instruction_pause_settings", "instruction_pause_restart", "instruction_pause_quit", ] try: key = instruction_keys[selected_option] instruction_text = _(key) except Exception: instruction_text = "" # Sécurité si index hors borne if instruction_text: # Calcul de la position du dernier bouton pour éviter chevauchement last_button_bottom = menu_y + margin_top_bottom + (len(options) - 1) * (button_height + 12) + button_height draw_menu_instruction(screen, instruction_text, last_button_bottom) def _draw_submenu_generic(screen, title, options, selected_index): """Helper générique pour dessiner un sous-menu hiérarchique.""" screen.blit(OVERLAY, (0, 0)) menu_width = int(config.screen_width * 0.72) button_height = int(config.screen_height * 0.045) margin_top_bottom = 26 menu_height = (len(options)+1) * (button_height + 10) + 2 * margin_top_bottom # +1 pour le titre menu_x = (config.screen_width - menu_width) // 2 menu_y = (config.screen_height - menu_height) // 2 pygame.draw.rect(screen, THEME_COLORS["button_idle"], (menu_x, menu_y, menu_width, menu_height), border_radius=14) pygame.draw.rect(screen, THEME_COLORS["border"], (menu_x, menu_y, menu_width, menu_height), 2, border_radius=14) # Title title_surface = config.font.render(title, True, THEME_COLORS["text"]) title_rect = title_surface.get_rect(center=(config.screen_width//2, menu_y + margin_top_bottom//2 + title_surface.get_height()//2)) screen.blit(title_surface, title_rect) # Options start_y = title_rect.bottom + 10 for i, opt in enumerate(options): draw_stylized_button( screen, opt, menu_x + 20, start_y + i * (button_height + 10), menu_width - 40, button_height, selected=(i == selected_index) ) def draw_pause_controls_menu(screen, selected_index): options = [ _("menu_controls"), # aide contrôles (réutilisée) _("menu_remap_controls"), # remap _("menu_back") if _ else "Back" ] _draw_submenu_generic(screen, _("menu_controls") if _ else "Controls", options, selected_index) # Instructions contextuelles instruction_keys = [ "instruction_controls_help", # pour menu_controls (afficher l'aide) "instruction_controls_remap", # remap "instruction_generic_back", # retour ] key = instruction_keys[selected_index] if 0 <= selected_index < len(instruction_keys) else None if key: last_button_bottom = None # recalculer via géométrie si nécessaire; ici on réutilise calcul simple # Reconstituer la position du dernier bouton comme dans _draw_submenu_generic menu_width = int(config.screen_width * 0.72) button_height = int(config.screen_height * 0.045) margin_top_bottom = 26 menu_height = (len(options)+1) * (button_height + 10) + 2 * margin_top_bottom menu_y = (config.screen_height - menu_height) // 2 # Title height approximatif title_surface = config.font.render("X", True, THEME_COLORS["text"]) # hauteur représentative title_rect_height = title_surface.get_height() start_y = menu_y + margin_top_bottom//2 + title_rect_height + 10 + 10 # approx: title center adjust + bottom spacing last_button_bottom = start_y + (len(options)-1) * (button_height + 10) + button_height text = _(key) if key == "instruction_display_hide_premium": # Inject dynamic list of premium providers from config.PREMIUM_HOST_MARKERS try: from config import PREMIUM_HOST_MARKERS # Clean, preserve order, remove duplicates (case-insensitive) seen = set() providers_clean = [] for p in PREMIUM_HOST_MARKERS: if not p: continue norm = p.strip() if not norm: continue low = norm.lower() if low in seen: continue seen.add(low) providers_clean.append(norm) providers_str = ", ".join(providers_clean) if not providers_str: providers_str = "-" if "{providers}" in text: try: text = text.format(providers=providers_str) except Exception: # Fallback if formatting fails text = f"{text.replace('{providers}','').strip()} {providers_str}".strip() else: # Append providers if placeholder missing (backward compatibility) text = f"{text} : {providers_str}" if providers_str else text except Exception: pass draw_menu_instruction(screen, text, last_button_bottom) def draw_pause_display_menu(screen, selected_index): from rgsx_settings import ( get_show_unsupported_platforms, get_allow_unknown_extensions, get_hide_premium_systems, get_font_family ) # Layout label layouts = [(3,3),(3,4),(4,3),(4,4)] try: idx = layouts.index((config.GRID_COLS, config.GRID_ROWS)) except ValueError: idx = 0 layout_value = f"{layouts[idx][0]}x{layouts[idx][1]}" layout_txt = f"{_('submenu_display_layout') if _ else 'Layout'}: < {layout_value} >" # Font size opts = getattr(config, 'font_scale_options', [0.75, 1.0, 1.25, 1.5, 1.75]) cur_idx = getattr(config, 'current_font_scale_index', 1) font_value = f"{opts[cur_idx]}x" font_txt = f"{_('submenu_display_font_size') if _ else 'Font Size'}: < {font_value} >" # Font family current_family = get_font_family() # Nom user-friendly family_map = { "pixel": "Pixel", "dejavu": "DejaVu Sans" } fam_label = family_map.get(current_family, current_family) font_family_txt = f"{_('submenu_display_font_family') if _ else 'Font'}: < {fam_label} >" unsupported = get_show_unsupported_platforms() status_unsupported = _('status_on') if unsupported else _('status_off') # Construire label sans statut pour insérer les chevrons proprement raw_unsupported_label = _('submenu_display_show_unsupported') if _ else 'Show unsupported systems: {status}' # Retirer éventuel placeholder et ponctuation finale if '{status}' in raw_unsupported_label: raw_unsupported_label = raw_unsupported_label.split('{status}')[0].rstrip(' :') unsupported_txt = f"{raw_unsupported_label}: < {status_unsupported} >" allow_unknown = get_allow_unknown_extensions() status_unknown = _('status_on') if allow_unknown else _('status_off') raw_unknown_label = _('submenu_display_allow_unknown_ext') if _ else 'Hide unknown ext warn: {status}' if '{status}' in raw_unknown_label: raw_unknown_label = raw_unknown_label.split('{status}')[0].rstrip(' :') unknown_txt = f"{raw_unknown_label}: < {status_unknown} >" # Hide premium systems hide_premium = get_hide_premium_systems() status_hide_premium = _('status_on') if hide_premium else _('status_off') hide_premium_label = _('menu_hide_premium_systems') if _ else 'Hide Premium systems' hide_premium_txt = f"{hide_premium_label}: < {status_hide_premium} >" filter_txt = _("submenu_display_filter_platforms") if _ else "Filter Platforms" back_txt = _("menu_back") if _ else "Back" options = [layout_txt, font_txt, font_family_txt, unsupported_txt, unknown_txt, hide_premium_txt, filter_txt, back_txt] _draw_submenu_generic(screen, _("menu_display"), options, selected_index) instruction_keys = [ "instruction_display_layout", "instruction_display_font_size", "instruction_display_font_family", "instruction_display_show_unsupported", "instruction_display_unknown_ext", "instruction_display_hide_premium", "instruction_display_filter_platforms", "instruction_generic_back", ] key = instruction_keys[selected_index] if 0 <= selected_index < len(instruction_keys) else None if key: button_height = int(config.screen_height * 0.045) menu_width = int(config.screen_width * 0.72) margin_top_bottom = 26 menu_height = (len(options)+1) * (button_height + 10) + 2 * margin_top_bottom menu_y = (config.screen_height - menu_height) // 2 title_surface = config.font.render("X", True, THEME_COLORS["text"]) title_rect_height = title_surface.get_height() start_y = menu_y + margin_top_bottom//2 + title_rect_height + 10 + 10 last_button_bottom = start_y + (len(options)-1) * (button_height + 10) + button_height draw_menu_instruction(screen, _(key), last_button_bottom) def draw_pause_games_menu(screen, selected_index): from rgsx_settings import get_sources_mode mode = get_sources_mode() source_label = _("games_source_rgsx") if mode == "rgsx" else _("games_source_custom") source_txt = f"{_('menu_games_source_prefix')}: < {source_label} >" update_txt = _("menu_redownload_cache") history_txt = _("menu_history") if _ else "History" back_txt = _("menu_back") if _ else "Back" options = [history_txt, source_txt, update_txt, back_txt] _draw_submenu_generic(screen, _("menu_games") if _ else "Games", options, selected_index) instruction_keys = [ "instruction_games_history", "instruction_games_source_mode", "instruction_games_update_cache", "instruction_generic_back", ] key = instruction_keys[selected_index] if 0 <= selected_index < len(instruction_keys) else None if key: button_height = int(config.screen_height * 0.045) margin_top_bottom = 26 menu_height = (len(options)+1) * (button_height + 10) + 2 * margin_top_bottom menu_y = (config.screen_height - menu_height) // 2 title_surface = config.font.render("X", True, THEME_COLORS["text"]) title_rect_height = title_surface.get_height() start_y = menu_y + margin_top_bottom//2 + title_rect_height + 10 + 10 last_button_bottom = start_y + (len(options)-1) * (button_height + 10) + button_height draw_menu_instruction(screen, _(key), last_button_bottom) def draw_pause_settings_menu(screen, selected_index): from rgsx_settings import get_symlink_option # Music if config.music_enabled: music_name = config.current_music_name or "" music_option = _("menu_music_enabled").format(music_name) else: music_option = _("menu_music_disabled") # Uniformiser en < value > pour les réglages basculables if ' : ' in music_option: base, val = music_option.split(' : ',1) music_option = f"{base} : < {val.strip()} >" symlink_option = _("symlink_option_enabled") if get_symlink_option() else _("symlink_option_disabled") if ' ' in symlink_option: parts = symlink_option.split(' ',1) # On garde phrase intacte si elle n'a pas de forme label: valeur ; sinon transformer if ' : ' in symlink_option: base, val = symlink_option.split(' : ',1) symlink_option = f"{base} : < {val.strip()} >" api_keys_txt = _("menu_api_keys_status") if _ else "API Keys" back_txt = _("menu_back") if _ else "Back" options = [music_option, symlink_option, api_keys_txt, back_txt] _draw_submenu_generic(screen, _("menu_settings_category") if _ else "Settings", options, selected_index) instruction_keys = [ "instruction_settings_music", "instruction_settings_symlink", "instruction_settings_api_keys", "instruction_generic_back", ] key = instruction_keys[selected_index] if 0 <= selected_index < len(instruction_keys) else None if key: button_height = int(config.screen_height * 0.045) margin_top_bottom = 26 menu_height = (len(options)+1) * (button_height + 10) + 2 * margin_top_bottom menu_y = (config.screen_height - menu_height) // 2 title_surface = config.font.render("X", True, THEME_COLORS["text"]) title_rect_height = title_surface.get_height() start_y = menu_y + margin_top_bottom//2 + title_rect_height + 10 + 10 last_button_bottom = start_y + (len(options)-1) * (button_height + 10) + button_height draw_menu_instruction(screen, _(key), last_button_bottom) def draw_pause_api_keys_status(screen): screen.blit(OVERLAY, (0,0)) from utils import load_api_keys keys = load_api_keys() title = _("api_keys_status_title") if _ else "API Keys Status" # Préparer données avec masquage partiel des clés (afficher 4 premiers et 2 derniers caractères si longueur > 10) def mask_key(value: str|None): if not value: return "" # rien si absent v = value.strip() if len(v) <= 10: return v # courte, afficher entière return f"{v[:4]}…{v[-2:]}" # masque au milieu providers = [ ("1fichier", keys.get('1fichier')), ("AllDebrid", keys.get('alldebrid')), ("RealDebrid", keys.get('realdebrid')) ] # Dimensions dynamiques en fonction du contenu row_height = config.small_font.get_height() + 14 header_height = 60 inner_rows = len(providers) menu_width = int(config.screen_width * 0.60) menu_height = header_height + inner_rows * row_height + 80 menu_x = (config.screen_width - menu_width)//2 menu_y = (config.screen_height - menu_height)//2 pygame.draw.rect(screen, THEME_COLORS["button_idle"], (menu_x, menu_y, menu_width, menu_height), border_radius=22) pygame.draw.rect(screen, THEME_COLORS["border"], (menu_x, menu_y, menu_width, menu_height), 2, border_radius=22) # Titre title_surface = config.font.render(title, True, THEME_COLORS["text"]) title_rect = title_surface.get_rect(center=(config.screen_width//2, menu_y + 36)) screen.blit(title_surface, title_rect) status_present_txt = _("status_present") if _ else "Present" status_missing_txt = _("status_missing") if _ else "Missing" # Plus de légende textuelle Présent / Missing (demandé) – seules les pastilles couleur serviront. legend_rect = pygame.Rect(0,0,0,0) # Colonnes: Provider | Status badge | (key masked) col_provider_x = menu_x + 40 col_status_x = menu_x + int(menu_width * 0.40) col_key_x = menu_x + int(menu_width * 0.58) # Démarrage des lignes sous le titre avec un padding y = title_rect.bottom + 24 badge_font = config.tiny_font if hasattr(config, 'tiny_font') else config.small_font for provider, value in providers: present = bool(value) # Provider name prov_surf = config.small_font.render(provider, True, THEME_COLORS["text"]) screen.blit(prov_surf, (col_provider_x, y)) # Pastille circulaire simple (couleur = statut) circle_color = (60, 170, 60) if present else (180, 55, 55) circle_bg = (30, 70, 30) if present else (70, 25, 25) radius = 14 center_x = col_status_x + radius center_y = y + badge_font.get_height()//2 pygame.draw.circle(screen, circle_bg, (center_x, center_y), radius) pygame.draw.circle(screen, circle_color, (center_x, center_y), radius, 2) # Masked key (dim color) or hint if present: masked = mask_key(value) key_color = THEME_COLORS.get("text_dim", (180,180,180)) key_label = masked else: key_color = THEME_COLORS.get("text_dim", (150,150,150)) # Afficher nom de fichier + 'empty' filename_display = { '1fichier': '1FichierAPI.txt', 'AllDebrid': 'AllDebridAPI.txt', 'RealDebrid': 'RealDebridAPI.txt' }.get(provider, 'key.txt') empty_suffix = _("api_key_empty_suffix") if _ and _("api_key_empty_suffix") != "api_key_empty_suffix" else "empty" key_label = f"{filename_display} {empty_suffix}" key_surf = config.tiny_font.render(key_label, True, key_color) if hasattr(config, 'tiny_font') else config.small_font.render(key_label, True, key_color) screen.blit(key_surf, (col_key_x, y)) # Ligne séparatrice (optionnelle) sep_y = y + row_height - 8 if provider != providers[-1][0]: pygame.draw.line(screen, THEME_COLORS["border"], (menu_x + 25, sep_y), (menu_x + menu_width - 25, sep_y), 1) y += row_height # Indication basique: utiliser config.SAVE_FOLDER (chemin dynamique) save_folder_path = getattr(config, 'SAVE_FOLDER', '/saves/ports/rgsx') # Utiliser placeholder {path} si traduction fournie if _ and _("api_keys_hint_manage") != "api_keys_hint_manage": try: hint_txt = _("api_keys_hint_manage").format(path=save_folder_path) except Exception: hint_txt = f"Put your keys in {save_folder_path}" else: hint_txt = f"Put your keys in {save_folder_path}" hint_font = config.tiny_font if hasattr(config, 'tiny_font') else config.small_font hint_surf = hint_font.render(hint_txt, True, THEME_COLORS.get("text_dim", THEME_COLORS["text"])) # Positionné un peu plus haut pour aérer hint_rect = hint_surf.get_rect(center=(config.screen_width//2, menu_y + menu_height - 30)) screen.blit(hint_surf, hint_rect) def draw_filter_platforms_menu(screen): """Affiche le menu de filtrage des plateformes (afficher/masquer).""" from rgsx_settings import load_rgsx_settings screen.blit(OVERLAY, (0, 0)) settings = load_rgsx_settings() hidden = set(settings.get("hidden_platforms", [])) if isinstance(settings, dict) else set() # Initialiser la copie de travail si vide ou taille différente if not config.filter_platforms_selection or len(config.filter_platforms_selection) != len(config.platform_dicts): # Liste alphabétique complète (sans filtrer hidden existant) all_names = sorted([d.get("platform_name", "") for d in config.platform_dicts if d.get("platform_name")]) config.filter_platforms_selection = [(name, name in hidden) for name in all_names] config.selected_filter_index = 0 config.filter_platforms_scroll_offset = 0 config.filter_platforms_dirty = False 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 + 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) screen.blit(title_surface, title_rect) # Zone liste list_width = int(config.screen_width * 0.7) list_height = int(config.screen_height * 0.6) list_x = (config.screen_width - list_width) // 2 list_y = title_rect_inflated.bottom + 20 pygame.draw.rect(screen, THEME_COLORS["button_idle"], (list_x, list_y, list_width, list_height), border_radius=12) pygame.draw.rect(screen, THEME_COLORS["border"], (list_x, list_y, list_width, list_height), 2, border_radius=12) line_height = config.small_font.get_height() + 8 visible_items = list_height // line_height - 1 # laisser un peu d'espace bas total_items = len(config.filter_platforms_selection) if config.selected_filter_index < 0: config.selected_filter_index = 0 # Ne pas forcer la réduction si on est sur les boutons (indices >= total_items) # Laisser controls.py gérer la borne max étendue # Ajuster scroll if config.selected_filter_index < config.filter_platforms_scroll_offset: config.filter_platforms_scroll_offset = config.selected_filter_index elif config.selected_filter_index >= config.filter_platforms_scroll_offset + visible_items: config.filter_platforms_scroll_offset = config.selected_filter_index - visible_items + 1 # Dessiner items for i in range(config.filter_platforms_scroll_offset, min(config.filter_platforms_scroll_offset + visible_items, total_items)): name, is_hidden = config.filter_platforms_selection[i] idx_on_screen = i - config.filter_platforms_scroll_offset y_center = list_y + 10 + idx_on_screen * line_height + line_height // 2 selected = (i == config.selected_filter_index) checkbox = "[ ]" if is_hidden else "[X]" # inversé: coché signifie visible # Correction: on veut [X] si visible => is_hidden False checkbox = "[X]" if not is_hidden else "[ ]" display_text = f"{checkbox} {name}" color = THEME_COLORS["fond_lignes"] if selected else THEME_COLORS["text"] text_surface = config.small_font.render(display_text, True, color) text_rect = text_surface.get_rect(midleft=(list_x + 20, y_center)) if selected: glow_surface = pygame.Surface((list_width - 40, line_height), pygame.SRCALPHA) pygame.draw.rect(glow_surface, THEME_COLORS["fond_lignes"] + (50,), (0, 0, list_width - 40, line_height), border_radius=8) screen.blit(glow_surface, (list_x + 20, y_center - line_height // 2)) screen.blit(text_surface, text_rect) # Scrollbar if total_items > visible_items: scroll_height = int((visible_items / total_items) * (list_height - 20)) scroll_y = int((config.filter_platforms_scroll_offset / max(1, total_items - visible_items)) * (list_height - 20 - scroll_height)) pygame.draw.rect(screen, THEME_COLORS["fond_lignes"], (list_x + list_width - 25, list_y + 10 + scroll_y, 10, scroll_height), border_radius=4) # Boutons d'action btn_width = 220 btn_height = int(config.screen_height * 0.0463) spacing = 30 buttons_y = list_y + list_height + 20 center_x = config.screen_width // 2 actions = [ ("filter_all", -2), ("filter_none", -3), ("filter_apply", -4), ("filter_back", -5) ] # Indice spécial sélection boutons quand selected_filter_index >= total_items extra_index_base = total_items # Ajuster selected_filter_index max pour inclure boutons extended_max = total_items + len(actions) - 1 if config.selected_filter_index > extended_max: config.selected_filter_index = extended_max for idx, (key, offset) in enumerate(actions): btn_x = center_x - (len(actions) * (btn_width + spacing) - spacing) // 2 + idx * (btn_width + spacing) is_selected = (config.selected_filter_index == total_items + idx) label = _(key) draw_stylized_button(screen, label, btn_x, buttons_y, btn_width, btn_height, selected=is_selected) # Infos bas hidden_count = sum(1 for _, h in config.filter_platforms_selection if h) visible_count = total_items - hidden_count info_text = _("filter_platforms_info").format(visible_count, hidden_count, total_items) info_surface = config.small_font.render(info_text, True, THEME_COLORS["text"]) info_rect = info_surface.get_rect(center=(config.screen_width // 2, buttons_y + btn_height + 30)) screen.blit(info_surface, info_rect) if config.filter_platforms_dirty: dirty_text = _("filter_unsaved_warning") dirty_surface = config.small_font.render(dirty_text, True, THEME_COLORS["warning_text"]) dirty_rect = dirty_surface.get_rect(center=(config.screen_width // 2, info_rect.bottom + 25)) screen.blit(dirty_surface, dirty_rect) # Menu aide contrôles def draw_controls_help(screen, previous_state): """Affiche la liste des contrôles (aide) avec mise en page adaptative.""" # Contenu des catégories (avec icônes si disponibles) control_categories = { _("controls_category_navigation"): [ ("icons", ["up", "down", "left", "right"], f"{get_control_display('up', '↑')} {get_control_display('down', '↓')} {get_control_display('left', '←')} {get_control_display('right', '→')} : {_('controls_navigation')}"), ("icons", ["page_up", "page_down"], f"{get_control_display('page_up', 'LB')} {get_control_display('page_down', 'RB')} : {_('controls_pages')}"), ], _("controls_category_main_actions"): [ ("icons", ["confirm"], f"{get_control_display('confirm', 'A')} : {_('controls_confirm_select')}"), ("icons", ["cancel"], f"{get_control_display('cancel', 'B')} : {_('controls_cancel_back')}"), ("icons", ["start"], f"{get_control_display('start', 'Start')} : {_('controls_action_start')}"), ], _("controls_category_downloads"): [ ("icons", ["history"], f"{get_control_display('history', 'Y')} : {_('controls_action_history')}"), ("icons", ["clear_history"], f"{get_control_display('clear_history', 'X')} : {_('controls_action_clear_history')}"), ], _("controls_category_search"): [ ("icons", ["filter"], f"{get_control_display('filter', 'Select')} : {_('controls_filter_search')}"), ("icons", ["delete"], f"{get_control_display('delete', 'Suppr')} : {_('controls_action_delete')}"), ("icons", ["space"], f"{get_control_display('space', 'Espace')} : {_('controls_action_space')}"), ], } # États autorisés (même logique qu'avant) allowed_states = { # États classiques où l'aide était accessible "error", "platform", "game", "confirm_exit", "extension_warning", "history", "clear_history", # Nouveaux états hiérarchiques pause "pause_controls_menu", "pause_menu" } if previous_state not in allowed_states: return screen.blit(OVERLAY, (0, 0)) # Paramètres d'affichage font = config.small_font title_font = config.title_font section_font = config.font line_spacing = max(4, font.get_height() // 6) section_spacing = font.get_height() // 2 title_spacing = font.get_height() padding = 24 inter_col_spacing = 48 max_panel_width = int(config.screen_width * 0.9) max_panel_height = int(config.screen_height * 0.9) # Découpage en 2 colonnes (équilibré) categories_list = list(control_categories.items()) mid = len(categories_list) // 2 col1_categories = categories_list[:mid] col2_categories = categories_list[mid:] # Largeur cible par colonne (avant wrapping) target_col_width = (max_panel_width - 2 * padding - inter_col_spacing) // 2 def wrap_lines_for_column(cat_pairs): wrapped = [] # liste de (is_section_title, surface) max_width = 0 total_height = 0 for section_title, lines in cat_pairs: # Titre section sec_surf = section_font.render(section_title, True, THEME_COLORS["fond_lignes"]) wrapped.append((True, sec_surf)) total_height += sec_surf.get_height() + line_spacing for raw_line in lines: # Deux formats possibles: # - tuple ("icons", [actions], text) # - chaîne texte simple line_surface = None if isinstance(raw_line, tuple) and len(raw_line) >= 3 and raw_line[0] == "icons": _, actions, text = raw_line try: line_surface = _render_icons_line(actions, text, target_col_width, font, THEME_COLORS["text"]) except Exception: line_surface = None if line_surface is None: # Fallback: traitement texte comme avant words = str(raw_line).split() cur = "" for word in words: test = (cur + " " + word).strip() if font.size(test)[0] <= target_col_width: cur = test else: if cur: line_surf = font.render(cur, True, THEME_COLORS["text"]) wrapped.append((False, line_surf)) total_height += line_surf.get_height() + line_spacing max_width = max(max_width, line_surf.get_width()) cur = word if cur: line_surf = font.render(cur, True, THEME_COLORS["text"]) wrapped.append((False, line_surf)) total_height += line_surf.get_height() + line_spacing max_width = max(max_width, line_surf.get_width()) else: wrapped.append((False, line_surface)) total_height += line_surface.get_height() + line_spacing max_width = max(max_width, line_surface.get_width()) total_height += section_spacing # espace après section max_width = max(max_width, sec_surf.get_width()) if wrapped and not wrapped[-1][0]: total_height -= section_spacing # retirer excédent final return wrapped, max_width, total_height col1_wrapped, col1_w, col1_h = wrap_lines_for_column(col1_categories) col2_wrapped, col2_w, col2_h = wrap_lines_for_column(col2_categories) col_widths_sum = col1_w + col2_w + inter_col_spacing content_width = min(max_panel_width - 2 * padding, max(col_widths_sum, col1_w + col2_w + inter_col_spacing)) panel_width = content_width + 2 * padding title_surf = title_font.render(_("controls_help_title"), True, THEME_COLORS["text"]) title_height = title_surf.get_height() content_height = max(col1_h, col2_h) panel_height = title_height + title_spacing + content_height + 2 * padding if panel_height > max_panel_height: panel_height = max_panel_height enable_clip = True else: enable_clip = False panel_x = (config.screen_width - panel_width) // 2 panel_y = (config.screen_height - panel_height) // 2 # Fond panel pygame.draw.rect(screen, THEME_COLORS["button_idle"], (panel_x, panel_y, panel_width, panel_height), border_radius=16) pygame.draw.rect(screen, THEME_COLORS["border"], (panel_x, panel_y, panel_width, panel_height), 2, border_radius=16) # Titre title_rect = title_surf.get_rect(center=(panel_x + panel_width // 2, panel_y + padding + title_height // 2)) screen.blit(title_surf, title_rect) # Zones de colonnes col_top = panel_y + padding + title_height + title_spacing col1_x = panel_x + padding col2_x = panel_x + panel_width - padding - col2_w # Clip si nécessaire prev_clip = None if enable_clip: prev_clip = screen.get_clip() clip_rect = pygame.Rect(panel_x + padding, col_top, panel_width - 2 * padding, panel_height - (col_top - panel_y) - padding) screen.set_clip(clip_rect) # Dessin colonne 1 y1 = col_top last_section = False for is_section, surf in col1_wrapped: if is_section: y1 += 0 if y1 + surf.get_height() > panel_y + panel_height - padding: break screen.blit(surf, (col1_x, y1)) y1 += surf.get_height() + (section_spacing if is_section else line_spacing) # Dessin colonne 2 y2 = col_top for is_section, surf in col2_wrapped: if y2 + surf.get_height() > panel_y + panel_height - padding: break screen.blit(surf, (col2_x, y2)) y2 += surf.get_height() + (section_spacing if is_section else line_spacing) if enable_clip and prev_clip is not None: screen.set_clip(prev_clip) # Menu Quitter Appli def draw_confirm_dialog(screen): """Affiche la boîte de dialogue de confirmation pour quitter.""" global OVERLAY if OVERLAY is None or OVERLAY.get_size() != (config.screen_width, config.screen_height): OVERLAY = pygame.Surface((config.screen_width, config.screen_height), pygame.SRCALPHA) OVERLAY.fill((0, 0, 0, 150)) logger.debug("OVERLAY recréé dans draw_confirm_dialog") screen.blit(OVERLAY, (0, 0)) # 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 button_height = int(config.screen_height * 0.0463) margin_top_bottom = 20 rect_height = text_height + button_height + 2 * margin_top_bottom max_text_width = max([config.font.size(line)[0] for line in wrapped_message], default=300) rect_width = max_text_width + 150 rect_x = (config.screen_width - rect_width) // 2 rect_y = (config.screen_height - rect_height) // 2 pygame.draw.rect(screen, THEME_COLORS["button_idle"], (rect_x, rect_y, rect_width, rect_height), border_radius=12) pygame.draw.rect(screen, THEME_COLORS["border"], (rect_x, rect_y, rect_width, rect_height), 2, border_radius=12) for i, line in enumerate(wrapped_message): text = config.font.render(line, True, THEME_COLORS["text"]) text_rect = text.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + i * line_height + line_height // 2)) screen.blit(text, text_rect) button_width = min(160, (rect_width - 60) // 2) draw_stylized_button(screen, _("button_yes"), rect_x + rect_width // 2 - button_width - 10, rect_y + text_height + margin_top_bottom, button_width, button_height, selected=config.confirm_selection == 1) draw_stylized_button(screen, _("button_no"), rect_x + rect_width // 2 + 10, rect_y + text_height + margin_top_bottom, button_width, button_height, selected=config.confirm_selection == 0) def draw_reload_games_data_dialog(screen): """Affiche la boîte de dialogue de confirmation pour retélécharger le cache des jeux.""" global OVERLAY if OVERLAY is None or OVERLAY.get_size() != (config.screen_width, config.screen_height): OVERLAY = pygame.Surface((config.screen_width, config.screen_height), pygame.SRCALPHA) OVERLAY.fill((0, 0, 0, 150)) screen.blit(OVERLAY, (0, 0)) message = _("confirm_redownload_cache") wrapped_message = wrap_text(message, config.small_font, config.screen_width - 80) line_height = config.small_font.get_height() + 5 text_height = len(wrapped_message) * line_height button_height = int(config.screen_height * 0.0463) margin_top_bottom = 20 rect_height = text_height + button_height + 2 * margin_top_bottom max_text_width = max([config.small_font.size(line)[0] for line in wrapped_message], default=300) rect_width = max_text_width + 80 rect_x = (config.screen_width - rect_width) // 2 rect_y = (config.screen_height - rect_height) // 2 pygame.draw.rect(screen, THEME_COLORS["button_idle"], (rect_x, rect_y, rect_width, rect_height), border_radius=12) pygame.draw.rect(screen, THEME_COLORS["border"], (rect_x, rect_y, rect_width, rect_height), 2, border_radius=12) for i, line in enumerate(wrapped_message): text = config.small_font.render(line, True, THEME_COLORS["text"]) text_rect = text.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + i * line_height + line_height // 2)) screen.blit(text, text_rect) # Calcule une largeur de bouton cohérente avec la boîte et centre les deux boutons button_width = min(160, (rect_width - 60) // 2) yes_x = rect_x + rect_width // 2 - button_width - 10 no_x = rect_x + rect_width // 2 + 10 buttons_y = rect_y + text_height + margin_top_bottom draw_stylized_button(screen, _("button_yes"), yes_x, buttons_y, button_width, button_height, selected=config.redownload_confirm_selection == 1) draw_stylized_button(screen, _("button_no"), no_x, buttons_y, button_width, button_height, selected=config.redownload_confirm_selection == 0) # Popup avec compte à rebours def draw_popup(screen): """Dessine un popup avec un message (adapté en largeur) et un compte à rebours.""" screen.blit(OVERLAY, (0, 0)) # Largeur de base (peut s'élargir un peu si très petit écran) popup_width = int(config.screen_width * 0.8) max_inner_width = popup_width - 60 # padding horizontal interne pour le texte line_height = config.small_font.get_height() + 8 margin_top_bottom = 24 raw_segments = config.popup_message.split('\n') if config.popup_message else [] wrapped_lines = [] for seg in raw_segments: if seg.strip() == "": wrapped_lines.append("") else: wrapped_lines.extend(wrap_text(seg, config.small_font, max_inner_width)) if not wrapped_lines: wrapped_lines = [""] text_height = len(wrapped_lines) * line_height # Ajouter une ligne pour le compte à rebours popup_height = text_height + 2 * margin_top_bottom + line_height popup_x = (config.screen_width - popup_width) // 2 popup_y = (config.screen_height - popup_height) // 2 pygame.draw.rect(screen, THEME_COLORS["button_idle"], (popup_x, popup_y, popup_width, popup_height), border_radius=12) pygame.draw.rect(screen, THEME_COLORS["border"], (popup_x, popup_y, popup_width, popup_height), 2, border_radius=12) for i, line in enumerate(wrapped_lines): # Alignment centre horizontal global text_surface = config.small_font.render(line, True, THEME_COLORS["text"]) text_rect = text_surface.get_rect(center=(config.screen_width // 2, popup_y + margin_top_bottom + i * line_height + line_height // 2)) screen.blit(text_surface, text_rect) remaining_time = max(0, config.popup_timer // 1000) countdown_text = _("popup_countdown").format(remaining_time, 's' if remaining_time != 1 else '') countdown_surface = config.small_font.render(countdown_text, True, THEME_COLORS["text"]) countdown_rect = countdown_surface.get_rect(center=(config.screen_width // 2, popup_y + margin_top_bottom + len(wrapped_lines) * line_height + line_height // 2)) screen.blit(countdown_surface, countdown_rect)