mirror of
https://github.com/RetroGameSets/RGSX.git
synced 2026-03-23 02:05:43 +01:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9cbf0196e | ||
|
|
eb86d69895 | ||
|
|
b09b3da371 |
@@ -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.2"
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
@@ -15,10 +15,11 @@ from utils import (
|
||||
load_games, check_extension_before_download, is_extension_supported,
|
||||
load_extensions_json, play_random_music, sanitize_filename,
|
||||
save_music_config, load_api_keys, _get_dest_folder_name,
|
||||
extract_zip, extract_rar, find_file_with_or_without_extension, toggle_web_service_at_boot, check_web_service_status,
|
||||
extract_zip, extract_rar, find_file_with_or_without_extension, find_matching_files, toggle_web_service_at_boot, check_web_service_status,
|
||||
restart_application, generate_support_zip, load_sources,
|
||||
ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string,
|
||||
start_connection_status_check
|
||||
start_connection_status_check, get_clean_display_name, get_existing_history_matches,
|
||||
move_files_to_directory
|
||||
)
|
||||
from history import load_history, clear_history, add_to_history, save_history, scan_roms_for_downloaded_games
|
||||
from language import _, get_available_languages, set_language
|
||||
@@ -442,12 +443,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 +466,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 +479,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
|
||||
@@ -551,7 +557,7 @@ def trigger_global_search_download(queue_only: bool = False) -> None:
|
||||
url = result.get("url")
|
||||
platform = result.get("platform_id")
|
||||
game_name = result.get("game_name")
|
||||
display_name = result.get("display_name") or game_name
|
||||
display_name = result.get("display_name") or get_clean_display_name(game_name, platform)
|
||||
|
||||
if not url or not platform or not game_name:
|
||||
logger.error(f"Resultat de recherche globale invalide: {result}")
|
||||
@@ -597,6 +603,7 @@ def trigger_global_search_download(queue_only: bool = False) -> None:
|
||||
config.history.append({
|
||||
'platform': platform,
|
||||
'game_name': game_name,
|
||||
'display_name': display_name,
|
||||
'status': 'Queued',
|
||||
'url': url,
|
||||
'progress': 0,
|
||||
@@ -1171,6 +1178,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.history.append({
|
||||
'platform': platform,
|
||||
'game_name': game_name,
|
||||
'display_name': get_clean_display_name(game_name, platform),
|
||||
'status': 'Queued',
|
||||
'url': url,
|
||||
'progress': 0,
|
||||
@@ -1243,6 +1251,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.history.append({
|
||||
'platform': platform,
|
||||
'game_name': game_name,
|
||||
'display_name': get_clean_display_name(game_name, platform),
|
||||
'status': 'Queued',
|
||||
'url': url,
|
||||
'progress': 0,
|
||||
@@ -1483,6 +1492,13 @@ def handle_controls(event, sources, joystick, screen):
|
||||
dest_folder = _get_dest_folder_name(platform)
|
||||
base_path = os.path.join(config.ROMS_FOLDER, dest_folder)
|
||||
file_exists, actual_filename, actual_path = find_file_with_or_without_extension(base_path, game_name)
|
||||
actual_matches = find_matching_files(base_path, game_name)
|
||||
if not actual_matches:
|
||||
actual_matches = get_existing_history_matches(entry)
|
||||
if actual_matches:
|
||||
actual_filename, actual_path = actual_matches[0]
|
||||
file_exists = True
|
||||
config.history_actual_matches = actual_matches
|
||||
|
||||
# Stocker les informations pour les autres handlers
|
||||
config.history_actual_filename = actual_filename
|
||||
@@ -1742,7 +1758,47 @@ def handle_controls(event, sources, joystick, screen):
|
||||
|
||||
# Affichage du dossier de téléchargement
|
||||
elif config.menu_state == "history_show_folder":
|
||||
if is_input_matched(event, "confirm") or is_input_matched(event, "cancel"):
|
||||
if is_input_matched(event, "clear_history"):
|
||||
if not config.history or config.current_history_item >= len(config.history):
|
||||
config.menu_state = "history"
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
entry = config.history[config.current_history_item]
|
||||
actual_matches = getattr(config, 'history_actual_matches', None) or []
|
||||
if not actual_matches:
|
||||
actual_matches = get_existing_history_matches(entry)
|
||||
|
||||
start_path = None
|
||||
if actual_matches:
|
||||
start_path = os.path.dirname(actual_matches[0][1])
|
||||
else:
|
||||
actual_path = getattr(config, 'history_actual_path', None)
|
||||
if actual_path and os.path.exists(actual_path):
|
||||
start_path = os.path.dirname(actual_path)
|
||||
|
||||
if not start_path or not os.path.isdir(start_path):
|
||||
start_path = config.ROMS_FOLDER
|
||||
|
||||
config.folder_browser_path = start_path
|
||||
config.folder_browser_selection = 0
|
||||
config.folder_browser_scroll_offset = 0
|
||||
config.folder_browser_mode = "history_move"
|
||||
config.platform_config_name = entry.get("display_name") or get_clean_display_name(entry.get("game_name", ""), entry.get("platform", ""))
|
||||
|
||||
try:
|
||||
items = [".."]
|
||||
for item in sorted(os.listdir(start_path)):
|
||||
full_path = os.path.join(start_path, item)
|
||||
if os.path.isdir(full_path):
|
||||
items.append(item)
|
||||
config.folder_browser_items = items
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lecture dossier {start_path}: {e}")
|
||||
config.folder_browser_items = [".."]
|
||||
|
||||
config.menu_state = "folder_browser"
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "confirm") or is_input_matched(event, "cancel"):
|
||||
config.menu_state = validate_menu_state(config.previous_menu_state)
|
||||
config.needs_redraw = True
|
||||
|
||||
@@ -2943,6 +2999,25 @@ def handle_controls(event, sources, joystick, screen):
|
||||
if config.folder_browser_selection >= config.folder_browser_scroll_offset + config.folder_browser_visible_items:
|
||||
config.folder_browser_scroll_offset = config.folder_browser_selection - config.folder_browser_visible_items + 1
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "page_up"):
|
||||
jump_size = 10
|
||||
if config.folder_browser_selection > 0:
|
||||
config.folder_browser_selection = max(0, config.folder_browser_selection - jump_size)
|
||||
config.folder_browser_scroll_offset = min(
|
||||
config.folder_browser_scroll_offset,
|
||||
config.folder_browser_selection
|
||||
)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "page_down"):
|
||||
jump_size = 10
|
||||
if config.folder_browser_selection < len(config.folder_browser_items) - 1:
|
||||
config.folder_browser_selection = min(
|
||||
len(config.folder_browser_items) - 1,
|
||||
config.folder_browser_selection + jump_size
|
||||
)
|
||||
if config.folder_browser_selection >= config.folder_browser_scroll_offset + config.folder_browser_visible_items:
|
||||
config.folder_browser_scroll_offset = config.folder_browser_selection - config.folder_browser_visible_items + 1
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "confirm"):
|
||||
if config.folder_browser_items:
|
||||
selected_item = config.folder_browser_items[config.folder_browser_selection]
|
||||
@@ -2996,6 +3071,34 @@ def handle_controls(event, sources, joystick, screen):
|
||||
# Informer qu'un redémarrage est nécessaire
|
||||
config.popup_message = _("roms_folder_set_restart").format(selected_path) if _ else f"ROMs folder set: {selected_path}\nRestart required!"
|
||||
config.menu_state = "pause_settings_menu"
|
||||
elif browser_mode == "history_move":
|
||||
entry = config.history[config.current_history_item] if config.history and config.current_history_item < len(config.history) else None
|
||||
actual_matches = getattr(config, 'history_actual_matches', None) or []
|
||||
if not actual_matches and entry:
|
||||
actual_matches = get_existing_history_matches(entry)
|
||||
|
||||
source_paths = [match_path for _, match_path in actual_matches]
|
||||
if not source_paths:
|
||||
actual_path = getattr(config, 'history_actual_path', None)
|
||||
if actual_path:
|
||||
source_paths = [actual_path]
|
||||
|
||||
success, moved_matches, error_message = move_files_to_directory(source_paths, selected_path)
|
||||
if success:
|
||||
config.history_actual_matches = moved_matches
|
||||
if moved_matches:
|
||||
config.history_actual_filename, config.history_actual_path = moved_matches[0]
|
||||
if entry is not None:
|
||||
entry["moved_paths"] = [path for _, path in moved_matches]
|
||||
save_history(config.history)
|
||||
config.popup_message = _("history_move_success").format(len(moved_matches), selected_path) if _ else f"Moved {len(moved_matches)} file(s) to {selected_path}"
|
||||
config.popup_timer = 3000
|
||||
logger.info(f"Déplacement historique terminé vers {selected_path}: {len(moved_matches)} fichier(s)")
|
||||
else:
|
||||
config.popup_message = _("history_move_error").format(error_message) if _ else f"Move error: {error_message}"
|
||||
config.popup_timer = 4000
|
||||
logger.error(f"Erreur déplacement historique vers {selected_path}: {error_message}")
|
||||
config.menu_state = "history_show_folder"
|
||||
else:
|
||||
# Mode dossier plateforme
|
||||
from rgsx_settings import set_platform_custom_path
|
||||
@@ -3011,6 +3114,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
browser_mode = getattr(config, 'folder_browser_mode', 'platform')
|
||||
if browser_mode == "roms_root":
|
||||
config.menu_state = "pause_settings_menu"
|
||||
elif browser_mode == "history_move":
|
||||
config.menu_state = "history_show_folder"
|
||||
else:
|
||||
config.menu_state = "platform_folder_config"
|
||||
config.needs_redraw = True
|
||||
|
||||
@@ -5,12 +5,14 @@ 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,
|
||||
check_web_service_status, check_custom_dns_status, load_api_keys,
|
||||
_get_dest_folder_name, find_file_with_or_without_extension,
|
||||
get_connection_status_targets, get_connection_status_snapshot)
|
||||
_get_dest_folder_name, find_file_with_or_without_extension, find_matching_files,
|
||||
get_connection_status_targets, get_connection_status_snapshot,
|
||||
get_clean_display_name, get_existing_history_matches)
|
||||
import logging
|
||||
import math
|
||||
from history import load_history, is_game_downloaded
|
||||
@@ -242,6 +244,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 +1019,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 +1128,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 +1293,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 +1439,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 +1469,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 +1815,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 +1897,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 +1907,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 +1923,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 +1935,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 +2104,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 +2130,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 +2158,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 +2180,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)
|
||||
@@ -1813,14 +2359,14 @@ def draw_history_list(screen):
|
||||
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)
|
||||
# Prioritize the game title by shrinking size/status columns.
|
||||
column_width_percentages = {
|
||||
"platform": 0.13,
|
||||
"game_name": 0.25,
|
||||
"ext": 0.08,
|
||||
"folder": 0.12,
|
||||
"size": 0.08,
|
||||
"status": 0.34
|
||||
"game_name": 0.40,
|
||||
"ext": 0.07,
|
||||
"folder": 0.16,
|
||||
"size": 0.06,
|
||||
"status": 0.18
|
||||
}
|
||||
available_width = int(0.95 * config.screen_width - 60) # Total available width for columns
|
||||
col_platform_width = int(available_width * column_width_percentages["platform"])
|
||||
@@ -1926,8 +2472,9 @@ def draw_history_list(screen):
|
||||
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")
|
||||
ext_text = get_display_extension(game_name)
|
||||
raw_game_name = entry.get("game_name", "Inconnu")
|
||||
game_name = entry.get("display_name") or get_clean_display_name(raw_game_name, platform)
|
||||
ext_text = get_display_extension(raw_game_name)
|
||||
folder_text = _get_dest_folder_name(platform)
|
||||
|
||||
# Correction du calcul de la taille
|
||||
@@ -2002,7 +2549,7 @@ def draw_history_list(screen):
|
||||
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(str(game_name).rsplit('.', 1)[0] if '.' in str(game_name) else str(game_name), config.small_font, col_game_width - 10)
|
||||
game_text = truncate_text_middle(str(game_name), config.small_font, col_game_width - 10, is_filename=False)
|
||||
ext_text = truncate_text_end(ext_text, config.small_font, col_ext_width - 10)
|
||||
folder_text = truncate_text_end(folder_text, config.small_font, col_folder_width - 10)
|
||||
size_text = truncate_text_end(size_text, config.small_font, col_size_width - 10)
|
||||
@@ -2338,6 +2885,11 @@ def draw_controls(screen, menu_state, current_music_name=None, music_popup_start
|
||||
("history", _("controls_action_close_history")),
|
||||
("cancel", _("controls_cancel_back")),
|
||||
],
|
||||
"history_show_folder": [
|
||||
("confirm", _("button_OK")),
|
||||
("clear_history", _("history_move_action")),
|
||||
("cancel", _("controls_cancel_back")),
|
||||
],
|
||||
"scraper": [
|
||||
("confirm", _("controls_confirm_select")),
|
||||
("cancel", _("controls_cancel_back")),
|
||||
@@ -2354,6 +2906,7 @@ def draw_controls(screen, menu_state, current_music_name=None, music_popup_start
|
||||
],
|
||||
"folder_browser": [
|
||||
("confirm", _("folder_browser_enter")),
|
||||
(("page_up", "page_down"), _("controls_pages")),
|
||||
("history", _("folder_browser_select")),
|
||||
("clear_history", _("folder_new_folder")),
|
||||
("cancel", _("controls_cancel_back")),
|
||||
@@ -2430,23 +2983,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:
|
||||
@@ -4159,6 +4700,8 @@ def draw_folder_browser(screen):
|
||||
# Titre selon le mode
|
||||
if browser_mode == "roms_root":
|
||||
title = _("folder_browser_title_roms_root") if _ else "Select default ROMs folder"
|
||||
elif browser_mode == "history_move":
|
||||
title = _("folder_browser_title_history_move") if _ else "Select destination folder"
|
||||
else:
|
||||
title = _("folder_browser_title").format(platform_name) if _ else f"Select folder for {platform_name}"
|
||||
title_text = config.font.render(title, True, THEME_COLORS["text"])
|
||||
@@ -4178,8 +4721,17 @@ def draw_folder_browser(screen):
|
||||
list_y = panel_y + 100
|
||||
list_height = panel_height - 180
|
||||
item_height = max(35, config.small_font.get_height() + 10)
|
||||
visible_items = min(visible_items, list_height // item_height)
|
||||
visible_items = max(1, list_height // item_height)
|
||||
config.folder_browser_visible_items = visible_items
|
||||
|
||||
max_scroll_offset = max(0, len(items) - visible_items)
|
||||
if scroll_offset > max_scroll_offset:
|
||||
scroll_offset = max_scroll_offset
|
||||
config.folder_browser_scroll_offset = scroll_offset
|
||||
|
||||
if selection >= len(items) and items:
|
||||
selection = len(items) - 1
|
||||
config.folder_browser_selection = selection
|
||||
|
||||
# Afficher les éléments visibles
|
||||
for i in range(visible_items):
|
||||
@@ -4513,6 +5065,12 @@ def draw_history_game_options(screen):
|
||||
dest_folder = _get_dest_folder_name(platform)
|
||||
base_path = os.path.join(config.ROMS_FOLDER, dest_folder)
|
||||
file_exists, actual_filename, actual_path = find_file_with_or_without_extension(base_path, game_name)
|
||||
actual_matches = find_matching_files(base_path, game_name)
|
||||
if not actual_matches:
|
||||
actual_matches = get_existing_history_matches(entry)
|
||||
if actual_matches:
|
||||
actual_filename, actual_path = actual_matches[0]
|
||||
file_exists = True
|
||||
|
||||
# Déterminer les options disponibles selon le statut
|
||||
options = []
|
||||
@@ -4623,6 +5181,7 @@ def draw_history_show_folder(screen):
|
||||
# Utiliser le chemin réel trouvé (avec ou sans extension)
|
||||
actual_path = getattr(config, 'history_actual_path', None)
|
||||
actual_filename = getattr(config, 'history_actual_filename', None)
|
||||
actual_matches = getattr(config, 'history_actual_matches', None) or []
|
||||
|
||||
if not actual_path or not actual_filename:
|
||||
# Fallback si pas trouvé
|
||||
@@ -4631,7 +5190,7 @@ def draw_history_show_folder(screen):
|
||||
actual_filename = game_name
|
||||
|
||||
# Vérifier si le fichier existe
|
||||
file_exists = os.path.exists(actual_path)
|
||||
file_exists = bool(actual_matches) or os.path.exists(actual_path)
|
||||
|
||||
# Message
|
||||
title = _("history_folder_path_label") if _ else "Destination path:"
|
||||
@@ -4642,8 +5201,18 @@ def draw_history_show_folder(screen):
|
||||
margin_top_bottom = 30
|
||||
rect_width = min(config.screen_width - 100, 800)
|
||||
|
||||
# Wrapper le chemin avec la bonne largeur (largeur de la boîte - marges)
|
||||
path_wrapped = wrap_text(actual_path, config.small_font, rect_width - 80)
|
||||
# Wrapper les chemins avec la bonne largeur (largeur de la boîte - marges)
|
||||
if actual_matches:
|
||||
path_wrapped = []
|
||||
for index, (match_filename, match_path) in enumerate(actual_matches, start=1):
|
||||
wrapped_match = wrap_text(match_path, config.small_font, rect_width - 80)
|
||||
if wrapped_match:
|
||||
path_wrapped.append(f"{index}. {wrapped_match[0]}")
|
||||
path_wrapped.extend(wrapped_match[1:])
|
||||
else:
|
||||
path_wrapped.append(f"{index}. {match_path}")
|
||||
else:
|
||||
path_wrapped = wrap_text(actual_path, config.small_font, rect_width - 80)
|
||||
|
||||
# Ajouter un message si le fichier n'existe pas
|
||||
warning_lines = []
|
||||
|
||||
@@ -307,6 +307,7 @@
|
||||
"history_option_delete_game": "Spiel löschen",
|
||||
"history_option_error_info": "Fehlerdetails",
|
||||
"history_option_retry": "Download wiederholen",
|
||||
"history_move_action": "Verschieben",
|
||||
"history_option_back": "Zurück",
|
||||
"history_folder_path_label": "Zielpfad:",
|
||||
"history_scraper_not_implemented": "Scraper noch nicht implementiert",
|
||||
@@ -316,6 +317,8 @@
|
||||
"history_extracted": "Extrahiert",
|
||||
"history_delete_success": "Spiel erfolgreich gelöscht",
|
||||
"history_delete_error": "Fehler beim Löschen des Spiels: {0}",
|
||||
"history_move_success": "{0} Datei(en) verschoben nach: {1}",
|
||||
"history_move_error": "Fehler beim Verschieben: {0}",
|
||||
"history_error_details_title": "Fehlerdetails",
|
||||
"history_no_error_message": "Keine Fehlermeldung verfügbar",
|
||||
"web_title": "RGSX Web-Oberfläche",
|
||||
@@ -480,6 +483,7 @@
|
||||
"platform_folder_set": "Ordner für {0} festgelegt: {1}",
|
||||
"platform_folder_default_path": "Standard: {0}",
|
||||
"folder_browser_title": "Ordner für {0} auswählen",
|
||||
"folder_browser_title_history_move": "Zielordner auswählen",
|
||||
"folder_browser_parent": "Übergeordneter Ordner",
|
||||
"folder_browser_enter": "Öffnen",
|
||||
"folder_browser_select": "Auswählen",
|
||||
|
||||
@@ -306,6 +306,7 @@
|
||||
"history_option_delete_game": "Delete game",
|
||||
"history_option_error_info": "Error details",
|
||||
"history_option_retry": "Retry download",
|
||||
"history_move_action": "Move",
|
||||
"menu_scan_owned_roms": "Scan owned ROMs",
|
||||
"popup_scan_owned_roms_done": "ROM scan complete: {0} games added across {1} platforms",
|
||||
"popup_scan_owned_roms_error": "ROM scan error: {0}",
|
||||
@@ -318,6 +319,8 @@
|
||||
"history_extracted": "Extracted",
|
||||
"history_delete_success": "Game deleted successfully",
|
||||
"history_delete_error": "Error deleting game: {0}",
|
||||
"history_move_success": "Moved {0} file(s) to: {1}",
|
||||
"history_move_error": "Error while moving files: {0}",
|
||||
"history_error_details_title": "Error Details",
|
||||
"history_no_error_message": "No error message available",
|
||||
"web_title": "RGSX Web Interface",
|
||||
@@ -480,6 +483,7 @@
|
||||
"platform_folder_set": "Folder set for {0}: {1}",
|
||||
"platform_folder_default_path": "Default: {0}",
|
||||
"folder_browser_title": "Select folder for {0}",
|
||||
"folder_browser_title_history_move": "Select destination folder",
|
||||
"folder_browser_parent": "Parent folder",
|
||||
"folder_browser_enter": "Enter",
|
||||
"folder_browser_select": "Select",
|
||||
|
||||
@@ -307,6 +307,7 @@
|
||||
"history_option_delete_game": "Eliminar juego",
|
||||
"history_option_error_info": "Detalles del error",
|
||||
"history_option_retry": "Reintentar descarga",
|
||||
"history_move_action": "Mover",
|
||||
"history_option_back": "Volver",
|
||||
"history_folder_path_label": "Ruta de destino:",
|
||||
"history_scraper_not_implemented": "Scraper aún no implementado",
|
||||
@@ -316,6 +317,8 @@
|
||||
"history_extracted": "Extraído",
|
||||
"history_delete_success": "Juego eliminado con éxito",
|
||||
"history_delete_error": "Error al eliminar juego: {0}",
|
||||
"history_move_success": "{0} archivo(s) movido(s) a: {1}",
|
||||
"history_move_error": "Error al mover los archivos: {0}",
|
||||
"history_error_details_title": "Detalles del error",
|
||||
"history_no_error_message": "No hay mensaje de error disponible",
|
||||
"web_title": "Interfaz Web RGSX",
|
||||
@@ -478,6 +481,7 @@
|
||||
"platform_folder_set": "Carpeta establecida para {0}: {1}",
|
||||
"platform_folder_default_path": "Por defecto: {0}",
|
||||
"folder_browser_title": "Seleccionar carpeta para {0}",
|
||||
"folder_browser_title_history_move": "Seleccionar carpeta de destino",
|
||||
"folder_browser_parent": "Carpeta superior",
|
||||
"folder_browser_enter": "Entrar",
|
||||
"folder_browser_select": "Seleccionar",
|
||||
|
||||
@@ -306,6 +306,7 @@
|
||||
"history_option_delete_game": "Supprimer le jeu",
|
||||
"history_option_error_info": "Détails de l'erreur",
|
||||
"history_option_retry": "Retenter le téléchargement",
|
||||
"history_move_action": "Déplacer",
|
||||
"menu_scan_owned_roms": "Scanner les ROMs présentes",
|
||||
"popup_scan_owned_roms_done": "Scan ROMs terminé : {0} jeux ajoutés sur {1} plateformes",
|
||||
"popup_scan_owned_roms_error": "Erreur scan ROMs : {0}",
|
||||
@@ -318,6 +319,8 @@
|
||||
"history_extracted": "Extrait",
|
||||
"history_delete_success": "Jeu supprimé avec succès",
|
||||
"history_delete_error": "Erreur lors de la suppression du jeu : {0}",
|
||||
"history_move_success": "{0} fichier(s) déplacé(s) vers : {1}",
|
||||
"history_move_error": "Erreur lors du déplacement : {0}",
|
||||
"history_error_details_title": "Détails de l'erreur",
|
||||
"history_no_error_message": "Aucun message d'erreur disponible",
|
||||
"web_title": "Interface Web RGSX",
|
||||
@@ -480,6 +483,7 @@
|
||||
"platform_folder_set": "Dossier défini pour {0}: {1}",
|
||||
"platform_folder_default_path": "Par défaut: {0}",
|
||||
"folder_browser_title": "Sélectionner le dossier pour {0}",
|
||||
"folder_browser_title_history_move": "Sélectionner le dossier de destination",
|
||||
"folder_browser_parent": "Dossier parent",
|
||||
"folder_browser_enter": "Entrer",
|
||||
"folder_browser_select": "Valider",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"history_option_delete_game": "Elimina gioco",
|
||||
"history_option_error_info": "Dettagli errore",
|
||||
"history_option_retry": "Riprova download",
|
||||
"history_move_action": "Sposta",
|
||||
"history_option_back": "Indietro",
|
||||
"history_folder_path_label": "Percorso destinazione:",
|
||||
"history_scraper_not_implemented": "Scraper non ancora implementato",
|
||||
@@ -311,6 +312,8 @@
|
||||
"history_extracted": "Estratto",
|
||||
"history_delete_success": "Gioco eliminato con successo",
|
||||
"history_delete_error": "Errore durante l'eliminazione del gioco: {0}",
|
||||
"history_move_success": "{0} file spostato/i in: {1}",
|
||||
"history_move_error": "Errore durante lo spostamento: {0}",
|
||||
"history_error_details_title": "Dettagli errore",
|
||||
"history_no_error_message": "Nessun messaggio di errore disponibile",
|
||||
"web_title": "Interfaccia Web RGSX",
|
||||
@@ -476,6 +479,7 @@
|
||||
"platform_folder_set": "Cartella impostata per {0}: {1}",
|
||||
"platform_folder_default_path": "Predefinito: {0}",
|
||||
"folder_browser_title": "Seleziona cartella per {0}",
|
||||
"folder_browser_title_history_move": "Seleziona cartella di destinazione",
|
||||
"folder_browser_parent": "Cartella superiore",
|
||||
"folder_browser_enter": "Entra",
|
||||
"folder_browser_select": "Seleziona",
|
||||
|
||||
@@ -308,6 +308,7 @@
|
||||
"history_option_delete_game": "Excluir jogo",
|
||||
"history_option_error_info": "Detalhes do erro",
|
||||
"history_option_retry": "Tentar novamente",
|
||||
"history_move_action": "Mover",
|
||||
"history_option_back": "Voltar",
|
||||
"history_folder_path_label": "Caminho de destino:",
|
||||
"history_scraper_not_implemented": "Scraper ainda não implementado",
|
||||
@@ -317,6 +318,8 @@
|
||||
"history_extracted": "Extraído",
|
||||
"history_delete_success": "Jogo excluído com sucesso",
|
||||
"history_delete_error": "Erro ao excluir jogo: {0}",
|
||||
"history_move_success": "{0} arquivo(s) movido(s) para: {1}",
|
||||
"history_move_error": "Erro ao mover os arquivos: {0}",
|
||||
"history_error_details_title": "Detalhes do erro",
|
||||
"history_no_error_message": "Nenhuma mensagem de erro disponível",
|
||||
"web_title": "Interface Web RGSX",
|
||||
@@ -480,6 +483,7 @@
|
||||
"platform_folder_set": "Pasta definida para {0}: {1}",
|
||||
"platform_folder_default_path": "Padrão: {0}",
|
||||
"folder_browser_title": "Selecionar pasta para {0}",
|
||||
"folder_browser_title_history_move": "Selecionar pasta de destino",
|
||||
"folder_browser_parent": "Pasta superior",
|
||||
"folder_browser_enter": "Entrar",
|
||||
"folder_browser_select": "Selecionar",
|
||||
|
||||
@@ -15,7 +15,7 @@ try:
|
||||
except Exception:
|
||||
pygame = None # type: ignore
|
||||
from config import OTA_VERSION_ENDPOINT,APP_FOLDER, UPDATE_FOLDER, OTA_UPDATE_ZIP
|
||||
from utils import sanitize_filename, extract_zip, extract_rar, extract_7z, load_api_key_1fichier, load_api_key_alldebrid, normalize_platform_name, load_api_keys, load_archive_org_cookie
|
||||
from utils import sanitize_filename, extract_zip, extract_rar, extract_7z, load_api_key_1fichier, load_api_key_alldebrid, normalize_platform_name, load_api_keys, load_archive_org_cookie, get_clean_display_name
|
||||
from history import save_history
|
||||
from display import show_toast
|
||||
import logging
|
||||
@@ -874,6 +874,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
entry["downloaded_size"] = 0
|
||||
entry["platform"] = platform
|
||||
entry["game_name"] = game_name
|
||||
entry["display_name"] = get_clean_display_name(game_name, platform)
|
||||
entry["timestamp"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
entry["task_id"] = task_id
|
||||
break
|
||||
@@ -883,6 +884,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
config.history.append({
|
||||
"platform": platform,
|
||||
"game_name": game_name,
|
||||
"display_name": get_clean_display_name(game_name, platform),
|
||||
"url": url,
|
||||
"status": "Downloading",
|
||||
"progress": 0,
|
||||
@@ -1766,6 +1768,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
entry["downloaded_size"] = 0
|
||||
entry["platform"] = platform
|
||||
entry["game_name"] = game_name
|
||||
entry["display_name"] = get_clean_display_name(game_name, platform)
|
||||
entry["timestamp"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
entry["task_id"] = task_id
|
||||
break
|
||||
@@ -1775,6 +1778,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
config.history.append({
|
||||
"platform": platform,
|
||||
"game_name": game_name,
|
||||
"display_name": get_clean_display_name(game_name, platform),
|
||||
"url": url,
|
||||
"status": "Downloading",
|
||||
"progress": 0,
|
||||
|
||||
@@ -21,7 +21,7 @@ from datetime import datetime, timezone
|
||||
from email.utils import formatdate, parsedate_to_datetime
|
||||
import config
|
||||
from history import load_history, save_history
|
||||
from utils import load_sources, load_games, extract_data
|
||||
from utils import load_sources, load_games, extract_data, get_clean_display_name
|
||||
from network import download_rom, download_from_1fichier
|
||||
from pathlib import Path
|
||||
from rgsx_settings import get_language
|
||||
@@ -1243,6 +1243,7 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
queue_history_entry = {
|
||||
'platform': platform,
|
||||
'game_name': game_name,
|
||||
'display_name': get_clean_display_name(game_name, platform),
|
||||
'status': 'Queued',
|
||||
'url': game_url,
|
||||
'progress': 0,
|
||||
@@ -1280,6 +1281,7 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
download_history_entry = {
|
||||
'platform': platform,
|
||||
'game_name': game_name,
|
||||
'display_name': get_clean_display_name(game_name, platform),
|
||||
'status': 'Downloading',
|
||||
'url': game_url,
|
||||
'progress': 0,
|
||||
|
||||
@@ -33,6 +33,36 @@ import tempfile
|
||||
logger = logging.getLogger(__name__)
|
||||
# Désactiver les logs DEBUG de urllib3 e requests pour supprimer les messages de connexion HTTP
|
||||
|
||||
|
||||
def get_clean_display_name(raw_name, platform_id=None):
|
||||
"""Return a user-facing game title from a raw file/path entry."""
|
||||
text = str(raw_name or "").strip()
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
normalized = text.replace("\\", "/")
|
||||
leaf_name = normalized.rsplit("/", 1)[-1]
|
||||
display_name = Path(leaf_name).stem.strip()
|
||||
|
||||
prefixes = []
|
||||
if platform_id:
|
||||
prefixes.append(str(platform_id).strip())
|
||||
platform_label = getattr(config, "platform_names", {}).get(platform_id)
|
||||
if platform_label:
|
||||
prefixes.append(str(platform_label).strip())
|
||||
|
||||
for prefix in prefixes:
|
||||
if not prefix:
|
||||
continue
|
||||
pattern = rf"^{re.escape(prefix)}[\s\-_:]+"
|
||||
updated_name = re.sub(pattern, "", display_name, flags=re.IGNORECASE).strip()
|
||||
if updated_name:
|
||||
display_name = updated_name
|
||||
|
||||
return display_name.strip(" -_/")
|
||||
|
||||
_games_cache = {}
|
||||
|
||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||
|
||||
@@ -1195,9 +1225,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)
|
||||
|
||||
@@ -1239,11 +1275,17 @@ def load_games(platform_id:str) -> list[Game]:
|
||||
|
||||
games_list: list[Game] = []
|
||||
for name, url, size in normalized:
|
||||
display_name = Path(name).stem
|
||||
display_name = display_name.replace(platform_id, "")
|
||||
display_name = get_clean_display_name(name, 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 []
|
||||
|
||||
@@ -3020,26 +3062,150 @@ def normalize_platform_name(platform):
|
||||
return platform.lower().replace(" ", "")
|
||||
|
||||
|
||||
def find_matching_files(base_path, filename):
|
||||
"""Return all matching files for a requested download name within a ROM folder."""
|
||||
if not base_path or not os.path.exists(base_path):
|
||||
return []
|
||||
|
||||
candidate_name = Path(str(filename or "")).name
|
||||
requested_stem, requested_ext = os.path.splitext(candidate_name)
|
||||
requested_normalized = re.sub(r'\s+', ' ', re.sub(r'\s*[\[(][^\])]*[\])]', '', requested_stem)).strip().lower()
|
||||
archive_exts = {'.zip', '.7z', '.rar', '.tar', '.gz', '.xz', '.bz2'}
|
||||
matches = []
|
||||
seen_paths = set()
|
||||
|
||||
full_path = os.path.join(base_path, candidate_name)
|
||||
if os.path.exists(full_path) and os.path.isfile(full_path):
|
||||
seen_paths.add(os.path.normcase(full_path))
|
||||
matches.append((1000, candidate_name, full_path))
|
||||
|
||||
for existing_file in os.listdir(base_path):
|
||||
existing_path = os.path.join(base_path, existing_file)
|
||||
if not os.path.isfile(existing_path):
|
||||
continue
|
||||
|
||||
normalized_path = os.path.normcase(existing_path)
|
||||
if normalized_path in seen_paths:
|
||||
continue
|
||||
|
||||
existing_stem, existing_ext = os.path.splitext(existing_file)
|
||||
score = None
|
||||
|
||||
if requested_stem and existing_stem == requested_stem:
|
||||
score = 900
|
||||
else:
|
||||
existing_normalized = re.sub(r'\s+', ' ', re.sub(r'\s*[\[(][^\])]*[\])]', '', existing_stem)).strip().lower()
|
||||
if requested_normalized and existing_normalized and existing_normalized == requested_normalized:
|
||||
score = 0
|
||||
if requested_ext and existing_ext.lower() == requested_ext.lower():
|
||||
score += 4
|
||||
if existing_ext.lower() not in archive_exts:
|
||||
score += 3
|
||||
score -= abs(len(existing_stem) - len(requested_stem))
|
||||
|
||||
if score is not None:
|
||||
seen_paths.add(normalized_path)
|
||||
matches.append((score, existing_file, existing_path))
|
||||
|
||||
matches.sort(key=lambda item: item[0], reverse=True)
|
||||
return [(actual_filename, actual_path) for _, actual_filename, actual_path in matches]
|
||||
|
||||
|
||||
def get_existing_history_matches(entry):
|
||||
"""Return persisted moved paths that still exist for a history entry."""
|
||||
if not isinstance(entry, dict):
|
||||
return []
|
||||
|
||||
moved_paths = entry.get("moved_paths", []) or []
|
||||
matches = []
|
||||
seen_paths = set()
|
||||
|
||||
for raw_path in moved_paths:
|
||||
if not raw_path:
|
||||
continue
|
||||
|
||||
actual_path = os.path.abspath(str(raw_path))
|
||||
normalized_path = os.path.normcase(actual_path)
|
||||
if normalized_path in seen_paths or not os.path.isfile(actual_path):
|
||||
continue
|
||||
|
||||
seen_paths.add(normalized_path)
|
||||
matches.append((os.path.basename(actual_path), actual_path))
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
def move_files_to_directory(file_paths, destination_dir):
|
||||
"""Move files to a destination directory, avoiding name collisions."""
|
||||
if not destination_dir:
|
||||
return False, [], "Destination directory is empty"
|
||||
|
||||
if not any(file_paths or []):
|
||||
return False, [], "No files to move"
|
||||
|
||||
try:
|
||||
os.makedirs(destination_dir, exist_ok=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Impossible de créer le dossier de destination {destination_dir}: {e}")
|
||||
return False, [], str(e)
|
||||
|
||||
moved_matches = []
|
||||
seen_sources = set()
|
||||
reserved_targets = set()
|
||||
|
||||
for raw_source in file_paths:
|
||||
if not raw_source:
|
||||
continue
|
||||
|
||||
source_path = os.path.abspath(str(raw_source))
|
||||
normalized_source = os.path.normcase(source_path)
|
||||
if normalized_source in seen_sources:
|
||||
continue
|
||||
seen_sources.add(normalized_source)
|
||||
|
||||
if not os.path.isfile(source_path):
|
||||
error_message = f"File not found: {source_path}"
|
||||
logger.warning(error_message)
|
||||
return False, moved_matches, error_message
|
||||
|
||||
source_name = os.path.basename(source_path)
|
||||
target_path = os.path.join(destination_dir, source_name)
|
||||
target_root, target_ext = os.path.splitext(target_path)
|
||||
suffix = 1
|
||||
|
||||
while os.path.normcase(target_path) in reserved_targets or (
|
||||
os.path.exists(target_path)
|
||||
and os.path.normcase(target_path) != os.path.normcase(source_path)
|
||||
):
|
||||
target_path = f"{target_root} ({suffix}){target_ext}"
|
||||
suffix += 1
|
||||
|
||||
reserved_targets.add(os.path.normcase(target_path))
|
||||
|
||||
try:
|
||||
if os.path.normcase(source_path) != os.path.normcase(target_path):
|
||||
shutil.move(source_path, target_path)
|
||||
logger.info(f"Fichier déplacé: {source_path} -> {target_path}")
|
||||
else:
|
||||
logger.debug(f"Déplacement ignoré, même chemin source/destination: {source_path}")
|
||||
moved_matches.append((os.path.basename(target_path), target_path))
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du déplacement de {source_path} vers {target_path}: {e}")
|
||||
return False, moved_matches, str(e)
|
||||
|
||||
return True, moved_matches, None
|
||||
|
||||
|
||||
def find_file_with_or_without_extension(base_path, filename):
|
||||
"""
|
||||
Cherche un fichier, avec son extension ou sans (cherche jeuxxx.* si jeuxxx.zip n'existe pas).
|
||||
Retourne (file_exists, actual_filename, actual_path).
|
||||
"""
|
||||
# 1. Tester d'abord le fichier tel quel
|
||||
full_path = os.path.join(base_path, filename)
|
||||
if os.path.exists(full_path):
|
||||
return True, filename, full_path
|
||||
|
||||
# 2. Si pas trouvé et que le fichier a une extension, chercher sans extension
|
||||
name_without_ext, ext = os.path.splitext(filename)
|
||||
if ext: # Si le fichier a une extension
|
||||
# Chercher tous les fichiers commençant par le nom sans extension
|
||||
if os.path.exists(base_path):
|
||||
for existing_file in os.listdir(base_path):
|
||||
existing_name, _ = os.path.splitext(existing_file)
|
||||
if existing_name == name_without_ext:
|
||||
found_path = os.path.join(base_path, existing_file)
|
||||
return True, existing_file, found_path
|
||||
|
||||
# 3. Fichier non trouvé
|
||||
return False, filename, full_path
|
||||
candidate_name = Path(str(filename or "")).name
|
||||
full_path = os.path.join(base_path, candidate_name)
|
||||
matches = find_matching_files(base_path, candidate_name)
|
||||
if matches:
|
||||
actual_filename, actual_path = matches[0]
|
||||
return True, actual_filename, actual_path
|
||||
|
||||
return False, candidate_name, full_path
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2.6.0.3"
|
||||
"version": "2.6.1.2"
|
||||
}
|
||||
Reference in New Issue
Block a user