Compare commits

..

2 Commits

Author SHA1 Message Date
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
14 changed files with 353 additions and 167 deletions

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.6"
def get_application_root():

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
@@ -1451,7 +1451,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 +1459,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 +1474,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 +1499,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 +1546,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 +1556,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 +1569,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 +1577,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 +1604,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 +1655,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 +1732,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)]
@@ -1919,44 +1923,53 @@ def handle_controls(event, sources, joystick, screen):
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 +2015,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__)
@@ -956,15 +956,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 +992,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 +1037,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 +2015,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 +2322,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 +2364,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 +2390,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:

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

@@ -371,6 +371,13 @@
"filter_none": "Alles abwählen",
"filter_apply": "Filter anwenden",
"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."
"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"
}

View File

@@ -372,5 +372,12 @@
"filter_apply": "Apply Filter",
"filter_back": "Back",
"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"
}

View File

@@ -372,5 +372,12 @@
"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"
}

View File

@@ -372,5 +372,12 @@
"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"
}

View File

@@ -372,5 +372,12 @@
"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"
}

View File

@@ -371,6 +371,13 @@
"filter_none": "Desmarcar tudo",
"filter_apply": "Aplicar filtro",
"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."
"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"
}

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

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
@@ -1102,8 +1130,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 +1226,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 +1252,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 +1292,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 +1320,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 +1474,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 +1538,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 +1569,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 +1587,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 +1772,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 +1792,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 +1804,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 +1828,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,13 +1883,24 @@
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 {
const settings = {
language: document.getElementById('setting-language').value,
@@ -1899,13 +1933,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.6"
}