From 6f17173a8c102e33457aed032aed074ff5bed0f4 Mon Sep 17 00:00:00 2001 From: skymike03 Date: Wed, 19 Nov 2025 23:15:12 +0100 Subject: [PATCH] v2.3.2.7 (2025.11.19) - BETA : add filtering options of games in RGSX main app / synced with options sets on web interface Filter by Region, hide beta and demos, show only one rom per game and select prefered display order --- ports/RGSX/__main__.py | 27 ++- ports/RGSX/config.py | 7 +- ports/RGSX/controls.py | 262 +++++++++++++++++++++++++++-- ports/RGSX/display.py | 318 ++++++++++++++++++++++++++++++++++- ports/RGSX/game_filters.py | 237 ++++++++++++++++++++++++++ ports/RGSX/languages/de.json | 35 +++- ports/RGSX/languages/en.json | 37 +++- ports/RGSX/languages/es.json | 31 +++- ports/RGSX/languages/fr.json | 31 +++- ports/RGSX/languages/it.json | 31 +++- ports/RGSX/languages/pt.json | 31 +++- ports/RGSX/rgsx_settings.py | 23 +++ ports/RGSX/rgsx_web.py | 41 +++++ ports/RGSX/static/js/app.js | 187 ++++++++++++++++++-- version.json | 2 +- 15 files changed, 1250 insertions(+), 50 deletions(-) create mode 100644 ports/RGSX/game_filters.py diff --git a/ports/RGSX/__main__.py b/ports/RGSX/__main__.py index 6252844..83f27d7 100644 --- a/ports/RGSX/__main__.py +++ b/ports/RGSX/__main__.py @@ -22,7 +22,7 @@ from display import ( init_display, draw_loading_screen, draw_error_screen, draw_platform_grid, draw_progress_screen, draw_controls, draw_virtual_keyboard, draw_extension_warning, draw_pause_menu, draw_controls_help, draw_game_list, - draw_display_menu, + draw_display_menu, draw_filter_menu_choice, draw_filter_advanced, draw_filter_priority_config, draw_history_list, draw_clear_history_dialog, draw_cancel_download_dialog, draw_confirm_dialog, draw_reload_games_data_dialog, draw_popup, draw_gradient, draw_toast, show_toast, THEME_COLORS @@ -420,6 +420,21 @@ async def main(): global current_music, music_files, music_folder, joystick logger.debug("Début main") + # Charger les filtres de jeux sauvegardés + try: + from game_filters import GameFilters + from rgsx_settings import load_game_filters + config.game_filter_obj = GameFilters() + filter_dict = load_game_filters() + if filter_dict: + config.game_filter_obj.load_from_dict(filter_dict) + if config.game_filter_obj.is_active(): + config.filter_active = True + logger.info("Filtres de jeux chargés et actifs") + except Exception as e: + logger.error(f"Erreur lors du chargement des filtres: {e}") + config.game_filter_obj = None + # Démarrer le serveur web en arrière-plan start_web_server() @@ -672,6 +687,10 @@ async def main(): "history_error_details", "history_confirm_delete", "history_extract_archive", + # Menus filtrage avancé + "filter_menu_choice", + "filter_advanced", + "filter_priority_config", } if config.menu_state in SIMPLE_HANDLE_STATES: action = handle_controls(event, sources, joystick, screen) @@ -1070,6 +1089,12 @@ async def main(): elif config.menu_state == "filter_platforms": from display import draw_filter_platforms_menu draw_filter_platforms_menu(screen) + elif config.menu_state == "filter_menu_choice": + draw_filter_menu_choice(screen) + elif config.menu_state == "filter_advanced": + draw_filter_advanced(screen) + elif config.menu_state == "filter_priority_config": + draw_filter_priority_config(screen) elif config.menu_state == "controls_help": draw_controls_help(screen, config.previous_menu_state) elif config.menu_state == "history": diff --git a/ports/RGSX/config.py b/ports/RGSX/config.py index d6681b5..de2c03d 100644 --- a/ports/RGSX/config.py +++ b/ports/RGSX/config.py @@ -13,7 +13,7 @@ except Exception: pygame = None # type: ignore # Version actuelle de l'application -app_version = "2.3.2.6" +app_version = "2.3.2.7" def get_application_root(): @@ -380,6 +380,11 @@ search_mode = False # Indicateur si le mode recherche est actif search_query = "" # Chaîne de recherche saisie par l'utilisateur filter_active = False # Indicateur si un filtre est appliqué +# Variables pour le filtrage avancé +selected_filter_choice = 0 # Index dans le menu de choix de filtrage (recherche / avancé) +selected_filter_option = 0 # Index dans le menu de filtrage avancé +game_filter_obj = None # Objet GameFilters pour le filtrage avancé + # Gestion des états du menu needs_redraw = False # Indicateur si l'écran doit être redessiné selected_option = 0 # Index de l'option sélectionnée dans le menu diff --git a/ports/RGSX/controls.py b/ports/RGSX/controls.py index ab149fa..4974fd9 100644 --- a/ports/RGSX/controls.py +++ b/ports/RGSX/controls.py @@ -58,7 +58,12 @@ VALID_STATES = [ "scraper", # écran du scraper avec métadonnées "history_error_details", # détails de l'erreur "history_confirm_delete", # confirmation suppression jeu - "history_extract_archive" # extraction d'archive + "history_extract_archive", # extraction d'archive + # Nouveaux menus filtrage avancé + "filter_menu_choice", # menu de choix entre recherche et filtrage avancé + "filter_search", # recherche par nom (existant, mais renommé) + "filter_advanced", # filtrage avancé par région, etc. + "filter_priority_config", # configuration priorité régions pour one-rom-per-game ] def validate_menu_state(state): @@ -476,8 +481,15 @@ def handle_controls(event, sources, joystick, screen): if config.platforms: config.current_platform = config.selected_platform config.games = load_games(config.platforms[config.current_platform]) - config.filtered_games = config.games - config.filter_active = False + + # Apply saved filters automatically if any + if config.game_filter_obj and config.game_filter_obj.is_active(): + config.filtered_games = config.game_filter_obj.apply_filters(config.games) + config.filter_active = True + else: + config.filtered_games = config.games + config.filter_active = False + config.current_game = 0 config.scroll_offset = 0 draw_validation_transition(screen, config.current_platform) @@ -656,14 +668,12 @@ def handle_controls(event, sources, joystick, screen): event.value) config.needs_redraw = True elif is_input_matched(event, "filter"): - config.search_mode = True - config.search_query = "" - config.filtered_games = config.games - config.current_game = 0 - config.scroll_offset = 0 - config.selected_key = (0, 0) + # Afficher le menu de choix entre recherche et filtrage avancé + config.menu_state = "filter_menu_choice" + config.selected_filter_choice = 0 + config.previous_menu_state = "game" config.needs_redraw = True - logger.debug("Entrée en mode recherche") + logger.debug("Ouverture du menu de filtrage") elif is_input_matched(event, "history"): config.menu_state = "history" config.needs_redraw = True @@ -1919,6 +1929,238 @@ def handle_controls(event, sources, joystick, screen): config.needs_redraw = True logger.debug("Annulation de la sélection de langue, retour au menu pause") + # Menu de choix filtrage + elif config.menu_state == "filter_menu_choice": + if is_input_matched(event, "up"): + config.selected_filter_choice = (config.selected_filter_choice - 1) % 2 + config.needs_redraw = True + elif is_input_matched(event, "down"): + config.selected_filter_choice = (config.selected_filter_choice + 1) % 2 + config.needs_redraw = True + elif is_input_matched(event, "confirm"): + if config.selected_filter_choice == 0: + # Recherche par nom (mode existant) + config.search_mode = True + config.search_query = "" + config.filtered_games = config.games + config.current_game = 0 + config.scroll_offset = 0 + config.selected_key = (0, 0) + config.menu_state = "game" + config.needs_redraw = True + logger.debug("Entrée en mode recherche par nom") + else: + # Filtrage avancé + from game_filters import GameFilters + from rgsx_settings import load_game_filters + + # Initialiser le filtre + if not hasattr(config, 'game_filter_obj'): + config.game_filter_obj = GameFilters() + filter_dict = load_game_filters() + if filter_dict: + config.game_filter_obj.load_from_dict(filter_dict) + + config.menu_state = "filter_advanced" + config.selected_filter_option = 0 + config.needs_redraw = True + logger.debug("Entrée en filtrage avancé") + elif is_input_matched(event, "cancel"): + config.menu_state = "game" + config.needs_redraw = True + logger.debug("Retour à la liste des jeux") + + # Filtrage avancé + elif config.menu_state == "filter_advanced": + from game_filters import GameFilters + from rgsx_settings import save_game_filters + + # Initialiser le filtre si nécessaire + if not hasattr(config, 'game_filter_obj'): + config.game_filter_obj = GameFilters() + from rgsx_settings import load_game_filters + filter_dict = load_game_filters() + if filter_dict: + config.game_filter_obj.load_from_dict(filter_dict) + + # Construire la liste des options (comme dans draw_filter_advanced) + options = [] + options.append(('header', 'region_title')) + for region in GameFilters.REGIONS: + options.append(('region', region)) + options.append(('separator', '')) + options.append(('header', 'other_options')) + options.append(('toggle', 'hide_non_release')) + options.append(('toggle', 'one_rom_per_game')) + options.append(('button_inline', 'priority_config')) + + # Boutons séparés (3 boutons au total) + buttons = [ + ('button', 'apply'), + ('button', 'reset'), + ('button', 'back') + ] + + # Total d'éléments sélectionnables + total_items = len(options) + len(buttons) + + if is_input_matched(event, "up"): + # Chercher l'option sélectionnable précédente + config.selected_filter_option = (config.selected_filter_option - 1) % total_items + while config.selected_filter_option < len(options) and options[config.selected_filter_option][0] in ['header', 'separator']: + config.selected_filter_option = (config.selected_filter_option - 1) % total_items + config.needs_redraw = True + update_key_state("up", True, event.type, event.key if event.type == pygame.KEYDOWN else + event.button if event.type == pygame.JOYBUTTONDOWN else + (event.axis, event.value) if event.type == pygame.JOYAXISMOTION else + event.value) + elif is_input_matched(event, "down"): + # Chercher l'option sélectionnable suivante + config.selected_filter_option = (config.selected_filter_option + 1) % total_items + while config.selected_filter_option < len(options) and options[config.selected_filter_option][0] in ['header', 'separator']: + config.selected_filter_option = (config.selected_filter_option + 1) % total_items + config.needs_redraw = True + update_key_state("down", True, event.type, event.key if event.type == pygame.KEYDOWN else + event.button if event.type == pygame.JOYBUTTONDOWN else + (event.axis, event.value) if event.type == pygame.JOYAXISMOTION else + event.value) + elif is_input_matched(event, "left") or is_input_matched(event, "right"): + # Navigation gauche/droite uniquement pour les boutons en bas + if config.selected_filter_option >= len(options): + button_index = config.selected_filter_option - len(options) + if is_input_matched(event, "left"): + button_index = (button_index - 1) % len(buttons) + else: + button_index = (button_index + 1) % len(buttons) + config.selected_filter_option = len(options) + button_index + config.needs_redraw = True + elif is_input_matched(event, "confirm"): + # Déterminer si c'est une option ou un bouton + if config.selected_filter_option < len(options): + option_type, *option_data = options[config.selected_filter_option] + else: + # C'est un bouton + button_index = config.selected_filter_option - len(options) + option_type, *option_data = buttons[button_index] + + if option_type == 'region': + # Basculer filtre région: include ↔ exclude (include par défaut) + region = option_data[0] + current_state = config.game_filter_obj.region_filters.get(region, 'include') + if current_state == 'include': + config.game_filter_obj.region_filters[region] = 'exclude' + else: + config.game_filter_obj.region_filters[region] = 'include' + config.needs_redraw = True + logger.debug(f"Filtre région {region} modifié: {config.game_filter_obj.region_filters[region]}") + + elif option_type == 'toggle': + toggle_name = option_data[0] + if toggle_name == 'hide_non_release': + config.game_filter_obj.hide_non_release = not config.game_filter_obj.hide_non_release + elif toggle_name == 'one_rom_per_game': + config.game_filter_obj.one_rom_per_game = not config.game_filter_obj.one_rom_per_game + config.needs_redraw = True + logger.debug(f"Toggle {toggle_name} modifié") + + elif option_type == 'button_inline': + button_name = option_data[0] + if button_name == 'priority_config': + # Ouvrir le menu de configuration de priorité + config.menu_state = "filter_priority_config" + config.selected_priority_index = 0 + config.needs_redraw = True + logger.debug("Ouverture configuration priorité régions") + + elif option_type == 'button': + button_name = option_data[0] + if button_name == 'apply': + # Appliquer les filtres + save_game_filters(config.game_filter_obj.to_dict()) + + # Appliquer aux jeux actuels + if config.game_filter_obj.is_active(): + config.filtered_games = config.game_filter_obj.apply_filters(config.games) + config.filter_active = True + else: + config.filtered_games = config.games + config.filter_active = False + + config.current_game = 0 + config.scroll_offset = 0 + config.menu_state = "game" + config.needs_redraw = True + logger.debug("Filtres appliqués") + + elif button_name == 'reset': + # Réinitialiser les filtres + config.game_filter_obj.reset() + save_game_filters(config.game_filter_obj.to_dict()) + config.filtered_games = config.games + config.filter_active = False + config.needs_redraw = True + logger.debug("Filtres réinitialisés") + + elif button_name == 'back': + # Retour sans appliquer + config.menu_state = "game" + config.needs_redraw = True + logger.debug("Retour sans appliquer les filtres") + + elif is_input_matched(event, "cancel"): + config.menu_state = "game" + config.needs_redraw = True + logger.debug("Annulation du filtrage avancé") + + # Configuration priorité régions + elif config.menu_state == "filter_priority_config": + from game_filters import GameFilters + from rgsx_settings import save_game_filters + + if not hasattr(config, 'game_filter_obj'): + config.game_filter_obj = GameFilters() + + priority_list = config.game_filter_obj.region_priority + total_items = len(priority_list) + 1 # +1 pour le bouton Back + + if not hasattr(config, 'selected_priority_index'): + config.selected_priority_index = 0 + + if is_input_matched(event, "up"): + config.selected_priority_index = (config.selected_priority_index - 1) % total_items + config.needs_redraw = True + elif is_input_matched(event, "down"): + config.selected_priority_index = (config.selected_priority_index + 1) % total_items + config.needs_redraw = True + elif is_input_matched(event, "confirm"): + if config.selected_priority_index >= len(priority_list): + # Bouton Back : retour au menu filtrage avancé + save_game_filters(config.game_filter_obj.to_dict()) + config.menu_state = "filter_advanced" + config.needs_redraw = True + logger.debug("Retour au filtrage avancé") + elif is_input_matched(event, "left") and config.selected_priority_index < len(priority_list): + # Monter la région dans la priorité + idx = config.selected_priority_index + if idx > 0: + priority_list[idx], priority_list[idx-1] = priority_list[idx-1], priority_list[idx] + config.selected_priority_index = idx - 1 + config.needs_redraw = True + logger.debug(f"Priorité modifiée: {priority_list}") + elif is_input_matched(event, "right") and config.selected_priority_index < len(priority_list): + # Descendre la région dans la priorité + idx = config.selected_priority_index + if idx < len(priority_list) - 1: + priority_list[idx], priority_list[idx+1] = priority_list[idx+1], priority_list[idx] + config.selected_priority_index = idx + 1 + config.needs_redraw = True + logger.debug(f"Priorité modifiée: {priority_list}") + elif is_input_matched(event, "cancel"): + # Retour sans sauvegarder + config.menu_state = "filter_advanced" + config.needs_redraw = True + logger.debug("Annulation configuration priorité") + # Menu filtre plateformes elif config.menu_state == "filter_platforms": total_items = len(config.filter_platforms_selection) diff --git a/ports/RGSX/display.py b/ports/RGSX/display.py index 07cf8ad..52ef03b 100644 --- a/ports/RGSX/display.py +++ b/ports/RGSX/display.py @@ -193,7 +193,9 @@ THEME_COLORS = { "background_bottom": (60, 80, 100), # noir vers bleu foncé # Fond des cadres "button_idle": (50, 50, 70, 150), # Bleu sombre métal - # Fond des boutons sélectionnés dans les popups ou menu + # Fond des boutons sélectionnés + "button_selected": (70, 70, 100, 200), # Bleu plus clair + # Fond des boutons hover dans les popups ou menu "button_hover": (255, 0, 255, 220), # Rose # Générique "text": (255, 255, 255), # blanc @@ -209,6 +211,10 @@ THEME_COLORS = { "title_text": (200, 200, 200), # gris clair # Bordures "border": (150, 150, 150), # Bordures grises subtiles + "border_selected": (0, 255, 0), # Bordure verte pour sélection + # Couleurs pour filtres + "green": (0, 255, 0), # vert + "red": (255, 0, 0), # rouge } # Général, résolution, overlay @@ -847,13 +853,19 @@ def draw_game_list(screen): pygame.draw.rect(screen, THEME_COLORS["border"], title_rect_inflated, 2, border_radius=12) screen.blit(title_surface, title_rect) elif config.filter_active: - filter_text = _("game_filter").format(config.search_query) - title_surface = config.font.render(filter_text, True, THEME_COLORS["fond_lignes"]) + # Display filter active indicator with count + if hasattr(config, 'game_filter_obj') and config.game_filter_obj and config.game_filter_obj.is_active(): + total_games = len(config.games) + filtered_count = len(games) + filter_text = _("filter_games_shown").format(filtered_count, total_games) + else: + filter_text = _("game_filter").format(config.search_query) + title_surface = config.font.render(filter_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) 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, THEME_COLORS["border_selected"], title_rect_inflated, 3, border_radius=12) screen.blit(title_surface, title_rect) else: title_text = _("game_count").format(platform_name, game_count) @@ -3346,3 +3358,301 @@ def draw_scraper_screen(screen): url_surface = config.small_font.render(url_text, True, THEME_COLORS["title_text"]) url_rect = url_surface.get_rect(center=(config.screen_width // 2, rect_y + rect_height - 20)) screen.blit(url_surface, url_rect) + + +def draw_filter_menu_choice(screen): + """Affiche le menu de choix entre recherche par nom et filtrage avancé""" + screen.blit(OVERLAY, (0, 0)) + + # Titre + title = _("filter_menu_title") + title_surface = config.title_font.render(title, True, THEME_COLORS["text"]) + title_rect = title_surface.get_rect(center=(config.screen_width // 2, 60)) + screen.blit(title_surface, title_rect) + + # Options + options = [ + _("filter_search_by_name"), + _("filter_advanced") + ] + + # Calculer positions + menu_y = 150 + button_height = 60 + button_spacing = 20 + button_width = 600 + + for i, option in enumerate(options): + y = menu_y + i * (button_height + button_spacing) + x = (config.screen_width - button_width) // 2 + + # Couleur selon sélection + if i == config.selected_filter_choice: + color = THEME_COLORS["button_selected"] + border_color = THEME_COLORS["border_selected"] + else: + color = THEME_COLORS["button_idle"] + border_color = THEME_COLORS["border"] + + # Dessiner bouton + pygame.draw.rect(screen, color, (x, y, button_width, button_height), border_radius=12) + pygame.draw.rect(screen, border_color, (x, y, button_width, button_height), 3, border_radius=12) + + # Texte + text_surface = config.font.render(option, True, THEME_COLORS["text"]) + text_rect = text_surface.get_rect(center=(config.screen_width // 2, y + button_height // 2)) + screen.blit(text_surface, text_rect) + + +def draw_filter_advanced(screen): + """Affiche l'écran de filtrage avancé""" + from game_filters import GameFilters + + screen.blit(OVERLAY, (0, 0)) + + # Titre + title = _("filter_advanced_title") + title_surface = config.title_font.render(title, True, THEME_COLORS["text"]) + title_rect = title_surface.get_rect(center=(config.screen_width // 2, 40)) + screen.blit(title_surface, title_rect) + + # Initialiser le filtre si nécessaire + if not hasattr(config, 'game_filter_obj'): + config.game_filter_obj = GameFilters() + # Charger depuis settings + from rgsx_settings import load_game_filters + filter_dict = load_game_filters() + if filter_dict: + config.game_filter_obj.load_from_dict(filter_dict) + + # Zones d'affichage + start_y = 100 + line_height = 50 + current_y = start_y + + # Liste des options + options = [] + + # Section Régions + region_title = _("filter_region_title") + options.append(('header', region_title)) + + for region in GameFilters.REGIONS: + region_key = f"filter_region_{region.lower()}" + region_label = _(region_key) + filter_state = config.game_filter_obj.region_filters.get(region, 'include') # Par défaut: include + + if filter_state == 'exclude': + status = f"[X] {_('filter_region_exclude')}" + color = THEME_COLORS["red"] + else: # 'include' + status = f"[V] {_('filter_region_include')}" + color = THEME_COLORS["green"] + + options.append(('region', region, f"{region_label}: {status}", color)) + + # Section Autres options + options.append(('separator', '')) + options.append(('header', _("filter_other_options"))) + + hide_text = _("filter_hide_non_release") + hide_status = "[X]" if config.game_filter_obj.hide_non_release else "[ ]" + options.append(('toggle', 'hide_non_release', f"{hide_text}: {hide_status}")) + + one_rom_text = _("filter_one_rom_per_game") + one_rom_status = "[X]" if config.game_filter_obj.one_rom_per_game else "[ ]" + # Afficher les 3 premières régions de priorité + priority_preview = " → ".join(config.game_filter_obj.region_priority[:3]) + "..." + options.append(('toggle', 'one_rom_per_game', f"{one_rom_text}: {one_rom_status}")) + options.append(('button_inline', 'priority_config', f"{_('filter_priority_order')}: {priority_preview}")) + + # Boutons d'action (seront affichés séparément en bas) + buttons = [ + ('apply', _("filter_apply_filters")), + ('reset', _("filter_reset_filters")), + ('back', _("filter_back")) + ] + + # Afficher les options (sans les boutons) + if not hasattr(config, 'selected_filter_option'): + config.selected_filter_option = 0 + + # S'assurer que l'index est valide (options + 3 boutons) + total_items = len(options) + len(buttons) + if config.selected_filter_option >= total_items: + config.selected_filter_option = total_items - 1 + + for i, option in enumerate(options): + option_type = option[0] + + if option_type == 'header': + # En-tête de section + text_surface = config.font.render(option[1], True, THEME_COLORS["title_text"]) + screen.blit(text_surface, (100, current_y)) + current_y += line_height + + elif option_type == 'separator': + current_y += 10 + + elif option_type in ['region', 'toggle', 'button_inline']: + # Option sélectionnable + x = 120 + width = config.screen_width - 240 + height = 45 + + # Couleur selon sélection + if i == config.selected_filter_option: + bg_color = THEME_COLORS["button_selected"] + border_color = THEME_COLORS["border_selected"] + else: + bg_color = THEME_COLORS["button_idle"] + border_color = THEME_COLORS["border"] + + # Dessiner fond + pygame.draw.rect(screen, bg_color, (x, current_y, width, height), border_radius=8) + pygame.draw.rect(screen, border_color, (x, current_y, width, height), 2, border_radius=8) + + # Texte + if option_type == 'region': + text = option[2] + text_color = option[3] + else: + text = option[2] + text_color = THEME_COLORS["text"] + + text_surface = config.font.render(text, True, text_color) + text_rect = text_surface.get_rect(left=x + 20, centery=current_y + height // 2) + screen.blit(text_surface, text_rect) + + current_y += height + 10 + + # Afficher les 3 boutons côte à côte en bas + # Calculer la position pour éviter la barre de contrôles (hauteur estimée ~60-80px) + control_bar_estimated_height = 80 + button_width = 200 + button_height = 50 + button_spacing = 20 + total_buttons_width = button_width * 3 + button_spacing * 2 + button_start_x = (config.screen_width - total_buttons_width) // 2 + button_y = config.screen_height - control_bar_estimated_height - button_height - 20 + + for i, (button_id, button_text) in enumerate(buttons): + button_index = len(options) + i + button_x = button_start_x + i * (button_width + button_spacing) + + # Couleur selon sélection + if button_index == config.selected_filter_option: + bg_color = THEME_COLORS["button_selected"] + border_color = THEME_COLORS["border_selected"] + else: + bg_color = THEME_COLORS["button_idle"] + border_color = THEME_COLORS["border"] + + # Dessiner bouton + pygame.draw.rect(screen, bg_color, (button_x, button_y, button_width, button_height), border_radius=8) + pygame.draw.rect(screen, border_color, (button_x, button_y, button_width, button_height), 2, border_radius=8) + + # Texte centré + text_surface = config.font.render(button_text, True, THEME_COLORS["text"]) + text_rect = text_surface.get_rect(center=(button_x + button_width // 2, button_y + button_height // 2)) + screen.blit(text_surface, text_rect) + + # Info filtre actif (au-dessus des boutons) + if config.game_filter_obj.is_active(): + info_text = _("filter_active") + info_surface = config.small_font.render(info_text, True, THEME_COLORS["green"]) + info_rect = info_surface.get_rect(center=(config.screen_width // 2, button_y - 30)) + screen.blit(info_surface, info_rect) + + +def draw_filter_priority_config(screen): + """Affiche l'écran de configuration de la priorité des régions pour One ROM per game""" + from game_filters import GameFilters + + screen.blit(OVERLAY, (0, 0)) + + # Titre + title = _("filter_priority_title") + title_surface = config.title_font.render(title, True, THEME_COLORS["text"]) + title_rect = title_surface.get_rect(center=(config.screen_width // 2, 40)) + screen.blit(title_surface, title_rect) + + # Description + desc = _("filter_priority_desc") + desc_surface = config.small_font.render(desc, True, THEME_COLORS["title_text"]) + desc_rect = desc_surface.get_rect(center=(config.screen_width // 2, 85)) + screen.blit(desc_surface, desc_rect) + + # Initialiser le filtre si nécessaire + if not hasattr(config, 'game_filter_obj'): + from game_filters import GameFilters + from rgsx_settings import load_game_filters + config.game_filter_obj = GameFilters() + filter_dict = load_game_filters() + if filter_dict: + config.game_filter_obj.load_from_dict(filter_dict) + + # Liste des régions avec leur priorité + start_y = 130 + line_height = 60 + + if not hasattr(config, 'selected_priority_index'): + config.selected_priority_index = 0 + + priority_list = config.game_filter_obj.region_priority.copy() + + # Afficher chaque région avec sa position + for i, region in enumerate(priority_list): + y = start_y + i * line_height + x = 120 + width = config.screen_width - 240 + height = 50 + + # Couleur selon sélection + if i == config.selected_priority_index: + bg_color = THEME_COLORS["button_selected"] + border_color = THEME_COLORS["border_selected"] + else: + bg_color = THEME_COLORS["button_idle"] + border_color = THEME_COLORS["border"] + + # Dessiner fond + pygame.draw.rect(screen, bg_color, (x, y, width, height), border_radius=8) + pygame.draw.rect(screen, border_color, (x, y, width, height), 2, border_radius=8) + + # Numéro de priorité + priority_text = f"#{i+1}" + priority_surface = config.font.render(priority_text, True, THEME_COLORS["text"]) + screen.blit(priority_surface, (x + 15, y + (height - priority_surface.get_height()) // 2)) + + # Nom de la région (traduit si possible) + region_key = f"filter_region_{region.lower()}" + region_label = _(region_key) + region_surface = config.font.render(region_label, True, THEME_COLORS["text"]) + screen.blit(region_surface, (x + 80, y + (height - region_surface.get_height()) // 2)) + + # Flèches pour réorganiser (si sélectionné) + if i == config.selected_priority_index: + arrows_text = "← →" + arrows_surface = config.font.render(arrows_text, True, THEME_COLORS["green"]) + screen.blit(arrows_surface, (x + width - 50, y + (height - arrows_surface.get_height()) // 2)) + + # Boutons en bas + control_bar_estimated_height = 80 + button_width = 300 + button_height = 50 + button_x = (config.screen_width - button_width) // 2 + button_y = config.screen_height - control_bar_estimated_height - button_height - 20 + + # Bouton Back + is_button_selected = config.selected_priority_index >= len(priority_list) + bg_color = THEME_COLORS["button_selected"] if is_button_selected else THEME_COLORS["button_idle"] + border_color = THEME_COLORS["border_selected"] if is_button_selected else THEME_COLORS["border"] + + pygame.draw.rect(screen, bg_color, (button_x, button_y, button_width, button_height), border_radius=8) + pygame.draw.rect(screen, border_color, (button_x, button_y, button_width, button_height), 2, border_radius=8) + + back_text = _("filter_back") + text_surface = config.font.render(back_text, True, THEME_COLORS["text"]) + text_rect = text_surface.get_rect(center=(button_x + button_width // 2, button_y + button_height // 2)) + screen.blit(text_surface, text_rect) diff --git a/ports/RGSX/game_filters.py b/ports/RGSX/game_filters.py new file mode 100644 index 0000000..7bbeaf0 --- /dev/null +++ b/ports/RGSX/game_filters.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Module de filtrage des jeux pour RGSX +Partagé entre l'interface graphique et l'interface web +""" + +import re +import logging +from typing import List, Tuple, Dict, Any + +logger = logging.getLogger(__name__) + + +class GameFilters: + """Classe pour gérer les filtres de jeux""" + + # Régions disponibles + REGIONS = ['USA', 'Canada', 'Europe', 'France', 'Germany', 'Japan', 'Korea', 'World', 'Other'] + + def __init__(self): + # Initialiser toutes les régions en mode 'include' par défaut + self.region_filters = {region: 'include' for region in self.REGIONS} + self.hide_non_release = False + self.one_rom_per_game = False + self.regex_mode = False + self.region_priority = ['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other'] + + def load_from_dict(self, filter_dict: Dict[str, Any]): + """Charge les filtres depuis un dictionnaire (depuis settings)""" + loaded_region_filters = filter_dict.get('region_filters', {}) + # Initialiser toutes les régions en 'include' par défaut, puis appliquer celles chargées + self.region_filters = {region: 'include' for region in self.REGIONS} + self.region_filters.update(loaded_region_filters) + + self.hide_non_release = filter_dict.get('hide_non_release', False) + self.one_rom_per_game = filter_dict.get('one_rom_per_game', False) + self.regex_mode = filter_dict.get('regex_mode', False) + self.region_priority = filter_dict.get('region_priority', + ['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other']) + + def to_dict(self) -> Dict[str, Any]: + """Convertit les filtres en dictionnaire (pour sauvegarder dans settings)""" + return { + 'region_filters': self.region_filters, + 'hide_non_release': self.hide_non_release, + 'one_rom_per_game': self.one_rom_per_game, + 'regex_mode': self.regex_mode, + 'region_priority': self.region_priority + } + + def is_active(self) -> bool: + """Vérifie si des filtres sont actifs (au moins une région en exclude ou options activées)""" + has_exclude = any(state == 'exclude' for state in self.region_filters.values()) + return (has_exclude or + self.hide_non_release or + self.one_rom_per_game) + + def reset(self): + """Réinitialise tous les filtres (toutes les régions en include)""" + self.region_filters = {region: 'include' for region in self.REGIONS} + self.hide_non_release = False + self.one_rom_per_game = False + self.regex_mode = False + + @staticmethod + def get_game_regions(game_name: str) -> List[str]: + """Extrait les régions d'un nom de jeu""" + name = game_name.upper() + regions = [] + + # Patterns de région communs + if 'USA' in name or 'US)' in name: + regions.append('USA') + if 'CANADA' in name or 'CA)' in name: + regions.append('Canada') + if 'EUROPE' in name or 'EU)' in name: + regions.append('Europe') + if 'FRANCE' in name or 'FR)' in name: + regions.append('France') + if 'GERMANY' in name or 'DE)' in name or 'GER)' in name: + regions.append('Germany') + if 'JAPAN' in name or 'JP)' in name or 'JPN)' in name: + regions.append('Japan') + if 'KOREA' in name or 'KR)' in name or 'KOR)' in name: + regions.append('Korea') + if 'WORLD' in name: + regions.append('World') + + # Autres régions + if re.search(r'\b(AUSTRALIA|ASIA|KOREA|BRAZIL|CHINA|RUSSIA|SCANDINAVIA|' + r'SPAIN|FRANCE|GERMANY|ITALY|CANADA)\b', name): + if 'CANADA' in name: + regions.append('Canada') + else: + regions.append('Other') + + # Si aucune région trouvée + if not regions: + regions.append('Other') + + return regions + + @staticmethod + def is_non_release_game(game_name: str) -> bool: + """Vérifie si un jeu est une version non-release (demo, beta, proto)""" + name = game_name.upper() + non_release_patterns = [ + r'\([^\)]*BETA[^\)]*\)', + r'\([^\)]*DEMO[^\)]*\)', + r'\([^\)]*PROTO[^\)]*\)', + r'\([^\)]*SAMPLE[^\)]*\)', + r'\([^\)]*KIOSK[^\)]*\)', + r'\([^\)]*PREVIEW[^\)]*\)', + r'\([^\)]*TEST[^\)]*\)', + r'\([^\)]*DEBUG[^\)]*\)', + r'\([^\)]*ALPHA[^\)]*\)', + r'\([^\)]*PRE-RELEASE[^\)]*\)', + r'\([^\)]*PRERELEASE[^\)]*\)', + r'\([^\)]*UNFINISHED[^\)]*\)', + r'\([^\)]*WIP[^\)]*\)', + r'\[[^\]]*BETA[^\]]*\]', + r'\[[^\]]*DEMO[^\]]*\]', + r'\[[^\]]*TEST[^\]]*\]' + ] + return any(re.search(pattern, name) for pattern in non_release_patterns) + + @staticmethod + def get_base_game_name(game_name: str) -> str: + """Obtient le nom de base du jeu (sans régions, versions, etc.)""" + base = game_name + + # Supprimer extensions + base = re.sub(r'\.(zip|7z|rar|gz|iso)$', '', base, flags=re.IGNORECASE) + + # Extraire info disque si présent + disc_info = '' + disc_match = (re.search(r'\(Dis[ck]\s*(\d+)\)', base, re.IGNORECASE) or + re.search(r'\[Dis[ck]\s*(\d+)\]', base, re.IGNORECASE) or + re.search(r'Dis[ck]\s*(\d+)', base, re.IGNORECASE) or + re.search(r'\(CD\s*(\d+)\)', base, re.IGNORECASE) or + re.search(r'CD\s*(\d+)', base, re.IGNORECASE)) + if disc_match: + disc_info = f' (Disc {disc_match.group(1)})' + + # Supprimer contenu entre parenthèses et crochets + base = re.sub(r'\([^)]*\)', '', base) + base = re.sub(r'\[[^\]]*\]', '', base) + + # Normaliser espaces + base = re.sub(r'\s+', ' ', base).strip() + + # Rajouter info disque + base = base + disc_info + + return base + + def get_region_priority(self, game_name: str) -> int: + """Obtient la priorité de région pour un jeu (pour one-rom-per-game)""" + name = game_name.upper() + + for i, region in enumerate(self.region_priority): + region_upper = region.upper() + if region_upper in name: + return i + + return len(self.region_priority) # Autres régions (priorité la plus basse) + + def apply_filters(self, games: List[Tuple]) -> List[Tuple]: + """ + Applique les filtres à une liste de jeux + games: Liste de tuples (game_name, game_url, size) + Retourne: Liste filtrée de tuples + """ + if not self.is_active(): + return games + + filtered_games = [] + + # Filtrage par région + for game in games: + game_name = game[0] + + # Vérifier les filtres de région + if self.region_filters: + game_regions = self.get_game_regions(game_name) + + # Vérifier si le jeu a au moins une région incluse + has_included_region = False + + for region in game_regions: + filter_state = self.region_filters.get(region, 'include') + if filter_state == 'include': + has_included_region = True + break # Si on trouve une région incluse, c'est bon + + # Le jeu est affiché seulement s'il a au moins une région incluse + if not has_included_region: + continue + + # Filtrer les non-release + if self.hide_non_release and self.is_non_release_game(game_name): + continue + + filtered_games.append(game) + + # Appliquer "one rom per game" + if self.one_rom_per_game: + filtered_games = self._apply_one_rom_per_game(filtered_games) + + return filtered_games + + def _apply_one_rom_per_game(self, games: List[Tuple]) -> List[Tuple]: + """Garde seulement une ROM par jeu selon la priorité de région""" + games_by_base = {} + + for game in games: + game_name = game[0] + base_name = self.get_base_game_name(game_name) + + if base_name not in games_by_base: + games_by_base[base_name] = [] + + games_by_base[base_name].append(game) + + # Pour chaque jeu de base, garder celui avec la meilleure priorité + result = [] + for base_name, game_list in games_by_base.items(): + if len(game_list) == 1: + result.append(game_list[0]) + else: + # Trier par priorité de région + sorted_games = sorted(game_list, + key=lambda g: self.get_region_priority(g[0])) + result.append(sorted_games[0]) + + return result diff --git a/ports/RGSX/languages/de.json b/ports/RGSX/languages/de.json index 730306b..111598d 100644 --- a/ports/RGSX/languages/de.json +++ b/ports/RGSX/languages/de.json @@ -367,10 +367,9 @@ "web_filter_regex_mode": "Regex-Suche aktivieren", "web_filter_one_rom_per_game": "Eine ROM pro Spiel", "web_filter_configure_priority": "Regions-Prioritätsreihenfolge konfigurieren", - "filter_all": "Alles auswählen", - "filter_none": "Alles abwählen", + "filter_all": "Alle auswählen", + "filter_none": "Alle abwählen", "filter_apply": "Filter anwenden", - "filter_back": "Zurück", "accessibility_footer_font_size": "Fußzeilenschriftgröße: {0}", "popup_layout_changed_restart": "Layout geändert auf {0}x{1}. Bitte starten Sie die App neu.", "web_started": "Gestartet", @@ -379,5 +378,33 @@ "web_added_to_queue": "zur Warteschlange hinzugefügt", "web_download_success": "erfolgreich heruntergeladen!", "web_download_error_for": "Fehler beim Herunterladen von", - "web_already_present": "war bereits vorhanden" + "web_already_present": "war bereits vorhanden", + "filter_menu_title": "Filtermenü", + "filter_search_by_name": "Nach Namen suchen", + "filter_advanced": "Erweiterte Filterung", + "filter_advanced_title": "Erweiterte Spielfilterung", + "filter_region_title": "Nach Region filtern", + "filter_region_include": "Einschließen", + "filter_region_exclude": "Ausschließen", + "filter_region_usa": "USA", + "filter_region_canada": "Kanada", + "filter_region_europe": "Europa", + "filter_region_france": "Frankreich", + "filter_region_germany": "Deutschland", + "filter_region_japan": "Japan", + "filter_region_korea": "Korea", + "filter_region_world": "Welt", + "filter_region_other": "Andere", + "filter_other_options": "Weitere Optionen", + "filter_hide_non_release": "Demos/Betas/Protos ausblenden", + "filter_one_rom_per_game": "Eine ROM pro Spiel", + "filter_priority_order": "Prioritätsreihenfolge", + "filter_priority_title": "Regionsprioritätskonfiguration", + "filter_priority_desc": "Prioritätsreihenfolge für \"Eine ROM pro Spiel\" festlegen", + "filter_regex_mode": "Regex-Modus", + "filter_apply_filters": "Anwenden", + "filter_reset_filters": "Zurücksetzen", + "filter_back": "Zurück", + "filter_active": "Filter aktiv", + "filter_games_shown": "{0} Spiel(e) angezeigt" } \ No newline at end of file diff --git a/ports/RGSX/languages/en.json b/ports/RGSX/languages/en.json index 5749683..8bc8f0f 100644 --- a/ports/RGSX/languages/en.json +++ b/ports/RGSX/languages/en.json @@ -367,10 +367,9 @@ "web_filter_regex_mode": "Enable Regex Search", "web_filter_one_rom_per_game": "One ROM Per Game", "web_filter_configure_priority": "Configure region priority order", - "filter_all": "Check All", - "filter_none": "Uncheck All", - "filter_apply": "Apply Filter", - "filter_back": "Back", + "filter_all": "Check all", + "filter_none": "Uncheck all", + "filter_apply": "Apply filter", "accessibility_footer_font_size": "Footer font size: {0}", "popup_layout_changed_restart": "Layout changed to {0}x{1}. Please restart the app to apply.", "web_started": "Started", @@ -379,5 +378,33 @@ "web_added_to_queue": "added to queue", "web_download_success": "downloaded successfully!", "web_download_error_for": "Error downloading", - "web_already_present": "was already present" + "web_already_present": "was already present", + "filter_menu_title": "Filter Menu", + "filter_search_by_name": "Search by name", + "filter_advanced": "Advanced filtering", + "filter_advanced_title": "Advanced Game Filtering", + "filter_region_title": "Filter by region", + "filter_region_include": "Include", + "filter_region_exclude": "Exclude", + "filter_region_usa": "USA", + "filter_region_canada": "Canada", + "filter_region_europe": "Europe", + "filter_region_france": "France", + "filter_region_germany": "Germany", + "filter_region_japan": "Japan", + "filter_region_korea": "Korea", + "filter_region_world": "World", + "filter_region_other": "Other", + "filter_other_options": "Other options", + "filter_hide_non_release": "Hide Demos/Betas/Protos", + "filter_one_rom_per_game": "One ROM per game", + "filter_priority_order": "Priority order", + "filter_priority_title": "Region Priority Configuration", + "filter_priority_desc": "Set preference order for \"One ROM per game\"", + "filter_regex_mode": "Regex Mode", + "filter_apply_filters": "Apply", + "filter_reset_filters": "Reset", + "filter_back": "Back", + "filter_active": "Filter active", + "filter_games_shown": "{0} game(s) shown" } \ No newline at end of file diff --git a/ports/RGSX/languages/es.json b/ports/RGSX/languages/es.json index 7495eff..d6d935c 100644 --- a/ports/RGSX/languages/es.json +++ b/ports/RGSX/languages/es.json @@ -370,7 +370,6 @@ "filter_all": "Marcar todo", "filter_none": "Desmarcar todo", "filter_apply": "Aplicar filtro", - "filter_back": "Volver", "accessibility_footer_font_size": "Tamaño fuente pie de página: {0}", "popup_layout_changed_restart": "Diseño cambiado a {0}x{1}. Reinicie la app para aplicar.", "web_started": "Iniciado", @@ -379,5 +378,33 @@ "web_added_to_queue": "añadido a la cola", "web_download_success": "¡descargado con éxito!", "web_download_error_for": "Error al descargar", - "web_already_present": "ya estaba presente" + "web_already_present": "ya estaba presente", + "filter_menu_title": "Menú de filtros", + "filter_search_by_name": "Buscar por nombre", + "filter_advanced": "Filtrado avanzado", + "filter_advanced_title": "Filtrado avanzado de juegos", + "filter_region_title": "Filtrar por región", + "filter_region_include": "Incluir", + "filter_region_exclude": "Excluir", + "filter_region_usa": "EE.UU.", + "filter_region_canada": "Canadá", + "filter_region_europe": "Europa", + "filter_region_france": "Francia", + "filter_region_germany": "Alemania", + "filter_region_japan": "Japón", + "filter_region_korea": "Corea", + "filter_region_world": "Mundial", + "filter_region_other": "Otros", + "filter_other_options": "Otras opciones", + "filter_hide_non_release": "Ocultar Demos/Betas/Protos", + "filter_one_rom_per_game": "Una ROM por juego", + "filter_priority_order": "Orden de prioridad", + "filter_priority_title": "Configuración de prioridad de regiones", + "filter_priority_desc": "Definir orden de preferencia para \"Una ROM por juego\"", + "filter_regex_mode": "Modo Regex", + "filter_apply_filters": "Aplicar", + "filter_reset_filters": "Restablecer", + "filter_back": "Volver", + "filter_active": "Filtro activo", + "filter_games_shown": "{0} juego(s) mostrado(s)" } \ No newline at end of file diff --git a/ports/RGSX/languages/fr.json b/ports/RGSX/languages/fr.json index 42c879e..733aa1f 100644 --- a/ports/RGSX/languages/fr.json +++ b/ports/RGSX/languages/fr.json @@ -370,7 +370,6 @@ "filter_all": "Tout cocher", "filter_none": "Tout décocher", "filter_apply": "Appliquer filtre", - "filter_back": "Retour", "accessibility_footer_font_size": "Taille police pied de page : {0}", "popup_layout_changed_restart": "Disposition changée en {0}x{1}. Veuillez redémarrer l'app pour appliquer.", "web_started": "Démarré", @@ -379,5 +378,33 @@ "web_added_to_queue": "ajouté à la queue", "web_download_success": "téléchargé avec succès!", "web_download_error_for": "Erreur lors du téléchargement de", - "web_already_present": "était déjà présent" + "web_already_present": "était déjà présent", + "filter_menu_title": "Menu Filtrage", + "filter_search_by_name": "Recherche par nom", + "filter_advanced": "Filtrage avancé", + "filter_advanced_title": "Filtrage avancé des jeux", + "filter_region_title": "Filtrer par région", + "filter_region_include": "Inclure", + "filter_region_exclude": "Exclure", + "filter_region_usa": "USA", + "filter_region_canada": "Canada", + "filter_region_europe": "Europe", + "filter_region_france": "France", + "filter_region_germany": "Allemagne", + "filter_region_japan": "Japon", + "filter_region_korea": "Corée", + "filter_region_world": "Monde", + "filter_region_other": "Autres", + "filter_other_options": "Autres options", + "filter_hide_non_release": "Masquer Démos/Betas/Protos", + "filter_one_rom_per_game": "Une ROM par jeu", + "filter_priority_order": "Ordre de priorité", + "filter_priority_title": "Configuration de la priorité des régions", + "filter_priority_desc": "Définir l'ordre de préférence pour \"Une ROM par jeu\"", + "filter_regex_mode": "Mode Regex", + "filter_apply_filters": "Appliquer", + "filter_reset_filters": "Réinitialiser", + "filter_back": "Retour", + "filter_active": "Filtre actif", + "filter_games_shown": "{0} jeu(x) affiché(s)" } \ No newline at end of file diff --git a/ports/RGSX/languages/it.json b/ports/RGSX/languages/it.json index 28efd61..eeac373 100644 --- a/ports/RGSX/languages/it.json +++ b/ports/RGSX/languages/it.json @@ -370,7 +370,6 @@ "filter_all": "Seleziona tutto", "filter_none": "Deseleziona tutto", "filter_apply": "Applica filtro", - "filter_back": "Indietro", "accessibility_footer_font_size": "Dimensione carattere piè di pagina: {0}", "popup_layout_changed_restart": "Layout cambiato a {0}x{1}. Riavvia l'app per applicare.", "web_started": "Avviato", @@ -379,5 +378,33 @@ "web_added_to_queue": "aggiunto alla coda", "web_download_success": "scaricato con successo!", "web_download_error_for": "Errore durante il download di", - "web_already_present": "era già presente" + "web_already_present": "era già presente", + "filter_menu_title": "Menu filtri", + "filter_search_by_name": "Cerca per nome", + "filter_advanced": "Filtro avanzato", + "filter_advanced_title": "Filtro avanzato giochi", + "filter_region_title": "Filtra per regione", + "filter_region_include": "Includi", + "filter_region_exclude": "Escludi", + "filter_region_usa": "USA", + "filter_region_canada": "Canada", + "filter_region_europe": "Europa", + "filter_region_france": "Francia", + "filter_region_germany": "Germania", + "filter_region_japan": "Giappone", + "filter_region_korea": "Corea", + "filter_region_world": "Mondo", + "filter_region_other": "Altro", + "filter_other_options": "Altre opzioni", + "filter_hide_non_release": "Nascondi Demo/Beta/Proto", + "filter_one_rom_per_game": "Una ROM per gioco", + "filter_priority_order": "Ordine di priorità", + "filter_priority_title": "Configurazione priorità regioni", + "filter_priority_desc": "Imposta ordine di preferenza per \"Una ROM per gioco\"", + "filter_regex_mode": "Modalità Regex", + "filter_apply_filters": "Applica", + "filter_reset_filters": "Reimposta", + "filter_back": "Indietro", + "filter_active": "Filtro attivo", + "filter_games_shown": "{0} gioco/i mostrato/i" } \ No newline at end of file diff --git a/ports/RGSX/languages/pt.json b/ports/RGSX/languages/pt.json index d66fac3..c9ec1b7 100644 --- a/ports/RGSX/languages/pt.json +++ b/ports/RGSX/languages/pt.json @@ -370,7 +370,6 @@ "filter_all": "Marcar tudo", "filter_none": "Desmarcar tudo", "filter_apply": "Aplicar filtro", - "filter_back": "Voltar", "accessibility_footer_font_size": "Tamanho da fonte do rodapé: {0}", "popup_layout_changed_restart": "Layout alterado para {0}x{1}. Reinicie o app para aplicar.", "web_started": "Iniciado", @@ -379,5 +378,33 @@ "web_added_to_queue": "adicionado à fila", "web_download_success": "baixado com sucesso!", "web_download_error_for": "Erro ao baixar", - "web_already_present": "já estava presente" + "web_already_present": "já estava presente", + "filter_menu_title": "Menu de filtros", + "filter_search_by_name": "Pesquisar por nome", + "filter_advanced": "Filtragem avançada", + "filter_advanced_title": "Filtragem avançada de jogos", + "filter_region_title": "Filtrar por região", + "filter_region_include": "Incluir", + "filter_region_exclude": "Excluir", + "filter_region_usa": "EUA", + "filter_region_canada": "Canadá", + "filter_region_europe": "Europa", + "filter_region_france": "França", + "filter_region_germany": "Alemanha", + "filter_region_japan": "Japão", + "filter_region_korea": "Coreia", + "filter_region_world": "Mundial", + "filter_region_other": "Outros", + "filter_other_options": "Outras opções", + "filter_hide_non_release": "Ocultar Demos/Betas/Protos", + "filter_one_rom_per_game": "Uma ROM por jogo", + "filter_priority_order": "Ordem de prioridade", + "filter_priority_title": "Configuração de prioridade de regiões", + "filter_priority_desc": "Definir ordem de preferência para \"Uma ROM por jogo\"", + "filter_regex_mode": "Modo Regex", + "filter_apply_filters": "Aplicar", + "filter_reset_filters": "Redefinir", + "filter_back": "Voltar", + "filter_active": "Filtro ativo", + "filter_games_shown": "{0} jogo(s) exibido(s)" } \ No newline at end of file diff --git a/ports/RGSX/rgsx_settings.py b/ports/RGSX/rgsx_settings.py index 354abfd..1c753f6 100644 --- a/ports/RGSX/rgsx_settings.py +++ b/ports/RGSX/rgsx_settings.py @@ -339,3 +339,26 @@ def get_language(settings=None): if settings is None: settings = load_rgsx_settings() return settings.get("language", "en") + + +def load_game_filters(): + """Charge les filtres de jeux depuis rgsx_settings.json.""" + try: + settings = load_rgsx_settings() + return settings.get("game_filters", {}) + except Exception as e: + logger.error(f"Error loading game filters: {str(e)}") + return {} + + +def save_game_filters(filters_dict): + """Sauvegarde les filtres de jeux dans rgsx_settings.json.""" + try: + settings = load_rgsx_settings() + settings["game_filters"] = filters_dict + save_rgsx_settings(settings) + logger.debug(f"Game filters saved: {filters_dict}") + return True + except Exception as e: + logger.error(f"Error saving game filters: {str(e)}") + return False diff --git a/ports/RGSX/rgsx_web.py b/ports/RGSX/rgsx_web.py index a59cdfc..98cf599 100644 --- a/ports/RGSX/rgsx_web.py +++ b/ports/RGSX/rgsx_web.py @@ -1469,6 +1469,47 @@ class RGSXHandler(BaseHTTPRequestHandler): 'error': str(e) }, status=500) + # Route: Sauvegarder seulement les filtres (sauvegarde rapide) + elif path == '/api/save_filters': + try: + from rgsx_settings import load_rgsx_settings, save_rgsx_settings + + # Charger les settings actuels + current_settings = load_rgsx_settings() + + # Mettre à jour seulement les filtres + if 'game_filters' not in current_settings: + current_settings['game_filters'] = {} + + current_settings['game_filters']['region_filters'] = data.get('region_filters', {}) + current_settings['game_filters']['hide_non_release'] = data.get('hide_non_release', False) + current_settings['game_filters']['one_rom_per_game'] = data.get('one_rom_per_game', False) + current_settings['game_filters']['regex_mode'] = data.get('regex_mode', False) + current_settings['game_filters']['region_priority'] = data.get('region_priority', ['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other']) + + # Sauvegarder + save_rgsx_settings(current_settings) + + # Mettre à jour config.game_filter_obj + if hasattr(config, 'game_filter_obj'): + config.game_filter_obj.region_filters = data.get('region_filters', {}) + config.game_filter_obj.hide_non_release = data.get('hide_non_release', False) + config.game_filter_obj.one_rom_per_game = data.get('one_rom_per_game', False) + config.game_filter_obj.regex_mode = data.get('regex_mode', False) + config.game_filter_obj.region_priority = data.get('region_priority', ['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other']) + + self._send_json({ + 'success': True, + 'message': 'Filtres sauvegardés' + }) + + except Exception as e: + logger.error(f"Erreur lors de la sauvegarde des filtres: {e}") + self._send_json({ + 'success': False, + 'error': str(e) + }, status=500) + # Route: Vider l'historique elif path == '/api/clear-history': try: diff --git a/ports/RGSX/static/js/app.js b/ports/RGSX/static/js/app.js index d6259af..993c8f6 100644 --- a/ports/RGSX/static/js/app.js +++ b/ports/RGSX/static/js/app.js @@ -309,6 +309,9 @@ // Restaurer l'état depuis l'URL au chargement window.addEventListener('DOMContentLoaded', function() { + // Load saved filters first + loadSavedFilters(); + const path = window.location.pathname; if (path.startsWith('/platform/')) { @@ -478,9 +481,130 @@ // Filter state: Map of region -> 'include' or 'exclude' let regionFilters = new Map(); + // Checkbox filter states (stored globally to restore after page changes) + let savedHideNonRelease = false; + let savedOneRomPerGame = false; + let savedRegexMode = false; + // Region priority order for "One ROM Per Game" (customizable) let regionPriorityOrder = JSON.parse(localStorage.getItem('regionPriorityOrder')) || - ['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other']; + ['USA', 'Canada', 'Europe', 'France', 'Germany', 'Japan', 'Korea', 'World', 'Other']; + + // Save filters to backend + async function saveFiltersToBackend() { + try { + const regionFiltersObj = {}; + regionFilters.forEach((mode, region) => { + regionFiltersObj[region] = mode; + }); + + // Update saved states from checkboxes if they exist + if (document.getElementById('hide-non-release')) { + savedHideNonRelease = document.getElementById('hide-non-release').checked; + } + if (document.getElementById('one-rom-per-game')) { + savedOneRomPerGame = document.getElementById('one-rom-per-game').checked; + } + if (document.getElementById('regex-mode')) { + savedRegexMode = document.getElementById('regex-mode').checked; + } + + const response = await fetch('/api/save_filters', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + region_filters: regionFiltersObj, + hide_non_release: savedHideNonRelease, + one_rom_per_game: savedOneRomPerGame, + regex_mode: savedRegexMode, + region_priority: regionPriorityOrder + }) + }); + + const data = await response.json(); + if (!data.success) { + console.warn('Failed to save filters:', data.error); + } + } catch (error) { + console.warn('Failed to save filters:', error); + } + } + + // Load saved filters from settings + async function loadSavedFilters() { + try { + const response = await fetch('/api/settings'); + const data = await response.json(); + + if (data.success && data.settings.game_filters) { + const filters = data.settings.game_filters; + + // Load region filters + if (filters.region_filters) { + regionFilters.clear(); + Object.entries(filters.region_filters).forEach(([region, mode]) => { + regionFilters.set(region, mode); + }); + } + + // Load region priority + if (filters.region_priority) { + regionPriorityOrder = filters.region_priority; + localStorage.setItem('regionPriorityOrder', JSON.stringify(regionPriorityOrder)); + } + + // Save checkbox states to global variables + savedHideNonRelease = filters.hide_non_release || false; + savedOneRomPerGame = filters.one_rom_per_game || false; + savedRegexMode = filters.regex_mode || false; + + // Load checkboxes when they exist (in games view) + if (document.getElementById('hide-non-release')) { + document.getElementById('hide-non-release').checked = savedHideNonRelease; + } + if (document.getElementById('one-rom-per-game')) { + document.getElementById('one-rom-per-game').checked = savedOneRomPerGame; + } + if (document.getElementById('regex-mode')) { + document.getElementById('regex-mode').checked = savedRegexMode; + } + } + } catch (error) { + console.warn('Failed to load saved filters:', error); + } + } + + // Restore filter button states in the UI + function restoreFilterStates() { + // Restore region button states + regionFilters.forEach((mode, region) => { + const btn = document.querySelector(`.region-btn[data-region="${region}"]`); + if (btn) { + if (mode === 'include') { + btn.classList.add('active'); + btn.classList.remove('excluded'); + } else if (mode === 'exclude') { + btn.classList.remove('active'); + btn.classList.add('excluded'); + } + } + }); + + // Restore checkbox states + if (document.getElementById('hide-non-release')) { + document.getElementById('hide-non-release').checked = savedHideNonRelease; + } + if (document.getElementById('one-rom-per-game')) { + document.getElementById('one-rom-per-game').checked = savedOneRomPerGame; + } + if (document.getElementById('regex-mode')) { + document.getElementById('regex-mode').checked = savedRegexMode; + } + + // Apply filters to display the games correctly + applyAllFilters(); + } + // Helper: Extract region(s) from game name - returns array of regions function getGameRegions(gameName) { @@ -490,12 +614,16 @@ // Common region patterns - check all, not just first match // Handle both "(USA)" and "(USA, Europe)" formats if (name.includes('USA') || name.includes('US)')) regions.push('USA'); + if (name.includes('CANADA')) regions.push('Canada'); if (name.includes('EUROPE') || name.includes('EU)')) regions.push('Europe'); + if (name.includes('FRANCE') || name.includes('FR)')) regions.push('France'); + if (name.includes('GERMANY') || name.includes('DE)')) regions.push('Germany'); if (name.includes('JAPAN') || name.includes('JP)') || name.includes('JPN)')) regions.push('Japan'); + if (name.includes('KOREA') || name.includes('KR)')) regions.push('Korea'); if (name.includes('WORLD')) regions.push('World'); - // Check for other regions - if (name.match(/\b(AUSTRALIA|ASIA|KOREA|BRAZIL|CHINA|RUSSIA|SCANDINAVIA|SPAIN|FRANCE|GERMANY|ITALY)\b/)) { + // Check for other regions (excluding the ones above) + if (name.match(/\b(AUSTRALIA|ASIA|BRAZIL|CHINA|RUSSIA|SCANDINAVIA|SPAIN|ITALY)\b/)) { if (!regions.includes('Other')) regions.push('Other'); } @@ -578,7 +706,10 @@ if (region === 'CANADA' && name.includes('CANADA')) return i; if (region === 'WORLD' && name.includes('WORLD')) return i; if (region === 'EUROPE' && (name.includes('EUROPE') || name.includes('EU)'))) return i; + if (region === 'FRANCE' && (name.includes('FRANCE') || name.includes('FR)'))) return i; + if (region === 'GERMANY' && (name.includes('GERMANY') || name.includes('DE)'))) return i; if (region === 'JAPAN' && (name.includes('JAPAN') || name.includes('JP)') || name.includes('JPN)'))) return i; + if (region === 'KOREA' && (name.includes('KOREA') || name.includes('KR)'))) return i; } return regionPriorityOrder.length; // Other regions (lowest priority) @@ -606,6 +737,7 @@ [regionPriorityOrder[idx-1], regionPriorityOrder[idx]]; saveRegionPriorityOrder(); renderRegionPriorityConfig(); + saveFiltersToBackend(); } } @@ -617,14 +749,16 @@ [regionPriorityOrder[idx+1], regionPriorityOrder[idx]]; saveRegionPriorityOrder(); renderRegionPriorityConfig(); + saveFiltersToBackend(); } } // Reset region priority to default function resetRegionPriority() { - regionPriorityOrder = ['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other']; + regionPriorityOrder = ['USA', 'Canada', 'Europe', 'France', 'Germany', 'Japan', 'Korea', 'World', 'Other']; saveRegionPriorityOrder(); renderRegionPriorityConfig(); + saveFiltersToBackend(); } // Render region priority configuration UI @@ -641,11 +775,11 @@ ${idx + 1}. ${region} + style="padding: 4px 8px; border: 1px solid #ccc; background: white; cursor: pointer; border-radius: 3px; font-size: 14px;" + ${idx === 0 ? 'disabled' : ''}>🔼 + style="padding: 4px 8px; border: 1px solid #ccc; background: white; cursor: pointer; border-radius: 3px; font-size: 14px;" + ${idx === regionPriorityOrder.length - 1 ? 'disabled' : ''}>🔽 `; }); @@ -706,14 +840,15 @@ } applyAllFilters(); + saveFiltersToBackend(); } // Apply all filters function applyAllFilters() { const searchInput = document.getElementById('game-search'); const searchTerm = searchInput ? searchInput.value : ''; - const hideNonRelease = document.getElementById('hide-non-release')?.checked || false; - const regexMode = document.getElementById('regex-mode')?.checked || false; + const hideNonRelease = document.getElementById('hide-non-release')?.checked || savedHideNonRelease; + const regexMode = document.getElementById('regex-mode')?.checked || savedRegexMode; const items = document.querySelectorAll('.game-item'); let visibleCount = 0; @@ -804,7 +939,7 @@ }); // Apply one-rom-per-game filter (after other filters) - const oneRomPerGame = document.getElementById('one-rom-per-game')?.checked || false; + const oneRomPerGame = document.getElementById('one-rom-per-game')?.checked || savedOneRomPerGame; if (oneRomPerGame) { // Group currently visible games by base name const gameGroups = new Map(); @@ -1032,22 +1167,26 @@
${t('web_filter_region')}: - + + + + +
@@ -1082,6 +1221,9 @@ `; container.innerHTML = html; + // Restore filter states from loaded settings + restoreFilterStates(); + // Appliquer le tri par défaut (A-Z) sortGames(currentGameSort); @@ -1902,6 +2044,12 @@ } try { + // Collect region filters + const regionFiltersObj = {}; + regionFilters.forEach((mode, region) => { + regionFiltersObj[region] = mode; + }); + const settings = { language: document.getElementById('setting-language').value, music_enabled: document.getElementById('setting-music').checked, @@ -1921,7 +2069,14 @@ }, show_unsupported_platforms: document.getElementById('setting-show-unsupported').checked, allow_unknown_extensions: document.getElementById('setting-allow-unknown').checked, - roms_folder: document.getElementById('setting-roms-folder').value.trim() + roms_folder: document.getElementById('setting-roms-folder').value.trim(), + game_filters: { + region_filters: regionFiltersObj, + hide_non_release: document.getElementById('hide-non-release')?.checked || savedHideNonRelease, + one_rom_per_game: document.getElementById('one-rom-per-game')?.checked || savedOneRomPerGame, + regex_mode: document.getElementById('regex-mode')?.checked || savedRegexMode, + region_priority: regionPriority + } }; const response = await fetch('/api/settings', { diff --git a/version.json b/version.json index 9d10749..0a807d1 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "2.3.2.6" + "version": "2.3.2.7" } \ No newline at end of file