From b09b3da371ba1cae3f86bf855c5aa77f76f3cd95 Mon Sep 17 00:00:00 2001 From: skymike03 Date: Fri, 20 Mar 2026 19:14:27 +0100 Subject: [PATCH] v2.6.1.0 (2026.03.20) - Added the IP address to the top-right info badge. - Added disk usage to the left badge using a used/total(percentage) format. - Added screen resolution below disk usage on the platforms page. - Improved entry speed for global cross-platform search. - Fixed virtual keyboard positioning in the search screen. - Adjusted the platform grid to avoid overlapping the footer on small resolutions. - Made the header badges responsive to prevent overlap on smaller screens. - Made the footer responsive with automatic scaling of text, icons, and spacing. - Asking to update gamelist once a day --- ports/RGSX/config.py | 30 +- ports/RGSX/controls.py | 11 +- ports/RGSX/display.py | 699 ++++++++++++++++++++++++++++++++++++----- ports/RGSX/utils.py | 15 + version.json | 2 +- 5 files changed, 668 insertions(+), 89 deletions(-) diff --git a/ports/RGSX/config.py b/ports/RGSX/config.py index 5679796..0c764fe 100644 --- a/ports/RGSX/config.py +++ b/ports/RGSX/config.py @@ -2,6 +2,7 @@ import os import logging import platform +import socket from typing import Optional from dataclasses import dataclass @@ -26,10 +27,10 @@ except Exception: pygame = None # type: ignore # Version actuelle de l'application -app_version = "2.6.0.3" +app_version = "2.6.1.0" # Nombre de jours avant de proposer la mise à jour de la liste des jeux -GAMELIST_UPDATE_DAYS = 7 +GAMELIST_UPDATE_DAYS = 1 def get_application_root(): @@ -250,6 +251,30 @@ SYSTEM_INFO = { def get_batocera_system_info(): """Récupère les informations système via la commande batocera-info.""" global SYSTEM_INFO + + def get_local_network_ip(): + try: + udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + udp_socket.connect(("8.8.8.8", 80)) + local_ip = udp_socket.getsockname()[0] + if local_ip and not local_ip.startswith("127."): + return local_ip + finally: + udp_socket.close() + except Exception: + pass + + try: + hostname = socket.gethostname() + local_ip = socket.gethostbyname(hostname) + if local_ip and not local_ip.startswith("127."): + return local_ip + except Exception: + pass + + return "" + try: import subprocess result = subprocess.run(['batocera-info'], capture_output=True, text=True, timeout=5) @@ -305,6 +330,7 @@ def get_batocera_system_info(): SYSTEM_INFO["system"] = f"{platform.system()} {platform.release()}" SYSTEM_INFO["architecture"] = platform.machine() SYSTEM_INFO["cpu_model"] = platform.processor() or "Unknown" + SYSTEM_INFO["network_ip"] = get_local_network_ip() return False diff --git a/ports/RGSX/controls.py b/ports/RGSX/controls.py index 383575b..d2fce9d 100644 --- a/ports/RGSX/controls.py +++ b/ports/RGSX/controls.py @@ -442,12 +442,14 @@ def build_global_search_index() -> list[dict]: platform_id = _get_platform_id(platform) platform_label = _get_platform_label(platform_id) for game in load_games(platform_id): + display_name = game.display_name or Path(game.name).stem indexed_games.append({ "platform_id": platform_id, "platform_label": platform_label, "platform_index": platform_index, "game_name": game.name, - "display_name": game.display_name or Path(game.name).stem, + "display_name": display_name, + "search_name": display_name.lower(), "url": game.url, "size": game.size, }) @@ -463,7 +465,7 @@ def refresh_global_search_results(reset_selection: bool = True) -> None: else: config.global_search_results = [ item for item in config.global_search_index - if query in item["display_name"].lower() + if query in item.get("search_name", item["display_name"].lower()) ] if reset_selection: @@ -476,7 +478,10 @@ def refresh_global_search_results(reset_selection: bool = True) -> None: def enter_global_search() -> None: - config.global_search_index = build_global_search_index() + index_signature = tuple(config.platforms) + if not getattr(config, 'global_search_index', None) or getattr(config, 'global_search_index_signature', None) != index_signature: + config.global_search_index = build_global_search_index() + config.global_search_index_signature = index_signature config.global_search_query = "" config.global_search_results = [] config.global_search_selected = 0 diff --git a/ports/RGSX/display.py b/ports/RGSX/display.py index c40760a..391f0c2 100644 --- a/ports/RGSX/display.py +++ b/ports/RGSX/display.py @@ -5,6 +5,7 @@ import os import io import platform import random +import shutil from datetime import datetime import config from utils import (truncate_text_middle, wrap_text, load_system_image, truncate_text_end, @@ -242,6 +243,145 @@ def _render_icons_line(actions, text, target_col_width, font, text_color, icon_s y += ls.get_height() + 4 return surf + +def _render_icons_line_singleline(actions, text, target_col_width, font, text_color, icon_size=28, icon_gap=8, icon_text_gap=12): + """Version mono-ligne pour le footer: réduit d'abord, tronque ensuite, sans retour à la ligne.""" + if not getattr(config, 'joystick', True): + action_labels = [] + for action_name in actions: + label = get_control_display(action_name, action_name.upper()) + action_labels.append(f"[{label}]") + full_text = " ".join(action_labels) + " : " + text + fitted_text = truncate_text_end(full_text, font, target_col_width) + text_surface = font.render(fitted_text, True, text_color) + surf = pygame.Surface(text_surface.get_size(), pygame.SRCALPHA) + surf.blit(text_surface, (0, 0)) + return surf + + icon_surfs = [] + for action_name in actions: + surf = get_help_icon_surface(action_name, icon_size) + if surf is not None: + icon_surfs.append(surf) + + if not icon_surfs: + fitted_text = truncate_text_end(text, font, target_col_width) + text_surface = font.render(fitted_text, True, text_color) + surf = pygame.Surface(text_surface.get_size(), pygame.SRCALPHA) + surf.blit(text_surface, (0, 0)) + return surf + + icons_width = sum(s.get_width() for s in icon_surfs) + (len(icon_surfs) - 1) * icon_gap + if icons_width + icon_text_gap > target_col_width: + scale = (target_col_width - icon_text_gap) / max(1, icons_width) + scale = max(0.5, min(1.0, scale)) + resized_surfs = [] + for surf in icon_surfs: + new_size = (max(1, int(surf.get_width() * scale)), max(1, int(surf.get_height() * scale))) + resized_surfs.append(pygame.transform.smoothscale(surf, new_size)) + icon_surfs = resized_surfs + icons_width = sum(s.get_width() for s in icon_surfs) + (len(icon_surfs) - 1) * icon_gap + + text_area_width = max(24, target_col_width - icons_width - icon_text_gap) + fitted_text = truncate_text_end(text, font, text_area_width) + text_surface = font.render(fitted_text, True, text_color) + + total_width = min(target_col_width, icons_width + icon_text_gap + text_surface.get_width()) + total_height = max(max((s.get_height() for s in icon_surfs), default=0), text_surface.get_height()) + surf = pygame.Surface((total_width, total_height), pygame.SRCALPHA) + + x = 0 + icon_y_center = total_height // 2 + for idx, icon_surf in enumerate(icon_surfs): + rect = icon_surf.get_rect() + y = icon_y_center - rect.height // 2 + surf.blit(icon_surf, (x, y)) + x += rect.width + (icon_gap if idx < len(icon_surfs) - 1 else 0) + + text_x = x + icon_text_gap + text_y = (total_height - text_surface.get_height()) // 2 + surf.blit(text_surface, (text_x, text_y)) + return surf + + +def _render_combined_footer_controls(all_controls, max_width, text_color): + footer_scale = config.accessibility_settings.get("footer_font_scale", 1.0) + nominal_size = max(10, int(20 * footer_scale)) + candidate_sizes = [] + for size in range(nominal_size, 9, -2): + if size not in candidate_sizes: + candidate_sizes.append(size) + if 10 not in candidate_sizes: + candidate_sizes.append(10) + + for font_size in candidate_sizes: + font = _get_badge_font(font_size) + ratio = font_size / max(1, nominal_size) + icon_size = max(12, int(20 * footer_scale * ratio)) + icon_gap = max(2, int(6 * ratio)) + icon_text_gap = max(4, int(10 * ratio)) + control_gap = max(8, int(20 * ratio)) + + rendered_controls = [] + total_width = 0 + for _, actions, label in all_controls: + surf = _render_icons_line_singleline( + actions, + label, + max_width, + font, + text_color, + icon_size=icon_size, + icon_gap=icon_gap, + icon_text_gap=icon_text_gap, + ) + rendered_controls.append(surf) + total_width += surf.get_width() + + total_width += max(0, len(rendered_controls) - 1) * control_gap + if total_width <= max_width: + total_height = max((surf.get_height() for surf in rendered_controls), default=1) + combined = pygame.Surface((total_width, total_height), pygame.SRCALPHA) + x_pos = 0 + for idx, surf in enumerate(rendered_controls): + combined.blit(surf, (x_pos, (total_height - surf.get_height()) // 2)) + x_pos += surf.get_width() + (control_gap if idx < len(rendered_controls) - 1 else 0) + return combined + + font = _get_badge_font(candidate_sizes[-1]) + icon_size = 12 + icon_gap = 2 + icon_text_gap = 4 + control_gap = 8 + remaining_width = max_width + rendered_controls = [] + for idx, (_, actions, label) in enumerate(all_controls): + controls_left = len(all_controls) - idx + target_width = max(40, remaining_width // max(1, controls_left)) + surf = _render_icons_line_singleline( + actions, + label, + target_width, + font, + text_color, + icon_size=icon_size, + icon_gap=icon_gap, + icon_text_gap=icon_text_gap, + ) + rendered_controls.append(surf) + remaining_width -= surf.get_width() + control_gap + + total_width = min(max_width, sum(surf.get_width() for surf in rendered_controls) + max(0, len(rendered_controls) - 1) * control_gap) + total_height = max((surf.get_height() for surf in rendered_controls), default=1) + combined = pygame.Surface((total_width, total_height), pygame.SRCALPHA) + x_pos = 0 + for idx, surf in enumerate(rendered_controls): + if x_pos + surf.get_width() > total_width: + break + combined.blit(surf, (x_pos, (total_height - surf.get_height()) // 2)) + x_pos += surf.get_width() + (control_gap if idx < len(rendered_controls) - 1 else 0) + return combined + # Couleurs modernes pour le thème THEME_COLORS = { # Fond des lignes sélectionnées @@ -878,21 +1018,94 @@ def get_control_display(action, default): # Cache pour les images des plateformes platform_images_cache = {} +_BADGE_FONT_CACHE = {} -def draw_header_badge(screen, lines, badge_x, badge_y, light_mode=False): - """Affiche une cartouche compacte de texte dans l'en-tete.""" - header_font = config.tiny_font - text_surfaces = [header_font.render(line, True, THEME_COLORS["text"]) for line in lines if line] - if not text_surfaces: - return +def _get_badge_font(size): + size = max(10, int(size)) + family_id = config.FONT_FAMILIES[config.current_font_family_index] if 0 <= config.current_font_family_index < len(config.FONT_FAMILIES) else "pixel" + cache_key = (family_id, size) + if cache_key in _BADGE_FONT_CACHE: + return _BADGE_FONT_CACHE[cache_key] + try: + if family_id == "pixel": + path = os.path.join(config.APP_FOLDER, "assets", "fonts", "Pixel-UniCode.ttf") + font = pygame.font.Font(path, size) + else: + try: + font = pygame.font.SysFont("dejavusans", size) + except Exception: + font = pygame.font.SysFont("dejavu sans", size) + except Exception: + font = config.tiny_font + + _BADGE_FONT_CACHE[cache_key] = font + return font + + +def _get_adaptive_badge_layout(lines, base_font, max_badge_width=None, padding_x=12, min_font_size=10): + clean_lines = [line for line in lines if isinstance(line, str) and line] + if not clean_lines: + return base_font, [] + if not max_badge_width: + return base_font, clean_lines + + max_text_width = max(40, max_badge_width - padding_x * 2) + footer_font_scale = config.accessibility_settings.get("footer_font_scale", 1.0) + nominal_size = max(min_font_size, int(20 * footer_font_scale)) + candidate_sizes = [] + for size in range(nominal_size, min_font_size - 1, -2): + if size not in candidate_sizes: + candidate_sizes.append(size) + if min_font_size not in candidate_sizes: + candidate_sizes.append(min_font_size) + + for size in candidate_sizes: + candidate_font = _get_badge_font(size) + if all(candidate_font.size(line)[0] <= max_text_width for line in clean_lines): + return candidate_font, clean_lines + + fallback_font = _get_badge_font(candidate_sizes[-1]) + fitted_lines = [truncate_text_end(line, fallback_font, max_text_width) for line in clean_lines] + return fallback_font, fitted_lines + + +def _fit_badge_lines(lines, font, max_badge_width=None, padding_x=12): + _, fitted_lines = _get_adaptive_badge_layout(lines, font, max_badge_width=max_badge_width, padding_x=padding_x) + return fitted_lines + + +def measure_header_badge(lines, font=None, max_badge_width=None, padding_x=12, padding_y=8, line_gap=4): + header_font = font or config.tiny_font + header_font, fitted_lines = _get_adaptive_badge_layout(lines, header_font, max_badge_width=max_badge_width, padding_x=padding_x) + if not fitted_lines: + return 0, 0, [] + + text_surfaces = [header_font.render(line, True, THEME_COLORS["text"]) for line in fitted_lines] content_width = max((surface.get_width() for surface in text_surfaces), default=0) - content_height = sum(surface.get_height() for surface in text_surfaces) + max(0, len(text_surfaces) - 1) * 4 - padding_x = 12 - padding_y = 8 + content_height = sum(surface.get_height() for surface in text_surfaces) + max(0, len(text_surfaces) - 1) * line_gap badge_width = content_width + padding_x * 2 badge_height = content_height + padding_y * 2 + return badge_width, badge_height, fitted_lines + + +def draw_header_badge(screen, lines, badge_x, badge_y, light_mode=False, font=None, max_badge_width=None, padding_x=12, padding_y=8, line_gap=4): + """Affiche une cartouche compacte de texte dans l'en-tete.""" + header_font = font or config.tiny_font + header_font, _ = _get_adaptive_badge_layout(lines, header_font, max_badge_width=max_badge_width, padding_x=padding_x) + badge_width, badge_height, fitted_lines = measure_header_badge( + lines, + font=header_font, + max_badge_width=max_badge_width, + padding_x=padding_x, + padding_y=padding_y, + line_gap=line_gap, + ) + if not fitted_lines: + return + + text_surfaces = [header_font.render(line, True, THEME_COLORS["text"]) for line in fitted_lines] if light_mode: pygame.draw.rect(screen, THEME_COLORS["button_idle"], (badge_x, badge_y, badge_width, badge_height), border_radius=12) @@ -914,21 +1127,140 @@ def draw_header_badge(screen, lines, badge_x, badge_y, light_mode=False): for surface in text_surfaces: line_x = badge_x + (badge_width - surface.get_width()) // 2 screen.blit(surface, (line_x, current_y)) - current_y += surface.get_height() + 4 + current_y += surface.get_height() + line_gap -def draw_platform_header_info(screen, light_mode=False): - """Affiche version et controleur connecte dans un cartouche en haut a droite.""" +def get_platform_header_info_lines(max_badge_width=None, include_details=True): + """Retourne les lignes du cartouche version/controleur/IP, adaptees a une largeur max.""" lines = [f"v{config.app_version}"] + if not include_details: + return _fit_badge_lines(lines, config.tiny_font, max_badge_width, padding_x=12) + device_name = (getattr(config, 'controller_device_name', '') or '').strip() if device_name: - lines.append(truncate_text_end(device_name, config.tiny_font, int(config.screen_width * 0.24))) + lines.append(device_name) - badge_width = max(config.tiny_font.size(line)[0] for line in lines) + 24 - badge_x = config.screen_width - badge_width - 14 + network_ip = "" + system_info = getattr(config, 'SYSTEM_INFO', None) + if isinstance(system_info, dict): + network_ip = (system_info.get('network_ip', '') or '').strip() + if network_ip: + lines.append(network_ip) + + return _fit_badge_lines(lines, config.tiny_font, max_badge_width, padding_x=12) + + +def _format_disk_size_gb(size_bytes): + gb_value = size_bytes / (1024 ** 3) + if gb_value >= 100: + return f"{gb_value:.0f} GB" + if gb_value >= 10: + return f"{gb_value:.1f} GB" + return f"{gb_value:.2f} GB" + + +def get_default_disk_space_line(): + """Retourne l'utilisation disque du dossier ROMs par defaut sous forme 'Disk : utilise/total(percent)'.""" + try: + target_path = getattr(config, 'ROMS_FOLDER', '') or '' + if not target_path: + return "" + + resolved_path = os.path.abspath(target_path) + while resolved_path and not os.path.exists(resolved_path): + parent_path = os.path.dirname(resolved_path) + if not parent_path or parent_path == resolved_path: + break + resolved_path = parent_path + + if not os.path.exists(resolved_path): + return "" + + usage = shutil.disk_usage(resolved_path) + used_bytes = max(0, usage.total - usage.free) + used_percent = int(round((used_bytes / usage.total) * 100)) if usage.total > 0 else 0 + return f"Disk : {_format_disk_size_gb(used_bytes)}/{_format_disk_size_gb(usage.total)}({used_percent}%)" + except Exception: + return "" + + +def get_display_resolution_line(): + """Retourne la resolution d'affichage pour le cartouche gauche de la page plateformes.""" + try: + system_info = getattr(config, 'SYSTEM_INFO', None) + if isinstance(system_info, dict): + display_resolution = (system_info.get('display_resolution', '') or '').strip() + if display_resolution: + return f"Res : {display_resolution}" + except Exception: + pass + + try: + if getattr(config, 'screen_width', 0) and getattr(config, 'screen_height', 0): + return f"Res : {config.screen_width}x{config.screen_height}" + except Exception: + pass + + return "" + + +def draw_platform_header_info(screen, light_mode=False, badge_x=None, max_badge_width=None, include_details=True): + """Affiche version, controleur connecte et IP reseau dans un cartouche en haut a droite.""" + lines = get_platform_header_info_lines(max_badge_width, include_details=include_details) + badge_width, _, fitted_lines = measure_header_badge(lines, font=config.tiny_font, max_badge_width=max_badge_width) + if not fitted_lines: + return + if badge_x is None: + badge_x = config.screen_width - badge_width - 14 badge_y = 10 - draw_header_badge(screen, lines, badge_x, badge_y, light_mode) + draw_header_badge(screen, fitted_lines, badge_x, badge_y, light_mode, font=config.tiny_font, max_badge_width=max_badge_width) + + +def get_platform_header_badge_layout(screen_width, left_lines=None, right_lines=None, center_min_width=None, header_margin_x=14, header_gap=None): + """Calcule une repartition responsive des 3 cartouches d'en-tete avec priorite au cartouche droit.""" + if header_gap is None: + header_gap = max(10, int(screen_width * 0.01)) + if center_min_width is None: + center_min_width = max(160, int(screen_width * 0.18)) + + left_lines = left_lines or [] + right_lines = right_lines or [] + + available_width = screen_width - 2 * header_margin_x + gap_count = (1 if left_lines else 0) + (1 if right_lines else 0) + available_without_gaps = max(120, available_width - gap_count * header_gap) + + left_target = max(160, int(screen_width * 0.28)) if left_lines else 0 + right_target = max(220, int(screen_width * 0.26)) if right_lines else 0 + + if left_lines and right_lines: + max_side_total = max(120, available_without_gaps - center_min_width) + desired_side_total = left_target + right_target + if desired_side_total > max_side_total: + scale = max_side_total / desired_side_total if desired_side_total > 0 else 1.0 + left_target = max(140, int(left_target * scale)) + right_target = max(180, int(right_target * scale)) + + overflow = left_target + right_target - max_side_total + if overflow > 0: + left_reduction = min(overflow, max(0, left_target - 140)) + left_target -= left_reduction + overflow -= left_reduction + if overflow > 0: + right_target = max(160, right_target - overflow) + + elif left_lines: + left_target = max(160, min(left_target, available_without_gaps - center_min_width)) + elif right_lines: + right_target = max(180, min(right_target, available_without_gaps - center_min_width)) + + return { + "header_gap": header_gap, + "center_min_width": center_min_width, + "left_max_width": left_target, + "right_max_width": right_target, + } # Grille des systèmes 3x3 def draw_platform_grid(screen): @@ -960,14 +1292,109 @@ def draw_platform_grid(screen): 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) + + header_margin_x = 14 + center_badge_min_width = max(160, int(config.screen_width * 0.18)) + header_y = 10 + num_cols = getattr(config, 'GRID_COLS', 3) + num_rows = getattr(config, 'GRID_ROWS', 4) + + total_pages = 0 + left_badge_lines = [] + left_badge_width = 0 + left_badge_height = 0 + page_indicator_text = "" # Effet de pulsation subtil pour le titre - calculé une seule fois par frame current_time = pygame.time.get_ticks() - + + # 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) + 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 + left_badge_candidate_lines = [] + if total_pages > 1: + page_indicator_text = _("platform_page").format(config.current_page + 1, total_pages) + left_badge_candidate_lines.append(page_indicator_text) + + disk_space_line = get_default_disk_space_line() + if disk_space_line: + left_badge_candidate_lines.append(disk_space_line) + + display_resolution_line = get_display_resolution_line() + if display_resolution_line: + left_badge_candidate_lines.append(display_resolution_line) + + right_badge_raw_lines = get_platform_header_info_lines(None, include_details=True) + header_layout = get_platform_header_badge_layout( + config.screen_width, + left_lines=left_badge_candidate_lines, + right_lines=right_badge_raw_lines, + center_min_width=center_badge_min_width, + header_margin_x=header_margin_x, + ) + header_gap = header_layout["header_gap"] + left_badge_max_width = header_layout["left_max_width"] + right_badge_max_width = header_layout["right_max_width"] + + left_badge_width, left_badge_height, left_badge_lines = measure_header_badge( + left_badge_candidate_lines, + font=config.tiny_font, + max_badge_width=left_badge_max_width, + ) + + right_badge_lines = get_platform_header_info_lines(right_badge_max_width, include_details=True) + right_badge_width, right_badge_height, right_badge_lines = measure_header_badge( + right_badge_lines, + font=config.tiny_font, + max_badge_width=right_badge_max_width, + ) + + center_left = header_margin_x + (left_badge_width + header_gap if left_badge_lines else 0) + center_right = config.screen_width - header_margin_x - (right_badge_width + header_gap if right_badge_lines else 0) + center_badge_max_width = max(center_badge_min_width, center_right - center_left) + + center_font_candidates = [config.title_font, config.search_font, config.font, config.small_font] + center_font = config.small_font + center_line = title_text + center_padding_x = 18 + center_padding_y = 10 + center_line_gap = 4 + + for candidate_font in center_font_candidates: + raw_width = candidate_font.size(title_text)[0] + center_padding_x * 2 + if raw_width <= center_badge_max_width: + center_font = candidate_font + center_line = title_text + break + else: + center_font = center_font_candidates[-1] + center_line = truncate_text_end(title_text, center_font, max(80, center_badge_max_width - center_padding_x * 2)) + + title_surface = center_font.render(center_line, True, THEME_COLORS["text"]) + title_rect = title_surface.get_rect() + title_rect_inflated = title_rect.inflate(center_padding_x * 2, center_padding_y * 2) + title_rect_inflated.x = center_left + max(0, (center_badge_max_width - title_rect_inflated.width) // 2) + title_rect_inflated.y = header_y + title_rect.center = title_rect_inflated.center + if not light_mode: # Mode normal : effets visuels complets pulse_factor = 0.08 * (1 + math.sin(current_time / 400)) @@ -1011,10 +1438,17 @@ def draw_platform_grid(screen): # 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) + header_bottom = title_rect_inflated.bottom + if left_badge_lines: + header_bottom = max(header_bottom, header_y + left_badge_height) + if right_badge_lines: + header_bottom = max(header_bottom, header_y + right_badge_height) + header_clearance = max(20, int(config.screen_height * 0.03)) + margin_top = max(int(config.screen_height * 0.140), header_bottom + header_clearance) + footer_height = 70 + min_footer_gap = max(12, int(config.screen_height * 0.018)) + footer_reserved = max(footer_height + min_footer_gap, int(config.screen_height * 0.118)) + margin_bottom = footer_reserved systems_per_page = num_cols * num_rows available_width = config.screen_width - margin_left - margin_right @@ -1034,35 +1468,37 @@ def draw_platform_grid(screen): cell_padding = int(cell_size * 0.15) # 15% d'espacement 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)] + first_row_center = margin_top + row_height // 2 + last_row_center = config.screen_height - margin_bottom - row_height // 2 + if num_rows <= 1: + y_positions = [margin_top + available_height // 2] + elif last_row_center <= first_row_center: + y_positions = [margin_top + row_height * i + row_height // 2 for i in range(num_rows)] else: - visible_platforms = list(config.platforms) + row_step = (last_row_center - first_row_center) / (num_rows - 1) + y_positions = [int(first_row_center + row_step * i) for i in range(num_rows)] - # 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 + if left_badge_lines: + draw_header_badge( + screen, + left_badge_lines, + header_margin_x, + header_y, + light_mode, + font=config.tiny_font, + max_badge_width=left_badge_max_width, + ) - 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) - draw_header_badge(screen, [page_indicator_text], 14, 10, light_mode) - - draw_platform_header_info(screen, light_mode) + if right_badge_lines: + right_badge_x = config.screen_width - right_badge_width - header_margin_x + draw_platform_header_info( + screen, + light_mode, + badge_x=right_badge_x, + max_badge_width=right_badge_max_width, + include_details=True, + ) # Calculer une seule fois la pulsation pour les éléments sélectionnés (réduite) if not light_mode: @@ -1378,12 +1814,76 @@ def draw_game_list(screen): screen.blit(OVERLAY, (0, 0)) + header_margin_x = 14 + header_y = 10 + left_badge_lines = [] + left_badge_width = 0 + right_badge_lines = get_platform_header_info_lines(None, include_details=False) + + disk_space_line = get_default_disk_space_line() + if disk_space_line: + left_badge_candidate_lines = [disk_space_line] + else: + left_badge_candidate_lines = [] + + header_layout = get_platform_header_badge_layout( + config.screen_width, + left_lines=left_badge_candidate_lines, + right_lines=right_badge_lines, + center_min_width=max(180, int(config.screen_width * 0.18)), + header_margin_x=header_margin_x, + ) + header_gap = header_layout["header_gap"] + left_badge_max_width = header_layout["left_max_width"] + right_badge_max_width = header_layout["right_max_width"] + + if left_badge_candidate_lines: + left_badge_width, left_badge_height, left_badge_lines = measure_header_badge( + left_badge_candidate_lines, + font=config.tiny_font, + max_badge_width=left_badge_max_width, + ) + + right_badge_lines = get_platform_header_info_lines(right_badge_max_width, include_details=False) + right_badge_width, right_badge_height, right_badge_lines = measure_header_badge( + right_badge_lines, + font=config.tiny_font, + max_badge_width=right_badge_max_width, + ) + + title_left = header_margin_x + (left_badge_width + header_gap if left_badge_lines else 0) + title_right = config.screen_width - header_margin_x - (right_badge_width + header_gap if right_badge_lines else 0) + title_badge_max_width = max(180, title_right - title_left) + + def _build_game_header_title(title_text_value, font_candidates, text_color, border_color=None): + padding_x = 18 + padding_y = 10 + selected_font = font_candidates[-1] + selected_text = title_text_value + for candidate_font in font_candidates: + raw_width = candidate_font.size(title_text_value)[0] + padding_x * 2 + if raw_width <= title_badge_max_width: + selected_font = candidate_font + selected_text = title_text_value + break + else: + selected_text = truncate_text_end(title_text_value, selected_font, max(80, title_badge_max_width - padding_x * 2)) + + title_surface_local = selected_font.render(selected_text, True, text_color) + title_rect_local = title_surface_local.get_rect() + title_rect_inflated_local = title_rect_local.inflate(padding_x * 2, padding_y * 2) + title_rect_inflated_local.x = title_left + max(0, (title_badge_max_width - title_rect_inflated_local.width) // 2) + title_rect_inflated_local.y = header_y + title_rect_local.center = title_rect_inflated_local.center + return title_surface_local, title_rect_local, title_rect_inflated_local, border_color or THEME_COLORS["border"] + 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) + title_surface, title_rect, title_rect_inflated, title_border_color = _build_game_header_title( + search_text, + [config.search_font, config.font, config.small_font], + THEME_COLORS["text"], + ) # Ombre pour le titre de recherche shadow = pygame.Surface((title_rect_inflated.width + 10, title_rect_inflated.height + 10), pygame.SRCALPHA) @@ -1396,7 +1896,7 @@ def draw_game_list(screen): screen.blit(glow, (title_rect_inflated.left - 10, title_rect_inflated.top - 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) + pygame.draw.rect(screen, title_border_color, title_rect_inflated, 2, border_radius=12) screen.blit(title_surface, title_rect) elif config.filter_active: # Afficher le nom de la plateforme avec indicateur de filtre actif @@ -1406,12 +1906,14 @@ def draw_game_list(screen): filter_indicator = f" - {_('game_filter').format(config.search_query)}" title_text = _("game_count").format(platform_name, game_count) + filter_indicator - title_surface = config.title_font.render(title_text, True, THEME_COLORS["green"]) - 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) + title_surface, title_rect, title_rect_inflated, title_border_color = _build_game_header_title( + title_text, + [config.title_font, config.search_font, config.font, config.small_font], + THEME_COLORS["green"], + border_color=THEME_COLORS["border_selected"], + ) pygame.draw.rect(screen, THEME_COLORS["button_idle"], title_rect_inflated, border_radius=12) - pygame.draw.rect(screen, THEME_COLORS["border_selected"], title_rect_inflated, 3, border_radius=12) + pygame.draw.rect(screen, title_border_color, title_rect_inflated, 3, border_radius=12) screen.blit(title_surface, title_rect) else: # Ajouter indicateur de filtre actif si filtres avancés sont actifs @@ -1420,10 +1922,11 @@ def draw_game_list(screen): filter_indicator = " (Active Filter)" title_text = _("game_count").format(platform_name, game_count) + filter_indicator - 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) + title_surface, title_rect, title_rect_inflated, title_border_color = _build_game_header_title( + title_text, + [config.title_font, config.search_font, config.font, config.small_font], + THEME_COLORS["text"], + ) # Ombre et glow pour titre normal shadow = pygame.Surface((title_rect_inflated.width + 10, title_rect_inflated.height + 10), pygame.SRCALPHA) @@ -1431,9 +1934,30 @@ def draw_game_list(screen): screen.blit(shadow, (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) + pygame.draw.rect(screen, title_border_color, title_rect_inflated, 2, border_radius=12) screen.blit(title_surface, title_rect) + if left_badge_lines: + draw_header_badge( + screen, + left_badge_lines, + header_margin_x, + header_y, + False, + font=config.tiny_font, + max_badge_width=left_badge_max_width, + ) + + if right_badge_lines: + right_badge_x = config.screen_width - right_badge_width - header_margin_x + draw_platform_header_info( + screen, + False, + badge_x=right_badge_x, + max_badge_width=right_badge_max_width, + include_details=False, + ) + # Ombre portée pour le cadre principal shadow_rect = pygame.Rect(rect_x + 6, rect_y + 6, rect_width, rect_height) shadow_surf = pygame.Surface((rect_width + 8, rect_height + 8), pygame.SRCALPHA) @@ -1579,6 +2103,7 @@ def draw_global_search_list(screen): """Affiche la recherche globale par nom sur toutes les plateformes.""" query = getattr(config, 'global_search_query', '') or '' results = getattr(config, 'global_search_results', []) or [] + keyboard_active = bool(getattr(config, 'joystick', False) and getattr(config, 'global_search_editing', False)) screen.blit(OVERLAY, (0, 0)) @@ -1604,6 +2129,24 @@ def draw_global_search_list(screen): pygame.draw.rect(screen, THEME_COLORS["border"], title_rect_inflated, 2, border_radius=12) screen.blit(title_surface, title_rect) + reserved_bottom = config.screen_height - 40 + if keyboard_active: + 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_layout = [10, 10, 10, 6] + keyboard_width = 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 + reserved_bottom = start_y - 24 + + message_zone_top = title_rect_inflated.bottom + 24 + message_zone_bottom = max(message_zone_top + 80, reserved_bottom) + if not query.strip(): message = _("global_search_empty_query") lines = wrap_text(message, config.font, config.screen_width - 80) @@ -1614,7 +2157,8 @@ def draw_global_search_list(screen): 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 + available_message_height = max(rect_height, message_zone_bottom - message_zone_top) + rect_y = message_zone_top + max(0, (available_message_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) @@ -1635,7 +2179,8 @@ def draw_global_search_list(screen): 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 + available_message_height = max(rect_height, message_zone_bottom - message_zone_top) + rect_y = message_zone_top + max(0, (available_message_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) @@ -2430,23 +2975,11 @@ def draw_controls(screen, menu_state, current_music_name=None, music_popup_start if line_data[0] == "icons_combined": # Combiner tous les contrôles sur une seule ligne all_controls = line_data[1] - combined_surf = pygame.Surface((max_width, 50), pygame.SRCALPHA) - x_pos = 10 - for action_tuple in all_controls: - ignored, actions, label = action_tuple - try: - surf = _render_icons_line(actions, label, max_width - x_pos - 10, config.tiny_font, THEME_COLORS["text"], icon_size=scaled_icon_size, icon_gap=scaled_icon_gap, icon_text_gap=scaled_icon_text_gap) - if x_pos + surf.get_width() > max_width - 10: - break # Pas assez de place - combined_surf.blit(surf, (x_pos, (50 - surf.get_height()) // 2)) - x_pos += surf.get_width() + 20 # Espacement entre contrôles - except Exception: - pass - # Redimensionner la surface au contenu réel - if x_pos > 10: - final_surf = pygame.Surface((x_pos - 10, 50), pygame.SRCALPHA) - final_surf.blit(combined_surf, (0, 0), (0, 0, x_pos - 10, 50)) + try: + final_surf = _render_combined_footer_controls(all_controls, max_width - 20, THEME_COLORS["text"]) icon_surfs.append(final_surf) + except Exception: + pass elif line_data[0] == "icons" and len(line_data) == 3: ignored, actions, label = line_data try: diff --git a/ports/RGSX/utils.py b/ports/RGSX/utils.py index 1aff7ed..f13860f 100644 --- a/ports/RGSX/utils.py +++ b/ports/RGSX/utils.py @@ -33,6 +33,8 @@ import tempfile logger = logging.getLogger(__name__) # Désactiver les logs DEBUG de urllib3 e requests pour supprimer les messages de connexion HTTP +_games_cache = {} + logging.getLogger("urllib3").setLevel(logging.WARNING) logging.getLogger("requests").setLevel(logging.WARNING) @@ -1195,9 +1197,15 @@ def load_games(platform_id:str) -> list[Game]: game_file = c break if not game_file: + _games_cache.pop(platform_id, None) logger.warning(f"Aucun fichier de jeux trouvé pour {platform_id} (candidats: {candidates})") return [] + game_mtime_ns = os.stat(game_file).st_mtime_ns + cached_entry = _games_cache.get(platform_id) + if cached_entry and cached_entry.get("path") == game_file and cached_entry.get("mtime_ns") == game_mtime_ns: + return cached_entry["games"] + with open(game_file, 'r', encoding='utf-8') as f: data = json.load(f) @@ -1242,8 +1250,15 @@ def load_games(platform_id:str) -> list[Game]: display_name = Path(name).stem display_name = display_name.replace(platform_id, "") games_list.append(Game(name=name, url=url, size=size, display_name=display_name)) + + _games_cache[platform_id] = { + "path": game_file, + "mtime_ns": game_mtime_ns, + "games": games_list, + } return games_list except Exception as e: + _games_cache.pop(platform_id, None) logger.error(f"Erreur lors du chargement des jeux pour {platform_id}: {e}") return [] diff --git a/version.json b/version.json index 969b539..7395a91 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "2.6.0.3" + "version": "2.6.1.0" } \ No newline at end of file