v2.0.0.0 - important update to game list structure and reading . you can now add a json and it will be automatically added to gamelist. Added menu to filter systems showing in main screen. Add size showing if available on game list view.

add custom game list source (advanced users only
This commit is contained in:
skymike03
2025-09-05 01:49:06 +02:00
parent 1c923086cf
commit 62d9bba85d
14 changed files with 685 additions and 238 deletions

View File

@@ -265,6 +265,12 @@ async def main():
#logger.debug(f"Événement transmis à handle_controls dans pause_menu: {event.type}") #logger.debug(f"Événement transmis à handle_controls dans pause_menu: {event.type}")
continue continue
# Gestion des événements pour le menu de filtrage des plateformes
if config.menu_state == "filter_platforms":
action = handle_controls(event, sources, joystick, screen)
config.needs_redraw = True
continue
if config.menu_state == "accessibility_menu": if config.menu_state == "accessibility_menu":
from accessibility import handle_accessibility_events from accessibility import handle_accessibility_events
if handle_accessibility_events(event): if handle_accessibility_events(event):
@@ -342,9 +348,14 @@ async def main():
logger.debug("Action quit détectée, arrêt de l'application") logger.debug("Action quit détectée, arrêt de l'application")
elif action == "download" and config.menu_state == "game" and config.filtered_games: elif action == "download" and config.menu_state == "game" and config.filtered_games:
game = config.filtered_games[config.current_game] game = config.filtered_games[config.current_game]
game_name = game[0] if isinstance(game, (list, tuple)) else game if isinstance(game, (list, tuple)):
platform = config.platforms[config.current_platform]["name"] # Utiliser le nom de la plateforme game_name = game[0]
url = game[1] if isinstance(game, (list, tuple)) and len(game) > 1 else None url = game[1] if len(game) > 1 else None
else: # fallback str
game_name = str(game)
url = None
# Nouveau schéma: config.platforms contient déjà platform_name (string)
platform = config.platforms[config.current_platform]
if url: if url:
logger.debug(f"Vérification pour {game_name}, URL: {url}") logger.debug(f"Vérification pour {game_name}, URL: {url}")
# Ajouter une entrée temporaire à l'historique # Ajouter une entrée temporaire à l'historique
@@ -422,8 +433,12 @@ async def main():
platform = entry["platform"] platform = entry["platform"]
game_name = entry["game_name"] game_name = entry["game_name"]
for game in config.games: for game in config.games:
if game[0] == game_name and config.platforms[config.current_platform] == platform: if isinstance(game, (list, tuple)) and game and game[0] == game_name and config.platforms[config.current_platform] == platform:
url = game[1] url = game[1] if len(game) > 1 else None
else:
continue
if not url:
continue
logger.debug(f"Vérification pour retéléchargement de {game_name}, URL: {url}") logger.debug(f"Vérification pour retéléchargement de {game_name}, URL: {url}")
if is_1fichier_url(url): if is_1fichier_url(url):
if not config.API_KEY_1FICHIER: if not config.API_KEY_1FICHIER:
@@ -604,6 +619,9 @@ async def main():
elif config.menu_state == "pause_menu": elif config.menu_state == "pause_menu":
draw_pause_menu(screen, config.selected_option) draw_pause_menu(screen, config.selected_option)
#logger.debug("Rendu de draw_pause_menu") #logger.debug("Rendu de draw_pause_menu")
elif config.menu_state == "filter_platforms":
from display import draw_filter_platforms_menu
draw_filter_platforms_menu(screen)
elif config.menu_state == "controls_help": elif config.menu_state == "controls_help":
draw_controls_help(screen, config.previous_menu_state) draw_controls_help(screen, config.previous_menu_state)
elif config.menu_state == "history": elif config.menu_state == "history":

BIN
ports/RGSX/assets/unrar.exe Normal file

Binary file not shown.

BIN
ports/RGSX/assets/xdvdfs Normal file

Binary file not shown.

View File

@@ -2,10 +2,10 @@ import pygame # type: ignore
import os import os
import logging import logging
import platform import platform
from rgsx_settings import load_rgsx_settings, save_rgsx_settings, migrate_old_settings from rgsx_settings import load_rgsx_settings, save_rgsx_settings
# Version actuelle de l'application # Version actuelle de l'application
app_version = "1.9.9.4" app_version = "2.0.0.0"
def get_operating_system(): def get_operating_system():
"""Renvoie le nom du système d'exploitation.""" """Renvoie le nom du système d'exploitation."""
@@ -85,28 +85,22 @@ GAMELISTXML = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(APP_F
UPDATE_FOLDER = os.path.join(APP_FOLDER, "update") UPDATE_FOLDER = os.path.join(APP_FOLDER, "update")
LANGUAGES_FOLDER = os.path.join(APP_FOLDER, "languages") LANGUAGES_FOLDER = os.path.join(APP_FOLDER, "languages")
JSON_EXTENSIONS = os.path.join(APP_FOLDER, "rom_extensions.json") JSON_EXTENSIONS = os.path.join(APP_FOLDER, "rom_extensions.json")
MUSIC_FOLDER = os.path.join(APP_FOLDER, "assets", "music")
#Dossier /saves/ports/rgsx #Dossier /saves/ports/rgsx
IMAGES_FOLDER = os.path.join(SAVE_FOLDER, "images", "systemes") IMAGES_FOLDER = os.path.join(SAVE_FOLDER, "images")
GAMES_FOLDER = os.path.join(SAVE_FOLDER, "games") GAMES_FOLDER = os.path.join(SAVE_FOLDER, "games")
SOURCES_FILE = os.path.join(SAVE_FOLDER, "sources.json") SOURCES_FILE = os.path.join(SAVE_FOLDER, "systems_list.json")
CONTROLS_CONFIG_PATH = os.path.join(SAVE_FOLDER, "controls.json") CONTROLS_CONFIG_PATH = os.path.join(SAVE_FOLDER, "controls.json")
HISTORY_PATH = os.path.join(SAVE_FOLDER, "history.json") HISTORY_PATH = os.path.join(SAVE_FOLDER, "history.json")
API_KEY_1FICHIER = os.path.join(SAVE_FOLDER, "1fichierAPI.txt")
# Nouveau fichier unifié pour les paramètres RGSX
RGSX_SETTINGS_PATH = os.path.join(SAVE_FOLDER, "rgsx_settings.json") RGSX_SETTINGS_PATH = os.path.join(SAVE_FOLDER, "rgsx_settings.json")
# Anciens chemins des fichiers de config (conservés temporairement pour la migration)
ACCESSIBILITY_FOLDER = os.path.join(SAVE_FOLDER, "accessibility.json")
LANGUAGE_CONFIG_PATH = os.path.join(SAVE_FOLDER, "language.json")
MUSIC_CONFIG_PATH = os.path.join(SAVE_FOLDER, "music_config.json")
SYMLINK_SETTINGS_PATH = os.path.join(SAVE_FOLDER, "symlink_settings.json")
# URL # URL
OTA_SERVER_URL = "https://retrogamesets.fr/softs/" OTA_SERVER_URL = "https://retrogamesets.fr/softs/"
OTA_VERSION_ENDPOINT = os.path.join(OTA_SERVER_URL, "version.json") OTA_VERSION_ENDPOINT = os.path.join(OTA_SERVER_URL, "version.json")
OTA_UPDATE_ZIP = os.path.join(OTA_SERVER_URL, "RGSX.zip") OTA_UPDATE_ZIP = os.path.join(OTA_SERVER_URL, "RGSX.zip")
OTA_data_ZIP = os.path.join(OTA_SERVER_URL, "rgsx-data.zip") OTA_data_ZIP = os.path.join(OTA_SERVER_URL, "test.zip")
#CHEMINS DES EXECUTABLES #CHEMINS DES EXECUTABLES
UNRAR_EXE = os.path.join(APP_FOLDER,"assets", "unrar.exe") UNRAR_EXE = os.path.join(APP_FOLDER,"assets", "unrar.exe")
@@ -197,6 +191,12 @@ batch_download_indices = [] # File d'attente des indices de jeux à traiter en
batch_in_progress = False # Indique qu'un lot est en cours batch_in_progress = False # Indique qu'un lot est en cours
batch_pending_game = None # Données du jeu en attente de confirmation d'extension batch_pending_game = None # Données du jeu en attente de confirmation d'extension
# --- Filtre plateformes (UI) ---
selected_filter_index = 0 # index dans la liste visible triée
filter_platforms_scroll_offset = 0 # défilement si liste longue
filter_platforms_dirty = False # indique si modifications non sauvegardées
filter_platforms_selection = [] # copie de travail des plateformes visibles (bool masque?) structure: list of (name, hidden_bool)
GRID_COLS = 3 # Number of columns in the platform grid GRID_COLS = 3 # Number of columns in the platform grid
GRID_ROWS = 4 # Number of rows in the platform grid GRID_ROWS = 4 # Number of rows in the platform grid

View File

@@ -32,7 +32,7 @@ VALID_STATES = [
"platform", "game", "confirm_exit", "platform", "game", "confirm_exit",
"extension_warning", "pause_menu", "controls_help", "history", "controls_mapping", "extension_warning", "pause_menu", "controls_help", "history", "controls_mapping",
"redownload_game_cache", "restart_popup", "error", "loading", "confirm_clear_history", "redownload_game_cache", "restart_popup", "error", "loading", "confirm_clear_history",
"language_select" "language_select", "filter_platforms"
] ]
def validate_menu_state(state): def validate_menu_state(state):
@@ -1036,7 +1036,13 @@ def handle_controls(event, sources, joystick, screen):
config.menu_state = "accessibility_menu" config.menu_state = "accessibility_menu"
config.needs_redraw = True config.needs_redraw = True
logger.debug("Passage au menu accessibilité") logger.debug("Passage au menu accessibilité")
elif config.selected_option == 5: # Source toggle elif config.selected_option == 5: # Filter platforms
# Ne pas écraser previous_menu_state; il référence l'état avant l'ouverture du pause menu
config.menu_state = "filter_platforms"
config.selected_filter_index = 0
config.filter_platforms_scroll_offset = 0
config.needs_redraw = True
elif config.selected_option == 6: # Source toggle (index shifted by new option)
try: try:
from rgsx_settings import get_sources_mode, set_sources_mode from rgsx_settings import get_sources_mode, set_sources_mode
current_mode = get_sources_mode() current_mode = get_sources_mode()
@@ -1052,13 +1058,13 @@ def handle_controls(event, sources, joystick, screen):
logger.info(f"Changement du mode des sources vers {new_mode}") logger.info(f"Changement du mode des sources vers {new_mode}")
except Exception as e: except Exception as e:
logger.error(f"Erreur changement mode sources: {e}") logger.error(f"Erreur changement mode sources: {e}")
elif config.selected_option == 6: # Redownload game cache elif config.selected_option == 7: # Redownload game cache
config.previous_menu_state = validate_menu_state(config.previous_menu_state) config.previous_menu_state = validate_menu_state(config.previous_menu_state)
config.menu_state = "redownload_game_cache" config.menu_state = "redownload_game_cache"
config.redownload_confirm_selection = 0 config.redownload_confirm_selection = 0
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Passage à redownload_game_cache depuis pause_menu") logger.debug(f"Passage à redownload_game_cache depuis pause_menu")
elif config.selected_option == 7: # Music toggle elif config.selected_option == 8: # Music toggle
config.music_enabled = not config.music_enabled config.music_enabled = not config.music_enabled
save_music_config() save_music_config()
if config.music_enabled: if config.music_enabled:
@@ -1070,7 +1076,7 @@ def handle_controls(event, sources, joystick, screen):
pygame.mixer.music.stop() pygame.mixer.music.stop()
config.needs_redraw = True config.needs_redraw = True
logger.info(f"Musique {'activée' if config.music_enabled else 'désactivée'} via menu pause") logger.info(f"Musique {'activée' if config.music_enabled else 'désactivée'} via menu pause")
elif config.selected_option == 8: # Symlink option elif config.selected_option == 9: # Symlink option
from rgsx_settings import set_symlink_option, get_symlink_option from rgsx_settings import set_symlink_option, get_symlink_option
current_status = get_symlink_option() current_status = get_symlink_option()
success, message = set_symlink_option(not current_status) success, message = set_symlink_option(not current_status)
@@ -1078,7 +1084,7 @@ def handle_controls(event, sources, joystick, screen):
config.popup_timer = 3000 if success else 5000 config.popup_timer = 3000 if success else 5000
config.needs_redraw = True config.needs_redraw = True
logger.info(f"Symlink option {'activée' if not current_status else 'désactivée'} via menu pause") logger.info(f"Symlink option {'activée' if not current_status else 'désactivée'} via menu pause")
elif config.selected_option == 9: # Quit elif config.selected_option == 10: # Quit
config.previous_menu_state = validate_menu_state(config.previous_menu_state) config.previous_menu_state = validate_menu_state(config.previous_menu_state)
config.menu_state = "confirm_exit" config.menu_state = "confirm_exit"
config.confirm_selection = 0 config.confirm_selection = 0
@@ -1117,8 +1123,12 @@ def handle_controls(event, sources, joystick, screen):
config.pending_download = None config.pending_download = None
if os.path.exists(config.SOURCES_FILE): if os.path.exists(config.SOURCES_FILE):
try: try:
os.remove(config.SOURCES_FILE) if os.path.exists(config.SOURCES_FILE):
logger.debug("Fichier sources.json supprimé avec succès") os.remove(config.SOURCES_FILE)
logger.debug("Fichier system_list.json supprimé avec succès")
if os.path.exists(config.SAVE_FOLDER + "/sources.json"):
os.remove(config.SAVE_FOLDER + "/sources.json")
logger.debug("Fichier sources.json supprimé avec succès")
if os.path.exists(config.GAMES_FOLDER): if os.path.exists(config.GAMES_FOLDER):
shutil.rmtree(config.GAMES_FOLDER) shutil.rmtree(config.GAMES_FOLDER)
logger.debug("Dossier games supprimé avec succès") logger.debug("Dossier games supprimé avec succès")
@@ -1203,6 +1213,71 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True config.needs_redraw = True
logger.debug("Annulation de la sélection de langue, retour au menu pause") logger.debug("Annulation de la sélection de langue, retour au menu pause")
# Menu filtre plateformes
elif config.menu_state == "filter_platforms":
total_items = len(config.filter_platforms_selection)
action_buttons = 4
extended_max = total_items + action_buttons - 1
if is_input_matched(event, "up"):
if config.selected_filter_index > 0:
config.selected_filter_index -= 1
config.needs_redraw = True
else:
# Wrap vers les boutons (premier bouton) depuis le haut
if total_items > 0:
config.selected_filter_index = total_items
config.needs_redraw = True
elif is_input_matched(event, "down"):
if config.selected_filter_index < extended_max:
config.selected_filter_index += 1
config.needs_redraw = True
else:
# Wrap retour en haut de la liste
config.selected_filter_index = 0
config.needs_redraw = True
elif is_input_matched(event, "left"):
if config.selected_filter_index >= total_items:
if config.selected_filter_index > total_items:
config.selected_filter_index -= 1
config.needs_redraw = True
# sinon ignorer
elif is_input_matched(event, "right"):
if config.selected_filter_index >= total_items:
if config.selected_filter_index < extended_max:
config.selected_filter_index += 1
config.needs_redraw = True
# sinon ignorer
elif is_input_matched(event, "confirm"):
if config.selected_filter_index < total_items:
name, hidden = config.filter_platforms_selection[config.selected_filter_index]
config.filter_platforms_selection[config.selected_filter_index] = (name, not hidden)
config.filter_platforms_dirty = True
config.needs_redraw = True
else:
btn_idx = config.selected_filter_index - total_items
from rgsx_settings import load_rgsx_settings, save_rgsx_settings
from utils import load_sources
settings = load_rgsx_settings()
if btn_idx == 0: # all visible
config.filter_platforms_selection = [(n, False) for n, _ in config.filter_platforms_selection]
config.filter_platforms_dirty = True
elif btn_idx == 1: # none visible
config.filter_platforms_selection = [(n, True) for n, _ in config.filter_platforms_selection]
config.filter_platforms_dirty = True
elif btn_idx == 2: # apply
hidden_list = [n for n, h in config.filter_platforms_selection if h]
settings["hidden_platforms"] = hidden_list
save_rgsx_settings(settings)
load_sources()
config.filter_platforms_dirty = False
config.menu_state = "pause_menu"
elif btn_idx == 3: # back
config.menu_state = "pause_menu"
config.needs_redraw = True
elif is_input_matched(event, "cancel"):
config.menu_state = "pause_menu"
config.needs_redraw = True
# Gestion des relâchements de touches # Gestion des relâchements de touches
if event.type == pygame.KEYUP: if event.type == pygame.KEYUP:

View File

@@ -83,8 +83,20 @@ def draw_stylized_button(screen, text, x, y, width, height, selected=False):
# Transition d'image lors de la sélection d'un système # Transition d'image lors de la sélection d'un système
def draw_validation_transition(screen, platform_index): def draw_validation_transition(screen, platform_index):
"""Affiche une animation de transition fluide pour la sélection dune plateforme.""" """Affiche une animation de transition fluide pour la sélection dune plateforme.
platform_dict = config.platform_dicts[platform_index] Utilise le mapping par nom pour éviter les décalages d'image si l'ordre d'affichage
diffère de l'ordre de stockage."""
# Récupérer le nom affiché correspondant à l'index trié
if platform_index < 0 or platform_index >= len(config.platforms):
return
platform_name = config.platforms[platform_index]
platform_dict = getattr(config, 'platform_dict_by_name', {}).get(platform_name)
if not platform_dict:
# Fallback index direct si mapping absent
try:
platform_dict = config.platform_dicts[platform_index]
except Exception:
return
image = load_system_image(platform_dict) image = load_system_image(platform_dict)
if not image: if not image:
return return
@@ -468,8 +480,16 @@ def draw_platform_grid(screen):
scale_base = 1.5 if is_selected else 1.0 scale_base = 1.5 if is_selected else 1.0
scale = scale_base + pulse if is_selected else scale_base scale = scale_base + pulse if is_selected else scale_base
platform_dict = config.platform_dicts[idx] # Récupération robuste du dict via nom
platform_id = platform_dict.get("platform", str(idx)) display_name = config.platforms[idx]
platform_dict = getattr(config, 'platform_dict_by_name', {}).get(display_name)
if not platform_dict:
# Fallback index brut
if idx < len(config.platform_dicts):
platform_dict = config.platform_dicts[idx]
else:
continue
platform_id = platform_dict.get("platform_name") or platform_dict.get("platform") or display_name
# Utiliser le cache d'images pour éviter de recharger/redimensionner à chaque frame # Utiliser le cache d'images pour éviter de recharger/redimensionner à chaque frame
cache_key = f"{platform_id}_{scale:.2f}" cache_key = f"{platform_id}_{scale:.2f}"
@@ -570,15 +590,17 @@ def draw_game_list(screen):
return return
line_height = config.small_font.get_height() + 10 line_height = config.small_font.get_height() + 10
header_height = line_height # hauteur de l'en-tête identique à une ligne
margin_top_bottom = 20 margin_top_bottom = 20
extra_margin_top = 20 extra_margin_top = 20
extra_margin_bottom = 60 extra_margin_bottom = 60
title_height = config.title_font.get_height() + 20 title_height = config.title_font.get_height() + 20
available_height = config.screen_height - title_height - extra_margin_top - extra_margin_bottom - 2 * margin_top_bottom # Réserver de l'espace pour l'en-tête (header_height)
items_per_page = available_height // line_height available_height = config.screen_height - title_height - extra_margin_top - extra_margin_bottom - 2 * margin_top_bottom - header_height
items_per_page = max(1, available_height // line_height)
rect_height = items_per_page * line_height + 2 * margin_top_bottom rect_height = header_height + items_per_page * line_height + 2 * margin_top_bottom
rect_width = int(0.95 * config.screen_width) rect_width = int(0.95 * config.screen_width)
rect_x = (config.screen_width - rect_width) // 2 rect_x = (config.screen_width - rect_width) // 2
rect_y = title_height + extra_margin_top + (config.screen_height - title_height - extra_margin_top - extra_margin_bottom - rect_height) // 2 rect_y = title_height + extra_margin_top + (config.screen_height - title_height - extra_margin_top - extra_margin_bottom - rect_height) // 2
@@ -622,21 +644,60 @@ def draw_game_list(screen):
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (rect_x, rect_y, rect_width, rect_height), border_radius=12) pygame.draw.rect(screen, THEME_COLORS["button_idle"], (rect_x, rect_y, rect_width, rect_height), border_radius=12)
pygame.draw.rect(screen, THEME_COLORS["border"], (rect_x, rect_y, rect_width, rect_height), 2, border_radius=12) pygame.draw.rect(screen, THEME_COLORS["border"], (rect_x, rect_y, rect_width, rect_height), 2, border_radius=12)
# Largeur colonne taille (15%) mini 120px, reste pour nom
size_col_width = max(120, int(rect_width * 0.15))
name_col_width = rect_width - 40 - size_col_width # padding horizontal 40
# ---- En-tête ----
header_name = _("game_header_name")
header_size = _("game_header_size")
header_y_center = rect_y + margin_top_bottom + header_height // 2
# Nom aligné gauche
header_name_surface = config.small_font.render(header_name, True, THEME_COLORS["text"])
header_name_rect = header_name_surface.get_rect()
header_name_rect.midleft = (rect_x + 20, header_y_center)
# Taille alignée droite
header_size_surface = config.small_font.render(header_size, True, THEME_COLORS["text"])
header_size_rect = header_size_surface.get_rect()
header_size_rect.midright = (rect_x + rect_width - 20, header_y_center)
screen.blit(header_name_surface, header_name_rect)
screen.blit(header_size_surface, header_size_rect)
# Ligne de séparation sous l'en-tête
separator_y = rect_y + margin_top_bottom + header_height
pygame.draw.line(screen, THEME_COLORS["border"], (rect_x + 20, separator_y), (rect_x + rect_width - 20, separator_y), 2)
# Position de départ des lignes après l'en-tête
list_start_y = rect_y + margin_top_bottom + header_height
for i in range(config.scroll_offset, min(config.scroll_offset + items_per_page, len(games))): for i in range(config.scroll_offset, min(config.scroll_offset + items_per_page, len(games))):
game_name = games[i][0] if isinstance(games[i], (list, tuple)) else games[i] item = games[i]
if isinstance(item, (list, tuple)) and item:
game_name = item[0]
size_val = item[2] if len(item) > 2 else None
else:
game_name = str(item)
size_val = None
size_text = size_val if (isinstance(size_val, str) and size_val.strip()) else "N/A"
is_marked = i in getattr(config, 'selected_games', set()) is_marked = i in getattr(config, 'selected_games', set())
# Couleur verte si jeu sous curseur OU marqué en multi-sélection
color = THEME_COLORS["fond_lignes"] if (i == config.current_game or is_marked) else THEME_COLORS["text"] color = THEME_COLORS["fond_lignes"] if (i == config.current_game or is_marked) else THEME_COLORS["text"]
# Préfixe ASCII pour distinguer les jeux marqués (éviter collision glyphes)
prefix = "[X] " if is_marked else " " prefix = "[X] " if is_marked else " "
game_text = truncate_text_middle(prefix + game_name, config.small_font, rect_width - 40, is_filename=False) truncated_name = truncate_text_middle(prefix + game_name, config.small_font, name_col_width, is_filename=False)
text_surface = config.small_font.render(game_text, True, color) name_surface = config.small_font.render(truncated_name, True, color)
text_rect = text_surface.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + (i - config.scroll_offset) * line_height + line_height // 2)) size_surface = config.small_font.render(size_text, True, THEME_COLORS["text"])
row_center_y = list_start_y + (i - config.scroll_offset) * line_height + line_height // 2
# Position nom (aligné à gauche dans la boite)
name_rect = name_surface.get_rect()
name_rect.midleft = (rect_x + 20, row_center_y)
size_rect = size_surface.get_rect()
size_rect.midright = (rect_x + rect_width - 20, row_center_y)
if i == config.current_game: if i == config.current_game:
glow_surface = pygame.Surface((text_rect.width + 20, text_rect.height + 10), pygame.SRCALPHA) glow_width = rect_width - 40
pygame.draw.rect(glow_surface, THEME_COLORS["fond_lignes"] + (50,), (10, 5, text_rect.width, text_rect.height), border_radius=8) glow_height = name_rect.height + 10
screen.blit(glow_surface, (text_rect.left - 10, text_rect.top - 5)) glow_surface = pygame.Surface((glow_width, glow_height), pygame.SRCALPHA)
screen.blit(text_surface, text_rect) pygame.draw.rect(glow_surface, THEME_COLORS["fond_lignes"] + (50,), (0, 0, glow_width, glow_height), border_radius=8)
screen.blit(glow_surface, (rect_x + 20, row_center_y - glow_height // 2))
screen.blit(name_surface, name_rect)
screen.blit(size_surface, size_rect)
if len(games) > items_per_page: if len(games) > items_per_page:
try: try:
@@ -1202,6 +1263,7 @@ def draw_pause_menu(screen, selected_option):
_("menu_history"), # 2 _("menu_history"), # 2
_("menu_language"), # 3 _("menu_language"), # 3
_("menu_accessibility"), # 4 _("menu_accessibility"), # 4
_("menu_filter_platforms"), # 5 new filter option
f"{_('menu_games_source_prefix')}: {source_label}", # 5 f"{_('menu_games_source_prefix')}: {source_label}", # 5
_("menu_redownload_cache"), # 6 _("menu_redownload_cache"), # 6
music_option, # 7 music_option, # 7
@@ -1232,6 +1294,116 @@ def draw_pause_menu(screen, selected_option):
# Stocker le nombre total d'options pour la navigation dynamique # Stocker le nombre total d'options pour la navigation dynamique
config.pause_menu_total_options = len(options) config.pause_menu_total_options = len(options)
def draw_filter_platforms_menu(screen):
"""Affiche le menu de filtrage des plateformes (afficher/masquer)."""
from rgsx_settings import load_rgsx_settings
screen.blit(OVERLAY, (0, 0))
settings = load_rgsx_settings()
hidden = set(settings.get("hidden_platforms", [])) if isinstance(settings, dict) else set()
# Initialiser la copie de travail si vide ou taille différente
if not config.filter_platforms_selection or len(config.filter_platforms_selection) != len(config.platform_dicts):
# Liste alphabétique complète (sans filtrer hidden existant)
all_names = sorted([d.get("platform_name", "") for d in config.platform_dicts if d.get("platform_name")])
config.filter_platforms_selection = [(name, name in hidden) for name in all_names]
config.selected_filter_index = 0
config.filter_platforms_scroll_offset = 0
config.filter_platforms_dirty = False
title_text = _("filter_platforms_title")
title_surface = config.title_font.render(title_text, True, THEME_COLORS["text"])
title_rect = title_surface.get_rect(center=(config.screen_width // 2, title_surface.get_height() // 2 + 20))
title_rect_inflated = title_rect.inflate(80, 40)
title_rect_inflated.topleft = ((config.screen_width - title_rect_inflated.width) // 2, 10)
pygame.draw.rect(screen, THEME_COLORS["button_idle"], title_rect_inflated, border_radius=12)
pygame.draw.rect(screen, THEME_COLORS["border"], title_rect_inflated, 2, border_radius=12)
screen.blit(title_surface, title_rect)
# Zone liste
list_width = int(config.screen_width * 0.7)
list_height = int(config.screen_height * 0.6)
list_x = (config.screen_width - list_width) // 2
list_y = title_rect_inflated.bottom + 20
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (list_x, list_y, list_width, list_height), border_radius=12)
pygame.draw.rect(screen, THEME_COLORS["border"], (list_x, list_y, list_width, list_height), 2, border_radius=12)
line_height = config.small_font.get_height() + 8
visible_items = list_height // line_height - 1 # laisser un peu d'espace bas
total_items = len(config.filter_platforms_selection)
if config.selected_filter_index < 0:
config.selected_filter_index = 0
# Ne pas forcer la réduction si on est sur les boutons (indices >= total_items)
# Laisser controls.py gérer la borne max étendue
# Ajuster scroll
if config.selected_filter_index < config.filter_platforms_scroll_offset:
config.filter_platforms_scroll_offset = config.selected_filter_index
elif config.selected_filter_index >= config.filter_platforms_scroll_offset + visible_items:
config.filter_platforms_scroll_offset = config.selected_filter_index - visible_items + 1
# Dessiner items
for i in range(config.filter_platforms_scroll_offset, min(config.filter_platforms_scroll_offset + visible_items, total_items)):
name, is_hidden = config.filter_platforms_selection[i]
idx_on_screen = i - config.filter_platforms_scroll_offset
y_center = list_y + 10 + idx_on_screen * line_height + line_height // 2
selected = (i == config.selected_filter_index)
checkbox = "[ ]" if is_hidden else "[X]" # inversé: coché signifie visible
# Correction: on veut [X] si visible => is_hidden False
checkbox = "[X]" if not is_hidden else "[ ]"
display_text = f"{checkbox} {name}"
color = THEME_COLORS["fond_lignes"] if selected else THEME_COLORS["text"]
text_surface = config.small_font.render(display_text, True, color)
text_rect = text_surface.get_rect(midleft=(list_x + 20, y_center))
if selected:
glow_surface = pygame.Surface((list_width - 40, line_height), pygame.SRCALPHA)
pygame.draw.rect(glow_surface, THEME_COLORS["fond_lignes"] + (50,), (0, 0, list_width - 40, line_height), border_radius=8)
screen.blit(glow_surface, (list_x + 20, y_center - line_height // 2))
screen.blit(text_surface, text_rect)
# Scrollbar
if total_items > visible_items:
scroll_height = int((visible_items / total_items) * (list_height - 20))
scroll_y = int((config.filter_platforms_scroll_offset / max(1, total_items - visible_items)) * (list_height - 20 - scroll_height))
pygame.draw.rect(screen, THEME_COLORS["fond_lignes"], (list_x + list_width - 25, list_y + 10 + scroll_y, 10, scroll_height), border_radius=4)
# Boutons d'action
btn_width = 220
btn_height = int(config.screen_height * 0.0463)
spacing = 30
buttons_y = list_y + list_height + 20
center_x = config.screen_width // 2
actions = [
("filter_all", -2),
("filter_none", -3),
("filter_apply", -4),
("filter_back", -5)
]
# Indice spécial sélection boutons quand selected_filter_index >= total_items
extra_index_base = total_items
# Ajuster selected_filter_index max pour inclure boutons
extended_max = total_items + len(actions) - 1
if config.selected_filter_index > extended_max:
config.selected_filter_index = extended_max
for idx, (key, offset) in enumerate(actions):
btn_x = center_x - (len(actions) * (btn_width + spacing) - spacing) // 2 + idx * (btn_width + spacing)
is_selected = (config.selected_filter_index == total_items + idx)
label = _(key)
draw_stylized_button(screen, label, btn_x, buttons_y, btn_width, btn_height, selected=is_selected)
# Infos bas
hidden_count = sum(1 for _, h in config.filter_platforms_selection if h)
visible_count = total_items - hidden_count
info_text = _("filter_platforms_info").format(visible_count, hidden_count, total_items)
info_surface = config.small_font.render(info_text, True, THEME_COLORS["text"])
info_rect = info_surface.get_rect(center=(config.screen_width // 2, buttons_y + btn_height + 30))
screen.blit(info_surface, info_rect)
if config.filter_platforms_dirty:
dirty_text = _("filter_unsaved_warning")
dirty_surface = config.small_font.render(dirty_text, True, THEME_COLORS["warning_text"])
dirty_rect = dirty_surface.get_rect(center=(config.screen_width // 2, info_rect.bottom + 25))
screen.blit(dirty_surface, dirty_rect)
# Menu aide contrôles # Menu aide contrôles
def draw_controls_help(screen, previous_state): def draw_controls_help(screen, previous_state):
"""Affiche la liste des contrôles (aide) avec mise en page adaptative.""" """Affiche la liste des contrôles (aide) avec mise en page adaptative."""

View File

@@ -26,7 +26,7 @@
"error_api_key": "Achtung, du musst deinen API-Schlüssel (nur Premium) in der Datei {0} eingeben", "error_api_key": "Achtung, du musst deinen API-Schlüssel (nur Premium) in der Datei {0} eingeben",
"error_api_key_extended": "Achtung, du musst deinen API-Schlüssel (nur Premium) in der Datei /userdata/saves/ports/rgsx/1fichierAPI.txt einfügen. Öffne die Datei in einem Texteditor und füge den API-Schlüssel ein", "error_api_key_extended": "Achtung, du musst deinen API-Schlüssel (nur Premium) in der Datei /userdata/saves/ports/rgsx/1fichierAPI.txt einfügen. Öffne die Datei in einem Texteditor und füge den API-Schlüssel ein",
"error_invalid_download_data": "Ungültige Downloaddaten", "error_invalid_download_data": "Ungültige Downloaddaten",
"error_delete_sources": "Fehler beim Löschen der Datei sources.json oder Ordner", "error_delete_sources": "Fehler beim Löschen der Datei systems_list.json oder Ordner",
"error_extension": "Nicht unterstützte Erweiterung oder Downloadfehler", "error_extension": "Nicht unterstützte Erweiterung oder Downloadfehler",
"error_no_download": "Keine Downloads ausstehend.", "error_no_download": "Keine Downloads ausstehend.",
@@ -37,6 +37,8 @@
"game_count": "{0} ({1} Spiele)", "game_count": "{0} ({1} Spiele)",
"game_filter": "Aktiver Filter: {0}", "game_filter": "Aktiver Filter: {0}",
"game_search": "Filtern: {0}", "game_search": "Filtern: {0}",
"game_header_name": "Name",
"game_header_size": "Größe",
"history_title": "Downloads ({0})", "history_title": "Downloads ({0})",
"history_empty": "Keine Downloads im Verlauf", "history_empty": "Keine Downloads im Verlauf",
@@ -78,6 +80,14 @@
"menu_music_toggle": "Musik ein/aus", "menu_music_toggle": "Musik ein/aus",
"menu_music_enabled": "Musik aktiviert: {0}", "menu_music_enabled": "Musik aktiviert: {0}",
"menu_music_disabled": "Musik deaktiviert", "menu_music_disabled": "Musik deaktiviert",
"menu_filter_platforms": "Systeme filtern",
"filter_platforms_title": "Systemsichtbarkeit",
"filter_all": "Alle anzeigen",
"filter_none": "Alle ausblenden",
"filter_apply": "Anwenden",
"filter_back": "Zurück",
"filter_platforms_info": "Sichtbar: {0} | Versteckt: {1} / Gesamt: {2}",
"filter_unsaved_warning": "Ungespeicherte Änderungen",
"menu_quit": "Beenden", "menu_quit": "Beenden",
"button_yes": "Ja", "button_yes": "Ja",

View File

@@ -26,7 +26,7 @@
"error_api_key": "Please enter your API key (premium only) in the file {0}", "error_api_key": "Please enter your API key (premium only) in the file {0}",
"error_api_key_extended": "Please enter your API key (premium only) in the file /userdata/saves/ports/rgsx/1fichierAPI.txt by opening it in a text editor and pasting your API key", "error_api_key_extended": "Please enter your API key (premium only) in the file /userdata/saves/ports/rgsx/1fichierAPI.txt by opening it in a text editor and pasting your API key",
"error_invalid_download_data": "Invalid download data", "error_invalid_download_data": "Invalid download data",
"error_delete_sources": "Error deleting sources.json file or folders", "error_delete_sources": "Error deleting systems_list.json file or folders",
"error_extension": "Unsupported extension or download error", "error_extension": "Unsupported extension or download error",
"error_no_download": "No pending download.", "error_no_download": "No pending download.",
@@ -37,6 +37,8 @@
"game_count": "{0} ({1} games)", "game_count": "{0} ({1} games)",
"game_filter": "Active filter: {0}", "game_filter": "Active filter: {0}",
"game_search": "Filter: {0}", "game_search": "Filter: {0}",
"game_header_name": "Name",
"game_header_size": "Size",
"history_title": "Downloads ({0})", "history_title": "Downloads ({0})",
"history_empty": "No downloads in history", "history_empty": "No downloads in history",
@@ -78,6 +80,14 @@
"menu_music_toggle": "Toggle music", "menu_music_toggle": "Toggle music",
"menu_music_enabled": "Music enabled: {0}", "menu_music_enabled": "Music enabled: {0}",
"menu_music_disabled": "Music disabled", "menu_music_disabled": "Music disabled",
"menu_filter_platforms": "Filter systems",
"filter_platforms_title": "Systems visibility",
"filter_all": "Show all",
"filter_none": "Hide all",
"filter_apply": "Apply",
"filter_back": "Back",
"filter_platforms_info": "Visible: {0} | Hidden: {1} / Total: {2}",
"filter_unsaved_warning": "Unsaved changes",
"menu_quit": "Quit", "menu_quit": "Quit",
"button_yes": "Yes", "button_yes": "Yes",

View File

@@ -27,7 +27,7 @@
"error_api_key": "Atención, debes ingresar tu clave API (solo premium) en el archivo {0}", "error_api_key": "Atención, debes ingresar tu clave API (solo premium) en el archivo {0}",
"error_api_key_extended": "Atención, debes ingresar tu clave API (solo premium) en el archivo /userdata/saves/ports/rgsx/1fichierAPI.txt, abrirlo en un editor de texto y pegar la clave API", "error_api_key_extended": "Atención, debes ingresar tu clave API (solo premium) en el archivo /userdata/saves/ports/rgsx/1fichierAPI.txt, abrirlo en un editor de texto y pegar la clave API",
"error_invalid_download_data": "Datos de descarga no válidos", "error_invalid_download_data": "Datos de descarga no válidos",
"error_delete_sources": "Error al eliminar el archivo sources.json o carpetas", "error_delete_sources": "Error al eliminar el archivo systems_list.json o carpetas",
"error_extension": "Extensión no soportada o error de descarga", "error_extension": "Extensión no soportada o error de descarga",
"error_no_download": "No hay descargas pendientes.", "error_no_download": "No hay descargas pendientes.",
@@ -38,6 +38,8 @@
"game_count": "{0} ({1} juegos)", "game_count": "{0} ({1} juegos)",
"game_filter": "Filtro activo: {0}", "game_filter": "Filtro activo: {0}",
"game_search": "Filtrar: {0}", "game_search": "Filtrar: {0}",
"game_header_name": "Nombre",
"game_header_size": "Tamaño",
"history_title": "Descargas ({0})", "history_title": "Descargas ({0})",
"history_empty": "No hay descargas en el historial", "history_empty": "No hay descargas en el historial",
@@ -79,6 +81,14 @@
"menu_music_toggle": "Activar/Desactivar música", "menu_music_toggle": "Activar/Desactivar música",
"menu_music_enabled": "Música activada: {0}", "menu_music_enabled": "Música activada: {0}",
"menu_music_disabled": "Música desactivada", "menu_music_disabled": "Música desactivada",
"menu_filter_platforms": "Filtrar sistemas",
"filter_platforms_title": "Visibilidad de sistemas",
"filter_all": "Mostrar todo",
"filter_none": "Ocultar todo",
"filter_apply": "Aplicar",
"filter_back": "Volver",
"filter_platforms_info": "Visibles: {0} | Ocultos: {1} / Total: {2}",
"filter_unsaved_warning": "Cambios no guardados",
"menu_quit": "Salir", "menu_quit": "Salir",
"button_yes": "Sí", "button_yes": "Sí",

View File

@@ -23,7 +23,7 @@
"error_api_key": "Attention il faut renseigner sa clé API (premium only) dans le fichier {0}", "error_api_key": "Attention il faut renseigner sa clé API (premium only) dans le fichier {0}",
"error_api_key_extended": "Attention il faut renseigner sa clé API (premium only) dans le fichier /userdata/saves/ports/rgsx/1fichierAPI.txt à ouvrir dans un éditeur de texte et coller la clé API", "error_api_key_extended": "Attention il faut renseigner sa clé API (premium only) dans le fichier /userdata/saves/ports/rgsx/1fichierAPI.txt à ouvrir dans un éditeur de texte et coller la clé API",
"error_invalid_download_data": "Données de téléchargement invalides", "error_invalid_download_data": "Données de téléchargement invalides",
"error_delete_sources": "Erreur lors de la suppression du fichier sources.json ou dossiers", "error_delete_sources": "Erreur lors de la suppression du fichier systems_list.json ou dossiers",
"error_extension": "Extension non supportée ou erreur de téléchargement", "error_extension": "Extension non supportée ou erreur de téléchargement",
"error_no_download": "Aucun téléchargement en attente.", "error_no_download": "Aucun téléchargement en attente.",
@@ -34,6 +34,8 @@
"game_count": "{0} ({1} jeux)", "game_count": "{0} ({1} jeux)",
"game_filter": "Filtre actif : {0}", "game_filter": "Filtre actif : {0}",
"game_search": "Filtrer : {0}", "game_search": "Filtrer : {0}",
"game_header_name": "Nom",
"game_header_size": "Taille",
"history_title": "Téléchargements ({0})", "history_title": "Téléchargements ({0})",
"history_empty": "Aucun téléchargement dans l'historique", "history_empty": "Aucun téléchargement dans l'historique",
@@ -76,6 +78,14 @@
"menu_music_toggle": "Activer/Désactiver la musique", "menu_music_toggle": "Activer/Désactiver la musique",
"menu_music_enabled": "Musique activée : {0}", "menu_music_enabled": "Musique activée : {0}",
"menu_music_disabled": "Musique désactivée", "menu_music_disabled": "Musique désactivée",
"menu_filter_platforms": "Filtrer les systèmes",
"filter_platforms_title": "Affichage des systèmes",
"filter_all": "Tout afficher",
"filter_none": "Tout masquer",
"filter_apply": "Appliquer",
"filter_back": "Retour",
"filter_platforms_info": "Visibles: {0} | Masqués: {1} / Total: {2}",
"filter_unsaved_warning": "Modifications non sauvegardées",
"button_yes": "Oui", "button_yes": "Oui",
"button_no": "Non", "button_no": "Non",

View File

@@ -260,7 +260,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
dest_dir = None dest_dir = None
for platform_dict in config.platform_dicts: for platform_dict in config.platform_dicts:
if platform_dict["platform"] == platform: if platform_dict.get("platform_name") == platform:
platform_folder = platform_dict.get("folder", normalize_platform_name(platform)) platform_folder = platform_dict.get("folder", normalize_platform_name(platform))
dest_dir = apply_symlink_path(config.ROMS_FOLDER, platform_folder) dest_dir = apply_symlink_path(config.ROMS_FOLDER, platform_folder)
logger.debug(f"Répertoire de destination trouvé pour {platform}: {dest_dir}") logger.debug(f"Répertoire de destination trouvé pour {platform}: {dest_dir}")
@@ -298,9 +298,82 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
download_headers = headers.copy() download_headers = headers.copy()
download_headers['Accept'] = 'application/octet-stream, */*' download_headers['Accept'] = 'application/octet-stream, */*'
download_headers['Referer'] = 'https://myrient.erista.me/' download_headers['Referer'] = 'https://myrient.erista.me/'
response = session.get(url, stream=True, timeout=30, allow_redirects=True, headers=download_headers)
logger.debug(f"Status code: {response.status_code}") # Préparation spécifique archive.org : récupérer quelques pages pour obtenir cookies éventuels
response.raise_for_status() if 'archive.org/download/' in url:
try:
pre_id = url.split('/download/')[1].split('/')[0]
session.get('https://archive.org/robots.txt', timeout=10)
session.get(f'https://archive.org/metadata/{pre_id}', timeout=10)
logger.debug(f"Pré-chargement cookies/metadata archive.org pour {pre_id}")
except Exception as e:
logger.debug(f"Pré-chargement archive.org ignoré: {e}")
# Tentatives multiples avec variations d'en-têtes pour contourner certains 401/403 (archive.org / hotlink protection)
header_variants = [
download_headers,
{ # Variante sans Referer spécifique
'User-Agent': headers['User-Agent'],
'Accept': 'application/octet-stream,*/*;q=0.8',
'Accept-Language': headers['Accept-Language'],
'Connection': 'keep-alive'
},
{ # Variante minimaliste type curl
'User-Agent': 'curl/8.4.0',
'Accept': '*/*'
},
{ # Variante avec Referer archive.org
'User-Agent': headers['User-Agent'],
'Accept': '*/*',
'Referer': 'https://archive.org/'
}
]
response = None
last_status = None
for attempt, hv in enumerate(header_variants, start=1):
try:
logger.debug(f"Tentative téléchargement {attempt}/{len(header_variants)} avec headers: {hv}")
r = session.get(url, stream=True, timeout=30, allow_redirects=True, headers=hv)
last_status = r.status_code
logger.debug(f"Status code tentative {attempt}: {r.status_code}")
if r.status_code in (401, 403):
# Lire un petit bout pour voir si message utile
try:
snippet = r.text[:200]
logger.debug(f"Réponse {r.status_code} snippet: {snippet}")
except Exception:
pass
continue # Essayer variante suivante
r.raise_for_status()
response = r
break
except requests.RequestException as e:
logger.debug(f"Erreur tentative {attempt}: {e}")
# Si ce n'est pas une erreur auth explicite et qu'on a un code => on sort
if isinstance(e, requests.HTTPError) and last_status not in (401, 403):
break
if response is None:
# Fallback metadata archive.org pour message clair
if 'archive.org/download/' in url:
try:
identifier = url.split('/download/')[1].split('/')[0]
meta_resp = session.get(f'https://archive.org/metadata/{identifier}', timeout=15)
if meta_resp.status_code == 200:
meta_json = meta_resp.json()
if meta_json.get('is_dark'):
raise requests.HTTPError(f"Item archive.org restreint (is_dark=true): {identifier}")
if not meta_json.get('files'):
raise requests.HTTPError(f"Item archive.org sans fichiers listés: {identifier}")
# Fichier peut avoir un nom différent : informer
available = [f.get('name') for f in meta_json.get('files', [])][:10]
raise requests.HTTPError(f"Accès refusé (HTTP {last_status}). Fichiers disponibles exemples: {available}")
else:
raise requests.HTTPError(f"HTTP {last_status} & metadata {meta_resp.status_code} pour {identifier}")
except requests.HTTPError:
raise
except Exception as e:
raise requests.HTTPError(f"HTTP {last_status} après variations; metadata échec: {e}")
auth_msg = f"HTTP {last_status} après variations d'en-têtes" if last_status else "Aucune réponse valide"
raise requests.HTTPError(auth_msg)
total_size = int(response.headers.get('content-length', 0)) total_size = int(response.headers.get('content-length', 0))
logger.debug(f"Taille totale: {total_size} octets") logger.debug(f"Taille totale: {total_size} octets")
@@ -471,7 +544,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
dest_dir = None dest_dir = None
for platform_dict in config.platform_dicts: for platform_dict in config.platform_dicts:
if platform_dict["platform"] == platform: if platform_dict.get("platform_name") == platform:
platform_folder = platform_dict.get("folder", normalize_platform_name(platform)) platform_folder = platform_dict.get("folder", normalize_platform_name(platform))
dest_dir = apply_symlink_path(config.ROMS_FOLDER, platform_folder) dest_dir = apply_symlink_path(config.ROMS_FOLDER, platform_folder)
break break

View File

@@ -29,9 +29,9 @@ def load_rgsx_settings():
"enabled": False, "enabled": False,
"target_directory": "" "target_directory": ""
}, },
"sources": { # Nouvelle section pour la source des jeux "sources": {
"mode": "rgsx", # "rgsx" ou "custom" "mode": "rgsx",
"custom_url": "" # URL personnalisée pour le ZIP des sources "custom_url": ""
} }
} }
@@ -44,12 +44,6 @@ def load_rgsx_settings():
if key not in settings: if key not in settings:
settings[key] = value settings[key] = value
return settings return settings
else:
# Tenter de migrer depuis les anciens fichiers
migrated_settings = migrate_old_settings()
if migrated_settings:
save_rgsx_settings(migrated_settings)
return migrated_settings
except Exception as e: except Exception as e:
print(f"Erreur lors du chargement de rgsx_settings.json: {str(e)}") print(f"Erreur lors du chargement de rgsx_settings.json: {str(e)}")
@@ -68,96 +62,6 @@ def save_rgsx_settings(settings):
print(f"Erreur lors de la sauvegarde de rgsx_settings.json: {str(e)}") print(f"Erreur lors de la sauvegarde de rgsx_settings.json: {str(e)}")
def migrate_old_settings():
"""Migre les anciens fichiers de configuration vers le nouveau format."""
from config import LANGUAGE_CONFIG_PATH, MUSIC_CONFIG_PATH, ACCESSIBILITY_FOLDER, SYMLINK_SETTINGS_PATH
migrated_settings = {
"language": "en",
"music_enabled": True,
"accessibility": {
"font_scale": 1.0
},
"symlink": {
"enabled": False,
"target_directory": ""
}
}
files_to_remove = [] # Liste des fichiers à supprimer après migration réussie
# Migrer language.json
if os.path.exists(LANGUAGE_CONFIG_PATH):
try:
with open(LANGUAGE_CONFIG_PATH, 'r', encoding='utf-8') as f:
content = f.read().strip()
# Gérer le cas où le fichier contient juste une chaîne (pas de JSON)
if content.startswith('"') and content.endswith('"'):
migrated_settings["language"] = content.strip('"')
elif not content.startswith('{'):
# Fichier texte simple sans guillemets
migrated_settings["language"] = content
else:
# Fichier JSON normal
lang_data = json.loads(content)
migrated_settings["language"] = lang_data.get("language", "en")
files_to_remove.append(LANGUAGE_CONFIG_PATH)
except:
pass
# Migrer music_config.json
if os.path.exists(MUSIC_CONFIG_PATH):
try:
with open(MUSIC_CONFIG_PATH, 'r', encoding='utf-8') as f:
content = f.read().strip()
# Gérer le cas où le fichier contient juste un booléen
if content.lower() in ['true', 'false']:
migrated_settings["music_enabled"] = content.lower() == 'true'
else:
# Fichier JSON normal
music_data = json.loads(content)
migrated_settings["music_enabled"] = music_data.get("music_enabled", True)
files_to_remove.append(MUSIC_CONFIG_PATH)
except:
pass
# Migrer accessibility.json
if os.path.exists(ACCESSIBILITY_FOLDER):
try:
with open(ACCESSIBILITY_FOLDER, 'r', encoding='utf-8') as f:
acc_data = json.load(f)
migrated_settings["accessibility"] = {
"font_scale": acc_data.get("font_scale", 1.0)
}
files_to_remove.append(ACCESSIBILITY_FOLDER)
except:
pass
# Migrer symlink_settings.json
if os.path.exists(SYMLINK_SETTINGS_PATH):
try:
with open(SYMLINK_SETTINGS_PATH, 'r', encoding='utf-8') as f:
symlink_data = json.load(f)
migrated_settings["symlink"] = {
"enabled": symlink_data.get("use_symlink_path", False),
"target_directory": symlink_data.get("target_directory", "")
}
files_to_remove.append(SYMLINK_SETTINGS_PATH)
except:
pass
# Supprimer les anciens fichiers après migration réussie
if files_to_remove:
print(f"Migration réussie. Suppression des anciens fichiers de configuration...")
for file_path in files_to_remove:
try:
os.remove(file_path)
print(f" - Supprimé: {os.path.basename(file_path)}")
except Exception as e:
print(f" - Erreur lors de la suppression de {os.path.basename(file_path)}: {e}")
return migrated_settings
def load_symlink_settings(): def load_symlink_settings():
"""Load symlink settings from rgsx_settings.json.""" """Load symlink settings from rgsx_settings.json."""

View File

@@ -12,9 +12,9 @@ from rgsx_settings import load_rgsx_settings, save_rgsx_settings
import zipfile import zipfile
import time import time
import random import random
from config import JSON_EXTENSIONS, SAVE_FOLDER import config
from history import save_history from history import save_history
from language import _ # Import de la fonction de traduction from language import _
from datetime import datetime from datetime import datetime
@@ -47,10 +47,10 @@ def detect_non_pc():
def load_extensions_json(): def load_extensions_json():
"""Charge le fichier JSON contenant les extensions supportées.""" """Charge le fichier JSON contenant les extensions supportées."""
try: try:
with open(JSON_EXTENSIONS, 'r', encoding='utf-8') as f: with open(config.JSON_EXTENSIONS, 'r', encoding='utf-8') as f:
return json.load(f) return json.load(f)
except Exception as e: except Exception as e:
logger.error(f"Erreur lors de la lecture de {JSON_EXTENSIONS}: {e}") logger.error(f"Erreur lors de la lecture de {config.JSON_EXTENSIONS}: {e}")
return [] return []
def check_extension_before_download(url, platform, game_name): def check_extension_before_download(url, platform, game_name):
@@ -59,7 +59,7 @@ def check_extension_before_download(url, platform, game_name):
sanitized_name = sanitize_filename(game_name) sanitized_name = sanitize_filename(game_name)
extensions_data = load_extensions_json() extensions_data = load_extensions_json()
if not extensions_data: if not extensions_data:
logger.error(f"Fichier {JSON_EXTENSIONS} vide ou introuvable") logger.error(f"Fichier {config.JSON_EXTENSIONS} vide ou introuvable")
return None return None
is_supported = is_extension_supported(sanitized_name, platform, extensions_data) is_supported = is_extension_supported(sanitized_name, platform, extensions_data)
@@ -80,13 +80,15 @@ def check_extension_before_download(url, platform, game_name):
return None return None
# Fonction pour vérifier si l'extension est supportée pour une plateforme donnée # Fonction pour vérifier si l'extension est supportée pour une plateforme donnée
def is_extension_supported(filename, platform, extensions_data): def is_extension_supported(filename, platform_key, extensions_data):
"""Vérifie si l'extension du fichier est supportée pour la plateforme donnée.""" """Vérifie si l'extension du fichier est supportée pour la plateforme donnée.
platform_key correspond maintenant à l'identifiant utilisé dans config.platforms (platform_name)."""
extension = os.path.splitext(filename)[1].lower() extension = os.path.splitext(filename)[1].lower()
dest_dir = None dest_dir = None
for platform_dict in config.platform_dicts: for platform_dict in config.platform_dicts:
if platform_dict["platform"] == platform: # Nouveau schéma: platform_name
if platform_dict.get("platform_name") == platform_key:
dest_dir = os.path.join(config.ROMS_FOLDER, platform_dict.get("folder")) dest_dir = os.path.join(config.ROMS_FOLDER, platform_dict.get("folder"))
break break
@@ -108,43 +110,225 @@ def is_extension_supported(filename, platform, extensions_data):
# Fonction pour charger sources.json # Fonction pour charger sources.json
def load_sources(): def load_sources():
"""Charge les sources depuis sources.json et initialise les plateformes.""" """Charge la liste de base depuis systems_list.json puis ajoute les plateformes
sources_path = os.path.join(config.SOURCES_FILE) implicites (fichiers de jeux *.json non listés) à la fin sans réordonner.
logger.debug(f"Chargement de {sources_path}")
Schéma attendu dans systems_list.json:
[ {"platform_name": str, "folder": str, (optionnel) "platform_image"|"system_image": str }, ... ]
Règles d'ajout automatique:
- Chaque fichier <nom>.json dans config.GAMES_FOLDER est candidat.
- Si <nom> ne correspond à aucune entrée existante (case sensitive simple),
on ajoute {"platform_name": <nom>, "folder": <nom>} en fin de liste.
- Aucun tri n'est appliqué pour préserver l'ordre original défini par l'utilisateur.
"""
try: try:
with open(sources_path, 'r', encoding='utf-8') as f: # Détection legacy: si sources.json (ancien format) existe encore, déclencher redownload automatique
sources = json.load(f) legacy_path = os.path.join(config.SAVE_FOLDER, "sources.json")
sources = sorted(sources, key=lambda x: x.get("nom", x.get("platform", "")).lower()) if os.path.exists(legacy_path):
config.platforms = [source["platform"] for source in sources] logger.warning("Ancien fichier sources.json détecté: déclenchement redownload cache jeux")
config.platform_dicts = sources try:
config.platform_names = {source["platform"]: source["nom"] for source in sources} # Supprimer ancien cache et forcer redémarrage logique comme dans l'option de menu
config.games_count = {platform: 0 for platform in config.platforms} # Initialiser à 0 if os.path.exists(config.SOURCES_FILE):
# Charger les jeux pour chaque plateforme try:
loaded_platforms = set() # Pour suivre les plateformes déjà loguées os.remove(config.SOURCES_FILE)
for platform in config.platforms: except Exception:
games = load_games(platform) pass
config.games_count[platform] = len(games) if os.path.exists(config.GAMES_FOLDER):
if platform not in loaded_platforms: shutil.rmtree(config.GAMES_FOLDER, ignore_errors=True)
loaded_platforms.add(platform) if os.path.exists(config.IMAGES_FOLDER):
# Appeler write_unavailable_systems une seule fois après la boucle shutil.rmtree(config.IMAGES_FOLDER, ignore_errors=True)
write_unavailable_systems() # Assurez-vous que cette fonction est définie # Renommer legacy pour éviter boucle
try:
os.replace(legacy_path, legacy_path + ".bak")
except Exception:
pass
# Préparer popup redémarrage si contexte graphique chargé
config.popup_message = _("popup_redownload_success") if hasattr(config, 'popup_message') else "Cache jeux réinitialisé"
config.popup_timer = 5000 if hasattr(config, 'popup_timer') else 0
config.menu_state = "restart_popup" if hasattr(config, 'menu_state') else getattr(config, 'menu_state', 'platform')
config.needs_redraw = True
logger.info("Redownload cache déclenché automatiquement (legacy sources.json)")
return [] # On sort pour laisser le processus de redémarrage gérer le rechargement
except Exception as e:
logger.error(f"Échec redownload automatique depuis legacy sources.json: {e}")
sources = []
if os.path.exists(config.SOURCES_FILE):
with open(config.SOURCES_FILE, 'r', encoding='utf-8') as f:
sources = json.load(f)
if not isinstance(sources, list):
logger.error("systems_list.json n'est pas une liste JSON valide")
sources = []
else:
logger.warning(f"Fichier systems_list absent: {config.SOURCES_FILE}")
# S'assurer que chaque entrée possède la clé platform_image (vide si absente)
for s in sources:
if "platform_image" not in s:
# Supporter ancienne clé system_image -> platform_image si présente
legacy = s.pop("system_image", "") if isinstance(s, dict) else ""
s["platform_image"] = legacy or ""
existing_names = {s.get("platform_name", "") for s in sources}
added = []
if os.path.isdir(config.GAMES_FOLDER):
for fname in sorted(os.listdir(config.GAMES_FOLDER)):
if not fname.lower().endswith('.json'):
continue
pname = os.path.splitext(fname)[0]
if not pname or pname in existing_names:
continue
new_entry = {"platform_name": pname, "folder": pname, "platform_image": ""}
sources.append(new_entry)
added.append(pname)
existing_names.add(pname)
# Déterminer les plateformes orphelines (fichier manquant)
existing_files = set()
if os.path.isdir(config.GAMES_FOLDER):
existing_files = {os.path.splitext(f)[0] for f in os.listdir(config.GAMES_FOLDER) if f.lower().endswith('.json')}
removed = []
filtered_sources = []
for entry in sources:
pname = entry.get("platform_name", "")
# Garder seulement si un fichier existe
if pname in existing_files:
filtered_sources.append(entry)
else:
# Ne retirer que si ce n'est pas un nom vide
if pname:
removed.append(pname)
sources = filtered_sources
if added:
logger.info(f"Plateformes ajoutées automatiquement: {', '.join(added)}")
if removed:
logger.info(f"Plateformes supprimées (fichiers absents): {', '.join(removed)}")
# Persister si modifications (ajouts ou suppressions)
if added or removed:
try:
# Pas de tri avant persistance: conserver ordre d'origine + ajouts fins
os.makedirs(os.path.dirname(config.SOURCES_FILE), exist_ok=True)
with open(config.SOURCES_FILE, 'w', encoding='utf-8') as f:
json.dump(sources, f, ensure_ascii=False, indent=2)
logger.info("systems_list.json mis à jour (ajouts/suppressions, ordre conservé)")
except Exception as e:
logger.error(f"Échec écriture systems_list.json après maj auto: {e}")
# Pour l'affichage on veut un tri alphabétique sans toucher l'ordre de persistance
sorted_for_display = sorted(sources, key=lambda x: x.get("platform_name", "").lower())
# Construire structures config: platform_dicts = ordre fichier, platforms = tri (avec filtre masqués)
config.platform_dicts = sources # ordre brut fichier
settings = load_rgsx_settings()
hidden = set(settings.get("hidden_platforms", [])) if isinstance(settings, dict) else set()
all_sorted_names = [s.get("platform_name", "") for s in sorted_for_display]
visible_names = [n for n in all_sorted_names if n and n not in hidden]
config.platforms = visible_names
config.platform_names = {p: p for p in config.platforms}
# Nouveau mapping par nom pour éviter décalages index après tri d'affichage
try:
config.platform_dict_by_name = {d.get("platform_name", ""): d for d in config.platform_dicts}
except Exception:
config.platform_dict_by_name = {}
config.games_count = {}
for platform_name in config.platforms:
games = load_games(platform_name)
config.games_count[platform_name] = len(games)
write_unavailable_systems()
return sources return sources
except Exception as e: except Exception as e:
logger.error(f"Erreur lors du chargement de sources.json : {str(e)}") logger.error(f"Erreur fusion systèmes + détection jeux: {e}")
return [] return []
def load_games(platform_id): def load_games(platform_id):
"""Charge les jeux pour une plateforme donnée en utilisant platform_id et teste la première URL.""" """Charge la liste des jeux pour une plateforme.
games_path = os.path.join(config.GAMES_FOLDER, f"{platform_id}.json")
#logger.debug(f"Chargement des jeux pour {platform_id} depuis {games_path}") Recherche des fichiers dans config.GAMES_FOLDER selon l'ordre:
1. <platform_id>.json (nom exact)
2. normalisé: normalize_platform_name(platform_id).json
3. <folder>.json (valeur 'folder' du dictionnaire de plateforme)
Format accepté du fichier JSON:
- liste de listes/tuples: [ [name, url], [name, url, size], ... ]
- liste de chaînes: [ name1, name2, ... ]
- liste de dicts: [ {"game_name"|"name"|"title": str, "url"|"download"|"link"|"href": str?, "size"|"filesize"|"length": str?}, ... ]
- dict contenant une clé 'games' avec un des formats ci-dessus
Retourne une liste normalisée de tuples (name, url_or_None, size_or_None).
"""
try: try:
with open(games_path, 'r', encoding='utf-8') as f: # Retrouver l'objet plateforme pour accéder éventuellement à 'folder'
games = json.load(f) platform_dict = None
for pd in config.platform_dicts:
logger.debug(f"Jeux chargés pour {platform_id}: {len(games)} jeux") if pd.get("platform_name") == platform_id or pd.get("platform") == platform_id:
return games platform_dict = pd
break
candidates = []
# 1. Nom exact
candidates.append(os.path.join(config.GAMES_FOLDER, f"{platform_id}.json"))
# 2. Nom normalisé
norm = normalize_platform_name(platform_id)
if norm and norm != platform_id:
candidates.append(os.path.join(config.GAMES_FOLDER, f"{norm}.json"))
# 3. Folder déclaré
if platform_dict:
folder_name = platform_dict.get("folder")
if folder_name:
candidates.append(os.path.join(config.GAMES_FOLDER, f"{folder_name}.json"))
game_file = None
for c in candidates:
if os.path.exists(c):
game_file = c
break
if not game_file:
logger.warning(f"Aucun fichier de jeux trouvé pour {platform_id} (candidats: {candidates})")
return []
with open(game_file, 'r', encoding='utf-8') as f:
data = json.load(f)
# Si dict avec clé 'games'
if isinstance(data, dict) and 'games' in data:
data = data['games']
normalized = [] # (name, url, size)
def extract_from_dict(d):
name = d.get('game_name') or d.get('name') or d.get('title') or d.get('game')
url = d.get('url') or d.get('download') or d.get('link') or d.get('href')
size = d.get('size') or d.get('filesize') or d.get('length')
if name:
normalized.append((str(name), url if isinstance(url, str) and url.strip() else None, str(size) if size else None))
if isinstance(data, list):
for item in data:
if isinstance(item, (list, tuple)):
if len(item) == 0:
continue
name = str(item[0])
url = item[1] if len(item) > 1 and isinstance(item[1], str) and item[1].strip() else None
size = item[2] if len(item) > 2 and isinstance(item[2], str) and item[2].strip() else None
normalized.append((name, url, size))
elif isinstance(item, dict):
extract_from_dict(item)
elif isinstance(item, str):
normalized.append((item, None, None))
else:
normalized.append((str(item), None, None))
elif isinstance(data, dict): # dict sans 'games'
extract_from_dict(data)
else:
logger.warning(f"Format de fichier jeux inattendu pour {platform_id}: {type(data)}")
logger.debug(f"Jeux chargés pour {platform_id} depuis {os.path.basename(game_file)}: {len(normalized)} entrées")
return normalized
except Exception as e: except Exception as e:
logger.error(f"Erreur lors du chargement des jeux pour {platform_id} : {str(e)}") logger.error(f"Erreur lors du chargement des jeux pour {platform_id}: {e}")
return [] return []
def write_unavailable_systems(): def write_unavailable_systems():
@@ -156,12 +340,11 @@ def write_unavailable_systems():
# Formater la date et l'heure pour le nom du fichier # Formater la date et l'heure pour le nom du fichier
current_time = datetime.now() current_time = datetime.now()
timestamp = current_time.strftime("%d-%m-%Y-%H-%M") timestamp = current_time.strftime("%d-%m-%Y-%H-%M")
log_dir = os.path.join(os.path.dirname(config.APP_FOLDER), "logs", "RGSX") log_file = os.path.join(config.log_dir, f"systemes_unavailable_{timestamp}.txt")
log_file = os.path.join(log_dir, f"systemes_unavailable_{timestamp}.txt")
try: try:
# Créer le répertoire s'il n'existe pas # Créer le répertoire s'il n'existe pas
os.makedirs(log_dir, exist_ok=True) os.makedirs(config.log_dir, exist_ok=True)
# Écrire les systèmes dans le fichier # Écrire les systèmes dans le fichier
with open(log_file, 'w', encoding='utf-8') as f: with open(log_file, 'w', encoding='utf-8') as f:
@@ -290,15 +473,28 @@ def wrap_text(text, font, max_width):
return lines return lines
def load_system_image(platform_dict): def load_system_image(platform_dict):
"""Charge une image système depuis le chemin spécifié dans system_image.""" """Charge une image système avec priorité:
image_path = os.path.join(config.IMAGES_FOLDER, platform_dict.get("system_image", "default.png")) 1. Fichier nommé exactement <platform_name>.png
platform_name = platform_dict.get("platform", "unknown") 2. Champ platform_image si non vide
#logger.debug(f"Chargement de l'image système pour {platform_name} depuis {image_path}") 3. Fallback default.png"""
platform_name = platform_dict.get("platform_name", "unknown")
preferred_filename = f"{platform_name}.png"
preferred_path = os.path.join(config.IMAGES_FOLDER, preferred_filename)
# Normaliser platform_image pouvant être vide
platform_image_field = platform_dict.get("platform_image") or ""
explicit_image_path = os.path.join(config.IMAGES_FOLDER, platform_image_field) if platform_image_field else None
default_path = os.path.join(config.IMAGES_FOLDER, "default.png")
try: try:
if not os.path.exists(image_path): if os.path.exists(preferred_path):
logger.error(f"Image introuvable pour {platform_name} à {image_path}") return pygame.image.load(preferred_path).convert_alpha()
return None if explicit_image_path and os.path.exists(explicit_image_path):
return pygame.image.load(image_path).convert_alpha() return pygame.image.load(explicit_image_path).convert_alpha()
if os.path.exists(default_path):
return pygame.image.load(default_path).convert_alpha()
logger.error(f"Aucune image trouvée pour {platform_name} (cherché: {preferred_path}, {explicit_image_path}, default.png)")
return None
except Exception as e: except Exception as e:
logger.error(f"Erreur lors du chargement de l'image pour {platform_name} : {str(e)}") logger.error(f"Erreur lors du chargement de l'image pour {platform_name} : {str(e)}")
return None return None
@@ -447,16 +643,6 @@ def extract_rar(rar_path, dest_dir, url):
if system_type == "Windows": if system_type == "Windows":
# Sur Windows, utiliser directement config.UNRAR_EXE # Sur Windows, utiliser directement config.UNRAR_EXE
unrar_exe = config.UNRAR_EXE unrar_exe = config.UNRAR_EXE
if not os.path.exists(unrar_exe):
logger.warning("unrar.exe absent, téléchargement en cours...")
try:
import urllib.request
os.makedirs(os.path.dirname(unrar_exe), exist_ok=True)
urllib.request.urlretrieve(config.unrar_download_exe, unrar_exe)
logger.info(f"unrar.exe téléchargé dans {unrar_exe}")
except Exception as e:
logger.error(f"Impossible de télécharger unrar.exe: {str(e)}")
return False, _("utils_unrar_unavailable")
unrar_cmd = [unrar_exe] unrar_cmd = [unrar_exe]
else: else:
# Linux/Batocera: utiliser 'unrar' du système # Linux/Batocera: utiliser 'unrar' du système
@@ -657,32 +843,11 @@ def handle_xbox(dest_dir, iso_files):
if system_type == "Windows": if system_type == "Windows":
# Sur Windows; telecharger le fichier exe # Sur Windows; telecharger le fichier exe
XDVDFS_EXE = config.XDVDFS_EXE XDVDFS_EXE = config.XDVDFS_EXE
if not os.path.exists(XDVDFS_EXE):
logger.warning("xdvdfs.exe absent, téléchargement en cours...")
try:
import urllib.request
os.makedirs(os.path.dirname(XDVDFS_EXE), exist_ok=True)
urllib.request.urlretrieve(config.xdvdfs_download_exe, XDVDFS_EXE)
logger.info(f"xdvdfs.exe téléchargé dans {XDVDFS_EXE}")
except Exception as e:
logger.error(f"Impossible de télécharger xdvdfs.exe: {str(e)}")
return False, _("utils_xdvdfs_unavailable")
xdvdfs_cmd = [XDVDFS_EXE, "pack"] # Liste avec 2 éléments xdvdfs_cmd = [XDVDFS_EXE, "pack"] # Liste avec 2 éléments
else: else:
# Linux/Batocera : télécharger le fichier xdvdfs # Linux/Batocera : télécharger le fichier xdvdfs
XDVDFS_LINUX = config.XDVDFS_LINUX XDVDFS_LINUX = config.XDVDFS_LINUX
if not os.path.exists(XDVDFS_LINUX):
logger.warning("xdvdfs non trouvé, téléchargement en cours...")
try:
import urllib.request
os.makedirs(os.path.dirname(XDVDFS_LINUX), exist_ok=True)
urllib.request.urlretrieve(config.xdvdfs_download_linux, XDVDFS_LINUX)
os.chmod(XDVDFS_LINUX, 0o755) # Rendre exécutable
logger.info(f"xdvdfs téléchargé dans {XDVDFS_LINUX}")
except Exception as e:
logger.error(f"Impossible de télécharger xdvdfs: {str(e)}")
return False, _("utils_xdvdfs_unavailable") # Vérifier les permissions après le téléchargement
try: try:
stat_info = os.stat(XDVDFS_LINUX) stat_info = os.stat(XDVDFS_LINUX)
mode = stat_info.st_mode mode = stat_info.st_mode
@@ -792,24 +957,24 @@ def set_music_popup(music_name):
def load_api_key_1fichier(): def load_api_key_1fichier():
"""Charge la clé API 1fichier depuis le dossier de sauvegarde, crée le fichier si absent.""" """Charge la clé API 1fichier depuis le dossier de sauvegarde, crée le fichier si absent."""
api_path = os.path.join(SAVE_FOLDER, "1fichierAPI.txt")
logger.debug(f"Tentative de chargement de la clé API depuis: {api_path}") logger.debug(f"Tentative de chargement de la clé API depuis: {config.API_KEY_1FICHIER}")
try: try:
# Vérifie si le fichier existe déjà # Vérifie si le fichier existe déjà
if not os.path.exists(api_path): if not os.path.exists(config.API_KEY_1FICHIER):
# Crée le dossier parent si nécessaire # Crée le dossier parent si nécessaire
os.makedirs(SAVE_FOLDER, exist_ok=True) os.makedirs(config.SAVE_FOLDER, exist_ok=True)
# Crée le fichier vide si absent # Crée le fichier vide si absent
with open(api_path, "w") as f: with open(config.API_KEY_1FICHIER, "w") as f:
f.write("") f.write("")
logger.info(f"Fichier de clé API créé : {api_path}") logger.info(f"Fichier de clé API créé : {config.API_KEY_1FICHIER}")
return "" return ""
except OSError as e: except OSError as e:
logger.error(f"Erreur lors de la création du fichier de clé API : {e}") logger.error(f"Erreur lors de la création du fichier de clé API : {e}")
return "" return ""
# Lit la clé API depuis le fichier # Lit la clé API depuis le fichier
try: try:
with open(api_path, "r", encoding="utf-8") as f: with open(config.API_KEY_1FICHIER, "r", encoding="utf-8") as f:
api_key = f.read().strip() api_key = f.read().strip()
logger.debug(f"Clé API 1fichier lue: '{api_key}' (longueur: {len(api_key)})") logger.debug(f"Clé API 1fichier lue: '{api_key}' (longueur: {len(api_key)})")
if not api_key: if not api_key: