Compare commits

...

3 Commits

Author SHA1 Message Date
skymike03
6f17173a8c v2.3.2.7 (2025.11.19)
- BETA : add filtering options of games in RGSX main app / synced with options sets on web interface
Filter by Region, hide beta and demos, show only one rom per game and select prefered display order
2025-11-19 23:15:12 +01:00
skymike03
05a8df5933 v2.3.2.6 (2025.19.11)
- add missing translations on web interface
- correct display bug in web interface settings
- correct units showing in french only
- correct save bug spotted in Web settings
2025-11-19 21:26:17 +01:00
skymike03
55231bb823 v2.3.2.5 (2025.11.18)
- bugs in menu solved and display tweak on filter systems
2025-11-18 16:16:41 +01:00
17 changed files with 1593 additions and 207 deletions

View File

@@ -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":

View File

@@ -13,7 +13,7 @@ except Exception:
pygame = None # type: ignore
# Version actuelle de l'application
app_version = "2.3.2.4"
app_version = "2.3.2.7"
def get_application_root():
@@ -380,6 +380,11 @@ search_mode = False # Indicateur si le mode recherche est actif
search_query = "" # Chaîne de recherche saisie par l'utilisateur
filter_active = False # Indicateur si un filtre est appliqué
# Variables pour le filtrage avancé
selected_filter_choice = 0 # Index dans le menu de choix de filtrage (recherche / avancé)
selected_filter_option = 0 # Index dans le menu de filtrage avancé
game_filter_obj = None # Objet GameFilters pour le filtrage avancé
# Gestion des états du menu
needs_redraw = False # Indicateur si l'écran doit être redessiné
selected_option = 0 # Index de l'option sélectionnée dans le menu

View File

