1
0
forked from Mirrors/RGSX
Files
RGSX/ports/RGSX/display.py
retrogamesets 9861afb9fb v2.2.2.5
- fix error messages in history
2025-09-17 12:40:56 +02:00

2287 lines
113 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import pygame # type: ignore
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()
# 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 dune 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 derreur 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}"
# 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
control_categories = {
_("controls_category_navigation"): [
f"{get_control_display('up', '')} {get_control_display('down', '')} {get_control_display('left', '')} {get_control_display('right', '')} : {_('controls_navigation')}",
f"{get_control_display('page_up', 'LB')} {get_control_display('page_down', 'RB')} : {_('controls_pages')}",
],
_("controls_category_main_actions"): [
f"{get_control_display('confirm', 'A')} : {_('controls_confirm_select')}",
f"{get_control_display('cancel', 'B')} : {_('controls_cancel_back')}",
f"{get_control_display('start', 'Start')} : {_('controls_action_start')}",
],
_("controls_category_downloads"): [
f"{get_control_display('history', 'Y')} : {_('controls_action_history')}",
f"{get_control_display('clear_history', 'X')} : {_('controls_action_clear_history')}",
],
_("controls_category_search"): [
f"{get_control_display('filter', 'Select')} : {_('controls_filter_search')}",
f"{get_control_display('delete', 'Suppr')} : {_('controls_action_delete')}",
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:
# Wrap par mots
words = 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())
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)