mirror of
https://github.com/RetroGameSets/RGSX.git
synced 2026-03-19 16:26:00 +01:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56c87ab05f | ||
|
|
b12d645fbf | ||
|
|
04e68adef0 | ||
|
|
52f2b960c2 | ||
|
|
1ea604840e | ||
|
|
802696e78f | ||
|
|
6f17173a8c |
@@ -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":
|
||||
|
||||
@@ -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.8"
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
@@ -505,6 +517,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
max_row = len(keyboard_layout) - 1
|
||||
max_col = len(keyboard_layout[row]) - 1
|
||||
if is_input_matched(event, "up"):
|
||||
if row == 0: # if you are in the first row and press UP jump to last row
|
||||
row = max_row + (1 if col <= 5 else 0)
|
||||
if row > 0:
|
||||
config.selected_key = (row - 1, min(col, len(keyboard_layout[row - 1]) - 1))
|
||||
config.repeat_action = "up"
|
||||
@@ -513,6 +527,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "down"):
|
||||
if (col <= 5 and row == max_row) or (col > 5 and row == max_row-1): # if you are in the last row and press DOWN jump to first row
|
||||
row = -1
|
||||
if row < max_row:
|
||||
config.selected_key = (row + 1, min(col, len(keyboard_layout[row + 1]) - 1))
|
||||
config.repeat_action = "down"
|
||||
@@ -521,6 +537,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "left"):
|
||||
if col == 0: # if you are in the first col and press LEFT jump to last col
|
||||
col = max_col + 1
|
||||
if col > 0:
|
||||
config.selected_key = (row, col - 1)
|
||||
config.repeat_action = "left"
|
||||
@@ -529,6 +547,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "right"):
|
||||
if col == max_col: # if you are in the last col and press RIGHT jump to first col
|
||||
col = -1
|
||||
if col < max_col:
|
||||
config.selected_key = (row, col + 1)
|
||||
config.repeat_action = "right"
|
||||
@@ -656,14 +676,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 +1937,261 @@ 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 linéaire des éléments sélectionnables (pour simplifier l'indexation)
|
||||
# Régions individuelles
|
||||
num_regions = len(GameFilters.REGIONS)
|
||||
# Options toggle/button
|
||||
num_other_options = 3 # hide_non_release, one_rom_per_game, priority_config
|
||||
# Boutons en bas
|
||||
num_buttons = 3 # apply, reset, back
|
||||
|
||||
total_items = num_regions + num_other_options + num_buttons
|
||||
|
||||
if is_input_matched(event, "up"):
|
||||
# Navigation verticale dans la grille ou entre sections
|
||||
if config.selected_filter_option < num_regions:
|
||||
# Dans la grille des régions (3 colonnes)
|
||||
if config.selected_filter_option >= 3:
|
||||
# Monter d'une ligne
|
||||
config.selected_filter_option -= 3
|
||||
else:
|
||||
# Déjà en haut, aller aux boutons
|
||||
config.selected_filter_option = total_items - 2 # Bouton du milieu (reset)
|
||||
else:
|
||||
# Dans les options ou boutons, monter normalement
|
||||
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"):
|
||||
# Navigation verticale
|
||||
if config.selected_filter_option < num_regions:
|
||||
# Dans la grille des régions
|
||||
if config.selected_filter_option + 3 < num_regions:
|
||||
# Descendre d'une ligne
|
||||
config.selected_filter_option += 3
|
||||
else:
|
||||
# Aller aux autres options
|
||||
config.selected_filter_option = num_regions
|
||||
else:
|
||||
# Dans les options ou boutons, descendre normalement
|
||||
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"):
|
||||
# Navigation horizontale
|
||||
if config.selected_filter_option < num_regions:
|
||||
# Dans la grille des régions
|
||||
if config.selected_filter_option % 3 > 0:
|
||||
config.selected_filter_option -= 1
|
||||
config.needs_redraw = True
|
||||
elif config.selected_filter_option >= num_regions + num_other_options:
|
||||
# Dans les boutons en bas
|
||||
button_idx = config.selected_filter_option - (num_regions + num_other_options)
|
||||
button_idx = (button_idx - 1) % num_buttons
|
||||
config.selected_filter_option = num_regions + num_other_options + button_idx
|
||||
config.needs_redraw = True
|
||||
|
||||
elif is_input_matched(event, "right"):
|
||||
# Navigation horizontale
|
||||
if config.selected_filter_option < num_regions:
|
||||
# Dans la grille des régions
|
||||
if config.selected_filter_option % 3 < 2 and config.selected_filter_option + 1 < num_regions:
|
||||
config.selected_filter_option += 1
|
||||
config.needs_redraw = True
|
||||
elif config.selected_filter_option >= num_regions + num_other_options:
|
||||
# Dans les boutons en bas
|
||||
button_idx = config.selected_filter_option - (num_regions + num_other_options)
|
||||
button_idx = (button_idx + 1) % num_buttons
|
||||
config.selected_filter_option = num_regions + num_other_options + button_idx
|
||||
config.needs_redraw = True
|
||||
|
||||
elif is_input_matched(event, "confirm"):
|
||||
# Déterminer quel élément a été sélectionné
|
||||
if config.selected_filter_option < num_regions:
|
||||
# C'est une région
|
||||
region = GameFilters.REGIONS[config.selected_filter_option]
|
||||
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 config.selected_filter_option < num_regions + num_other_options:
|
||||
# C'est une autre option
|
||||
option_idx = config.selected_filter_option - num_regions
|
||||
if option_idx == 0:
|
||||
# hide_non_release
|
||||
config.game_filter_obj.hide_non_release = not config.game_filter_obj.hide_non_release
|
||||
config.needs_redraw = True
|
||||
logger.debug("Toggle hide_non_release modifié")
|
||||
elif option_idx == 1:
|
||||
# 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("Toggle one_rom_per_game modifié")
|
||||
elif option_idx == 2:
|
||||
# priority_config
|
||||
config.menu_state = "filter_priority_config"
|
||||
config.selected_priority_index = 0
|
||||
config.needs_redraw = True
|
||||
logger.debug("Ouverture configuration priorité régions")
|
||||
|
||||
else:
|
||||
# C'est un bouton
|
||||
button_idx = config.selected_filter_option - (num_regions + num_other_options)
|
||||
if button_idx == 0:
|
||||
# Apply
|
||||
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_idx == 1:
|
||||
# Reset
|
||||
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_idx == 2:
|
||||
# Back
|
||||
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)
|
||||
@@ -2351,4 +2624,4 @@ def get_emergency_controls():
|
||||
# manette basique
|
||||
"confirm_joy": {"type": "button", "button": 0},
|
||||
"cancel_joy": {"type": "button", "button": 1},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -251,7 +257,18 @@ def draw_stylized_button(screen, text, x, y, width, height, selected=False):
|
||||
pygame.draw.rect(glow_surface, THEME_COLORS["fond_lignes"] + (50,), (5, 5, width, height), border_radius=12)
|
||||
screen.blit(glow_surface, (x - 5, y - 5))
|
||||
screen.blit(button_surface, (x, y))
|
||||
|
||||
# Vérifier si le texte dépasse la largeur disponible
|
||||
text_surface = config.font.render(text, True, THEME_COLORS["text"])
|
||||
available_width = width - 20 # Marge de 10px de chaque côté
|
||||
|
||||
if text_surface.get_width() > available_width:
|
||||
# Tronquer le texte avec "..."
|
||||
truncated_text = text
|
||||
while text_surface.get_width() > available_width and len(truncated_text) > 0:
|
||||
truncated_text = truncated_text[:-1]
|
||||
text_surface = config.font.render(truncated_text + "...", True, THEME_COLORS["text"])
|
||||
|
||||
text_rect = text_surface.get_rect(center=(x + width // 2, y + height // 2))
|
||||
screen.blit(text_surface, text_rect)
|
||||
|
||||
@@ -847,13 +864,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)
|
||||
@@ -1670,11 +1693,23 @@ def draw_language_menu(screen):
|
||||
vpad = max(8, min(14, int(title_surface.get_height() * 0.4)))
|
||||
title_bg_rect = title_rect.inflate(hpad, vpad)
|
||||
|
||||
# Dimensions responsives des boutons
|
||||
# Largeur bornée entre 260 et 380px (~40% de la largeur écran)
|
||||
button_width = max(260, min(380, int(config.screen_width * 0.4)))
|
||||
# Hauteur réduite et responsive (env. 5.5% de la hauteur écran), bornée 28..56
|
||||
button_height = max(28, min(56, int(config.screen_height * 0.055)))
|
||||
# Calculer hauteur dynamique basée sur la taille de police
|
||||
sample_text = config.font.render("Sample", True, THEME_COLORS["text"])
|
||||
font_height = sample_text.get_height()
|
||||
|
||||
# Calculer largeur maximale nécessaire pour les noms de langues
|
||||
max_text_width = 0
|
||||
for lang_code in available_languages:
|
||||
lang_name = get_language_name(lang_code)
|
||||
text_surface = config.font.render(lang_name, True, THEME_COLORS["text"])
|
||||
if text_surface.get_width() > max_text_width:
|
||||
max_text_width = text_surface.get_width()
|
||||
|
||||
# Largeur bornée entre valeur calculée et limites raisonnables
|
||||
button_width = max(260, min(500, max_text_width + 60))
|
||||
# Hauteur réduite et responsive (env. 5.5% de la hauteur écran), bornée mais aussi fonction de la police
|
||||
# Augmenter le padding pour grandes polices
|
||||
button_height = max(28, min(70, max(int(config.screen_height * 0.055), font_height + 20)))
|
||||
# Espacement vertical proportionnel et borné
|
||||
button_spacing = max(8, int(button_height * 0.35))
|
||||
|
||||
@@ -1725,8 +1760,17 @@ def draw_language_menu(screen):
|
||||
pygame.draw.rect(screen, button_color, (button_x, button_y, button_width, button_height), border_radius=10)
|
||||
pygame.draw.rect(screen, THEME_COLORS["border"], (button_x, button_y, button_width, button_height), 2, border_radius=10)
|
||||
|
||||
# Texte du bouton
|
||||
# Texte avec gestion du dépassement
|
||||
text_surface = config.font.render(lang_name, True, THEME_COLORS["text"])
|
||||
available_width = button_width - 20 # Marge de 10px de chaque côté
|
||||
|
||||
if text_surface.get_width() > available_width:
|
||||
# Tronquer le texte avec "..."
|
||||
truncated_text = lang_name
|
||||
while text_surface.get_width() > available_width and len(truncated_text) > 0:
|
||||
truncated_text = truncated_text[:-1]
|
||||
text_surface = config.font.render(truncated_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)
|
||||
|
||||
@@ -1852,8 +1896,20 @@ def draw_pause_menu(screen, selected_option):
|
||||
_("menu_support"), # 6 -> support
|
||||
_("menu_quit") # 7 -> quit
|
||||
]
|
||||
menu_width = int(config.screen_width * 0.6)
|
||||
button_height = int(config.screen_height * 0.048)
|
||||
# Calculer hauteur dynamique basée sur la taille de police
|
||||
sample_text = config.font.render("Sample", True, THEME_COLORS["text"])
|
||||
font_height = sample_text.get_height()
|
||||
button_height = max(int(config.screen_height * 0.048), font_height + 20)
|
||||
|
||||
# Calculer largeur maximale nécessaire pour le texte
|
||||
max_text_width = 0
|
||||
for option in options:
|
||||
text_surface = config.font.render(option, True, THEME_COLORS["text"])
|
||||
if text_surface.get_width() > max_text_width:
|
||||
max_text_width = text_surface.get_width()
|
||||
|
||||
# Largeur du menu basée sur le texte le plus long + marges
|
||||
menu_width = min(int(config.screen_width * 0.8), max(int(config.screen_width * 0.5), max_text_width + 80))
|
||||
margin_top_bottom = 24
|
||||
menu_height = len(options) * (button_height + 12) + 2 * margin_top_bottom
|
||||
menu_x = (config.screen_width - menu_width) // 2
|
||||
@@ -1898,8 +1954,23 @@ def draw_pause_menu(screen, selected_option):
|
||||
def _draw_submenu_generic(screen, title, options, selected_index):
|
||||
"""Helper générique pour dessiner un sous-menu hiérarchique."""
|
||||
screen.blit(OVERLAY, (0, 0))
|
||||
menu_width = int(config.screen_width * 0.72)
|
||||
button_height = int(config.screen_height * 0.045)
|
||||
|
||||
# Calculer hauteur dynamique basée sur la taille de police
|
||||
sample_text = config.font.render("Sample", True, THEME_COLORS["text"])
|
||||
font_height = sample_text.get_height()
|
||||
button_height = max(int(config.screen_height * 0.045), font_height + 18)
|
||||
|
||||
# Calculer largeur maximale nécessaire pour le texte (titre + options)
|
||||
max_text_width = 0
|
||||
title_surface = config.font.render(title, True, THEME_COLORS["text"])
|
||||
max_text_width = title_surface.get_width()
|
||||
for option in options:
|
||||
text_surface = config.font.render(option, True, THEME_COLORS["text"])
|
||||
if text_surface.get_width() > max_text_width:
|
||||
max_text_width = text_surface.get_width()
|
||||
|
||||
# Largeur du menu basée sur le texte le plus long + marges
|
||||
menu_width = min(int(config.screen_width * 0.85), max(int(config.screen_width * 0.55), max_text_width + 80))
|
||||
margin_top_bottom = 26
|
||||
menu_height = (len(options)+1) * (button_height + 10) + 2 * margin_top_bottom # +1 pour le titre
|
||||
menu_x = (config.screen_width - menu_width) // 2
|
||||
@@ -2611,7 +2682,10 @@ def draw_confirm_dialog(screen):
|
||||
wrapped_message = wrap_text(message, config.font, config.screen_width - 80)
|
||||
line_height = config.font.get_height() + 5
|
||||
text_height = len(wrapped_message) * line_height
|
||||
button_height = int(config.screen_height * 0.0463)
|
||||
# Adapter hauteur bouton en fonction de la taille de police
|
||||
sample_text = config.font.render("Sample", True, THEME_COLORS["text"])
|
||||
font_height = sample_text.get_height()
|
||||
button_height = max(int(config.screen_height * 0.0463), font_height + 15)
|
||||
margin_top_bottom = 20
|
||||
rect_height = text_height + button_height + 2 * margin_top_bottom
|
||||
max_text_width = max([config.font.size(line)[0] for line in wrapped_message], default=300)
|
||||
@@ -2644,7 +2718,10 @@ def draw_reload_games_data_dialog(screen):
|
||||
wrapped_message = wrap_text(message, config.small_font, config.screen_width - 80)
|
||||
line_height = config.small_font.get_height() + 5
|
||||
text_height = len(wrapped_message) * line_height
|
||||
button_height = int(config.screen_height * 0.0463)
|
||||
# Adapter hauteur bouton en fonction de la taille de police
|
||||
sample_text = config.small_font.render("Sample", True, THEME_COLORS["text"])
|
||||
font_height = sample_text.get_height()
|
||||
button_height = max(int(config.screen_height * 0.0463), font_height + 15)
|
||||
margin_top_bottom = 20
|
||||
rect_height = text_height + button_height + 2 * margin_top_bottom
|
||||
max_text_width = max([config.small_font.size(line)[0] for line in wrapped_message], default=300)
|
||||
@@ -3346,3 +3423,442 @@ 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 hauteur dynamique basée sur la taille de police
|
||||
sample_text = config.font.render("Sample", True, THEME_COLORS["text"])
|
||||
font_height = sample_text.get_height()
|
||||
button_height = max(60, font_height + 30)
|
||||
|
||||
# Calculer largeur maximale nécessaire pour le texte
|
||||
max_text_width = 0
|
||||
for option in options:
|
||||
text_surface = config.font.render(option, True, THEME_COLORS["text"])
|
||||
if text_surface.get_width() > max_text_width:
|
||||
max_text_width = text_surface.get_width()
|
||||
|
||||
# Largeur du bouton basée sur le texte le plus long + marges
|
||||
button_width = max(400, max_text_width + 80)
|
||||
|
||||
# Calculer positions
|
||||
menu_y = 150
|
||||
button_spacing = 20
|
||||
|
||||
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 avec gestion du dépassement
|
||||
text_surface = config.font.render(option, True, THEME_COLORS["text"])
|
||||
available_width = button_width - 40 # Marge de 20px de chaque côté
|
||||
|
||||
if text_surface.get_width() > available_width:
|
||||
# Tronquer le texte avec "..."
|
||||
truncated_text = option
|
||||
while text_surface.get_width() > available_width and len(truncated_text) > 0:
|
||||
truncated_text = truncated_text[:-1]
|
||||
text_surface = config.font.render(truncated_text + "...", 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))
|
||||
|
||||
# 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)
|
||||
|
||||
# Liste des options (sans les régions pour l'instant)
|
||||
options = []
|
||||
|
||||
# Section Régions (titre seulement)
|
||||
region_title = _("filter_region_title")
|
||||
options.append(('header', region_title))
|
||||
|
||||
# On va afficher les régions en grille 3x3, donc on ajoute des placeholders
|
||||
regions_list = []
|
||||
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"]
|
||||
|
||||
regions_list.append(('region', region, f"{region_label}: {status}", color))
|
||||
|
||||
# Ajouter les régions comme une seule entrée "grid" dans options
|
||||
options.append(('region_grid', regions_list))
|
||||
|
||||
# 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
|
||||
|
||||
# Calculer le nombre total d'items sélectionnables (régions individuelles + autres options + boutons)
|
||||
total_items = len(regions_list) + len([opt for opt in options if opt[0] in ['toggle', 'button_inline']]) + len(buttons)
|
||||
if config.selected_filter_option >= total_items:
|
||||
config.selected_filter_option = total_items - 1
|
||||
|
||||
# Calculer d'abord la hauteur totale nécessaire
|
||||
# Adapter la hauteur en fonction de la taille de police
|
||||
sample_text = config.font.render("Sample", True, THEME_COLORS["text"])
|
||||
font_height = sample_text.get_height()
|
||||
line_height = max(50, font_height + 30)
|
||||
item_height = max(45, font_height + 20)
|
||||
item_spacing_y = 10
|
||||
items_per_row = 3
|
||||
|
||||
# Titre
|
||||
title_height = 60
|
||||
|
||||
# Hauteur du header régions
|
||||
header_height = line_height
|
||||
|
||||
# Hauteur de la grille de régions
|
||||
num_rows = (len(regions_list) + items_per_row - 1) // items_per_row
|
||||
grid_height = num_rows * (item_height + item_spacing_y)
|
||||
|
||||
# Hauteur du séparateur
|
||||
separator_height = 10
|
||||
|
||||
# Hauteur du header autres options
|
||||
header2_height = line_height
|
||||
|
||||
# Hauteur des autres options (3 options)
|
||||
num_other_options = len([opt for opt in options if opt[0] in ['toggle', 'button_inline']])
|
||||
other_options_height = num_other_options * (item_height + 10)
|
||||
|
||||
# Hauteur des boutons
|
||||
# Adapter en fonction de la taille de police
|
||||
sample_text = config.font.render("Sample", True, THEME_COLORS["text"])
|
||||
font_height = sample_text.get_height()
|
||||
button_height = max(50, font_height + 20)
|
||||
buttons_top_margin = 30
|
||||
|
||||
# Hauteur totale du contenu
|
||||
total_content_height = (title_height + header_height + grid_height + separator_height +
|
||||
header2_height + other_options_height + buttons_top_margin + button_height)
|
||||
|
||||
# Calculer position de départ pour centrer verticalement
|
||||
control_bar_estimated_height = 80
|
||||
available_height = config.screen_height - control_bar_estimated_height
|
||||
start_y = (available_height - total_content_height) // 2
|
||||
if start_y < 20:
|
||||
start_y = 20 # Marge minimale du haut
|
||||
|
||||
current_y = start_y
|
||||
|
||||
# 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, current_y + 20))
|
||||
screen.blit(title_surface, title_rect)
|
||||
current_y += title_height
|
||||
|
||||
region_index_start = 0 # Les régions commencent à l'index 0
|
||||
|
||||
for option in 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"])
|
||||
text_rect = text_surface.get_rect(center=(config.screen_width // 2, current_y + 20))
|
||||
screen.blit(text_surface, text_rect)
|
||||
current_y += line_height
|
||||
|
||||
elif option_type == 'separator':
|
||||
current_y += separator_height
|
||||
|
||||
elif option_type == 'region_grid':
|
||||
# Afficher les régions en grille 3 par ligne
|
||||
regions_data = option[1]
|
||||
item_spacing_x = 20
|
||||
|
||||
# Calculer la largeur maximale nécessaire pour les boutons de régions
|
||||
max_region_width = 0
|
||||
for region_data in regions_data:
|
||||
text = region_data[2]
|
||||
text_surface = config.font.render(text, True, THEME_COLORS["text"])
|
||||
text_width = text_surface.get_width() + 30 # Padding de 30px
|
||||
if text_width > max_region_width:
|
||||
max_region_width = text_width
|
||||
|
||||
# Largeur minimale de 200px
|
||||
item_width = max(max_region_width, 200)
|
||||
|
||||
# Calculer la largeur totale de la grille
|
||||
total_grid_width = items_per_row * item_width + (items_per_row - 1) * item_spacing_x
|
||||
grid_start_x = (config.screen_width - total_grid_width) // 2
|
||||
|
||||
for idx, region_data in enumerate(regions_data):
|
||||
row = idx // items_per_row
|
||||
col = idx % items_per_row
|
||||
|
||||
x = grid_start_x + col * (item_width + item_spacing_x)
|
||||
y = current_y + row * (item_height + item_spacing_y)
|
||||
|
||||
# Index global de cette région
|
||||
global_idx = region_index_start + idx
|
||||
|
||||
# Couleur selon sélection
|
||||
if global_idx == 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, y, item_width, item_height), border_radius=8)
|
||||
pygame.draw.rect(screen, border_color, (x, y, item_width, item_height), 2, border_radius=8)
|
||||
|
||||
# Texte centré
|
||||
text = region_data[2]
|
||||
text_color = region_data[3]
|
||||
|
||||
text_surface = config.font.render(text, True, text_color)
|
||||
text_rect = text_surface.get_rect(center=(x + item_width // 2, y + item_height // 2))
|
||||
screen.blit(text_surface, text_rect)
|
||||
|
||||
# Calculer la hauteur occupée par la grille
|
||||
current_y += num_rows * (item_height + item_spacing_y) + 10
|
||||
|
||||
elif option_type in ['toggle', 'button_inline']:
|
||||
# Option sélectionnable - largeur adaptée au texte
|
||||
text = option[2]
|
||||
text_surface = config.font.render(text, True, THEME_COLORS["text"])
|
||||
text_width = text_surface.get_width()
|
||||
|
||||
# Largeur avec padding
|
||||
width = text_width + 40
|
||||
x = (config.screen_width - width) // 2 # Centrer
|
||||
height = item_height
|
||||
|
||||
# Index global de cette option (après les régions)
|
||||
global_idx = len(regions_list) + len([opt for opt in options[:options.index(option)] if opt[0] in ['toggle', 'button_inline']])
|
||||
|
||||
# Couleur selon sélection
|
||||
if global_idx == 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 centré
|
||||
text_color = THEME_COLORS["text"]
|
||||
text_rect = text_surface.get_rect(center=(x + width // 2, current_y + height // 2))
|
||||
screen.blit(text_surface, text_rect)
|
||||
|
||||
current_y += height + 10
|
||||
|
||||
# Afficher les 3 boutons côte à côte en bas
|
||||
current_y += buttons_top_margin
|
||||
button_y = current_y
|
||||
button_spacing = 20
|
||||
|
||||
# Calculer la largeur de chaque bouton en fonction du texte
|
||||
button_widths = []
|
||||
for button_id, button_text in buttons:
|
||||
text_surface = config.font.render(button_text, True, THEME_COLORS["text"])
|
||||
button_widths.append(text_surface.get_width() + 40) # Padding de 40px
|
||||
|
||||
# Largeur totale des boutons
|
||||
total_buttons_width = sum(button_widths) + button_spacing * (len(buttons) - 1)
|
||||
button_start_x = (config.screen_width - total_buttons_width) // 2
|
||||
|
||||
# Calculer l'index de début des boutons (après toutes les régions et autres options)
|
||||
button_index_start = len(regions_list) + num_other_options
|
||||
|
||||
current_button_x = button_start_x
|
||||
for i, (button_id, button_text) in enumerate(buttons):
|
||||
button_index = button_index_start + i
|
||||
button_width = button_widths[i]
|
||||
|
||||
# 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, (current_button_x, button_y, button_width, button_height), border_radius=8)
|
||||
pygame.draw.rect(screen, border_color, (current_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=(current_button_x + button_width // 2, button_y + button_height // 2))
|
||||
screen.blit(text_surface, text_rect)
|
||||
|
||||
current_button_x += button_width + button_spacing
|
||||
|
||||
# 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 - 20))
|
||||
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)
|
||||
|
||||
237
ports/RGSX/game_filters.py
Normal file
237
ports/RGSX/game_filters.py
Normal file
@@ -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
|
||||
@@ -339,7 +339,7 @@
|
||||
"web_restart_error": "Fehler beim Neustart: {0}",
|
||||
"web_support": "Support",
|
||||
"web_support_title": "📦 Support-Datei erstellt",
|
||||
"web_support_message": "Support-Datei erfolgreich erstellt!\\n\\n📁 Inhalt:\\n• Steuerungskonfiguration\\n• Download-Verlauf\\n• RGSX-Einstellungen\\n• Anwendungsprotokolle\\n• Webserver-Protokolle\\n\\n💬 Um Hilfe zu erhalten:\\n1. Trete dem RGSX Discord bei\\n2. Beschreibe dein Problem\\n3. Teile diese ZIP-Datei\\n\\nDownload startet...",
|
||||
"web_support_message": "Support-Datei erfolgreich erstellt!\n\n📁 Inhalt:\n• Steuerungskonfiguration\n• Download-Verlauf\n• RGSX-Einstellungen\n• Anwendungsprotokolle\n• Webserver-Protokolle\n\n💬 Um Hilfe zu erhalten:\n1. Trete dem RGSX Discord bei\n2. Beschreibe dein Problem\n3. Teile diese ZIP-Datei\n\nDownload startet...",
|
||||
"web_support_generating": "Support-Datei wird generiert...",
|
||||
"web_support_download": "Support-Datei herunterladen",
|
||||
"web_support_error": "Fehler beim Erstellen der Support-Datei: {0}",
|
||||
@@ -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"
|
||||
}
|
||||
@@ -341,7 +341,7 @@
|
||||
"web_restart_error": "Restart error: {0}",
|
||||
"web_support": "Support",
|
||||
"web_support_title": "📦 Support File Generated",
|
||||
"web_support_message": "Support file created successfully!\\n\\n📁 Contents:\\n• Controls configuration\\n• Download history\\n• RGSX settings\\n• Application logs\\n• Web server logs\\n\\n💬 To get help:\\n1. Join RGSX Discord\\n2. Describe your issue\\n3. Share this ZIP file\\n\\nDownload will start...",
|
||||
"web_support_message": "Support file created successfully!\n\n📁 Contents:\n• Controls configuration\n• Download history\n• RGSX settings\n• Application logs\n• Web server logs\n\n💬 To get help:\n1. Join RGSX Discord\n2. Describe your issue\n3. Share this ZIP file\n\nDownload will start...",
|
||||
"web_support_generating": "Generating support file...",
|
||||
"web_support_download": "Download support file",
|
||||
"web_support_error": "Error generating support file: {0}",
|
||||
@@ -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"
|
||||
}
|
||||
@@ -341,7 +341,7 @@
|
||||
"web_restart_error": "Error al reiniciar: {0}",
|
||||
"web_support": "Soporte",
|
||||
"web_support_title": "📦 Archivo de soporte generado",
|
||||
"web_support_message": "¡Archivo de soporte creado con éxito!\\n\\n📁 Contenido:\\n• Configuración de controles\\n• Historial de descargas\\n• Configuración RGSX\\n• Registros de la aplicación\\n• Registros del servidor web\\n\\n💬 Para obtener ayuda:\\n1. Únete al Discord de RGSX\\n2. Describe tu problema\\n3. Comparte este archivo ZIP\\n\\nLa descarga comenzará...",
|
||||
"web_support_message": "¡Archivo de soporte creado con éxito!\n\n📁 Contenido:\n• Configuración de controles\n• Historial de descargas\n• Configuración RGSX\n• Registros de la aplicación\n• Registros del servidor web\n\n💬 Para obtener ayuda:\n1. Únete al Discord de RGSX\n2. Describe tu problema\n3. Comparte este archivo ZIP\n\nLa descarga comenzará...",
|
||||
"web_support_generating": "Generando archivo de soporte...",
|
||||
"web_support_download": "Descargar archivo de soporte",
|
||||
"web_support_error": "Error al generar el archivo de soporte: {0}",
|
||||
@@ -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)"
|
||||
}
|
||||
@@ -341,7 +341,7 @@
|
||||
"web_restart_error": "Erreur lors du redémarrage : {0}",
|
||||
"web_support": "Support",
|
||||
"web_support_title": "📦 Fichier de support généré",
|
||||
"web_support_message": "Le fichier de support a été créé avec succès !\\n\\n📁 Contenu :\\n• Configuration des contrôles\\n• Historique des téléchargements\\n• Paramètres RGSX\\n• Logs de l'application\\n• Logs du serveur web\\n\\n💬 Pour obtenir de l'aide :\\n1. Rejoignez le Discord RGSX\\n2. Décrivez votre problème\\n3. Partagez ce fichier ZIP\\n\\nLe téléchargement va démarrer...",
|
||||
"web_support_message": "Le fichier de support a été créé avec succès !\n\n📁 Contenu :\n• Configuration des contrôles\n• Historique des téléchargements\n• Paramètres RGSX\n• Logs de l'application\n• Logs du serveur web\n\n💬 Pour obtenir de l'aide :\n1. Rejoignez le Discord RGSX\n2. Décrivez votre problème\n3. Partagez ce fichier ZIP\n\nLe téléchargement va démarrer...",
|
||||
"web_support_generating": "Génération du fichier de support...",
|
||||
"web_support_download": "Télécharger le fichier de support",
|
||||
"web_support_error": "Erreur lors de la génération du fichier de support : {0}",
|
||||
@@ -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)"
|
||||
}
|
||||
@@ -338,7 +338,7 @@
|
||||
"web_restart_error": "Errore durante il riavvio: {0}",
|
||||
"web_support": "Supporto",
|
||||
"web_support_title": "📦 File di supporto generato",
|
||||
"web_support_message": "File di supporto creato con successo!\\n\\n📁 Contenuto:\\n• Configurazione controlli\\n• Cronologia download\\n• Impostazioni RGSX\\n• Log dell'applicazione\\n• Log del server web\\n\\n💬 Per ottenere aiuto:\\n1. Unisciti al Discord RGSX\\n2. Descrivi il tuo problema\\n3. Condividi questo file ZIP\\n\\nIl download inizierà...",
|
||||
"web_support_message": "File di supporto creato con successo!\n\n📁 Contenuto:\n• Configurazione controlli\n• Cronologia download\n• Impostazioni RGSX\n• Log dell'applicazione\n• Log del server web\n\n💬 Per ottenere aiuto:\n1. Unisciti al Discord RGSX\n2. Descrivi il tuo problema\n3. Condividi questo file ZIP\n\nIl download inizierà...",
|
||||
"web_support_generating": "Generazione file di supporto...",
|
||||
"web_support_download": "Scarica file di supporto",
|
||||
"web_support_error": "Errore nella generazione del file di supporto: {0}",
|
||||
@@ -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"
|
||||
}
|
||||
@@ -340,7 +340,7 @@
|
||||
"web_restart_error": "Erro ao reiniciar: {0}",
|
||||
"web_support": "Suporte",
|
||||
"web_support_title": "📦 Arquivo de suporte gerado",
|
||||
"web_support_message": "Arquivo de suporte criado com sucesso!\\n\\n📁 Conteúdo:\\n• Configuração de controles\\n• Histórico de downloads\\n• Configurações RGSX\\n• Logs da aplicação\\n• Logs do servidor web\\n\\n💬 Para obter ajuda:\\n1. Entre no Discord RGSX\\n2. Descreva seu problema\\n3. Compartilhe este arquivo ZIP\\n\\nO download vai começar...",
|
||||
"web_support_message": "Arquivo de suporte criado com sucesso!\n\n📁 Conteúdo:\n• Configuração de controles\n• Histórico de downloads\n• Configurações RGSX\n• Logs da aplicação\n• Logs do servidor web\n\n💬 Para obter ajuda:\n1. Entre no Discord RGSX\n2. Descreva seu problema\n3. Compartilhe este arquivo ZIP\n\nO download vai começar...",
|
||||
"web_support_generating": "Gerando arquivo de suporte...",
|
||||
"web_support_download": "Baixar arquivo de suporte",
|
||||
"web_support_error": "Erro ao gerar arquivo de suporte: {0}",
|
||||
@@ -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)"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -246,11 +246,11 @@ def get_translation(key, default=None):
|
||||
return key
|
||||
|
||||
# Fonction pour normaliser les tailles de fichier
|
||||
def normalize_size(size_str):
|
||||
def normalize_size(size_str, lang='en'):
|
||||
"""
|
||||
Normalise une taille de fichier dans différents formats (Ko, KiB, Mo, MiB, Go, GiB)
|
||||
en un format uniforme (Mo ou Go).
|
||||
Exemples: "150 Mo" -> "150 Mo", "1.5 Go" -> "1.5 Go", "500 Ko" -> "0.5 Mo", "2 GiB" -> "2.15 Go"
|
||||
en un format uniforme selon la langue (MB/GB pour anglais, Mo/Go pour français).
|
||||
Exemples: "150 Mo" -> "150 MB" (en), "1.5 Go" -> "1.5 GB" (en), "500 Ko" -> "0.5 MB"
|
||||
"""
|
||||
if not size_str:
|
||||
return None
|
||||
@@ -282,16 +282,24 @@ def normalize_size(size_str):
|
||||
elif unit in ['gio', 'gib']:
|
||||
value = value * 1024 # GiB en Mo
|
||||
|
||||
# Afficher en Go si > 1024 Mo, sinon en Mo
|
||||
if value >= 1024:
|
||||
return f"{value / 1024:.2f} Go".rstrip('0').rstrip('.')
|
||||
# Déterminer les unités selon la langue
|
||||
if lang == 'fr':
|
||||
mb_unit = 'Mo'
|
||||
gb_unit = 'Go'
|
||||
else:
|
||||
# Arrondir à 1 décimale pour Mo
|
||||
mb_unit = 'MB'
|
||||
gb_unit = 'GB'
|
||||
|
||||
# Afficher en GB/Go si > 1024 Mo, sinon en MB/Mo
|
||||
if value >= 1024:
|
||||
return f"{value / 1024:.2f} {gb_unit}".replace('.00 ', ' ').rstrip('0').rstrip('.')
|
||||
else:
|
||||
# Arrondir à 1 décimale pour MB/Mo
|
||||
rounded = round(value, 1)
|
||||
if rounded == int(rounded):
|
||||
return f"{int(rounded)} Mo"
|
||||
return f"{int(rounded)} {mb_unit}"
|
||||
else:
|
||||
return f"{rounded} Mo".rstrip('0').rstrip('.')
|
||||
return f"{rounded} {mb_unit}".rstrip('0').rstrip('.')
|
||||
except (ValueError, TypeError):
|
||||
return size_str # Retourner original si conversion échoue
|
||||
|
||||
@@ -472,6 +480,20 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
"""Répond avec un 404 générique."""
|
||||
self._set_headers('text/plain; charset=utf-8', status=404)
|
||||
self.wfile.write(b'Not found')
|
||||
|
||||
def _get_language_from_cookies(self):
|
||||
"""Récupère la langue depuis les cookies ou retourne 'en' par défaut"""
|
||||
cookie_header = self.headers.get('Cookie', '')
|
||||
if cookie_header:
|
||||
# Parser les cookies
|
||||
cookies = {}
|
||||
for cookie in cookie_header.split(';'):
|
||||
cookie = cookie.strip()
|
||||
if '=' in cookie:
|
||||
key, value = cookie.split('=', 1)
|
||||
cookies[key] = value
|
||||
return cookies.get('language', 'en')
|
||||
return 'en'
|
||||
|
||||
def _asset_version(self, relative_path: str) -> str:
|
||||
"""Retourne un identifiant de version basé sur la date de modification du fichier statique."""
|
||||
@@ -681,7 +703,7 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
'game_name': game_name,
|
||||
'platform': platform_name,
|
||||
'url': game[1] if len(game) > 1 and isinstance(game, (list, tuple)) else None,
|
||||
'size': normalize_size(game[2] if len(game) > 2 and isinstance(game, (list, tuple)) else None)
|
||||
'size': normalize_size(game[2] if len(game) > 2 and isinstance(game, (list, tuple)) else None, self._get_language_from_cookies())
|
||||
})
|
||||
except Exception as e:
|
||||
logger.debug(f"Erreur lors de la recherche dans {platform_name}: {e}")
|
||||
@@ -722,12 +744,15 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
platform_name = path.split('/api/games/')[-1]
|
||||
platform_name = urllib.parse.unquote(platform_name)
|
||||
|
||||
# Récupérer la langue depuis les cookies ou utiliser 'en' par défaut
|
||||
lang = self._get_language_from_cookies()
|
||||
|
||||
games, _, games_last_modified = get_cached_games(platform_name)
|
||||
games_formatted = [
|
||||
{
|
||||
'name': g[0],
|
||||
'url': g[1] if len(g) > 1 else None,
|
||||
'size': normalize_size(g[2] if len(g) > 2 else None)
|
||||
'size': normalize_size(g[2] if len(g) > 2 else None, lang)
|
||||
}
|
||||
for g in games
|
||||
]
|
||||
@@ -1469,6 +1494,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:
|
||||
|
||||
@@ -473,3 +473,70 @@ header p { opacity: 0.9; font-size: 1.1em; }
|
||||
padding: 3px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal Support */
|
||||
.support-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10000;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.support-modal-content {
|
||||
background: #2c2c2c;
|
||||
color: #ffffff;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.support-modal h2 {
|
||||
margin: 0 0 20px 0;
|
||||
color: #4CAF50;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.support-modal-message {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 25px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.support-modal button {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 30px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
width: 100%;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.support-modal button:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
@@ -109,6 +109,53 @@
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// Modal pour afficher les messages support avec formatage
|
||||
function showSupportModal(title, message) {
|
||||
// Remplacer les \n littéraux par de vrais retours à la ligne
|
||||
message = message.replace(/\\n/g, '\n');
|
||||
|
||||
// Créer la modal
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'support-modal';
|
||||
|
||||
const modalContent = document.createElement('div');
|
||||
modalContent.className = 'support-modal-content';
|
||||
|
||||
// Titre
|
||||
const titleElement = document.createElement('h2');
|
||||
titleElement.textContent = title;
|
||||
|
||||
// Message avec retours à la ligne préservés
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.className = 'support-modal-message';
|
||||
messageElement.textContent = message;
|
||||
|
||||
// Bouton OK
|
||||
const okButton = document.createElement('button');
|
||||
okButton.textContent = 'OK';
|
||||
okButton.onclick = () => {
|
||||
modal.style.animation = 'fadeOut 0.2s ease-in';
|
||||
setTimeout(() => modal.remove(), 200);
|
||||
};
|
||||
|
||||
// Assembler la modal
|
||||
modalContent.appendChild(titleElement);
|
||||
modalContent.appendChild(messageElement);
|
||||
modalContent.appendChild(okButton);
|
||||
modal.appendChild(modalContent);
|
||||
|
||||
// Ajouter au DOM
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Fermer en cliquant sur le fond
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) {
|
||||
modal.style.animation = 'fadeOut 0.2s ease-in';
|
||||
setTimeout(() => modal.remove(), 200);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Charger les traductions au démarrage
|
||||
async function loadTranslations() {
|
||||
try {
|
||||
@@ -309,6 +356,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 +528,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 +661,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 +753,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 +784,7 @@
|
||||
[regionPriorityOrder[idx-1], regionPriorityOrder[idx]];
|
||||
saveRegionPriorityOrder();
|
||||
renderRegionPriorityConfig();
|
||||
saveFiltersToBackend();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,14 +796,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 +822,11 @@
|
||||
<span style="font-weight: bold; color: #666; min-width: 25px;">${idx + 1}.</span>
|
||||
<span style="flex: 1; font-weight: 500;">${region}</span>
|
||||
<button onclick="moveRegionUp('${region}')"
|
||||
style="padding: 4px 8px; border: 1px solid #ccc; background: white; cursor: pointer; border-radius: 3px;"
|
||||
${idx === 0 ? 'disabled' : ''}>▲</button>
|
||||
style="padding: 4px 8px; border: 1px solid #ccc; background: white; cursor: pointer; border-radius: 3px; font-size: 14px;"
|
||||
${idx === 0 ? 'disabled' : ''}>🔼</button>
|
||||
<button onclick="moveRegionDown('${region}')"
|
||||
style="padding: 4px 8px; border: 1px solid #ccc; background: white; cursor: pointer; border-radius: 3px;"
|
||||
${idx === regionPriorityOrder.length - 1 ? 'disabled' : ''}>▼</button>
|
||||
style="padding: 4px 8px; border: 1px solid #ccc; background: white; cursor: pointer; border-radius: 3px; font-size: 14px;"
|
||||
${idx === regionPriorityOrder.length - 1 ? 'disabled' : ''}>🔽</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
@@ -706,14 +887,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 +986,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();
|
||||
@@ -901,13 +1083,24 @@
|
||||
const getSizeInMo = (sizeElem) => {
|
||||
if (!sizeElem) return 0;
|
||||
const text = sizeElem.textContent;
|
||||
// Les tailles sont maintenant normalisées: "100 Mo" ou "2.5 Go"
|
||||
const match = text.match(/([0-9.]+)\\s*(Mo|Go)/i);
|
||||
// Support des formats: "100 Mo", "2.5 Go" (français) et "100 MB", "2.5 GB" (anglais)
|
||||
// Plus Ko/KB, o/B, To/TB
|
||||
const match = text.match(/([0-9.]+)\s*(o|B|Ko|KB|Mo|MB|Go|GB|To|TB)/i);
|
||||
if (!match) return 0;
|
||||
let size = parseFloat(match[1]);
|
||||
// Convertir Go en Mo pour comparaison
|
||||
if (match[2].toUpperCase() === 'GO') {
|
||||
size *= 1024;
|
||||
const unit = match[2].toUpperCase();
|
||||
|
||||
// Convertir tout en Mo
|
||||
if (unit === 'O' || unit === 'B') {
|
||||
size /= (1024 * 1024); // octets/bytes vers Mo
|
||||
} else if (unit === 'KO' || unit === 'KB') {
|
||||
size /= 1024; // Ko vers Mo
|
||||
} else if (unit === 'MO' || unit === 'MB') {
|
||||
// Déjà en Mo
|
||||
} else if (unit === 'GO' || unit === 'GB') {
|
||||
size *= 1024; // Go vers Mo
|
||||
} else if (unit === 'TO' || unit === 'TB') {
|
||||
size *= 1024 * 1024; // To vers Mo
|
||||
}
|
||||
return size;
|
||||
};
|
||||
@@ -1032,22 +1225,26 @@
|
||||
<div class="filter-row">
|
||||
<span class="filter-label">${t('web_filter_region')}:</span>
|
||||
<button class="region-btn" data-region="USA" onclick="toggleRegionFilter('USA')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1fa-1f1f8.svg" style="width:16px;height:16px" /> USA</button>
|
||||
<button class="region-btn" data-region="Europe" onclick="toggleRegionFilter('Europe')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1ea-1f1fa.svg" style="width:16px;height:16px" /> Europe</button>
|
||||
<button class="region-btn" data-region="Canada" onclick="toggleRegionFilter('Canada')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1e8-1f1e6.svg" style="width:16px;height:16px" /> Canada</button>
|
||||
<button class="region-btn" data-region="Europe" onclick="toggleRegionFilter('Europe')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1ea-1f1fa.svg" style="width:16px;height:16px" /> Europe</button>
|
||||
<button class="region-btn" data-region="France" onclick="toggleRegionFilter('France')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1eb-1f1f7.svg" style="width:16px;height:16px" /> France</button>
|
||||
<button class="region-btn" data-region="Germany" onclick="toggleRegionFilter('Germany')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1e9-1f1ea.svg" style="width:16px;height:16px" /> Germany</button>
|
||||
<button class="region-btn" data-region="Japan" onclick="toggleRegionFilter('Japan')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1ef-1f1f5.svg" style="width:16px;height:16px" /> Japan</button>
|
||||
<button class="region-btn" data-region="Korea" onclick="toggleRegionFilter('Korea')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1f0-1f1f7.svg" style="width:16px;height:16px" /> Korea</button>
|
||||
<button class="region-btn" data-region="World" onclick="toggleRegionFilter('World')">🌍 World</button>
|
||||
<button class="region-btn" data-region="Other" onclick="toggleRegionFilter('Other')">🌐 Other</button>
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox" id="hide-non-release" onchange="applyAllFilters()">
|
||||
<input type="checkbox" id="hide-non-release" onchange="applyAllFilters(); saveFiltersToBackend();">
|
||||
<span>${t('web_filter_hide_non_release')}</span>
|
||||
</label>
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox" id="regex-mode" onchange="applyAllFilters()">
|
||||
<input type="checkbox" id="regex-mode" onchange="applyAllFilters(); saveFiltersToBackend();">
|
||||
<span>${t('web_filter_regex_mode')}</span>
|
||||
</label>
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox" id="one-rom-per-game" onchange="applyAllFilters()">
|
||||
<input type="checkbox" id="one-rom-per-game" onchange="applyAllFilters(); saveFiltersToBackend();">
|
||||
<span>${t('web_filter_one_rom_per_game')} (<span id="region-priority-display">USA → Canada → World → Europe → Japan → Other</span>)</span>
|
||||
<button onclick="showRegionPriorityConfig()" style="margin-left: 8px; padding: 2px 8px; font-size: 0.9em; background: #666; color: white; border: none; border-radius: 3px; cursor: pointer;" title="${t('web_filter_configure_priority')}">⚙️</button>
|
||||
</label>
|
||||
@@ -1082,6 +1279,9 @@
|
||||
`;
|
||||
container.innerHTML = html;
|
||||
|
||||
// Restore filter states from loaded settings
|
||||
restoreFilterStates();
|
||||
|
||||
// Appliquer le tri par défaut (A-Z)
|
||||
sortGames(currentGameSort);
|
||||
|
||||
@@ -1902,6 +2102,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 +2127,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: regionPriorityOrder
|
||||
}
|
||||
};
|
||||
|
||||
const response = await fetch('/api/settings', {
|
||||
@@ -2020,7 +2233,7 @@
|
||||
}
|
||||
|
||||
// Générer un fichier ZIP de support
|
||||
async function generateSupportZip() {
|
||||
async function generateSupportZip(event) {
|
||||
try {
|
||||
// Afficher un message de chargement
|
||||
const loadingMsg = t('web_support_generating');
|
||||
@@ -2063,8 +2276,8 @@
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
// Afficher le message d'instructions
|
||||
alert(t('web_support_title') + '\\n\\n' + t('web_support_message'));
|
||||
// Afficher le message d'instructions dans une modal
|
||||
showSupportModal(t('web_support_title'), t('web_support_message'));
|
||||
|
||||
// Restaurer le bouton
|
||||
if (originalButton) {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2.3.2.6"
|
||||
"version": "2.3.2.8"
|
||||
}
|
||||
Reference in New Issue
Block a user