@@ -20,12 +20,12 @@ from utils import (
ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string
)
from history import load_history, clear_history, add_to_history, save_history
from language import _ # Import de la fonction de traduction
from language import _, get_available_languages, set_language
from rgsx_settings import (
get_allow_unknown_extensions, set_display_grid, get_font_family, set_font_family,
get_show_unsupported_platforms, set_show_unsupported_platforms,
set_allow_unknown_extensions, get_hide_premium_systems, set_hide_premium_systems,
get_sources_mode, set_sources_mode, set_symlink_option, get_symlink_option
get_sources_mode, set_sources_mode, set_symlink_option, get_symlink_option, load_rgsx_settings, save_rgsx_settings
)
from accessibility import save_accessibility_settings
from scraper import get_game_metadata, download_image_to_surface
@@ -58,7 +58,12 @@ VALID_STATES = [
"scraper", # écran du scraper avec métadonnées
"history_error_details", # détails de l'erreur
"history_confirm_delete", # confirmation suppression jeu
"history_extract_archive" # extraction d'archive
"history_extract_archive", # extraction d'archive
# Nouveaux menus filtrage avancé
"filter_menu_choice", # menu de choix entre recherche et filtrage avancé
"filter_search", # recherche par nom (existant, mais renommé)
"filter_advanced", # filtrage avancé par région, etc.
"filter_priority_config", # configuration priorité régions pour one-rom-per-game
]
def validate_menu_state(state):
@@ -476,8 +481,15 @@ def handle_controls(event, sources, joystick, screen):
if config.platforms:
config.current_platform = config.selected_platform
config.games = load_games(config.platforms[config.current_platform])
config.filtered_games = config.games
config.filter_active = False
# Apply saved filters automatically if any
if config.game_filter_obj and config.game_filter_obj.is_active():
config.filtered_games = config.game_filter_obj.apply_filters(config.games)
config.filter_active = True
else:
config.filtered_games = config.games
config.filter_active = False
config.current_game = 0
config.scroll_offset = 0
draw_validation_transition(screen, config.current_platform)
@@ -656,14 +668,12 @@ def handle_controls(event, sources, joystick, screen):
event.value)
config.needs_redraw = True
elif is_input_matched(event, "filter"):
config.search_mode = True
config.search_query = ""
config.filtered_games = config.games
config.current_game = 0
config.scroll_offset = 0
config.selected_key = (0, 0)
# Afficher le menu de choix entre recherche et filtrage avancé
config.menu_state = "filter_menu_choice"
config.selected_filter_choice = 0
config.previous_menu_state = "game"
config.needs_redraw = True
logger.debug("Entrée en mode recherche")
logger.debug("Ouverture du menu de filtrage")
elif is_input_matched(event, "history"):
config.menu_state = "history"
config.needs_redraw = True
@@ -1451,7 +1461,7 @@ def handle_controls(event, sources, joystick, screen):
# Sous-menu Display
elif config.menu_state == "pause_display_menu":
sel = getattr(config, 'pause_display_selection', 0)
total = 8 # layout, font size, font family, unsupported, unknown, hide premium, filter, back
total = 6 # layout, font size, footer font size, font family, allow unknown extensions, back
if is_input_matched(event, "up"):
config.pause_display_selection = (sel - 1) % total
config.needs_redraw = True
@@ -1459,7 +1469,6 @@ def handle_controls(event, sources, joystick, screen):
config.pause_display_selection = (sel + 1) % total
config.needs_redraw = True
elif is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm"):
sel = getattr(config, 'pause_display_selection', 0)
# 0 layout cycle
if sel == 0 and (is_input_matched(event, "left") or is_input_matched(event, "right")):
layouts = [(3,3),(3,4),(4,3),(4,4)]
@@ -1475,14 +1484,12 @@ def handle_controls(event, sources, joystick, screen):
logger.error(f"Erreur set_display_grid: {e}")
config.GRID_COLS = new_cols
config.GRID_ROWS = new_rows
# Redémarrage automatique
# Afficher un popup indiquant que le changement sera effectif après redémarrage
try:
config.menu_state = "restart_popup"
config.popup_message = _("popup_restarting") if _ else "Restarting..."
config.popup_timer = 2000
restart_application(2000)
config.popup_message = _("popup_layout_changed_restart_required") if _ else "Layout changed. Restart required to apply."
config.popup_timer = 3000
except Exception as e:
logger.error(f"Erreur restart après layout: {e}")
logger.error(f"Erreur popup layout: {e}")
config.needs_redraw = True
# 1 font size
elif sel == 1 and (is_input_matched(event, "left") or is_input_matched(event, "right")):
@@ -1502,8 +1509,21 @@ def handle_controls(event, sources, joystick, screen):
except Exception as e:
logger.error(f"Erreur init polices: {e}")
config.needs_redraw = True
# 2 font family cycle
elif sel == 2 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
# 2 footer font size
elif sel == 2 and (is_input_matched(event, "left") or is_input_matched(event, "right")):
from accessibility import update_footer_font_scale
footer_opts = getattr(config, 'footer_font_scale_options', [0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0])
idx = getattr(config, 'current_footer_font_scale_index', 3)
idx = max(0, idx-1) if is_input_matched(event, "left") else min(len(footer_opts)-1, idx+1)
if idx != getattr(config, 'current_footer_font_scale_index', 3):
config.current_footer_font_scale_index = idx
try:
update_footer_font_scale()
except Exception as e:
logger.error(f"Erreur update footer font scale: {e}")
config.needs_redraw = True
# 3 font family cycle
elif sel == 3 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
try:
families = getattr(config, 'FONT_FAMILIES', ["pixel"]) or ["pixel"]
current = get_font_family()
@@ -1536,17 +1556,6 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur changement font family: {e}")
# 3 unsupported toggle
elif sel == 3 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
try:
current = get_show_unsupported_platforms()
new_val = set_show_unsupported_platforms(not current)
load_sources()
config.popup_message = _("menu_show_unsupported_enabled") if new_val else _("menu_show_unsupported_disabled")
config.popup_timer = 3000
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur toggle unsupported: {e}")
# 4 allow unknown extensions
elif sel == 4 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
try:
@@ -1557,25 +1566,8 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur toggle allow_unknown_extensions: {e}")
# 5 hide premium systems
elif sel == 5 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")):
try:
cur = get_hide_premium_systems()
new_val = set_hide_premium_systems(not cur)
config.popup_message = ("Premium hidden" if new_val else "Premium visible") if _ is None else (_("popup_hide_premium_on") if new_val else _("popup_hide_premium_off"))
config.popup_timer = 2500
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur toggle hide_premium_systems: {e}")
# 6 filter platforms
elif sel == 6 and (is_input_matched(event, "confirm") or is_input_matched(event, "right")):
config.filter_return_to = "pause_display_menu"
config.menu_state = "filter_platforms"
config.selected_filter_index = 0
config.filter_platforms_scroll_offset = 0
config.needs_redraw = True
# 7 back
elif sel == 7 and (is_input_matched(event, "confirm")):
# 5 back
elif sel == 5 and is_input_matched(event, "confirm"):
config.menu_state = "pause_menu"
config.last_state_change_time = pygame.time.get_ticks()
config.needs_redraw = True
@@ -1587,7 +1579,7 @@ def handle_controls(event, sources, joystick, screen):
# Sous-menu Games
elif config.menu_state == "pause_games_menu":
sel = getattr(config, 'pause_games_selection', 0)
total = 4 # history, source, redownload, back
total = 7 # history, source, redownload, unsupported, hide premium, filter, back
if is_input_matched(event, "up"):
config.pause_games_selection = (sel - 1) % total
config.needs_redraw = True
@@ -1595,7 +1587,6 @@ def handle_controls(event, sources, joystick, screen):
config.pause_games_selection = (sel + 1) % total
config.needs_redraw = True
elif is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right"):
sel = getattr(config, 'pause_games_selection', 0)
if sel == 0 and is_input_matched(event, "confirm"): # history
config.history = load_history()
config.current_history_item = 0
@@ -1623,7 +1614,32 @@ def handle_controls(event, sources, joystick, screen):
config.menu_state = "reload_games_data"
config.redownload_confirm_selection = 0
config.needs_redraw = True
elif sel == 3 and is_input_matched(event, "confirm"): # back
elif sel == 3 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # unsupported toggle
try:
current = get_show_unsupported_platforms()
new_val = set_show_unsupported_platforms(not current)
load_sources()
config.popup_message = _("menu_show_unsupported_enabled") if new_val else _("menu_show_unsupported_disabled")
config.popup_timer = 3000
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur toggle unsupported: {e}")
elif sel == 4 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # hide premium
try:
cur = get_hide_premium_systems()
new_val = set_hide_premium_systems(not cur)
config.popup_message = ("Premium hidden" if new_val else "Premium visible") if _ is None else (_("popup_hide_premium_on") if new_val else _("popup_hide_premium_off"))
config.popup_timer = 2500
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur toggle hide_premium_systems: {e}")
elif sel == 5 and is_input_matched(event, "confirm"): # filter platforms
config.filter_return_to = "pause_games_menu"
config.menu_state = "filter_platforms"
config.selected_filter_index = 0
config.filter_platforms_scroll_offset = 0
config.needs_redraw = True
elif sel == 6 and is_input_matched(event, "confirm"): # back
config.menu_state = "pause_menu"
config.last_state_change_time = pygame.time.get_ticks()
config.needs_redraw = True
@@ -1649,7 +1665,6 @@ def handle_controls(event, sources, joystick, screen):
config.pause_settings_selection = (sel + 1) % total
config.needs_redraw = True
elif is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right"):
sel = getattr(config, 'pause_settings_selection', 0)
# Option 0: Music toggle
if sel == 0 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")):
config.music_enabled = not config.music_enabled
@@ -1727,7 +1742,6 @@ def handle_controls(event, sources, joystick, screen):
config.display_menu_selection = (sel + 1) % 5
config.needs_redraw = True
elif is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm"):
sel = getattr(config, 'display_menu_selection', 0)
# 0: layout change
if sel == 0 and (is_input_matched(event, "left") or is_input_matched(event, "right")):
layouts = [(3,3),(3,4),(4,3),(4,4)]
@@ -1915,48 +1929,289 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
logger.debug("Annulation de la sélection de langue, retour au menu pause")
# Menu de choix filtrage
elif config.menu_state == "filter_menu_choice":
if is_input_matched(event, "up"):
config.selected_filter_choice = (config.selected_filter_choice - 1) % 2
config.needs_redraw = True
elif is_input_matched(event, "down"):
config.selected_filter_choice = (config.selected_filter_choice + 1) % 2
config.needs_redraw = True
elif is_input_matched(event, "confirm"):
if config.selected_filter_choice == 0:
# Recherche par nom (mode existant)
config.search_mode = True
config.search_query = ""
config.filtered_games = config.games
config.current_game = 0
config.scroll_offset = 0
config.selected_key = (0, 0)
config.menu_state = "game"
config.needs_redraw = True
logger.debug("Entrée en mode recherche par nom")
else:
# Filtrage avancé
from game_filters import GameFilters
from rgsx_settings import load_game_filters
# Initialiser le filtre
if not hasattr(config, 'game_filter_obj'):
config.game_filter_obj = GameFilters()
filter_dict = load_game_filters()
if filter_dict:
config.game_filter_obj.load_from_dict(filter_dict)
config.menu_state = "filter_advanced"
config.selected_filter_option = 0
config.needs_redraw = True
logger.debug("Entrée en filtrage avancé")
elif is_input_matched(event, "cancel"):
config.menu_state = "game"
config.needs_redraw = True
logger.debug("Retour à la liste des jeux")
# Filtrage avancé
elif config.menu_state == "filter_advanced":
from game_filters import GameFilters
from rgsx_settings import save_game_filters
# Initialiser le filtre si nécessaire
if not hasattr(config, 'game_filter_obj'):
config.game_filter_obj = GameFilters()
from rgsx_settings import load_game_filters
filter_dict = load_game_filters()
if filter_dict:
config.game_filter_obj.load_from_dict(filter_dict)
# Construire la liste des options (comme dans draw_filter_advanced)
options = []
options.append(('header', 'region_title'))
for region in GameFilters.REGIONS:
options.append(('region', region))
options.append(('separator', ''))
options.append(('header', 'other_options'))
options.append(('toggle', 'hide_non_release'))
options.append(('toggle', 'one_rom_per_game'))
options.append(('button_inline', 'priority_config'))
# Boutons séparés (3 boutons au total)
buttons = [
('button', 'apply'),
('button', 'reset'),
('button', 'back')
]
# Total d'éléments sélectionnables
total_items = len(options) + len(buttons)
if is_input_matched(event, "up"):
# Chercher l'option sélectionnable précédente
config.selected_filter_option = (config.selected_filter_option - 1) % total_items
while config.selected_filter_option < len(options) and options[config.selected_filter_option][0] in ['header', 'separator']:
config.selected_filter_option = (config.selected_filter_option - 1) % total_items
config.needs_redraw = True
update_key_state("up", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
elif is_input_matched(event, "down"):
# Chercher l'option sélectionnable suivante
config.selected_filter_option = (config.selected_filter_option + 1) % total_items
while config.selected_filter_option < len(options) and options[config.selected_filter_option][0] in ['header', 'separator']:
config.selected_filter_option = (config.selected_filter_option + 1) % total_items
config.needs_redraw = True
update_key_state("down", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
elif is_input_matched(event, "left") or is_input_matched(event, "right"):
# Navigation gauche/droite uniquement pour les boutons en bas
if config.selected_filter_option >= len(options):
button_index = config.selected_filter_option - len(options)
if is_input_matched(event, "left"):
button_index = (button_index - 1) % len(buttons)
else:
button_index = (button_index + 1) % len(buttons)
config.selected_filter_option = len(options) + button_index
config.needs_redraw = True
elif is_input_matched(event, "confirm"):
# Déterminer si c'est une option ou un bouton
if config.selected_filter_option < len(options):
option_type, *option_data = options[config.selected_filter_option]
else:
# C'est un bouton
button_index = config.selected_filter_option - len(options)
option_type, *option_data = buttons[button_index]
if option_type == 'region':
# Basculer filtre région: include ↔ exclude (include par défaut)
region = option_data[0]
current_state = config.game_filter_obj.region_filters.get(region, 'include')
if current_state == 'include':
config.game_filter_obj.region_filters[region] = 'exclude'
else:
config.game_filter_obj.region_filters[region] = 'include'
config.needs_redraw = True
logger.debug(f"Filtre région {region} modifié: {config.game_filter_obj.region_filters[region]}")
elif option_type == 'toggle':
toggle_name = option_data[0]
if toggle_name == 'hide_non_release':
config.game_filter_obj.hide_non_release = not config.game_filter_obj.hide_non_release
elif toggle_name == 'one_rom_per_game':
config.game_filter_obj.one_rom_per_game = not config.game_filter_obj.one_rom_per_game
config.needs_redraw = True
logger.debug(f"Toggle {toggle_name} modifié")
elif option_type == 'button_inline':
button_name = option_data[0]
if button_name == 'priority_config':
# Ouvrir le menu de configuration de priorité
config.menu_state = "filter_priority_config"
config.selected_priority_index = 0
config.needs_redraw = True
logger.debug("Ouverture configuration priorité régions")
elif option_type == 'button':
button_name = option_data[0]
if button_name == 'apply':
# Appliquer les filtres
save_game_filters(config.game_filter_obj.to_dict())
# Appliquer aux jeux actuels
if config.game_filter_obj.is_active():
config.filtered_games = config.game_filter_obj.apply_filters(config.games)
config.filter_active = True
else:
config.filtered_games = config.games
config.filter_active = False
config.current_game = 0
config.scroll_offset = 0
config.menu_state = "game"
config.needs_redraw = True
logger.debug("Filtres appliqués")
elif button_name == 'reset':
# Réinitialiser les filtres
config.game_filter_obj.reset()
save_game_filters(config.game_filter_obj.to_dict())
config.filtered_games = config.games
config.filter_active = False
config.needs_redraw = True
logger.debug("Filtres réinitialisés")
elif button_name == 'back':
# Retour sans appliquer
config.menu_state = "game"
config.needs_redraw = True
logger.debug("Retour sans appliquer les filtres")
elif is_input_matched(event, "cancel"):
config.menu_state = "game"
config.needs_redraw = True
logger.debug("Annulation du filtrage avancé")
# Configuration priorité régions
elif config.menu_state == "filter_priority_config":
from game_filters import GameFilters
from rgsx_settings import save_game_filters
if not hasattr(config, 'game_filter_obj'):
config.game_filter_obj = GameFilters()
priority_list = config.game_filter_obj.region_priority
total_items = len(priority_list) + 1 # +1 pour le bouton Back
if not hasattr(config, 'selected_priority_index'):
config.selected_priority_index = 0
if is_input_matched(event, "up"):
config.selected_priority_index = (config.selected_priority_index - 1) % total_items
config.needs_redraw = True
elif is_input_matched(event, "down"):
config.selected_priority_index = (config.selected_priority_index + 1) % total_items
config.needs_redraw = True
elif is_input_matched(event, "confirm"):
if config.selected_priority_index >= len(priority_list):
# Bouton Back : retour au menu filtrage avancé
save_game_filters(config.game_filter_obj.to_dict())
config.menu_state = "filter_advanced"
config.needs_redraw = True
logger.debug("Retour au filtrage avancé")
elif is_input_matched(event, "left") and config.selected_priority_index < len(priority_list):
# Monter la région dans la priorité
idx = config.selected_priority_index
if idx > 0:
priority_list[idx], priority_list[idx-1] = priority_list[idx-1], priority_list[idx]
config.selected_priority_index = idx - 1
config.needs_redraw = True
logger.debug(f"Priorité modifiée: {priority_list}")
elif is_input_matched(event, "right") and config.selected_priority_index < len(priority_list):
# Descendre la région dans la priorité
idx = config.selected_priority_index
if idx < len(priority_list) - 1:
priority_list[idx], priority_list[idx+1] = priority_list[idx+1], priority_list[idx]
config.selected_priority_index = idx + 1
config.needs_redraw = True
logger.debug(f"Priorité modifiée: {priority_list}")
elif is_input_matched(event, "cancel"):
# Retour sans sauvegarder
config.menu_state = "filter_advanced"
config.needs_redraw = True
logger.debug("Annulation configuration priorité")
# Menu filtre plateformes
elif config.menu_state == "filter_platforms":
total_items = len(config.filter_platforms_selection)
action_buttons = 4
extended_max = total_items + action_buttons - 1
# Indices: 0-3 = boutons, 4+ = liste des systèmes
extended_max = action_buttons + total_items - 1
if is_input_matched(event, "up"):
if config.selected_filter_index > 0:
config.selected_filter_index -= 1
config.needs_redraw = True
else:
# Wrap vers les boutons (premier bouton) depuis le haut
if total_items > 0:
config.selected_filter_index = total_items
config.needs_redraw = True
# Wrap vers le bas (dernière ligne de la liste)
config.selected_filter_index = extended_max
config.needs_redraw = True
# Activer la répétition automatique
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"):
if config.selected_filter_index < extended_max:
config.selected_filter_index += 1
config.needs_redraw = True
else:
# Wrap retour en haut de la liste
# Wrap retour en haut (premier bouton)
config.selected_filter_index = 0
config.needs_redraw = True
# Activer la répétition automatique
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"):
if config.selected_filter_index >= total_items:
if config.selected_filter_index > total_items:
# Navigation gauche/droite uniquement pour les boutons (indices 0-3)
if config.selected_filter_index < action_buttons:
if config.selected_filter_index > 0:
config.selected_filter_index -= 1
config.needs_redraw = True
# sinon ignorer
# sinon ignorer (dans la liste)
elif is_input_matched(event, "right"):
if config.selected_filter_index >= total_items:
if config.selected_filter_index < extended_max:
# Navigation gauche/droite uniquement pour les boutons (indices 0-3)
if config.selected_filter_index < action_buttons:
if config.selected_filter_index < action_buttons - 1:
config.selected_filter_index += 1
config.needs_redraw = True
# sinon ignorer
# sinon ignorer (dans la liste)
elif is_input_matched(event, "confirm"):
if config.selected_filter_index < total_items:
name, hidden = config.filter_platforms_selection[config.selected_filter_index]
config.filter_platforms_selection[config.selected_filter_index] = (name, not hidden)
config.filter_platforms_dirty = True
config.needs_redraw = True
else:
btn_idx = config.selected_filter_index - total_items
# Indices 0-3 = boutons, 4+ = liste
if config.selected_filter_index < action_buttons:
# Action sur un bouton
btn_idx = config.selected_filter_index
settings = load_rgsx_settings()
if btn_idx == 0: # all visible
config.filter_platforms_selection = [(n, False) for n, _ in config.filter_platforms_selection]
@@ -2002,6 +2257,14 @@ def handle_controls(event, sources, joystick, screen):
config.selected_option = 5
config.filter_return_to = None
config.needs_redraw = True
else:
# Action sur un élément de la liste (indices >= action_buttons)
list_index = config.selected_filter_index - action_buttons
if list_index < total_items:
name, hidden = config.filter_platforms_selection[list_index]
config.filter_platforms_selection[list_index] = (name, not hidden)
config.filter_platforms_dirty = True
config.needs_redraw = True
elif is_input_matched(event, "cancel"):
target = getattr(config, 'filter_return_to', 'pause_menu')
config.menu_state = target

View File

@@ -8,7 +8,7 @@ from utils import truncate_text_middle, wrap_text, load_system_image, truncate_t
import logging
import math
from history import load_history, is_game_downloaded
from language import _
from language import _, get_size_units, get_speed_unit
logger = logging.getLogger(__name__)
@@ -193,7 +193,9 @@ THEME_COLORS = {
"background_bottom": (60, 80, 100), # noir vers bleu foncé
# Fond des cadres
"button_idle": (50, 50, 70, 150), # Bleu sombre métal
# Fond des boutons sélectionnés dans les popups ou menu
# Fond des boutons sélectionnés
"button_selected": (70, 70, 100, 200), # Bleu plus clair
# Fond des boutons hover dans les popups ou menu
"button_hover": (255, 0, 255, 220), # Rose
# Générique
"text": (255, 255, 255), # blanc
@@ -209,6 +211,10 @@ THEME_COLORS = {
"title_text": (200, 200, 200), # gris clair
# Bordures
"border": (150, 150, 150), # Bordures grises subtiles
"border_selected": (0, 255, 0), # Bordure verte pour sélection
# Couleurs pour filtres
"green": (0, 255, 0), # vert
"red": (255, 0, 0), # rouge
}
# Général, résolution, overlay
@@ -847,13 +853,19 @@ def draw_game_list(screen):
pygame.draw.rect(screen, THEME_COLORS["border"], title_rect_inflated, 2, border_radius=12)
screen.blit(title_surface, title_rect)
elif config.filter_active:
filter_text = _("game_filter").format(config.search_query)
title_surface = config.font.render(filter_text, True, THEME_COLORS["fond_lignes"])
# Display filter active indicator with count
if hasattr(config, 'game_filter_obj') and config.game_filter_obj and config.game_filter_obj.is_active():
total_games = len(config.games)
filtered_count = len(games)
filter_text = _("filter_games_shown").format(filtered_count, total_games)
else:
filter_text = _("game_filter").format(config.search_query)
title_surface = config.font.render(filter_text, True, THEME_COLORS["green"])
title_rect = title_surface.get_rect(center=(config.screen_width // 2, title_surface.get_height() // 2 + 20))
title_rect_inflated = title_rect.inflate(60, 30)
title_rect_inflated.topleft = ((config.screen_width - title_rect_inflated.width) // 2, 10)
pygame.draw.rect(screen, THEME_COLORS["button_idle"], title_rect_inflated, border_radius=12)
pygame.draw.rect(screen, THEME_COLORS["border"], title_rect_inflated, 2, border_radius=12)
pygame.draw.rect(screen, THEME_COLORS["border_selected"], title_rect_inflated, 3, border_radius=12)
screen.blit(title_surface, title_rect)
else:
title_text = _("game_count").format(platform_name, game_count)
@@ -956,15 +968,16 @@ def draw_game_scrollbar(screen, scroll_offset, total_items, visible_items, x, y,
pygame.draw.rect(screen, THEME_COLORS["fond_lignes"], (x, scrollbar_y, 15, scrollbar_height), border_radius=4)
def format_size(size):
"""Convertit une taille en octets en format lisible."""
"""Convertit une taille en octets en format lisible avec unités adaptées à la langue."""
if not isinstance(size, (int, float)) or size == 0:
return "N/A"
for unit in ['o', 'Ko', 'Mo', 'Go', 'To']:
units = get_size_units()
for unit in units[:-1]: # Tous sauf le dernier (Po/PB)
if size < 1024.0:
return f"{size:.1f} {unit}"
size /= 1024.0
return f"{size:.1f} Po"
return f"{size:.1f} {units[-1]}" # Dernier niveau (Po/PB)
def draw_history_list(screen):
@@ -991,7 +1004,7 @@ def draw_history_list(screen):
if entry.get("status") in ["Téléchargement", "Downloading"]:
speed = entry.get("speed", 0.0)
if speed and speed > 0:
speed_str = f" - {speed:.2f} Mo/s"
speed_str = f" - {speed:.2f} {get_speed_unit()}"
break
screen.blit(OVERLAY, (0, 0))
@@ -1036,7 +1049,7 @@ def draw_history_list(screen):
if history and history[current_history_item_inverted].get("status") in ["Téléchargement", "Downloading"]:
speed = history[current_history_item_inverted].get("speed", 0.0)
if speed > 0:
speed_str = f"{speed:.2f} Mo/s"
speed_str = f"{speed:.2f} {get_speed_unit()}"
title_text = _("history_title").format(history_count) + f" {speed_str}"
else:
title_text = _("history_title").format(history_count)
@@ -2014,12 +2027,14 @@ def draw_pause_display_menu(screen, selected_index):
fam_label = family_map.get(current_family, current_family)
font_family_txt = f"{_('submenu_display_font_family') if _ else 'Font'}: < {fam_label} >"
# Allow unknown extensions
allow_unknown = get_allow_unknown_extensions()
status_unknown = _('status_on') if allow_unknown else _('status_off')
raw_unknown_label = _('submenu_display_allow_unknown_ext') if _ else 'Hide unknown ext warn: {status}'
if '{status}' in raw_unknown_label:
raw_unknown_label = raw_unknown_label.split('{status}')[0].rstrip(' :')
unknown_txt = f"{raw_unknown_label}: < {status_unknown} >"
back_txt = _("menu_back") if _ else "Back"
options = [layout_txt, font_txt, footer_font_txt, font_family_txt, unknown_txt, back_txt]
_draw_submenu_generic(screen, _("menu_display"), options, selected_index)
@@ -2319,11 +2334,32 @@ def draw_filter_platforms_menu(screen):
pygame.draw.rect(screen, THEME_COLORS["border"], title_rect_inflated, 2, border_radius=12)
screen.blit(title_surface, title_rect)
# Zone liste
# Boutons d'action en haut (avant la liste)
btn_width = 220
btn_height = int(config.screen_height * 0.0463)
spacing = 30
buttons_y = title_rect_inflated.bottom + 20
center_x = config.screen_width // 2
actions = [
("filter_all", 0),
("filter_none", 1),
("filter_apply", 2),
("filter_back", 3)
]
total_items = len(config.filter_platforms_selection)
action_buttons = len(actions)
for idx, (key, btn_idx) in enumerate(actions):
btn_x = center_x - (len(actions) * (btn_width + spacing) - spacing) // 2 + idx * (btn_width + spacing)
is_selected = (config.selected_filter_index == btn_idx)
label = _(key)
draw_stylized_button(screen, label, btn_x, buttons_y, btn_width, btn_height, selected=is_selected)
# Zone liste (après les boutons)
list_width = int(config.screen_width * 0.7)
list_height = int(config.screen_height * 0.6)
list_height = int(config.screen_height * 0.5)
list_x = (config.screen_width - list_width) // 2
list_y = title_rect_inflated.bottom + 20
list_y = buttons_y + btn_height + 20
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (list_x, list_y, list_width, list_height), border_radius=12)
pygame.draw.rect(screen, THEME_COLORS["border"], (list_x, list_y, list_width, list_height), 2, border_radius=12)
@@ -2340,12 +2376,13 @@ def draw_filter_platforms_menu(screen):
elif config.selected_filter_index >= config.filter_platforms_scroll_offset + visible_items:
config.filter_platforms_scroll_offset = config.selected_filter_index - visible_items + 1
# Dessiner items
# Dessiner items (les indices de la liste commencent à action_buttons)
for i in range(config.filter_platforms_scroll_offset, min(config.filter_platforms_scroll_offset + visible_items, total_items)):
name, is_hidden = config.filter_platforms_selection[i]
idx_on_screen = i - config.filter_platforms_scroll_offset
y_center = list_y + 10 + idx_on_screen * line_height + line_height // 2
selected = (i == config.selected_filter_index)
# Les éléments de la liste ont des indices à partir de action_buttons
selected = (config.selected_filter_index == action_buttons + i)
checkbox = "[ ]" if is_hidden else "[X]" # inversé: coché signifie visible
# Correction: on veut [X] si visible => is_hidden False
checkbox = "[X]" if not is_hidden else "[ ]"
@@ -2365,37 +2402,12 @@ def draw_filter_platforms_menu(screen):
scroll_y = int((config.filter_platforms_scroll_offset / max(1, total_items - visible_items)) * (list_height - 20 - scroll_height))
pygame.draw.rect(screen, THEME_COLORS["fond_lignes"], (list_x + list_width - 25, list_y + 10 + scroll_y, 10, scroll_height), border_radius=4)
# Boutons d'action
btn_width = 220
btn_height = int(config.screen_height * 0.0463)
spacing = 30
buttons_y = list_y + list_height + 20
center_x = config.screen_width // 2
actions = [
("filter_all", -2),
("filter_none", -3),
("filter_apply", -4),
("filter_back", -5)
]
# Indice spécial sélection boutons quand selected_filter_index >= total_items
extra_index_base = total_items
# Ajuster selected_filter_index max pour inclure boutons
extended_max = total_items + len(actions) - 1
if config.selected_filter_index > extended_max:
config.selected_filter_index = extended_max
for idx, (key, offset) in enumerate(actions):
btn_x = center_x - (len(actions) * (btn_width + spacing) - spacing) // 2 + idx * (btn_width + spacing)
is_selected = (config.selected_filter_index == total_items + idx)
label = _(key)
draw_stylized_button(screen, label, btn_x, buttons_y, btn_width, btn_height, selected=is_selected)
# Infos bas
hidden_count = sum(1 for _, h in config.filter_platforms_selection if h)
visible_count = total_items - hidden_count
info_text = _("filter_platforms_info").format(visible_count, hidden_count, total_items)
info_surface = config.small_font.render(info_text, True, THEME_COLORS["text"])
info_rect = info_surface.get_rect(center=(config.screen_width // 2, buttons_y + btn_height + 30))
info_rect = info_surface.get_rect(center=(config.screen_width // 2, list_y + list_height + 20))
screen.blit(info_surface, info_rect)
if config.filter_platforms_dirty:
@@ -3346,3 +3358,301 @@ def draw_scraper_screen(screen):
url_surface = config.small_font.render(url_text, True, THEME_COLORS["title_text"])
url_rect = url_surface.get_rect(center=(config.screen_width // 2, rect_y + rect_height - 20))
screen.blit(url_surface, url_rect)
def draw_filter_menu_choice(screen):
"""Affiche le menu de choix entre recherche par nom et filtrage avancé"""
screen.blit(OVERLAY, (0, 0))
# Titre
title = _("filter_menu_title")
title_surface = config.title_font.render(title, True, THEME_COLORS["text"])
title_rect = title_surface.get_rect(center=(config.screen_width // 2, 60))
screen.blit(title_surface, title_rect)
# Options
options = [
_("filter_search_by_name"),
_("filter_advanced")
]
# Calculer positions
menu_y = 150
button_height = 60
button_spacing = 20
button_width = 600
for i, option in enumerate(options):
y = menu_y + i * (button_height + button_spacing)
x = (config.screen_width - button_width) // 2
# Couleur selon sélection
if i == config.selected_filter_choice:
color = THEME_COLORS["button_selected"]
border_color = THEME_COLORS["border_selected"]
else:
color = THEME_COLORS["button_idle"]
border_color = THEME_COLORS["border"]
# Dessiner bouton
pygame.draw.rect(screen, color, (x, y, button_width, button_height), border_radius=12)
pygame.draw.rect(screen, border_color, (x, y, button_width, button_height), 3, border_radius=12)
# Texte
text_surface = config.font.render(option, True, THEME_COLORS["text"])
text_rect = text_surface.get_rect(center=(config.screen_width // 2, y + button_height // 2))
screen.blit(text_surface, text_rect)
def draw_filter_advanced(screen):
"""Affiche l'écran de filtrage avancé"""
from game_filters import GameFilters
screen.blit(OVERLAY, (0, 0))
# Titre
title = _("filter_advanced_title")
title_surface = config.title_font.render(title, True, THEME_COLORS["text"])
title_rect = title_surface.get_rect(center=(config.screen_width // 2, 40))
screen.blit(title_surface, title_rect)
# Initialiser le filtre si nécessaire
if not hasattr(config, 'game_filter_obj'):
config.game_filter_obj = GameFilters()
# Charger depuis settings
from rgsx_settings import load_game_filters
filter_dict = load_game_filters()
if filter_dict:
config.game_filter_obj.load_from_dict(filter_dict)
# Zones d'affichage
start_y = 100
line_height = 50
current_y = start_y
# Liste des options
options = []
# Section Régions
region_title = _("filter_region_title")
options.append(('header', region_title))
for region in GameFilters.REGIONS:
region_key = f"filter_region_{region.lower()}"
region_label = _(region_key)
filter_state = config.game_filter_obj.region_filters.get(region, 'include') # Par défaut: include
if filter_state == 'exclude':
status = f"[X] {_('filter_region_exclude')}"
color = THEME_COLORS["red"]
else: # 'include'
status = f"[V] {_('filter_region_include')}"
color = THEME_COLORS["green"]
options.append(('region', region, f"{region_label}: {status}", color))
# Section Autres options
options.append(('separator', ''))
options.append(('header', _("filter_other_options")))
hide_text = _("filter_hide_non_release")
hide_status = "[X]" if config.game_filter_obj.hide_non_release else "[ ]"
options.append(('toggle', 'hide_non_release', f"{hide_text}: {hide_status}"))
one_rom_text = _("filter_one_rom_per_game")
one_rom_status = "[X]" if config.game_filter_obj.one_rom_per_game else "[ ]"
# Afficher les 3 premières régions de priorité
priority_preview = "".join(config.game_filter_obj.region_priority[:3]) + "..."
options.append(('toggle', 'one_rom_per_game', f"{one_rom_text}: {one_rom_status}"))
options.append(('button_inline', 'priority_config', f"{_('filter_priority_order')}: {priority_preview}"))
# Boutons d'action (seront affichés séparément en bas)
buttons = [
('apply', _("filter_apply_filters")),
('reset', _("filter_reset_filters")),
('back', _("filter_back"))
]
# Afficher les options (sans les boutons)
if not hasattr(config, 'selected_filter_option'):
config.selected_filter_option = 0
# S'assurer que l'index est valide (options + 3 boutons)
total_items = len(options) + len(buttons)
if config.selected_filter_option >= total_items:
config.selected_filter_option = total_items - 1
for i, option in enumerate(options):
option_type = option[0]
if option_type == 'header':
# En-tête de section
text_surface = config.font.render(option[1], True, THEME_COLORS["title_text"])
screen.blit(text_surface, (100, current_y))
current_y += line_height
elif option_type == 'separator':
current_y += 10
elif option_type in ['region', 'toggle', 'button_inline']:
# Option sélectionnable
x = 120
width = config.screen_width - 240
height = 45
# Couleur selon sélection
if i == config.selected_filter_option:
bg_color = THEME_COLORS["button_selected"]
border_color = THEME_COLORS["border_selected"]
else:
bg_color = THEME_COLORS["button_idle"]
border_color = THEME_COLORS["border"]
# Dessiner fond
pygame.draw.rect(screen, bg_color, (x, current_y, width, height), border_radius=8)
pygame.draw.rect(screen, border_color, (x, current_y, width, height), 2, border_radius=8)
# Texte
if option_type == 'region':
text = option[2]
text_color = option[3]
else:
text = option[2]
text_color = THEME_COLORS["text"]
text_surface = config.font.render(text, True, text_color)
text_rect = text_surface.get_rect(left=x + 20, centery=current_y + height // 2)
screen.blit(text_surface, text_rect)
current_y += height + 10
# Afficher les 3 boutons côte à côte en bas
# Calculer la position pour éviter la barre de contrôles (hauteur estimée ~60-80px)
control_bar_estimated_height = 80
button_width = 200
button_height = 50
button_spacing = 20
total_buttons_width = button_width * 3 + button_spacing * 2
button_start_x = (config.screen_width - total_buttons_width) // 2
button_y = config.screen_height - control_bar_estimated_height - button_height - 20
for i, (button_id, button_text) in enumerate(buttons):
button_index = len(options) + i
button_x = button_start_x + i * (button_width + button_spacing)
# Couleur selon sélection
if button_index == config.selected_filter_option:
bg_color = THEME_COLORS["button_selected"]
border_color = THEME_COLORS["border_selected"]
else:
bg_color = THEME_COLORS["button_idle"]
border_color = THEME_COLORS["border"]
# Dessiner bouton
pygame.draw.rect(screen, bg_color, (button_x, button_y, button_width, button_height), border_radius=8)
pygame.draw.rect(screen, border_color, (button_x, button_y, button_width, button_height), 2, border_radius=8)
# Texte centré
text_surface = config.font.render(button_text, True, THEME_COLORS["text"])
text_rect = text_surface.get_rect(center=(button_x + button_width // 2, button_y + button_height // 2))
screen.blit(text_surface, text_rect)
# Info filtre actif (au-dessus des boutons)
if config.game_filter_obj.is_active():
info_text = _("filter_active")
info_surface = config.small_font.render(info_text, True, THEME_COLORS["green"])
info_rect = info_surface.get_rect(center=(config.screen_width // 2, button_y - 30))
screen.blit(info_surface, info_rect)
def draw_filter_priority_config(screen):
"""Affiche l'écran de configuration de la priorité des régions pour One ROM per game"""
from game_filters import GameFilters
screen.blit(OVERLAY, (0, 0))
# Titre
title = _("filter_priority_title")
title_surface = config.title_font.render(title, True, THEME_COLORS["text"])
title_rect = title_surface.get_rect(center=(config.screen_width // 2, 40))
screen.blit(title_surface, title_rect)
# Description
desc = _("filter_priority_desc")
desc_surface = config.small_font.render(desc, True, THEME_COLORS["title_text"])
desc_rect = desc_surface.get_rect(center=(config.screen_width // 2, 85))
screen.blit(desc_surface, desc_rect)
# Initialiser le filtre si nécessaire
if not hasattr(config, 'game_filter_obj'):
from game_filters import GameFilters
from rgsx_settings import load_game_filters
config.game_filter_obj = GameFilters()
filter_dict = load_game_filters()
if filter_dict:
config.game_filter_obj.load_from_dict(filter_dict)
# Liste des régions avec leur priorité
start_y = 130
line_height = 60
if not hasattr(config, 'selected_priority_index'):
config.selected_priority_index = 0
priority_list = config.game_filter_obj.region_priority.copy()
# Afficher chaque région avec sa position
for i, region in enumerate(priority_list):
y = start_y + i * line_height
x = 120
width = config.screen_width - 240
height = 50
# Couleur selon sélection
if i == config.selected_priority_index:
bg_color = THEME_COLORS["button_selected"]
border_color = THEME_COLORS["border_selected"]
else:
bg_color = THEME_COLORS["button_idle"]
border_color = THEME_COLORS["border"]
# Dessiner fond
pygame.draw.rect(screen, bg_color, (x, y, width, height), border_radius=8)
pygame.draw.rect(screen, border_color, (x, y, width, height), 2, border_radius=8)
# Numéro de priorité
priority_text = f"#{i+1}"
priority_surface = config.font.render(priority_text, True, THEME_COLORS["text"])
screen.blit(priority_surface, (x + 15, y + (height - priority_surface.get_height()) // 2))
# Nom de la région (traduit si possible)
region_key = f"filter_region_{region.lower()}"
region_label = _(region_key)
region_surface = config.font.render(region_label, True, THEME_COLORS["text"])
screen.blit(region_surface, (x + 80, y + (height - region_surface.get_height()) // 2))
# Flèches pour réorganiser (si sélectionné)
if i == config.selected_priority_index:
arrows_text = "← →"
arrows_surface = config.font.render(arrows_text, True, THEME_COLORS["green"])
screen.blit(arrows_surface, (x + width - 50, y + (height - arrows_surface.get_height()) // 2))
# Boutons en bas
control_bar_estimated_height = 80
button_width = 300
button_height = 50
button_x = (config.screen_width - button_width) // 2
button_y = config.screen_height - control_bar_estimated_height - button_height - 20
# Bouton Back
is_button_selected = config.selected_priority_index >= len(priority_list)
bg_color = THEME_COLORS["button_selected"] if is_button_selected else THEME_COLORS["button_idle"]
border_color = THEME_COLORS["border_selected"] if is_button_selected else THEME_COLORS["border"]
pygame.draw.rect(screen, bg_color, (button_x, button_y, button_width, button_height), border_radius=8)
pygame.draw.rect(screen, border_color, (button_x, button_y, button_width, button_height), 2, border_radius=8)
back_text = _("filter_back")
text_surface = config.font.render(back_text, True, THEME_COLORS["text"])
text_rect = text_surface.get_rect(center=(button_x + button_width // 2, button_y + button_height // 2))
screen.blit(text_surface, text_rect)

237
ports/RGSX/game_filters.py Normal file
View 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

View File

@@ -67,6 +67,28 @@ def load_language(lang_code=None):
return load_language(DEFAULT_LANGUAGE)
return False
def get_size_units():
"""Retourne les unités de taille adaptées à la langue courante.
Français utilise l'octet (o, Ko, Mo, Go, To, Po)
Autres langues utilisent byte (B, KB, MB, GB, TB, PB)
"""
if current_language == "fr":
return ['o', 'Ko', 'Mo', 'Go', 'To', 'Po']
else:
return ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
def get_speed_unit():
"""Retourne l'unité de vitesse adaptée à la langue courante.
Français utilise Mo/s
Autres langues utilisent MB/s
"""
if current_language == "fr":
return "Mo/s"
else:
return "MB/s"
def get_text(key, default=None):
"""Récupère la traduction correspondant à la clé en garantissant une chaîne.

View File

@@ -367,10 +367,44 @@
"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",
"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",
"web_downloading": "Download",
"web_in_progress": "In Bearbeitung",
"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",
"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",
"accessibility_footer_font_size": "Fußzeilen-Schriftgröße: {0}",
"popup_layout_changed_restart": "Layout geändert auf {0}x{1}. Bitte starten Sie die App neu."
"filter_active": "Filter aktiv",
"filter_games_shown": "{0} Spiel(e) angezeigt"
}

View File

@@ -367,10 +367,44 @@
"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."
"popup_layout_changed_restart": "Layout changed to {0}x{1}. Please restart the app to apply.",
"web_started": "Started",
"web_downloading": "Download",
"web_in_progress": "In progress",
"web_added_to_queue": "added to queue",
"web_download_success": "downloaded successfully!",
"web_download_error_for": "Error downloading",
"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"
}

View File

@@ -370,7 +370,41 @@
"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."
"popup_layout_changed_restart": "Diseño cambiado a {0}x{1}. Reinicie la app para aplicar.",
"web_started": "Iniciado",
"web_downloading": "Descarga",
"web_in_progress": "En curso",
"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",
"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)"
}

View File

@@ -370,7 +370,41 @@
"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."
"popup_layout_changed_restart": "Disposition changée en {0}x{1}. Veuillez redémarrer l'app pour appliquer.",
"web_started": "Démarré",
"web_downloading": "Téléchargement",
"web_in_progress": "En cours",
"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",
"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)"
}

View File

@@ -370,7 +370,41 @@
"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 in {0}x{1}. Riavviare l'app per applicare."
"popup_layout_changed_restart": "Layout cambiato a {0}x{1}. Riavvia l'app per applicare.",
"web_started": "Avviato",
"web_downloading": "Download",
"web_in_progress": "In corso",
"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",
"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"
}

View File

@@ -370,7 +370,41 @@
"filter_all": "Marcar tudo",
"filter_none": "Desmarcar tudo",
"filter_apply": "Aplicar filtro",
"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",
"web_downloading": "Download",
"web_in_progress": "Em andamento",
"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",
"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",
"accessibility_footer_font_size": "Tamanho fonte rodapé: {0}",
"popup_layout_changed_restart": "Layout alterado para {0}x{1}. Reinicie o app para aplicar."
"filter_active": "Filtro ativo",
"filter_games_shown": "{0} jogo(s) exibido(s)"
}

View File

@@ -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

View File

@@ -460,8 +460,13 @@ class RGSXHandler(BaseHTTPRequestHandler):
def _send_html(self, html, status=200, etag=None, last_modified=None):
"""Envoie une réponse HTML"""
self._set_headers('text/html; charset=utf-8', status, etag=etag, last_modified=last_modified)
self.wfile.write(html.encode('utf-8'))
try:
self._set_headers('text/html; charset=utf-8', status, etag=etag, last_modified=last_modified)
self.wfile.write(html.encode('utf-8'))
except (ConnectionAbortedError, BrokenPipeError) as e:
# La connexion a été fermée par le client, ce n'est pas une erreur critique
logger.debug(f"Connexion fermée par le client pendant l'envoi HTML: {e}")
pass
def _send_not_found(self):
"""Répond avec un 404 générique."""
@@ -703,10 +708,13 @@ class RGSXHandler(BaseHTTPRequestHandler):
# Route: API - Traductions
elif path == '/api/translations':
# Ajouter le code de langue dans les traductions pour que JS puisse l'utiliser
translations_with_lang = TRANSLATIONS.copy()
translations_with_lang['_language'] = get_language()
self._send_json({
'success': True,
'language': get_language(),
'translations': TRANSLATIONS
'translations': translations_with_lang
})
# Route: API - Liste des jeux d'une plateforme
@@ -1461,6 +1469,47 @@ class RGSXHandler(BaseHTTPRequestHandler):
'error': str(e)
}, status=500)
# Route: Sauvegarder seulement les filtres (sauvegarde rapide)
elif path == '/api/save_filters':
try:
from rgsx_settings import load_rgsx_settings, save_rgsx_settings
# Charger les settings actuels
current_settings = load_rgsx_settings()
# Mettre à jour seulement les filtres
if 'game_filters' not in current_settings:
current_settings['game_filters'] = {}
current_settings['game_filters']['region_filters'] = data.get('region_filters', {})
current_settings['game_filters']['hide_non_release'] = data.get('hide_non_release', False)
current_settings['game_filters']['one_rom_per_game'] = data.get('one_rom_per_game', False)
current_settings['game_filters']['regex_mode'] = data.get('regex_mode', False)
current_settings['game_filters']['region_priority'] = data.get('region_priority', ['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other'])
# Sauvegarder
save_rgsx_settings(current_settings)
# Mettre à jour config.game_filter_obj
if hasattr(config, 'game_filter_obj'):
config.game_filter_obj.region_filters = data.get('region_filters', {})
config.game_filter_obj.hide_non_release = data.get('hide_non_release', False)
config.game_filter_obj.one_rom_per_game = data.get('one_rom_per_game', False)
config.game_filter_obj.regex_mode = data.get('regex_mode', False)
config.game_filter_obj.region_priority = data.get('region_priority', ['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other'])
self._send_json({
'success': True,
'message': 'Filtres sauvegardés'
})
except Exception as e:
logger.error(f"Erreur lors de la sauvegarde des filtres: {e}")
self._send_json({
'success': False,
'error': str(e)
}, status=500)
# Route: Vider l'historique
elif path == '/api/clear-history':
try:

View File

@@ -401,6 +401,55 @@ header p { opacity: 0.9; font-size: 1.1em; }
}
}
/* Amélioration de la lisibilité des settings */
#settings-content label {
display: block;
color: #000;
font-weight: bold;
margin-bottom: 5px;
font-size: 1em;
}
#settings-content select,
#settings-content input[type="text"] {
width: 100%;
padding: 10px;
border: 2px solid #ccc;
border-radius: 5px;
font-size: 16px;
background-color: #f8f8f8;
color: #000;
transition: border-color 0.3s;
}
#settings-content select:focus,
#settings-content input[type="text"]:focus {
outline: none;
border-color: #667eea;
}
#settings-content input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
margin-right: 10px;
}
#settings-content label.checkbox-label {
display: flex;
align-items: center;
cursor: pointer;
}
#settings-content label.checkbox-label span {
font-weight: bold;
color: #000;
}
#settings-content button {
margin-top: 20px;
}
@media (max-width: 480px) {
header h1 {
font-size: 1.5em;

View File

@@ -135,6 +135,34 @@
return text;
}
// Fonction pour obtenir les unités de taille selon la langue
function getSizeUnits() {
// Détecter la langue depuis les traductions chargées ou le navigateur
const lang = translations['_language'] || navigator.language.substring(0, 2);
// Français utilise o, Ko, Mo, Go, To
// Autres langues utilisent B, KB, MB, GB, TB
return lang === 'fr' ? ['o', 'Ko', 'Mo', 'Go', 'To', 'Po'] : ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
}
// Fonction pour obtenir l'unité de vitesse selon la langue
function getSpeedUnit() {
const lang = translations['_language'] || navigator.language.substring(0, 2);
return lang === 'fr' ? 'Mo/s' : 'MB/s';
}
// Fonction pour formater une taille en octets
function formatSize(bytes) {
if (!bytes || bytes === 0) return 'N/A';
const units = getSizeUnits();
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(1)} ${units[unitIndex]}`;
}
// Appliquer les traductions à tous les éléments marqués
function applyTranslations() {
// Mettre à jour le titre de la page
@@ -281,6 +309,9 @@
// Restaurer l'état depuis l'URL au chargement
window.addEventListener('DOMContentLoaded', function() {
// Load saved filters first
loadSavedFilters();
const path = window.location.pathname;
if (path.startsWith('/platform/')) {
@@ -450,9 +481,130 @@
// Filter state: Map of region -> 'include' or 'exclude'
let regionFilters = new Map();
// Checkbox filter states (stored globally to restore after page changes)
let savedHideNonRelease = false;
let savedOneRomPerGame = false;
let savedRegexMode = false;
// Region priority order for "One ROM Per Game" (customizable)
let regionPriorityOrder = JSON.parse(localStorage.getItem('regionPriorityOrder')) ||
['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other'];
['USA', 'Canada', 'Europe', 'France', 'Germany', 'Japan', 'Korea', 'World', 'Other'];
// Save filters to backend
async function saveFiltersToBackend() {
try {
const regionFiltersObj = {};
regionFilters.forEach((mode, region) => {
regionFiltersObj[region] = mode;
});
// Update saved states from checkboxes if they exist
if (document.getElementById('hide-non-release')) {
savedHideNonRelease = document.getElementById('hide-non-release').checked;
}
if (document.getElementById('one-rom-per-game')) {
savedOneRomPerGame = document.getElementById('one-rom-per-game').checked;
}
if (document.getElementById('regex-mode')) {
savedRegexMode = document.getElementById('regex-mode').checked;
}
const response = await fetch('/api/save_filters', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
region_filters: regionFiltersObj,
hide_non_release: savedHideNonRelease,
one_rom_per_game: savedOneRomPerGame,
regex_mode: savedRegexMode,
region_priority: regionPriorityOrder
})
});
const data = await response.json();
if (!data.success) {
console.warn('Failed to save filters:', data.error);
}
} catch (error) {
console.warn('Failed to save filters:', error);
}
}
// Load saved filters from settings
async function loadSavedFilters() {
try {
const response = await fetch('/api/settings');
const data = await response.json();
if (data.success && data.settings.game_filters) {
const filters = data.settings.game_filters;
// Load region filters
if (filters.region_filters) {
regionFilters.clear();
Object.entries(filters.region_filters).forEach(([region, mode]) => {
regionFilters.set(region, mode);
});
}
// Load region priority
if (filters.region_priority) {
regionPriorityOrder = filters.region_priority;
localStorage.setItem('regionPriorityOrder', JSON.stringify(regionPriorityOrder));
}
// Save checkbox states to global variables
savedHideNonRelease = filters.hide_non_release || false;
savedOneRomPerGame = filters.one_rom_per_game || false;
savedRegexMode = filters.regex_mode || false;
// Load checkboxes when they exist (in games view)
if (document.getElementById('hide-non-release')) {
document.getElementById('hide-non-release').checked = savedHideNonRelease;
}
if (document.getElementById('one-rom-per-game')) {
document.getElementById('one-rom-per-game').checked = savedOneRomPerGame;
}
if (document.getElementById('regex-mode')) {
document.getElementById('regex-mode').checked = savedRegexMode;
}
}
} catch (error) {
console.warn('Failed to load saved filters:', error);
}
}
// Restore filter button states in the UI
function restoreFilterStates() {
// Restore region button states
regionFilters.forEach((mode, region) => {
const btn = document.querySelector(`.region-btn[data-region="${region}"]`);
if (btn) {
if (mode === 'include') {
btn.classList.add('active');
btn.classList.remove('excluded');
} else if (mode === 'exclude') {
btn.classList.remove('active');
btn.classList.add('excluded');
}
}
});
// Restore checkbox states
if (document.getElementById('hide-non-release')) {
document.getElementById('hide-non-release').checked = savedHideNonRelease;
}
if (document.getElementById('one-rom-per-game')) {
document.getElementById('one-rom-per-game').checked = savedOneRomPerGame;
}
if (document.getElementById('regex-mode')) {
document.getElementById('regex-mode').checked = savedRegexMode;
}
// Apply filters to display the games correctly
applyAllFilters();
}
// Helper: Extract region(s) from game name - returns array of regions
function getGameRegions(gameName) {
@@ -462,12 +614,16 @@
// Common region patterns - check all, not just first match
// Handle both "(USA)" and "(USA, Europe)" formats
if (name.includes('USA') || name.includes('US)')) regions.push('USA');
if (name.includes('CANADA')) regions.push('Canada');
if (name.includes('EUROPE') || name.includes('EU)')) regions.push('Europe');
if (name.includes('FRANCE') || name.includes('FR)')) regions.push('France');
if (name.includes('GERMANY') || name.includes('DE)')) regions.push('Germany');
if (name.includes('JAPAN') || name.includes('JP)') || name.includes('JPN)')) regions.push('Japan');
if (name.includes('KOREA') || name.includes('KR)')) regions.push('Korea');
if (name.includes('WORLD')) regions.push('World');
// Check for other regions
if (name.match(/\b(AUSTRALIA|ASIA|KOREA|BRAZIL|CHINA|RUSSIA|SCANDINAVIA|SPAIN|FRANCE|GERMANY|ITALY)\b/)) {
// Check for other regions (excluding the ones above)
if (name.match(/\b(AUSTRALIA|ASIA|BRAZIL|CHINA|RUSSIA|SCANDINAVIA|SPAIN|ITALY)\b/)) {
if (!regions.includes('Other')) regions.push('Other');
}
@@ -550,7 +706,10 @@
if (region === 'CANADA' && name.includes('CANADA')) return i;
if (region === 'WORLD' && name.includes('WORLD')) return i;
if (region === 'EUROPE' && (name.includes('EUROPE') || name.includes('EU)'))) return i;
if (region === 'FRANCE' && (name.includes('FRANCE') || name.includes('FR)'))) return i;
if (region === 'GERMANY' && (name.includes('GERMANY') || name.includes('DE)'))) return i;
if (region === 'JAPAN' && (name.includes('JAPAN') || name.includes('JP)') || name.includes('JPN)'))) return i;
if (region === 'KOREA' && (name.includes('KOREA') || name.includes('KR)'))) return i;
}
return regionPriorityOrder.length; // Other regions (lowest priority)
@@ -578,6 +737,7 @@
[regionPriorityOrder[idx-1], regionPriorityOrder[idx]];
saveRegionPriorityOrder();
renderRegionPriorityConfig();
saveFiltersToBackend();
}
}
@@ -589,14 +749,16 @@
[regionPriorityOrder[idx+1], regionPriorityOrder[idx]];
saveRegionPriorityOrder();
renderRegionPriorityConfig();
saveFiltersToBackend();
}
}
// Reset region priority to default
function resetRegionPriority() {
regionPriorityOrder = ['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other'];
regionPriorityOrder = ['USA', 'Canada', 'Europe', 'France', 'Germany', 'Japan', 'Korea', 'World', 'Other'];
saveRegionPriorityOrder();
renderRegionPriorityConfig();
saveFiltersToBackend();
}
// Render region priority configuration UI
@@ -613,11 +775,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>
`;
});
@@ -678,14 +840,15 @@
}
applyAllFilters();
saveFiltersToBackend();
}
// Apply all filters
function applyAllFilters() {
const searchInput = document.getElementById('game-search');
const searchTerm = searchInput ? searchInput.value : '';
const hideNonRelease = document.getElementById('hide-non-release')?.checked || false;
const regexMode = document.getElementById('regex-mode')?.checked || false;
const hideNonRelease = document.getElementById('hide-non-release')?.checked || savedHideNonRelease;
const regexMode = document.getElementById('regex-mode')?.checked || savedRegexMode;
const items = document.querySelectorAll('.game-item');
let visibleCount = 0;
@@ -776,7 +939,7 @@
});
// Apply one-rom-per-game filter (after other filters)
const oneRomPerGame = document.getElementById('one-rom-per-game')?.checked || false;
const oneRomPerGame = document.getElementById('one-rom-per-game')?.checked || savedOneRomPerGame;
if (oneRomPerGame) {
// Group currently visible games by base name
const gameGroups = new Map();
@@ -1004,22 +1167,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>
@@ -1054,6 +1221,9 @@
`;
container.innerHTML = html;
// Restore filter states from loaded settings
restoreFilterStates();
// Appliquer le tri par défaut (A-Z)
sortGames(currentGameSort);
@@ -1102,8 +1272,8 @@
// Afficher un toast de succès (pas de redirection de page)
const toastMsg = mode === 'queue'
? `📋 "${gameName}" ajouté à la queue`
: `⬇️ Téléchargement de "${gameName}" lancé`;
? `📋 "${gameName}" ${t('web_added_to_queue')}`
: `⬇️ ${t('web_downloading')}: "${gameName}"`;
showToast(toastMsg, 'success', 3000);
} else {
@@ -1198,7 +1368,7 @@
const speed = info.speed || 0;
// Utiliser game_name si disponible, sinon extraire de l'URL
let fileName = info.game_name || 'Téléchargement';
let fileName = info.game_name || t('web_downloading');
if (!info.game_name) {
try {
fileName = decodeURIComponent(url.split('/').pop());
@@ -1224,11 +1394,11 @@
</div>
<div style="display: flex; justify-content: space-between; margin-top: 5px; font-size: 0.9em;">
<span>${status} - ${percent.toFixed(1)}%</span>
<span>${speed > 0 ? speed.toFixed(2) + ' Mo/s' : ''}</span>
<span>${speed > 0 ? speed.toFixed(2) + ' ' + getSpeedUnit() : ''}</span>
</div>
${total > 0 ? `<div style="font-size: 0.85em; color: #666;">${(downloaded / 1024 / 1024).toFixed(1)} Mo / ${(total / 1024 / 1024).toFixed(1)} Mo</div>` : ''}
${total > 0 ? `<div style="font-size: 0.85em; color: #666;">${formatSize(downloaded)} / ${formatSize(total)}</div>` : ''}
<div style="margin-top: 3px; font-size: 0.85em; color: #666;">
📅 Démarré: ${info.timestamp || 'N/A'}
📅 ${t('web_started')}: ${info.timestamp || 'N/A'}
</div>
</div>
</div>
@@ -1264,10 +1434,10 @@
const percent = info.progress_percent || 0;
const downloaded = info.downloaded_size || 0;
const total = info.total_size || 0;
const status = info.status || 'En cours';
const status = info.status || t('web_in_progress');
const speed = info.speed || 0;
let fileName = info.game_name || 'Téléchargement';
let fileName = info.game_name || t('web_downloading');
if (!info.game_name) {
try {
fileName = decodeURIComponent(url.split('/').pop());
@@ -1292,11 +1462,11 @@
</div>
<div style="display: flex; justify-content: space-between; margin-top: 5px; font-size: 0.9em;">
<span>${status} - ${percent.toFixed(1)}%</span>
<span>${speed > 0 ? speed.toFixed(2) + ' Mo/s' : ''}</span>
<span>${speed > 0 ? speed.toFixed(2) + ' ' + getSpeedUnit() : ''}</span>
</div>
${total > 0 ? `<div style="font-size: 0.85em; color: #666;">${(downloaded / 1024 / 1024).toFixed(1)} Mo / ${(total / 1024 / 1024).toFixed(1)} Mo</div>` : ''}
${total > 0 ? `<div style="font-size: 0.85em; color: #666;">${formatSize(downloaded)} / ${formatSize(total)}</div>` : ''}
<div style="margin-top: 3px; font-size: 0.85em; color: #666;">
📅 Démarré: ${info.timestamp || 'N/A'}
📅 ${t('web_started')}: ${info.timestamp || 'N/A'}
</div>
</div>
</div>
@@ -1446,13 +1616,13 @@
// Si ce téléchargement n'était pas tracké et il est maintenant complété/erreur/etc
if (!trackedDownloads[gameKey]) {
if (status === 'Download_OK' || status === 'Completed') {
showToast(`✅ "${entry.game_name}" téléchargé avec succès!`, 'success', 4000);
showToast(`✅ "${entry.game_name}" ${t('web_download_success')}`, 'success', 4000);
trackedDownloads[gameKey] = 'completed';
} else if (status === 'Erreur' || status === 'error') {
showToast(`Erreur lors du téléchargement de "${entry.game_name}"`, 'error', 5000);
showToast(`${t('web_download_error_for')} "${entry.game_name}"`, 'error', 5000);
trackedDownloads[gameKey] = 'error';
} else if (status === 'Already_Present') {
showToast(` "${entry.game_name}" était déjà présent`, 'info', 3000);
showToast(` "${entry.game_name}" ${t('web_already_present')}`, 'info', 3000);
trackedDownloads[gameKey] = 'already_present';
} else if (status === 'Canceled') {
// Ne pas afficher de toast pour les téléchargements annulés
@@ -1510,8 +1680,8 @@
const isCanceled = status === 'Canceled';
const isAlreadyPresent = status === 'Already_Present';
const isQueued = status === 'Queued';
const isDownloading = status === 'Downloading' || status === 'Téléchargement' || status === 'Downloading' ||
status === 'Connecting' || status === 'Extracting' || status.startsWith('Try ');
const isDownloading = status === 'Downloading' || status === 'Connecting' ||
status === 'Extracting' || status.startsWith('Try ');
const isSuccess = status === 'Download_OK' || status === 'Completed';
// Déterminer l'icône et la couleur
@@ -1541,7 +1711,7 @@
statusText = statusDownloading;
}
const totalMo = h.total_size ? (h.total_size / 1024 / 1024).toFixed(1) : 'N/A';
const sizeFormatted = h.total_size ? formatSize(h.total_size) : 'N/A';
const platform = h.platform || 'N/A';
const timestamp = h.timestamp || 'N/A';
@@ -1559,7 +1729,7 @@
📦 ${platformLabel}: ${platform}
</div>
<div style="margin-top: 3px; font-size: 0.85em; color: #666;">
💾 ${sizeLabel}: ${totalMo} Mo
💾 ${sizeLabel}: ${sizeFormatted}
</div>
<div style="margin-top: 3px; font-size: 0.85em; color: #666;">
📅 Date: ${timestamp}
@@ -1744,12 +1914,12 @@
<h3 style="margin-top: 30px; margin-bottom: 15px;">RGSX Configuration ⚙️</h3>
<div style="margin-bottom: 20px; background: #f0f8ff; padding: 15px; border-radius: 8px; border: 2px solid #007bff;">
<label style="display: block; font-weight: bold; margin-bottom: 10px; font-size: 1.1em;">📁 ${t('web_settings_roms_folder')}</label>
<label style="display: block; margin-bottom: 10px; font-size: 1.1em;">📁 ${t('web_settings_roms_folder')}</label>
<div style="display: flex; gap: 10px; margin-bottom: 8px; flex-wrap: wrap;">
<input type="text" id="setting-roms-folder" value="${settings.roms_folder || ''}"
data-translate-placeholder="web_settings_roms_placeholder"
placeholder="${t('web_settings_roms_placeholder')}"
style="flex: 1; min-width: 200px; padding: 10px; border: 2px solid #ddd; border-radius: 5px; font-size: 16px;">
style="flex: 1; min-width: 200px;">
<button onclick="browseRomsFolder()"
style="background: linear-gradient(135deg, #007bff 0%, #0056b3 100%); color: white; border: none; padding: 10px 20px; border-radius: 5px; font-weight: bold; cursor: pointer; white-space: nowrap; flex-shrink: 0;">
📂 ${t('web_settings_browse')}
@@ -1764,8 +1934,8 @@
<div style="background: #f9f9f9; padding: 20px; border-radius: 8px;">
<div style="margin-bottom: 20px;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">🌍 ${t('web_settings_language')}</label>
<select id="setting-language" style="width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 5px; font-size: 16px;">
<label>🌍 ${t('web_settings_language')}</label>
<select id="setting-language">
<option value="en" ${settings.language === 'en' ? 'selected' : ''}>English</option>
<option value="fr" ${settings.language === 'fr' ? 'selected' : ''}>Français</option>
<option value="es" ${settings.language === 'es' ? 'selected' : ''}>Español</option>
@@ -1776,23 +1946,22 @@
</div>
<div style="margin-bottom: 20px;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="setting-music" ${settings.music_enabled ? 'checked' : ''}
style="width: 20px; height: 20px; margin-right: 10px;">
<span style="font-weight: bold;">🎵 ${t('web_settings_music')}</span>
<label class="checkbox-label">
<input type="checkbox" id="setting-music" ${settings.music_enabled ? 'checked' : ''}>
<span>🎵 ${t('web_settings_music')}</span>
</label>
</div>
<div style="margin-bottom: 20px;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">🔤 ${t('web_settings_font_scale')} (${settings.accessibility?.font_scale || 1.0})</label>
<label>🔤 ${t('web_settings_font_scale')} (${settings.accessibility?.font_scale || 1.0})</label>
<input type="range" id="setting-font-scale" min="0.5" max="2.0" step="0.1"
value="${settings.accessibility?.font_scale || 1.0}"
style="width: 100%;">
</div>
<div style="margin-bottom: 20px;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">📐 ${t('web_settings_grid')}</label>
<select id="setting-grid" style="width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 5px; font-size: 16px;">
<label>📐 ${t('web_settings_grid')}</label>
<select id="setting-grid">
<option value="3x3" ${settings.display?.grid === '3x3' ? 'selected' : ''}>3x3</option>
<option value="3x4" ${settings.display?.grid === '3x4' ? 'selected' : ''}>3x4</option>
<option value="4x3" ${settings.display?.grid === '4x3' ? 'selected' : ''}>4x3</option>
@@ -1801,54 +1970,50 @@
</div>
<div style="margin-bottom: 20px;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">🖋️ ${t('web_settings_font_family')}</label>
<select id="setting-font-family" style="width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 5px; font-size: 16px;">
<label>🖋️ ${t('web_settings_font_family')}</label>
<select id="setting-font-family">
<option value="pixel" ${settings.display?.font_family === 'pixel' ? 'selected' : ''}>Pixel</option>
<option value="dejavu" ${settings.display?.font_family === 'dejavu' ? 'selected' : ''}>DejaVu</option>
</select>
</div>
<div style="margin-bottom: 20px;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="setting-symlink" ${settings.symlink?.enabled ? 'checked' : ''}
style="width: 20px; height: 20px; margin-right: 10px;">
<span style="font-weight: bold;">🔗 ${t('web_settings_symlink')}</span>
<label class="checkbox-label">
<input type="checkbox" id="setting-symlink" ${settings.symlink?.enabled ? 'checked' : ''}>
<span>🔗 ${t('web_settings_symlink')}</span>
</label>
</div>
<div style="margin-bottom: 20px;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">📦 ${t('web_settings_source_mode')}</label>
<select id="setting-sources-mode" style="width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 5px; font-size: 16px;">
<label>📦 ${t('web_settings_source_mode')}</label>
<select id="setting-sources-mode">
<option value="rgsx" ${settings.sources?.mode === 'rgsx' ? 'selected' : ''}>RGSX (default)</option>
<option value="custom" ${settings.sources?.mode === 'custom' ? 'selected' : ''}>Custom</option>
</select>
</div>
<div style="margin-bottom: 20px;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">🔗 ${t('web_settings_custom_url')}</label>
<label>🔗 ${t('web_settings_custom_url')}</label>
<input type="text" id="setting-custom-url" value="${settings.sources?.custom_url || ''}"
data-translate-placeholder="web_settings_custom_url_placeholder"
placeholder="${t('web_settings_custom_url_placeholder')}"
style="width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 5px; font-size: 16px;">
placeholder="${t('web_settings_custom_url_placeholder')}">
</div>
<div style="margin-bottom: 20px;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="setting-show-unsupported" ${settings.show_unsupported_platforms ? 'checked' : ''}
style="width: 20px; height: 20px; margin-right: 10px;">
<span style="font-weight: bold;">👀 ${showUnsupportedLabel}</span>
<label class="checkbox-label">
<input type="checkbox" id="setting-show-unsupported" ${settings.show_unsupported_platforms ? 'checked' : ''}>
<span>👀 ${showUnsupportedLabel}</span>
</label>
</div>
<div style="margin-bottom: 20px;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="setting-allow-unknown" ${settings.allow_unknown_extensions ? 'checked' : ''}
style="width: 20px; height: 20px; margin-right: 10px;">
<span style="font-weight: bold;">⚠️ ${allowUnknownLabel}</span>
<label class="checkbox-label">
<input type="checkbox" id="setting-allow-unknown" ${settings.allow_unknown_extensions ? 'checked' : ''}>
<span>⚠️ ${allowUnknownLabel}</span>
</label>
</div>
<button onclick="saveSettings()" style="width: 100%; background: linear-gradient(135deg, #28a745 0%, #20c997 100%); color: white; border: none; padding: 15px; border-radius: 8px; font-size: 18px; font-weight: bold; cursor: pointer; margin-top: 10px;">
<button id="save-settings-btn" style="width: 100%; background: linear-gradient(135deg, #28a745 0%, #20c997 100%); color: white; border: none; padding: 15px; border-radius: 8px; font-size: 18px; font-weight: bold; cursor: pointer; margin-top: 10px;">
💾 ${t('web_settings_save')}
</button>
</div>
@@ -1860,14 +2025,31 @@
label.textContent = `🔤 ${t('web_settings_font_scale')} (${e.target.value})`;
});
// Attacher l'événement de sauvegarde au bouton
document.getElementById('save-settings-btn').addEventListener('click', saveSettings);
} catch (error) {
container.innerHTML = `<p style="color:red;">${t('web_error')}: ${error.message}</p>`;
}
}
// Sauvegarder les settings
async function saveSettings() {
async function saveSettings(event) {
// Désactiver le bouton pendant la sauvegarde
const saveButton = event?.target;
const originalText = saveButton?.textContent;
if (saveButton) {
saveButton.disabled = true;
saveButton.textContent = '⏳ Saving...';
}
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,
@@ -1887,7 +2069,14 @@
},
show_unsupported_platforms: document.getElementById('setting-show-unsupported').checked,
allow_unknown_extensions: document.getElementById('setting-allow-unknown').checked,
roms_folder: document.getElementById('setting-roms-folder').value.trim()
roms_folder: document.getElementById('setting-roms-folder').value.trim(),
game_filters: {
region_filters: regionFiltersObj,
hide_non_release: document.getElementById('hide-non-release')?.checked || savedHideNonRelease,
one_rom_per_game: document.getElementById('one-rom-per-game')?.checked || savedOneRomPerGame,
regex_mode: document.getElementById('regex-mode')?.checked || savedRegexMode,
region_priority: regionPriority
}
};
const response = await fetch('/api/settings', {
@@ -1899,13 +2088,23 @@
const data = await response.json();
if (data.success) {
// Réactiver le bouton
if (saveButton) {
saveButton.disabled = false;
saveButton.textContent = originalText;
}
// Afficher le dialogue de confirmation de redémarrage
showRestartDialog();
} else {
throw new Error(data.error || t('web_error_unknown'));
}
} catch (error) {
alert('❌ ' + t('web_error_save_settings', error.message));
// Réactiver le bouton en cas d'erreur
if (saveButton) {
saveButton.disabled = false;
saveButton.textContent = originalText;
}
alert('❌ ' + t('web_error_save_settings') + ': ' + error.message);
}
}

View File

@@ -1,3 +1,3 @@
{
"version": "2.3.2.4"
"version": "2.3.2.7"
}