Compare commits

...

1 Commits

Author SHA1 Message Date
skymike03
fbb1a2aa68 v2.5.0.7 (2026.03.16)
• improve filters/search performance with lazy cache without slowing unfiltered game list access
• fix fbneo logging traceback and avoid re-downloading/parsing fbneo gamelist when cache is already available
• add rom scan to rebuild downloaded games database from local files with extension-insensitive matching and backward compatibility
• fix clear history to remove stale converting entries and keep only real active transfers
• fix keyboard mode controls display to ignore joystick-only mappings and restore proper keyboard labels
• update displayed keyboard labels with ascii-safe names (Esc/Echap, Debut, Verr Def)
• move version/controller info to a top-right header badge and add page number badge on top-left
• simplify platform footer and loading footer layout
• add global search from platform menu across all available systems
• fix global search input handling and routing
• update global search confirm action to download directly and allow queue action from results
• add size and ext columns to global search results
• add ext column to game list and history
• add folder column to history to show download destination folder
• fix return from history to restore current game list instead of jumping back to platform list
• fix history crash caused by ext column text truncation
2026-03-16 19:09:47 +01:00
17 changed files with 1064 additions and 128 deletions

View File

@@ -28,6 +28,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_global_search_list,
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,
@@ -661,6 +662,7 @@ async def main():
# Basculer sur les contrôles clavier
config.joystick = False
config.keyboard = True
config.controller_device_name = ""
# Recharger la configuration des contrôles pour le clavier
config.controls_config = load_controls_config()
logger.info("Contrôles clavier chargés")
@@ -669,6 +671,7 @@ async def main():
# Basculer sur les contrôles clavier
config.joystick = False
config.keyboard = True
config.controller_device_name = ""
# Recharger la configuration des contrôles pour le clavier
config.controls_config = load_controls_config()
logger.info("Contrôles clavier chargés")
@@ -729,6 +732,7 @@ async def main():
"filter_menu_choice",
"filter_advanced",
"filter_priority_config",
"platform_search",
}
if config.menu_state in SIMPLE_HANDLE_STATES:
action = handle_controls(event, sources, joystick, screen)
@@ -1116,6 +1120,10 @@ async def main():
draw_game_list(screen)
if getattr(config, 'joystick', False):
draw_virtual_keyboard(screen)
elif config.menu_state == "platform_search":
draw_global_search_list(screen)
if getattr(config, 'joystick', False) and getattr(config, 'global_search_editing', False):
draw_virtual_keyboard(screen)
elif config.menu_state == "download_progress":
draw_progress_screen(screen)
# État download_result supprimé

View File

@@ -7,7 +7,7 @@
"cancel": {
"type": "key",
"key": 27,
"display": "\u00c9chap"
"display": "Esc/Echap"
},
"up": {
"type": "key",

View File

@@ -11,6 +11,9 @@ class Game:
url: str
size: str
display_name: str # name withou file extension or platform prefix
regions: Optional[list[str]] = None
is_non_release: Optional[bool] = None
base_name: Optional[str] = None
# Headless mode for CLI: set env RGSX_HEADLESS=1 to avoid pygame and noisy prints
HEADLESS = os.environ.get("RGSX_HEADLESS") == "1"
@@ -23,7 +26,7 @@ except Exception:
pygame = None # type: ignore
# Version actuelle de l'application
app_version = "2.5.0.6"
app_version = "2.6.0.0"
# Nombre de jours avant de proposer la mise à jour de la liste des jeux
GAMELIST_UPDATE_DAYS = 7
@@ -403,6 +406,12 @@ filtered_games: list[Game] = [] # Liste des jeux filtrés par recherche ou filt
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é
global_search_index = [] # Index de recherche global {platform, jeu} construit a l'ouverture
global_search_results = [] # Resultats de la recherche globale inter-plateformes
global_search_query = "" # Texte saisi pour la recherche globale
global_search_selected = 0 # Index du resultat global selectionne
global_search_scroll_offset = 0 # Offset de defilement des resultats globaux
global_search_editing = False # True si le clavier virtuel est actif pour la recherche globale
# Variables pour le filtrage avancé
selected_filter_choice = 0 # Index dans le menu de choix de filtrage (recherche / avancé)

View File

@@ -20,7 +20,7 @@ from utils import (
ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string,
start_connection_status_check
)
from history import load_history, clear_history, add_to_history, save_history
from history import load_history, clear_history, add_to_history, save_history, scan_roms_for_downloaded_games
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,
@@ -72,6 +72,7 @@ VALID_STATES = [
"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
"platform_search", # recherche globale inter-plateformes
"platform_folder_config", # configuration du dossier personnalisé pour une plateforme
"folder_browser", # navigateur de dossiers intégré
"folder_browser_new_folder", # création d'un nouveau dossier
@@ -109,6 +110,18 @@ def load_controls_config(path=CONTROLS_CONFIG_PATH):
"delete": {"type": "key", "key": pygame.K_BACKSPACE},
"space": {"type": "key", "key": pygame.K_SPACE}
}
def _is_keyboard_only_config(data):
if not isinstance(data, dict) or not data:
return False
for action_name, mapping in data.items():
if action_name == "device":
continue
if not isinstance(mapping, dict):
return False
if mapping.get("type") != "key":
return False
return True
try:
# 1) Fichier utilisateur
@@ -117,21 +130,25 @@ def load_controls_config(path=CONTROLS_CONFIG_PATH):
data = json.load(f)
if not isinstance(data, dict):
data = {}
keyboard_mode = (not getattr(config, 'joystick', False)) or getattr(config, 'keyboard', False)
if keyboard_mode and not _is_keyboard_only_config(data):
logging.getLogger(__name__).info("Configuration utilisateur manette ignorée en mode clavier")
else:
# Compléter les actions manquantes, et sauve seulement si le fichier utilisateur existe
changed = False
for k, v in default_config.items():
if k not in data:
data[k] = v
changed = True
if changed:
try:
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
logging.getLogger(__name__).debug(f"controls.json complété avec les actions manquantes: {path}")
except Exception as e:
logging.getLogger(__name__).warning(f"Impossible d'écrire les actions manquantes dans {path}: {e}")
return data
changed = False
for k, v in default_config.items():
if k not in data:
data[k] = v
changed = True
if changed:
try:
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
logging.getLogger(__name__).debug(f"controls.json complété avec les actions manquantes: {path}")
except Exception as e:
logging.getLogger(__name__).warning(f"Impossible d'écrire les actions manquantes dans {path}: {e}")
return data
# 2) Préréglages sans copie si aucun fichier utilisateur
try:
@@ -260,6 +277,67 @@ def is_input_matched(event, action_name):
return False
def is_global_search_input_matched(event, action_name):
"""Fallback robuste pour la recherche globale, independant du preset courant."""
if is_input_matched(event, action_name):
return True
if event.type == pygame.KEYDOWN:
keyboard_fallback = {
"up": pygame.K_UP,
"down": pygame.K_DOWN,
"left": pygame.K_LEFT,
"right": pygame.K_RIGHT,
"confirm": pygame.K_RETURN,
"cancel": pygame.K_ESCAPE,
"filter": pygame.K_f,
"delete": pygame.K_BACKSPACE,
"space": pygame.K_SPACE,
"page_up": pygame.K_PAGEUP,
"page_down": pygame.K_PAGEDOWN,
}
if action_name in keyboard_fallback and event.key == keyboard_fallback[action_name]:
return True
if event.type == pygame.JOYBUTTONDOWN:
common_button_fallback = {
"confirm": {0},
"cancel": {1},
"filter": {6},
"start": {7},
"delete": {2},
"space": {5},
"page_up": {4},
"page_down": {5},
}
if action_name in common_button_fallback and event.button in common_button_fallback[action_name]:
return True
if event.type == pygame.JOYHATMOTION:
hat_fallback = {
"up": (0, 1),
"down": (0, -1),
"left": (-1, 0),
"right": (1, 0),
}
if action_name in hat_fallback and event.value == hat_fallback[action_name]:
return True
if event.type == pygame.JOYAXISMOTION:
axis_fallback = {
"left": (0, -1),
"right": (0, 1),
"up": (1, -1),
"down": (1, 1),
}
if action_name in axis_fallback:
axis_id, direction = axis_fallback[action_name]
if event.axis == axis_id and abs(event.value) > 0.5 and (1 if event.value > 0 else -1) == direction:
return True
return False
def _launch_next_queued_download():
"""Lance le prochain téléchargement de la queue si aucun n'est actif.
Gère la liaison entre le système Desktop et le système de download_rom/download_from_1fichier.
@@ -340,6 +418,217 @@ def filter_games_by_search_query() -> list[Game]:
filtered_games.append(game)
return filtered_games
GLOBAL_SEARCH_KEYBOARD_LAYOUT = [
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
['A', 'Z', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'],
['Q', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M'],
['W', 'X', 'C', 'V', 'B', 'N']
]
def _get_platform_id(platform) -> str:
return platform.get("name") if isinstance(platform, dict) else str(platform)
def _get_platform_label(platform_id: str) -> str:
return config.platform_names.get(platform_id, platform_id)
def build_global_search_index() -> list[dict]:
indexed_games = []
for platform_index, platform in enumerate(config.platforms):
platform_id = _get_platform_id(platform)
platform_label = _get_platform_label(platform_id)
for game in load_games(platform_id):
indexed_games.append({
"platform_id": platform_id,
"platform_label": platform_label,
"platform_index": platform_index,
"game_name": game.name,
"display_name": game.display_name or Path(game.name).stem,
"url": game.url,
"size": game.size,
})
indexed_games.sort(key=lambda item: (item["platform_label"].lower(), item["display_name"].lower()))
return indexed_games
def refresh_global_search_results(reset_selection: bool = True) -> None:
query = (config.global_search_query or "").strip().lower()
if not query:
config.global_search_results = []
else:
config.global_search_results = [
item for item in config.global_search_index
if query in item["display_name"].lower()
]
if reset_selection:
config.global_search_selected = 0
config.global_search_scroll_offset = 0
else:
max_index = max(0, len(config.global_search_results) - 1)
config.global_search_selected = max(0, min(config.global_search_selected, max_index))
config.global_search_scroll_offset = max(0, min(config.global_search_scroll_offset, config.global_search_selected))
def enter_global_search() -> None:
config.global_search_index = build_global_search_index()
config.global_search_query = ""
config.global_search_results = []
config.global_search_selected = 0
config.global_search_scroll_offset = 0
config.global_search_editing = bool(getattr(config, 'joystick', False))
config.selected_key = (0, 0)
config.previous_menu_state = "platform"
config.menu_state = "platform_search"
config.needs_redraw = True
logger.debug("Entree en recherche globale inter-plateformes")
def exit_global_search() -> None:
config.global_search_query = ""
config.global_search_results = []
config.global_search_selected = 0
config.global_search_scroll_offset = 0
config.global_search_editing = False
config.selected_key = (0, 0)
config.menu_state = "platform"
config.needs_redraw = True
def open_global_search_result(screen) -> None:
if not config.global_search_results:
return
result = config.global_search_results[config.global_search_selected]
platform_index = result.get("platform_index", 0)
if platform_index < 0 or platform_index >= len(config.platforms):
return
config.current_platform = platform_index
config.selected_platform = platform_index
config.current_page = platform_index // max(1, config.GRID_COLS * config.GRID_ROWS)
platform_id = result["platform_id"]
config.games = load_games(platform_id)
config.filtered_games = config.games
config.search_mode = False
config.search_query = ""
config.filter_active = False
target_name = result["game_name"]
target_display_name = result["display_name"]
target_index = 0
for index, game in enumerate(config.games):
if game.name == target_name:
target_index = index
break
if game.display_name == target_display_name:
target_index = index
config.current_game = target_index
config.scroll_offset = 0
config.global_search_editing = False
from rgsx_settings import get_light_mode
if not get_light_mode():
draw_validation_transition(screen, config.current_platform)
config.menu_state = "game"
config.needs_redraw = True
logger.debug(f"Ouverture du resultat global {target_display_name} sur {platform_id}")
def trigger_global_search_download(queue_only: bool = False) -> None:
if not config.global_search_results:
return
result = config.global_search_results[config.global_search_selected]
url = result.get("url")
platform = result.get("platform_id")
game_name = result.get("game_name")
display_name = result.get("display_name") or game_name
if not url or not platform or not game_name:
logger.error(f"Resultat de recherche globale invalide: {result}")
return
pending_download = check_extension_before_download(url, platform, game_name)
if not pending_download:
logger.error(f"config.pending_download est None pour {game_name}")
config.needs_redraw = True
return
is_supported = is_extension_supported(
sanitize_filename(game_name),
platform,
load_extensions_json()
)
zip_ok = bool(pending_download[3])
allow_unknown = get_allow_unknown_extensions()
if (not is_supported and not zip_ok) and not allow_unknown:
config.pending_download = pending_download
config.pending_download_is_queue = queue_only
config.previous_menu_state = config.menu_state
config.menu_state = "extension_warning"
config.extension_confirm_selection = 0
config.needs_redraw = True
logger.debug(f"Extension non supportee, passage a extension_warning pour {game_name}")
return
if queue_only:
task_id = str(pygame.time.get_ticks())
queue_item = {
'url': url,
'platform': platform,
'game_name': game_name,
'is_zip_non_supported': pending_download[3],
'is_1fichier': is_1fichier_url(url),
'task_id': task_id,
'status': 'Queued'
}
config.download_queue.append(queue_item)
config.history.append({
'platform': platform,
'game_name': game_name,
'status': 'Queued',
'url': url,
'progress': 0,
'message': _("download_queued"),
'timestamp': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
'downloaded_size': 0,
'total_size': 0,
'task_id': task_id
})
save_history(config.history)
show_toast(f"{display_name}\n{_('download_queued')}")
config.needs_redraw = True
logger.debug(f"{game_name} ajoute a la file d'attente depuis la recherche globale. Queue size: {len(config.download_queue)}")
if not config.download_active and config.download_queue:
_launch_next_queued_download()
return
if is_1fichier_url(url):
ensure_download_provider_keys(False)
if missing_all_provider_keys():
logger.warning("Aucune cle API - Mode gratuit 1fichier sera utilise (attente requise)")
task_id = str(pygame.time.get_ticks())
task = asyncio.create_task(download_from_1fichier(url, platform, game_name, pending_download[3], task_id))
else:
task_id = str(pygame.time.get_ticks())
task = asyncio.create_task(download_rom(url, platform, game_name, pending_download[3], task_id))
config.download_tasks[task_id] = (task, url, game_name, platform)
show_toast(f"{_('download_started')}: {display_name}")
config.needs_redraw = True
logger.debug(f"Telechargement demarre depuis la recherche globale: {game_name} pour {platform}, task_id={task_id}")
...
def handle_controls(event, sources, joystick, screen):
@@ -504,6 +793,8 @@ def handle_controls(event, sources, joystick, screen):
config.menu_state = "history"
config.needs_redraw = True
logger.debug("Ouverture history depuis platform")
elif is_input_matched(event, "filter"):
enter_global_search()
elif is_input_matched(event, "confirm"):
# Démarrer le chronomètre pour l'appui long - ne pas exécuter immédiatement
# L'action sera exécutée au relâchement si appui court, ou config dossier si appui long
@@ -523,6 +814,117 @@ def handle_controls(event, sources, joystick, screen):
config.confirm_selection = 0
config.needs_redraw = True
elif config.menu_state == "platform_search":
if getattr(config, 'joystick', False) and getattr(config, 'global_search_editing', False):
row, col = config.selected_key
max_row = len(GLOBAL_SEARCH_KEYBOARD_LAYOUT) - 1
max_col = len(GLOBAL_SEARCH_KEYBOARD_LAYOUT[row]) - 1
if is_global_search_input_matched(event, "up"):
if row == 0:
row = max_row + (1 if col <= 5 else 0)
if row > 0:
config.selected_key = (row - 1, min(col, len(GLOBAL_SEARCH_KEYBOARD_LAYOUT[row - 1]) - 1))
config.repeat_action = "up"
config.repeat_start_time = current_time + REPEAT_DELAY
config.repeat_last_action = current_time
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
config.needs_redraw = True
elif is_global_search_input_matched(event, "down"):
if (col <= 5 and row == max_row) or (col > 5 and row == max_row - 1):
row = -1
if row < max_row:
config.selected_key = (row + 1, min(col, len(GLOBAL_SEARCH_KEYBOARD_LAYOUT[row + 1]) - 1))
config.repeat_action = "down"
config.repeat_start_time = current_time + REPEAT_DELAY
config.repeat_last_action = current_time
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
config.needs_redraw = True
elif is_global_search_input_matched(event, "left"):
if col == 0:
col = max_col + 1
if col > 0:
config.selected_key = (row, col - 1)
config.repeat_action = "left"
config.repeat_start_time = current_time + REPEAT_DELAY
config.repeat_last_action = current_time
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
config.needs_redraw = True
elif is_global_search_input_matched(event, "right"):
if col == max_col:
col = -1
if col < max_col:
config.selected_key = (row, col + 1)
config.repeat_action = "right"
config.repeat_start_time = current_time + REPEAT_DELAY
config.repeat_last_action = current_time
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
config.needs_redraw = True
elif is_global_search_input_matched(event, "confirm"):
config.global_search_query += GLOBAL_SEARCH_KEYBOARD_LAYOUT[row][col]
refresh_global_search_results()
logger.debug(f"Recherche globale mise a jour: query={config.global_search_query}, resultats={len(config.global_search_results)}")
config.needs_redraw = True
elif is_global_search_input_matched(event, "delete"):
if config.global_search_query:
config.global_search_query = config.global_search_query[:-1]
refresh_global_search_results()
logger.debug(f"Recherche globale suppression: query={config.global_search_query}, resultats={len(config.global_search_results)}")
config.needs_redraw = True
elif is_global_search_input_matched(event, "space"):
config.global_search_query += " "
refresh_global_search_results()
logger.debug(f"Recherche globale espace: query={config.global_search_query}, resultats={len(config.global_search_results)}")
config.needs_redraw = True
elif is_global_search_input_matched(event, "filter"):
config.global_search_editing = False
config.needs_redraw = True
elif is_global_search_input_matched(event, "cancel"):
exit_global_search()
else:
results = config.global_search_results
if is_global_search_input_matched(event, "up"):
if config.global_search_selected > 0:
config.global_search_selected -= 1
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)
config.needs_redraw = True
elif is_global_search_input_matched(event, "down"):
if config.global_search_selected < len(results) - 1:
config.global_search_selected += 1
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)
config.needs_redraw = True
elif is_global_search_input_matched(event, "page_up") or is_global_search_input_matched(event, "left"):
config.global_search_selected = max(0, config.global_search_selected - 10)
config.needs_redraw = True
elif is_global_search_input_matched(event, "page_down") or is_global_search_input_matched(event, "right"):
config.global_search_selected = min(max(0, len(results) - 1), config.global_search_selected + 10)
config.needs_redraw = True
elif is_global_search_input_matched(event, "confirm"):
trigger_global_search_download(queue_only=False)
elif is_global_search_input_matched(event, "clear_history"):
trigger_global_search_download(queue_only=True)
elif is_global_search_input_matched(event, "filter") and getattr(config, 'joystick', False):
config.global_search_editing = True
config.needs_redraw = True
elif is_global_search_input_matched(event, "cancel"):
exit_global_search()
elif not getattr(config, 'joystick', False) and event.type == pygame.KEYDOWN:
if event.unicode.isalnum() or event.unicode == ' ':
config.global_search_query += event.unicode
refresh_global_search_results()
logger.debug(f"Recherche globale clavier: query={config.global_search_query}, resultats={len(config.global_search_results)}")
config.needs_redraw = True
elif is_global_search_input_matched(event, "delete"):
if config.global_search_query:
config.global_search_query = config.global_search_query[:-1]
refresh_global_search_results()
logger.debug(f"Recherche globale clavier suppression: query={config.global_search_query}, resultats={len(config.global_search_results)}")
config.needs_redraw = True
if config.global_search_results:
config.global_search_selected = max(0, min(config.global_search_selected, len(config.global_search_results) - 1))
else:
config.global_search_selected = 0
# Jeux
elif config.menu_state == "game":
games: list[Game] = config.filtered_games if config.filter_active or config.search_mode else config.games
@@ -719,6 +1121,7 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
logger.debug("Ouverture du menu de filtrage")
elif is_input_matched(event, "history"):
config.history_origin = "game"
config.menu_state = "history"
config.needs_redraw = True
logger.debug("Ouverture history depuis game")
@@ -2006,7 +2409,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 = 7 # update cache, history, source, unsupported, hide premium, filter, back
total = 8 # update cache, scan roms, history, source, unsupported, hide premium, filter, back
if is_input_matched(event, "up"):
config.pause_games_selection = (sel - 1) % total
config.needs_redraw = True
@@ -2019,14 +2422,25 @@ def handle_controls(event, sources, joystick, screen):
config.menu_state = "reload_games_data"
config.redownload_confirm_selection = 0
config.needs_redraw = True
elif sel == 1 and is_input_matched(event, "confirm"): # history
elif sel == 1 and is_input_matched(event, "confirm"): # scan local roms
try:
added_games, scanned_platforms = scan_roms_for_downloaded_games()
config.popup_message = _("popup_scan_owned_roms_done").format(added_games, scanned_platforms) if _ else f"ROM scan complete: {added_games} games added across {scanned_platforms} platforms"
config.popup_timer = 4000
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur scan ROMs locaux: {e}")
config.popup_message = _("popup_scan_owned_roms_error").format(str(e)) if _ else f"ROM scan error: {e}"
config.popup_timer = 5000
config.needs_redraw = True
elif sel == 2 and is_input_matched(event, "confirm"): # history
config.history = load_history()
config.current_history_item = 0
config.history_scroll_offset = 0
config.previous_menu_state = "pause_games_menu"
config.menu_state = "history"
config.needs_redraw = True
elif sel == 2 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # source mode
elif sel == 3 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # source mode
try:
current_mode = get_sources_mode()
new_mode = set_sources_mode('custom' if current_mode == 'rgsx' else 'rgsx')
@@ -2041,7 +2455,7 @@ def handle_controls(event, sources, joystick, screen):
logger.info(f"Changement du mode des sources vers {new_mode}")
except Exception as e:
logger.error(f"Erreur changement mode sources: {e}")
elif sel == 3 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # unsupported toggle
elif sel == 4 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)
@@ -2051,7 +2465,7 @@ def handle_controls(event, sources, joystick, screen):
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
elif sel == 5 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)
@@ -2060,13 +2474,13 @@ def handle_controls(event, sources, joystick, screen):
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
elif sel == 6 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
elif sel == 7 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

View File

@@ -69,7 +69,7 @@ SDL_TO_PYGAME_KEY = {
# Noms lisibles pour les touches clavier
KEY_NAMES = {
pygame.K_RETURN: "Enter",
pygame.K_ESCAPE: "Échap",
pygame.K_ESCAPE: "Esc/Echap",
pygame.K_SPACE: "Espace",
pygame.K_UP: "",
pygame.K_DOWN: "",
@@ -87,7 +87,7 @@ KEY_NAMES = {
pygame.K_RMETA: "RMeta",
pygame.K_CAPSLOCK: "Verr Maj",
pygame.K_NUMLOCK: "Verr Num",
pygame.K_SCROLLOCK: "Verr Déf",
pygame.K_SCROLLOCK: "Verr Def",
pygame.K_a: "A",
pygame.K_b: "B",
pygame.K_c: "C",
@@ -158,7 +158,7 @@ KEY_NAMES = {
pygame.K_F15: "F15",
pygame.K_INSERT: "Inser",
pygame.K_DELETE: "Suppr",
pygame.K_HOME: "Début",
pygame.K_HOME: "Debut",
pygame.K_END: "Fin",
pygame.K_PAGEUP: "Page+",
pygame.K_PAGEDOWN: "Page-",

View File

@@ -666,12 +666,32 @@ def draw_error_screen(screen):
# Récupérer les noms d'affichage des contrôles
def get_control_display(action, default):
"""Récupère le nom d'affichage d'une action depuis controls_config."""
keyboard_defaults = {
"confirm": "Enter",
"cancel": "Esc/Echap",
"left": "",
"right": "",
"up": "",
"down": "",
"start": "AltGR",
"clear_history": "X",
"history": "H",
"page_up": "Page+",
"page_down": "Page-",
"filter": "F",
"delete": "Backspace",
"space": "Espace",
}
keyboard_default = keyboard_defaults.get(action)
if not config.controls_config:
logger.warning(f"controls_config vide pour l'action {action}, utilisation de la valeur par défaut")
return default
return keyboard_default or default
control_config = config.controls_config.get(action, {})
control_type = control_config.get('type', '')
if getattr(config, 'keyboard', False) and control_type != 'key' and keyboard_default:
return keyboard_default
# Si un libellé personnalisé est défini dans controls.json, on le privilégie
custom_label = control_config.get('display')
@@ -683,7 +703,7 @@ def get_control_display(action, default):
key_code = control_config.get('key')
key_names = {
pygame.K_RETURN: "Enter",
pygame.K_ESCAPE: "Échap",
pygame.K_ESCAPE: "Esc/Echap",
pygame.K_SPACE: "Espace",
pygame.K_UP: "",
pygame.K_DOWN: "",
@@ -701,7 +721,7 @@ def get_control_display(action, default):
pygame.K_RMETA: "RMeta",
pygame.K_CAPSLOCK: "Verr Maj",
pygame.K_NUMLOCK: "Verr Num",
pygame.K_SCROLLOCK: "Verr Déf",
pygame.K_SCROLLOCK: "Verr Def",
pygame.K_a: "A",
pygame.K_b: "B",
pygame.K_c: "C",
@@ -772,7 +792,7 @@ def get_control_display(action, default):
pygame.K_F15: "F15",
pygame.K_INSERT: "Inser",
pygame.K_DELETE: "Suppr",
pygame.K_HOME: "Début",
pygame.K_HOME: "Debut",
pygame.K_END: "Fin",
pygame.K_PAGEUP: "Page+",
pygame.K_PAGEDOWN: "Page-",
@@ -829,6 +849,57 @@ def get_control_display(action, default):
# Cache pour les images des plateformes
platform_images_cache = {}
def draw_header_badge(screen, lines, badge_x, badge_y, light_mode=False):
"""Affiche une cartouche compacte de texte dans l'en-tete."""
header_font = config.tiny_font
text_surfaces = [header_font.render(line, True, THEME_COLORS["text"]) for line in lines if line]
if not text_surfaces:
return
content_width = max((surface.get_width() for surface in text_surfaces), default=0)
content_height = sum(surface.get_height() for surface in text_surfaces) + max(0, len(text_surfaces) - 1) * 4
padding_x = 12
padding_y = 8
badge_width = content_width + padding_x * 2
badge_height = content_height + padding_y * 2
if light_mode:
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (badge_x, badge_y, badge_width, badge_height), border_radius=12)
else:
shadow = pygame.Surface((badge_width + 8, badge_height + 8), pygame.SRCALPHA)
pygame.draw.rect(shadow, (0, 0, 0, 110), (4, 4, badge_width, badge_height), border_radius=12)
screen.blit(shadow, (badge_x - 4, badge_y - 4))
badge_surface = pygame.Surface((badge_width, badge_height), pygame.SRCALPHA)
pygame.draw.rect(badge_surface, THEME_COLORS["button_idle"], (0, 0, badge_width, badge_height), border_radius=12)
highlight = pygame.Surface((badge_width - 6, max(10, badge_height // 3)), pygame.SRCALPHA)
highlight.fill((255, 255, 255, 18))
badge_surface.blit(highlight, (3, 3))
screen.blit(badge_surface, (badge_x, badge_y))
pygame.draw.rect(screen, THEME_COLORS["border"], (badge_x, badge_y, badge_width, badge_height), 2, border_radius=12)
current_y = badge_y + padding_y
for surface in text_surfaces:
line_x = badge_x + (badge_width - surface.get_width()) // 2
screen.blit(surface, (line_x, current_y))
current_y += surface.get_height() + 4
def draw_platform_header_info(screen, light_mode=False):
"""Affiche version et controleur connecte dans un cartouche en haut a droite."""
lines = [f"v{config.app_version}"]
device_name = (getattr(config, 'controller_device_name', '') or '').strip()
if device_name:
lines.append(truncate_text_end(device_name, config.tiny_font, int(config.screen_width * 0.24)))
badge_width = max(config.tiny_font.size(line)[0] for line in lines) + 24
badge_x = config.screen_width - badge_width - 14
badge_y = 10
draw_header_badge(screen, lines, badge_x, badge_y, light_mode)
# Grille des systèmes 3x3
def draw_platform_grid(screen):
"""Affiche la grille des plateformes avec un style moderne et fluide."""
@@ -959,11 +1030,9 @@ def draw_platform_grid(screen):
total_pages = (len(visible_platforms) + systems_per_page - 1) // systems_per_page
if total_pages > 1:
page_indicator_text = _("platform_page").format(config.current_page + 1, total_pages)
page_indicator = config.small_font.render(page_indicator_text, True, THEME_COLORS["text"])
# Position en haut à gauche
page_x = 10
page_y = 10
screen.blit(page_indicator, (page_x, page_y))
draw_header_badge(screen, [page_indicator_text], 14, 10, light_mode)
draw_platform_header_info(screen, light_mode)
# Calculer une seule fois la pulsation pour les éléments sélectionnés (réduite)
if not light_mode:
@@ -1170,8 +1239,7 @@ def download_fbneo_list(path_to_save: str) -> None:
if not path.exists():
logger.debug("Downloading fbneo gamelist.txt from github ...")
urllib.request.urlretrieve(url, path)
logger.debug("Download finished:", path)
logger.debug("Download finished: %s", path)
...
def parse_fbneo_list(path: str) -> Dict[str, Any]:
@@ -1211,13 +1279,19 @@ def draw_game_list(screen):
fbneo_selected = platform_name == 'Final Burn Neo'
if fbneo_selected:
fbneo_game_list_path = os.path.join(config.SAVE_FOLDER, FBNEO_GAME_LIST)
download_fbneo_list(fbneo_game_list_path) # download the fbneo game list if necessary - 10 MB file
config.fbneo_games = parse_fbneo_list(fbneo_game_list_path)
if not config.fbneo_games:
download_fbneo_list(fbneo_game_list_path) # download the fbneo game list if necessary - 10 MB file
config.fbneo_games = parse_fbneo_list(fbneo_game_list_path)
for game in config.games:
clean_name = game.display_name
if clean_name in config.fbneo_games:
fbneo_game = config.fbneo_games[clean_name]
game.display_name = fbneo_game["full name"]
full_name = fbneo_game["full name"]
if game.display_name != full_name:
game.display_name = full_name
game.regions = None
game.is_non_release = None
game.base_name = None
...
if config.game_filter_obj and config.game_filter_obj.is_active() and not config.search_query:
@@ -1346,23 +1420,30 @@ def draw_game_list(screen):
pygame.draw.rect(screen, THEME_COLORS["border"], (rect_x, rect_y, rect_width, rect_height), 2, border_radius=12)
# Largeur colonne taille (15%) mini 120px, reste pour nom
# Largeur colonnes nom / ext / taille
ext_col_width = max(90, int(rect_width * 0.08))
size_col_width = max(120, int(rect_width * 0.15))
name_col_width = rect_width - 40 - size_col_width # padding horizontal 40
name_col_width = rect_width - 40 - ext_col_width - size_col_width
# ---- En-tête ----
header_name = _("game_header_name")
header_ext = _("game_header_ext")
header_size = _("game_header_size")
header_y_center = rect_y + margin_top_bottom + header_height // 2
# Nom aligné gauche
header_name_surface = config.small_font.render(header_name, True, THEME_COLORS["text"])
header_name_rect = header_name_surface.get_rect()
header_name_rect.midleft = (rect_x + 20, header_y_center)
# Extension centree
header_ext_surface = config.small_font.render(header_ext, True, THEME_COLORS["text"])
header_ext_rect = header_ext_surface.get_rect()
header_ext_rect.center = (rect_x + rect_width - 20 - size_col_width - ext_col_width // 2, header_y_center)
# Taille alignée droite
header_size_surface = config.small_font.render(header_size, True, THEME_COLORS["text"])
header_size_rect = header_size_surface.get_rect()
header_size_rect.midright = (rect_x + rect_width - 20, header_y_center)
screen.blit(header_name_surface, header_name_rect)
screen.blit(header_ext_surface, header_ext_rect)
screen.blit(header_size_surface, header_size_rect)
# Ligne de séparation sous l'en-tête
separator_y = rect_y + margin_top_bottom + header_height
@@ -1376,9 +1457,10 @@ def draw_game_list(screen):
game_name = item.display_name
size_val = item.size
# Vérifier si le jeu est déjà téléchargé
is_downloaded = is_game_downloaded(platform_name, game_name)
# Vérifier si le jeu est déjà téléchargé en comparant le nom réel sans extension
is_downloaded = is_game_downloaded(platform_name, item.name)
ext_text = get_display_extension(item.name)
size_text = size_val if (isinstance(size_val, str) and size_val.strip()) else "N/A"
color = THEME_COLORS["fond_lignes"] if i == config.current_game else THEME_COLORS["text"]
@@ -1389,11 +1471,14 @@ def draw_game_list(screen):
# Utiliser une couleur verte pour les jeux téléchargés
name_color = (100, 255, 100) if is_downloaded else color # Vert clair si téléchargé
name_surface = config.small_font.render(truncated_name, True, name_color)
ext_surface = config.small_font.render(ext_text, True, THEME_COLORS["text"])
size_surface = config.small_font.render(size_text, True, THEME_COLORS["text"])
row_center_y = list_start_y + (i - config.scroll_offset) * line_height + line_height // 2
# Position nom (aligné à gauche dans la boite)
name_rect = name_surface.get_rect()
name_rect.midleft = (rect_x + 20, row_center_y)
ext_rect = ext_surface.get_rect()
ext_rect.center = (rect_x + rect_width - 20 - size_col_width - ext_col_width // 2, row_center_y)
size_rect = size_surface.get_rect()
size_rect.midright = (rect_x + rect_width - 20, row_center_y)
if i == config.current_game:
@@ -1422,6 +1507,7 @@ def draw_game_list(screen):
pygame.draw.rect(screen, (*THEME_COLORS["fond_lignes"][:3], 120), border_rect, width=1, border_radius=8)
screen.blit(name_surface, name_rect)
screen.blit(ext_surface, ext_rect)
screen.blit(size_surface, size_rect)
if len(games) > items_per_page:
@@ -1448,6 +1534,205 @@ def draw_game_scrollbar(screen, scroll_offset, total_items, visible_items, x, y,
scrollbar_y = y + (game_area_height - scrollbar_height) * (scroll_offset / max(1, total_items - visible_items))
pygame.draw.rect(screen, THEME_COLORS["fond_lignes"], (x, scrollbar_y, 15, scrollbar_height), border_radius=4)
def get_display_extension(file_name):
"""Retourne l'extension finale d'un nom de fichier pour affichage."""
if not isinstance(file_name, str) or not file_name.strip():
return "-"
suffix = Path(file_name).suffix.strip()
if not suffix:
return "-"
return suffix.lower()
def draw_global_search_list(screen):
"""Affiche la recherche globale par nom sur toutes les plateformes."""
query = getattr(config, 'global_search_query', '') or ''
results = getattr(config, 'global_search_results', []) or []
screen.blit(OVERLAY, (0, 0))
title_query = query + "_" if (getattr(config, 'joystick', False) and getattr(config, 'global_search_editing', False)) or (not getattr(config, 'joystick', False)) else query
title_text = _("global_search_title").format(title_query)
if results:
title_text += f" ({len(results)})"
title_surface = config.search_font.render(title_text, True, THEME_COLORS["text"])
title_rect = title_surface.get_rect(center=(config.screen_width // 2, title_surface.get_height() // 2 + 20))
title_rect_inflated = title_rect.inflate(60, 30)
title_rect_inflated.topleft = ((config.screen_width - title_rect_inflated.width) // 2, 10)
shadow = pygame.Surface((title_rect_inflated.width + 10, title_rect_inflated.height + 10), pygame.SRCALPHA)
pygame.draw.rect(shadow, (0, 0, 0, 120), (5, 5, title_rect_inflated.width, title_rect_inflated.height), border_radius=14)
screen.blit(shadow, (title_rect_inflated.left - 5, title_rect_inflated.top - 5))
glow = pygame.Surface((title_rect_inflated.width + 20, title_rect_inflated.height + 20), pygame.SRCALPHA)
pygame.draw.rect(glow, (*THEME_COLORS["glow"][:3], 60), glow.get_rect(), border_radius=16)
screen.blit(glow, (title_rect_inflated.left - 10, title_rect_inflated.top - 10))
pygame.draw.rect(screen, THEME_COLORS["button_idle"], title_rect_inflated, border_radius=12)
pygame.draw.rect(screen, THEME_COLORS["border"], title_rect_inflated, 2, border_radius=12)
screen.blit(title_surface, title_rect)
if not query.strip():
message = _("global_search_empty_query")
lines = wrap_text(message, config.font, config.screen_width - 80)
line_height = config.font.get_height() + 5
text_height = len(lines) * line_height
margin_top_bottom = 20
rect_height = text_height + 2 * margin_top_bottom
max_text_width = max([config.font.size(line)[0] for line in lines], default=300)
rect_width = max_text_width + 80
rect_x = (config.screen_width - rect_width) // 2
rect_y = (config.screen_height - rect_height) // 2
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (rect_x, rect_y, rect_width, rect_height), border_radius=12)
pygame.draw.rect(screen, THEME_COLORS["border"], (rect_x, rect_y, rect_width, rect_height), 2, border_radius=12)
for i, line in enumerate(lines):
text_surface = config.font.render(line, True, THEME_COLORS["text"])
text_rect = text_surface.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + i * line_height + line_height // 2))
screen.blit(text_surface, text_rect)
return
if not results:
message = _("global_search_no_results").format(query)
lines = wrap_text(message, config.font, config.screen_width - 80)
line_height = config.font.get_height() + 5
text_height = len(lines) * line_height
margin_top_bottom = 20
rect_height = text_height + 2 * margin_top_bottom
max_text_width = max([config.font.size(line)[0] for line in lines], default=300)
rect_width = max_text_width + 80
rect_x = (config.screen_width - rect_width) // 2
rect_y = (config.screen_height - rect_height) // 2
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (rect_x, rect_y, rect_width, rect_height), border_radius=12)
pygame.draw.rect(screen, THEME_COLORS["border"], (rect_x, rect_y, rect_width, rect_height), 2, border_radius=12)
for i, line in enumerate(lines):
text_surface = config.font.render(line, True, THEME_COLORS["text"])
text_rect = text_surface.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + i * line_height + line_height // 2))
screen.blit(text_surface, text_rect)
return
line_height = config.small_font.get_height() + 10
header_height = line_height
margin_top_bottom = 20
extra_margin_top = 20
extra_margin_bottom = 60
title_height = config.title_font.get_height() + 20
available_height = config.screen_height - title_height - extra_margin_top - extra_margin_bottom - 2 * margin_top_bottom - header_height
items_per_page = max(1, available_height // line_height)
rect_height = header_height + items_per_page * line_height + 2 * margin_top_bottom
rect_width = int(0.95 * config.screen_width)
rect_x = (config.screen_width - rect_width) // 2
rect_y = title_height + extra_margin_top + (config.screen_height - title_height - extra_margin_top - extra_margin_bottom - rect_height) // 2
config.global_search_scroll_offset = max(0, min(config.global_search_scroll_offset, max(0, len(results) - items_per_page)))
if config.global_search_selected < config.global_search_scroll_offset:
config.global_search_scroll_offset = config.global_search_selected
elif config.global_search_selected >= config.global_search_scroll_offset + items_per_page:
config.global_search_scroll_offset = config.global_search_selected - items_per_page + 1
shadow_rect = pygame.Rect(rect_x + 6, rect_y + 6, rect_width, rect_height)
shadow_surf = pygame.Surface((rect_width + 8, rect_height + 8), pygame.SRCALPHA)
pygame.draw.rect(shadow_surf, (0, 0, 0, 100), (4, 4, rect_width, rect_height), border_radius=14)
screen.blit(shadow_surf, (rect_x - 4, rect_y - 4))
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (rect_x, rect_y, rect_width, rect_height), border_radius=12)
highlight = pygame.Surface((rect_width - 8, 40), pygame.SRCALPHA)
highlight.fill((255, 255, 255, 15))
screen.blit(highlight, (rect_x + 4, rect_y + 4))
pygame.draw.rect(screen, THEME_COLORS["border"], (rect_x, rect_y, rect_width, rect_height), 2, border_radius=12)
ext_col_width = max(90, int(rect_width * 0.08))
size_col_width = max(120, int(rect_width * 0.15))
platform_col_width = max(220, int(rect_width * 0.22))
name_col_width = rect_width - 40 - platform_col_width - ext_col_width - size_col_width
header_y_center = rect_y + margin_top_bottom + header_height // 2
header_platform_surface = config.small_font.render(_("history_column_system"), True, THEME_COLORS["text"])
header_platform_rect = header_platform_surface.get_rect()
header_platform_rect.midleft = (rect_x + 20, header_y_center)
header_name_surface = config.small_font.render(_("game_header_name"), True, THEME_COLORS["text"])
header_name_rect = header_name_surface.get_rect()
header_name_rect.midleft = (rect_x + 20 + platform_col_width, header_y_center)
header_ext_surface = config.small_font.render(_("game_header_ext"), True, THEME_COLORS["text"])
header_ext_rect = header_ext_surface.get_rect()
header_ext_rect.center = (rect_x + rect_width - 20 - size_col_width - ext_col_width // 2, header_y_center)
header_size_surface = config.small_font.render(_("game_header_size"), True, THEME_COLORS["text"])
header_size_rect = header_size_surface.get_rect()
header_size_rect.midright = (rect_x + rect_width - 20, header_y_center)
screen.blit(header_platform_surface, header_platform_rect)
screen.blit(header_name_surface, header_name_rect)
screen.blit(header_ext_surface, header_ext_rect)
screen.blit(header_size_surface, header_size_rect)
separator_y = rect_y + margin_top_bottom + header_height
pygame.draw.line(screen, THEME_COLORS["border"], (rect_x + 20, separator_y), (rect_x + rect_width - 20, separator_y), 2)
list_start_y = rect_y + margin_top_bottom + header_height
for i in range(config.global_search_scroll_offset, min(config.global_search_scroll_offset + items_per_page, len(results))):
item = results[i]
row_center_y = list_start_y + (i - config.global_search_scroll_offset) * line_height + line_height // 2
is_selected = i == config.global_search_selected
row_color = THEME_COLORS["fond_lignes"] if is_selected else THEME_COLORS["text"]
platform_text = truncate_text_end(item["platform_label"], config.small_font, platform_col_width - 10)
game_text = truncate_text_middle(item["display_name"], config.small_font, name_col_width - 10, is_filename=False)
ext_text = get_display_extension(item.get("game_name"))
size_value = item.get("size")
size_text = size_value if (isinstance(size_value, str) and size_value.strip()) else "N/A"
platform_surface = config.small_font.render(platform_text, True, row_color)
game_surface = config.small_font.render(game_text, True, row_color)
ext_surface = config.small_font.render(ext_text, True, THEME_COLORS["text"])
size_surface = config.small_font.render(size_text, True, THEME_COLORS["text"])
platform_rect = platform_surface.get_rect()
platform_rect.midleft = (rect_x + 20, row_center_y)
game_rect = game_surface.get_rect()
game_rect.midleft = (rect_x + 20 + platform_col_width, row_center_y)
ext_rect = ext_surface.get_rect()
ext_rect.center = (rect_x + rect_width - 20 - size_col_width - ext_col_width // 2, row_center_y)
size_rect = size_surface.get_rect()
size_rect.midright = (rect_x + rect_width - 20, row_center_y)
if is_selected:
glow_width = rect_width - 40
glow_height = game_rect.height + 12
glow_surface = pygame.Surface((glow_width + 6, glow_height + 6), pygame.SRCALPHA)
pygame.draw.rect(glow_surface, (*THEME_COLORS["fond_lignes"][:3], 50), (3, 3, glow_width, glow_height), border_radius=8)
screen.blit(glow_surface, (rect_x + 17, row_center_y - glow_height // 2 - 3))
selection_bg = pygame.Surface((glow_width, glow_height), pygame.SRCALPHA)
for j in range(glow_height):
ratio = j / glow_height
alpha = int(60 + 20 * ratio)
pygame.draw.line(selection_bg, (*THEME_COLORS["fond_lignes"][:3], alpha), (0, j), (glow_width, j))
screen.blit(selection_bg, (rect_x + 20, row_center_y - glow_height // 2))
border_rect = pygame.Rect(rect_x + 20, row_center_y - glow_height // 2, glow_width, glow_height)
pygame.draw.rect(screen, (*THEME_COLORS["fond_lignes"][:3], 120), border_rect, width=1, border_radius=8)
screen.blit(platform_surface, platform_rect)
screen.blit(game_surface, game_rect)
screen.blit(ext_surface, ext_rect)
screen.blit(size_surface, size_rect)
if len(results) > items_per_page:
draw_game_scrollbar(
screen,
config.global_search_scroll_offset,
len(results),
items_per_page,
rect_x + rect_width - 10,
rect_y,
rect_height
)
def format_size(size):
"""Convertit une taille en octets en format lisible avec unités adaptées à la langue."""
if not isinstance(size, (int, float)) or size == 0:
@@ -1500,14 +1785,18 @@ def draw_history_list(screen):
# Define column widths as percentages of available space (give more space to status/error messages)
column_width_percentages = {
"platform": 0.15, # narrower platform column
"game_name": 0.45, # game name column
"size": 0.10, # size column remains compact
"status": 0.30 # wider status column for long error codes/messages
"platform": 0.13,
"game_name": 0.25,
"ext": 0.08,
"folder": 0.12,
"size": 0.08,
"status": 0.34
}
available_width = int(0.95 * config.screen_width - 60) # Total available width for columns
col_platform_width = int(available_width * column_width_percentages["platform"])
col_game_width = int(available_width * column_width_percentages["game_name"])
col_ext_width = int(available_width * column_width_percentages["ext"])
col_folder_width = int(available_width * column_width_percentages["folder"])
col_size_width = int(available_width * column_width_percentages["size"])
col_status_width = int(available_width * column_width_percentages["status"])
rect_width = int(0.95 * config.screen_width)
@@ -1586,13 +1875,15 @@ def draw_history_list(screen):
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (rect_x, rect_y, rect_width, rect_height), border_radius=12)
pygame.draw.rect(screen, THEME_COLORS["border"], (rect_x, rect_y, rect_width, rect_height), 2, border_radius=12)
headers = [_("history_column_system"), _("history_column_game"), _("history_column_size"), _("history_column_status")]
headers = [_("history_column_system"), _("history_column_game"), _("game_header_ext"), _("history_column_folder"), _("history_column_size"), _("history_column_status")]
header_y = rect_y + margin_top_bottom + header_height // 2
header_x_positions = [
rect_x + 20 + col_platform_width // 2,
rect_x + 20 + col_platform_width + col_game_width // 2,
rect_x + 20 + col_platform_width + col_game_width + col_size_width // 2,
rect_x + 20 + col_platform_width + col_game_width + col_size_width + col_status_width // 2
rect_x + 20 + col_platform_width + col_game_width + col_ext_width // 2,
rect_x + 20 + col_platform_width + col_game_width + col_ext_width + col_folder_width // 2,
rect_x + 20 + col_platform_width + col_game_width + col_ext_width + col_folder_width + col_size_width // 2,
rect_x + 20 + col_platform_width + col_game_width + col_ext_width + col_folder_width + col_size_width + col_status_width // 2
]
for header, x_pos in zip(headers, header_x_positions):
text_surface = config.small_font.render(header, True, THEME_COLORS["text"])
@@ -1606,6 +1897,8 @@ def draw_history_list(screen):
entry = history[i]
platform = entry.get("platform", "Inconnu")
game_name = entry.get("game_name", "Inconnu")
ext_text = get_display_extension(game_name)
folder_text = _get_dest_folder_name(platform)
# Correction du calcul de la taille
size = entry.get("total_size", 0)
@@ -1679,20 +1972,26 @@ def draw_history_list(screen):
status_color = THEME_COLORS.get("text", (255, 255, 255))
platform_text = truncate_text_end(platform, config.small_font, col_platform_width - 10)
game_text = truncate_text_end(game_name, config.small_font, col_game_width - 10)
game_text = truncate_text_end(str(game_name).rsplit('.', 1)[0] if '.' in str(game_name) else str(game_name), config.small_font, col_game_width - 10)
ext_text = truncate_text_end(ext_text, config.small_font, col_ext_width - 10)
folder_text = truncate_text_end(folder_text, config.small_font, col_folder_width - 10)
size_text = truncate_text_end(size_text, config.small_font, col_size_width - 10)
status_text = truncate_text_middle(str(status_text or ""), config.small_font, col_status_width - 10, is_filename=False)
y_pos = rect_y + margin_top_bottom + header_height + idx * line_height + line_height // 2
platform_surface = config.small_font.render(platform_text, True, color)
game_surface = config.small_font.render(game_text, True, color)
ext_surface = config.small_font.render(ext_text, True, color)
folder_surface = config.small_font.render(folder_text, True, color)
size_surface = config.small_font.render(size_text, True, color) # Correction ici
status_surface = config.small_font.render(status_text, True, status_color)
platform_rect = platform_surface.get_rect(center=(header_x_positions[0], y_pos))
game_rect = game_surface.get_rect(center=(header_x_positions[1], y_pos))
size_rect = size_surface.get_rect(center=(header_x_positions[2], y_pos))
status_rect = status_surface.get_rect(center=(header_x_positions[3], y_pos))
ext_rect = ext_surface.get_rect(center=(header_x_positions[2], y_pos))
folder_rect = folder_surface.get_rect(center=(header_x_positions[3], y_pos))
size_rect = size_surface.get_rect(center=(header_x_positions[4], y_pos))
status_rect = status_surface.get_rect(center=(header_x_positions[5], y_pos))
if i == current_history_item_inverted:
glow_surface = pygame.Surface((rect_width - 40, line_height), pygame.SRCALPHA)
@@ -1701,6 +2000,8 @@ def draw_history_list(screen):
screen.blit(platform_surface, platform_rect)
screen.blit(game_surface, game_rect)
screen.blit(ext_surface, ext_rect)
screen.blit(folder_surface, folder_rect)
screen.blit(size_surface, size_rect)
screen.blit(status_surface, status_rect)
@@ -1969,14 +2270,31 @@ def draw_extension_warning(screen):
# Affichage des contrôles en bas de page
def draw_controls(screen, menu_state, current_music_name=None, music_popup_start_time=0):
"""Affiche les contrôles contextuels en bas de l'écran selon le menu_state."""
if menu_state == "platform_search" and getattr(config, 'joystick', False) and getattr(config, 'global_search_editing', False):
menu_state = "platform_search_edit"
# Mapping des contrôles par menu_state
controls_map = {
"platform": [
("history", _("controls_action_history")),
("filter", _("controls_filter_search")),
("confirm", _("controls_confirm_select")),
("start", _("controls_action_start")),
],
"platform_search": [
("confirm", _("controls_confirm_select")),
("clear_history", _("controls_action_queue")),
(("page_up", "page_down"), _("controls_pages")),
("filter", _("controls_action_edit_search")),
("cancel", _("controls_cancel_back")),
],
"platform_search_edit": [
("confirm", _("controls_action_select_char")),
("delete", _("controls_action_delete")),
("space", _("controls_action_space")),
("filter", _("controls_action_show_results")),
("cancel", _("controls_cancel_back")),
],
"game": [
("confirm", _("controls_confirm_select")),
("clear_history", _("controls_action_queue")),
@@ -2046,41 +2364,9 @@ def draw_controls(screen, menu_state, current_music_name=None, music_popup_start
# Construire les lignes avec icônes
icon_lines = []
# Sur la page d'accueil et la page loading afficher version et musique
if menu_state == "platform" or menu_state == "loading":
control_parts = []
start_button = get_control_display('start', 'START')
# Si aucun joystick, afficher la touche entre crochets
if not getattr(config, 'joystick', True):
start_button = f"[{start_button}]"
start_text = _("controls_action_start")
control_parts.append(f"RGSX v{config.app_version} - {start_button} : {start_text}")
# Afficher le nom du joystick s'il est détecté
try:
device_name = getattr(config, 'controller_device_name', '') or ''
if device_name:
try:
joy_label = _("footer_joystick")
except Exception:
joy_label = "Joystick: {0}"
if isinstance(joy_label, str) and "{0}" in joy_label:
joy_text = joy_label.format(device_name)
else:
joy_text = f"{joy_label} {device_name}" if joy_label else f"Joystick: {device_name}"
control_parts.append(f"| {joy_text}")
except Exception:
pass
# Ajouter le nom de la musique si disponible
if config.current_music_name and config.music_popup_start_time > 0:
current_time = pygame.time.get_ticks() / 1000
if current_time - config.music_popup_start_time < 3.0:
control_parts.append(f"| {config.current_music_name}")
control_text = " ".join(control_parts)
icon_lines.append(control_text)
# Sur la page loading afficher version et musique
if menu_state == "loading":
icon_lines.append(f"RGSX v{config.app_version}")
else:
# Pour les autres menus: affichage avec icônes et contrôles contextuels sur une seule ligne
all_controls = []
@@ -2903,6 +3189,7 @@ def draw_pause_games_menu(screen, selected_index):
source_label = _("games_source_rgsx") if mode == "rgsx" else _("games_source_custom")
source_txt = f"{_('menu_games_source_prefix')}: < {source_label} >"
update_txt = _("menu_redownload_cache")
scan_txt = _("menu_scan_owned_roms") if _ else "Scan owned ROMs"
history_txt = _("menu_history") if _ else "History"
# Show unsupported systems
@@ -2923,9 +3210,10 @@ def draw_pause_games_menu(screen, selected_index):
filter_txt = _("submenu_display_filter_platforms") if _ else "Filter Platforms"
back_txt = _("menu_back") if _ else "Back"
options = [update_txt, history_txt, source_txt, unsupported_txt, hide_premium_txt, filter_txt, back_txt]
options = [update_txt, scan_txt, history_txt, source_txt, unsupported_txt, hide_premium_txt, filter_txt, back_txt]
instruction_keys = [
"instruction_games_update_cache",
"instruction_games_scan_owned",
"instruction_games_history",
"instruction_games_source_mode",
"instruction_display_show_unsupported",
@@ -4651,7 +4939,7 @@ def draw_scraper_screen(screen):
# Redimensionner l'image en conservant le ratio
image = config.scraper_image_surface
img_width, img_height = image.get_size()
img_width, img_height = image.get_size() if image else (0, 0)
# Calculer le ratio de redimensionnement
width_ratio = max_image_width / img_width

View File

@@ -191,11 +191,31 @@ class GameFilters:
base = base + disc_info
return base
@staticmethod
def get_cached_regions(game: Game) -> List[str]:
"""Retourne les régions en les calculant une seule fois par jeu."""
if game.regions is None:
game.regions = GameFilters.get_game_regions(game.display_name)
return game.regions
@staticmethod
def get_cached_non_release(game: Game) -> bool:
"""Retourne le flag non-release en le calculant à la demande."""
if game.is_non_release is None:
game.is_non_release = GameFilters.is_non_release_game(game.display_name)
return game.is_non_release
@staticmethod
def get_cached_base_name(game: Game) -> str:
"""Retourne le nom de base en le calculant une seule fois par jeu."""
if game.base_name is None:
game.base_name = GameFilters.get_base_game_name(game.display_name)
return game.base_name
def get_region_priority(self, game_name: str) -> int:
def get_region_priority(self, game: Game) -> int:
"""Obtient la priorité de région pour un jeu (pour one-rom-per-game)"""
# Utiliser la fonction de détection de régions pour être cohérent
game_regions = self.get_game_regions(game_name)
game_regions = self.get_cached_regions(game)
# Trouver la meilleure priorité parmi toutes les régions détectées
best_priority = len(self.region_priority) # Par défaut: priorité la plus basse
@@ -221,14 +241,13 @@ class GameFilters:
return games
filtered_games = []
has_region_excludes = any(state == 'exclude' for state in self.region_filters.values())
# Filtrage par région
for game in games:
game_name = game.display_name
# Vérifier les filtres de région
if self.region_filters:
game_regions = self.get_game_regions(game_name)
if has_region_excludes:
game_regions = self.get_cached_regions(game)
# Vérifier si le jeu a au moins une région incluse
has_included_region = False
@@ -244,7 +263,7 @@ class GameFilters:
continue
# Filtrer les non-release
if self.hide_non_release and self.is_non_release_game(game_name):
if self.hide_non_release and self.get_cached_non_release(game):
continue
filtered_games.append(game)
@@ -260,8 +279,7 @@ class GameFilters:
games_by_base = {}
for game in games:
game_name = game.display_name
base_name = self.get_base_game_name(game_name)
base_name = self.get_cached_base_name(game)
if base_name not in games_by_base:
games_by_base[base_name] = []
@@ -276,7 +294,7 @@ class GameFilters:
else:
# Trier par priorité de région
sorted_games = sorted(game_list,
key=lambda g: self.get_region_priority(g.display_name))
key=self.get_region_priority)
result.append(sorted_games[0])
return result

View File

@@ -119,18 +119,39 @@ def clear_history():
try:
# Charger l'historique actuel
current_history = load_history()
# Conserver uniquement les entrées avec statut actif (téléchargement, extraction ou conversion en cours)
# Supporter les deux variantes de statut (anglais et français)
active_statuses = {"Downloading", "Téléchargement", "downloading", "Extracting", "Converting", "Queued"}
preserved_entries = [
entry for entry in current_history
if entry.get("status") in active_statuses
]
# Sauvegarder l'historique filtré
with open(history_path, "w", encoding='utf-8') as f:
json.dump(preserved_entries, f, indent=2, ensure_ascii=False)
active_task_ids = set(getattr(config, 'download_tasks', {}).keys())
active_progress_urls = set(getattr(config, 'download_progress', {}).keys())
queued_urls = {
item.get("url") for item in getattr(config, 'download_queue', [])
if isinstance(item, dict) and item.get("url")
}
queued_task_ids = {
item.get("task_id") for item in getattr(config, 'download_queue', [])
if isinstance(item, dict) and item.get("task_id")
}
def is_truly_active(entry):
if not isinstance(entry, dict):
return False
status = entry.get("status")
if status not in active_statuses:
return False
task_id = entry.get("task_id")
url = entry.get("url")
if status == "Queued":
return task_id in queued_task_ids or url in queued_urls
return task_id in active_task_ids or url in active_progress_urls
preserved_entries = [entry for entry in current_history if is_truly_active(entry)]
save_history(preserved_entries)
removed_count = len(current_history) - len(preserved_entries)
logger.info(f"Historique vidé : {history_path} ({removed_count} entrées supprimées, {len(preserved_entries)} conservées)")
@@ -140,6 +161,115 @@ def clear_history():
# ==================== GESTION DES JEUX TÉLÉCHARGÉS ====================
IGNORED_ROM_SCAN_EXTENSIONS = {
'.bak', '.bmp', '.db', '.gif', '.ini', '.jpeg', '.jpg', '.json', '.log', '.mp4',
'.nfo', '.pdf', '.png', '.srm', '.sav', '.state', '.svg', '.txt', '.webp', '.xml'
}
def normalize_downloaded_game_name(game_name):
"""Normalise un nom de jeu pour les comparaisons en ignorant l'extension."""
if not isinstance(game_name, str):
return ""
normalized = os.path.basename(game_name.strip())
if not normalized:
return ""
return os.path.splitext(normalized)[0].strip().lower()
def _normalize_downloaded_games_dict(downloaded):
"""Normalise la structure de downloaded_games.json en restant rétrocompatible."""
normalized_downloaded = {}
if not isinstance(downloaded, dict):
return normalized_downloaded
for platform_name, games in downloaded.items():
if not isinstance(platform_name, str):
continue
if not isinstance(games, dict):
continue
normalized_games = {}
for game_name, metadata in games.items():
normalized_name = normalize_downloaded_game_name(game_name)
if not normalized_name:
continue
normalized_games[normalized_name] = metadata if isinstance(metadata, dict) else {}
if normalized_games:
normalized_downloaded[platform_name] = normalized_games
return normalized_downloaded
def _count_downloaded_games(downloaded_games_dict):
return sum(len(games) for games in downloaded_games_dict.values() if isinstance(games, dict))
def scan_roms_for_downloaded_games():
"""Scanne les dossiers ROMs et ajoute les jeux trouvés à downloaded_games.json."""
from utils import load_games
downloaded = _normalize_downloaded_games_dict(getattr(config, 'downloaded_games', {}))
platform_dicts = list(getattr(config, 'platform_dicts', []) or [])
if not platform_dicts:
return 0, 0
scanned_platforms = 0
added_games = 0
for platform_entry in platform_dicts:
if not isinstance(platform_entry, dict):
continue
platform_name = (platform_entry.get('platform_name') or '').strip()
folder_name = (platform_entry.get('folder') or '').strip()
if not platform_name or not folder_name:
continue
roms_path = os.path.join(config.ROMS_FOLDER, folder_name)
if not os.path.isdir(roms_path):
continue
available_games = load_games(platform_name)
available_names = {
normalize_downloaded_game_name(game.name)
for game in available_games
if normalize_downloaded_game_name(game.name)
}
if not available_names:
continue
platform_games = downloaded.setdefault(platform_name, {})
scanned_platforms += 1
for root, _, filenames in os.walk(roms_path):
for filename in filenames:
file_ext = os.path.splitext(filename)[1].lower()
if file_ext in IGNORED_ROM_SCAN_EXTENSIONS:
continue
normalized_name = normalize_downloaded_game_name(filename)
if not normalized_name or normalized_name not in available_names:
continue
if normalized_name not in platform_games:
platform_games[normalized_name] = {}
added_games += 1
config.downloaded_games = downloaded
save_downloaded_games(downloaded)
logger.info(
"Scan ROMs terminé : %s jeux ajoutés sur %s plateformes",
added_games,
scanned_platforms,
)
return added_games, scanned_platforms
def load_downloaded_games():
"""Charge la liste des jeux déjà téléchargés depuis downloaded_games.json."""
downloaded_path = getattr(config, 'DOWNLOADED_GAMES_PATH')
@@ -162,9 +292,10 @@ def load_downloaded_games():
if not isinstance(downloaded, dict):
logger.warning(f"Format downloaded_games.json invalide (pas un dict)")
return {}
logger.debug(f"Jeux téléchargés chargés : {sum(len(v) for v in downloaded.values())} jeux")
return downloaded
normalized_downloaded = _normalize_downloaded_games_dict(downloaded)
logger.debug(f"Jeux téléchargés chargés : {_count_downloaded_games(normalized_downloaded)} jeux")
return normalized_downloaded
except (FileNotFoundError, json.JSONDecodeError) as e:
logger.error(f"Erreur lors de la lecture de {downloaded_path} : {e}")
return {}
@@ -177,17 +308,18 @@ def save_downloaded_games(downloaded_games_dict):
"""Sauvegarde la liste des jeux téléchargés dans downloaded_games.json."""
downloaded_path = getattr(config, 'DOWNLOADED_GAMES_PATH')
try:
normalized_downloaded = _normalize_downloaded_games_dict(downloaded_games_dict)
os.makedirs(os.path.dirname(downloaded_path), exist_ok=True)
# Écriture atomique
temp_path = downloaded_path + '.tmp'
with open(temp_path, "w", encoding='utf-8') as f:
json.dump(downloaded_games_dict, f, indent=2, ensure_ascii=False)
json.dump(normalized_downloaded, f, indent=2, ensure_ascii=False)
f.flush()
os.fsync(f.fileno())
os.replace(temp_path, downloaded_path)
logger.debug(f"Jeux téléchargés sauvegardés : {sum(len(v) for v in downloaded_games_dict.values())} jeux")
logger.debug(f"Jeux téléchargés sauvegardés : {_count_downloaded_games(normalized_downloaded)} jeux")
except Exception as e:
logger.error(f"Erreur lors de l'écriture de {downloaded_path} : {e}")
try:
@@ -200,21 +332,22 @@ def save_downloaded_games(downloaded_games_dict):
def mark_game_as_downloaded(platform_name, game_name, file_size=None):
"""Marque un jeu comme téléchargé."""
downloaded = config.downloaded_games
normalized_name = normalize_downloaded_game_name(game_name)
if not normalized_name:
return
if platform_name not in downloaded:
downloaded[platform_name] = {}
downloaded[platform_name][game_name] = {
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"size": file_size or "N/A"
}
downloaded[platform_name][normalized_name] = {}
# Sauvegarder immédiatement
save_downloaded_games(downloaded)
logger.info(f"Jeu marqué comme téléchargé : {platform_name} / {game_name}")
logger.info(f"Jeu marqué comme téléchargé : {platform_name} / {normalized_name}")
def is_game_downloaded(platform_name, game_name):
"""Vérifie si un jeu a déjà été téléchargé."""
downloaded = config.downloaded_games
return platform_name in downloaded and game_name in downloaded.get(platform_name, {})
normalized_name = normalize_downloaded_game_name(game_name)
return bool(normalized_name) and platform_name in downloaded and normalized_name in downloaded.get(platform_name, {})

View File

@@ -114,14 +114,14 @@ def get_text(key, default=None):
pass
return str(default) if default is not None else str(key)
def get_available_languages():
def get_available_languages() -> list[str]:
"""Récupère la liste des langues disponibles."""
if not os.path.exists(config.LANGUAGES_FOLDER):
logger.warning(f"Dossier des langues {config.LANGUAGES_FOLDER} non trouvé")
return []
languages = []
languages: list[str] = []
for file in os.listdir(config.LANGUAGES_FOLDER):
if file.endswith(".json"):
lang_code = os.path.splitext(file)[0]

View File

@@ -24,7 +24,11 @@
"game_count": "{0} ({1} Spiele)",
"game_filter": "Aktiver Filter: {0}",
"game_search": "Filtern: {0}",
"global_search_title": "Globale Suche: {0}",
"global_search_empty_query": "Geben Sie einen Namen ein, um alle Systeme zu durchsuchen",
"global_search_no_results": "Keine Ergebnisse fur: {0}",
"game_header_name": "Name",
"game_header_ext": "Ext",
"game_header_size": "Größe",
"history_title": "Downloads ({0})",
"history_empty": "Keine Downloads im Verlauf",
@@ -111,6 +115,7 @@
"controls_action_clear_history": "Verlauf leeren",
"controls_action_history": "Verlauf / Downloads",
"controls_action_close_history": "Verlauf schließen",
"history_column_folder": "Ordner",
"controls_action_queue": "Warteschlange",
"controls_action_delete": "Löschen",
"controls_action_space": "Leerzeichen",
@@ -144,6 +149,8 @@
"controls_confirm_select": "Bestätigen/Auswählen",
"controls_cancel_back": "Abbrechen/Zurück",
"controls_filter_search": "Filtern/Suchen",
"controls_action_edit_search": "Suche bearbeiten",
"controls_action_show_results": "Ergebnisse zeigen",
"network_download_failed": "Download nach {0} Versuchen fehlgeschlagen",
"network_api_error": "Fehler bei der API-Anfrage, der Schlüssel könnte falsch sein: {0}",
"network_download_error": "Downloadfehler {0}: {1}",
@@ -232,6 +239,7 @@
"instruction_games_history": "Vergangene Downloads und Status anzeigen",
"instruction_games_source_mode": "Zwischen RGSX oder eigener Quellliste wechseln",
"instruction_games_update_cache": "Aktuelle Spieleliste erneut herunterladen & aktualisieren",
"instruction_games_scan_owned": "ROM-Ordner scannen und bereits vorhandene Spiele markieren",
"instruction_settings_music": "Hintergrundmusik aktivieren oder deaktivieren",
"instruction_settings_symlink": "Verwendung von Symlinks für Installationen umschalten",
"instruction_settings_auto_extract": "Automatische Archivextraktion nach Download aktivieren/deaktivieren",
@@ -258,6 +266,9 @@
"settings_web_service_success_disabled": "Web-Dienst beim Booten deaktiviert",
"settings_web_service_error": "Fehler: {0}",
"settings_custom_dns": "Custom DNS beim Booten",
"menu_scan_owned_roms": "Vorhandene ROMs scannen",
"popup_scan_owned_roms_done": "ROM-Scan abgeschlossen: {0} Spiele auf {1} Plattformen hinzugefügt",
"popup_scan_owned_roms_error": "ROM-Scan-Fehler: {0}",
"settings_custom_dns_enabled": "Aktiviert",
"settings_custom_dns_disabled": "Deaktiviert",
"settings_custom_dns_enabling": "Custom DNS wird aktiviert...",

View File

@@ -24,7 +24,11 @@
"game_count": "{0} ({1} games)",
"game_filter": "Active filter: {0}",
"game_search": "Filter: {0}",
"global_search_title": "Global search: {0}",
"global_search_empty_query": "Type a game name to search across all systems",
"global_search_no_results": "No results for: {0}",
"game_header_name": "Name",
"game_header_ext": "Ext",
"game_header_size": "Size",
"history_title": "Downloads ({0})",
"history_empty": "No downloads in history",
@@ -112,6 +116,7 @@
"support_dialog_error": "Error generating support file:\n{0}\n\nPress {1} to return to the menu.",
"controls_action_history": "History / Downloads",
"controls_action_close_history": "Close History",
"history_column_folder": "Folder",
"network_checking_updates": "Update in progress please wait...",
"network_update_available": "Update available: {0}",
"network_extracting_update": "Extracting update...",
@@ -165,6 +170,8 @@
"controls_confirm_select": "Confirm/Select",
"controls_cancel_back": "Cancel/Back",
"controls_filter_search": "Filter/Search",
"controls_action_edit_search": "Edit search",
"controls_action_show_results": "Show results",
"symlink_option_enabled": "Symlink option enabled",
"symlink_option_disabled": "Symlink option disabled",
"menu_games_source_prefix": "Game source",
@@ -234,6 +241,7 @@
"instruction_games_history": "List past downloads and statuses",
"instruction_games_source_mode": "Switch between RGSX or your own custom list source",
"instruction_games_update_cache": "Redownload & refresh current games list",
"instruction_games_scan_owned": "Scan your ROM folders and mark matching games as already owned",
"instruction_settings_music": "Enable or disable background music playback",
"instruction_settings_symlink": "Toggle using filesystem symlinks for installs",
"instruction_settings_auto_extract": "Toggle automatic archive extraction after download",
@@ -298,6 +306,9 @@
"history_option_delete_game": "Delete game",
"history_option_error_info": "Error details",
"history_option_retry": "Retry download",
"menu_scan_owned_roms": "Scan owned ROMs",
"popup_scan_owned_roms_done": "ROM scan complete: {0} games added across {1} platforms",
"popup_scan_owned_roms_error": "ROM scan error: {0}",
"history_option_back": "Back",
"history_folder_path_label": "Destination path:",
"history_scraper_not_implemented": "Scraper not yet implemented",

View File

@@ -24,7 +24,11 @@
"game_count": "{0} ({1} juegos)",
"game_filter": "Filtro activo: {0}",
"game_search": "Filtrar: {0}",
"global_search_title": "Busqueda global: {0}",
"global_search_empty_query": "Escribe un nombre para buscar en todas las consolas",
"global_search_no_results": "Sin resultados para: {0}",
"game_header_name": "Nombre",
"game_header_ext": "Ext",
"game_header_size": "Tamaño",
"history_title": "Descargas ({0})",
"history_empty": "No hay descargas en el historial",
@@ -109,6 +113,7 @@
"controls_action_clear_history": "Vaciar historial",
"controls_action_history": "Historial / Descargas",
"controls_action_close_history": "Cerrar Historial",
"history_column_folder": "Carpeta",
"controls_action_delete": "Eliminar",
"controls_action_space": "Espacio",
"controls_action_start": "Ayuda / Configuración",
@@ -142,6 +147,8 @@
"controls_confirm_select": "Confirmar/Seleccionar",
"controls_cancel_back": "Cancelar/Volver",
"controls_filter_search": "Filtrar/Buscar",
"controls_action_edit_search": "Editar busqueda",
"controls_action_show_results": "Ver resultados",
"network_download_failed": "Error en la descarga tras {0} intentos",
"network_api_error": "Error en la solicitud de API, la clave puede ser incorrecta: {0}",
"network_download_error": "Error en la descarga {0}: {1}",
@@ -232,6 +239,7 @@
"instruction_games_history": "Ver descargas pasadas y su estado",
"instruction_games_source_mode": "Cambiar entre lista RGSX o fuente personalizada",
"instruction_games_update_cache": "Volver a descargar y refrescar la lista de juegos",
"instruction_games_scan_owned": "Escanear las carpetas ROMs y marcar los juegos que ya posees",
"instruction_settings_music": "Activar o desactivar música de fondo",
"instruction_settings_symlink": "Alternar uso de symlinks en instalaciones",
"instruction_settings_auto_extract": "Activar/desactivar extracción automática de archivos después de descargar",
@@ -258,6 +266,9 @@
"settings_web_service_success_disabled": "Servicio web desactivado al inicio",
"settings_web_service_error": "Error: {0}",
"settings_custom_dns": "DNS Personalizado al Inicio",
"menu_scan_owned_roms": "Escanear ROMs disponibles",
"popup_scan_owned_roms_done": "Escaneo ROM completado: {0} juegos añadidos en {1} plataformas",
"popup_scan_owned_roms_error": "Error al escanear ROMs: {0}",
"settings_custom_dns_enabled": "Activado",
"settings_custom_dns_disabled": "Desactivado",
"settings_custom_dns_enabling": "Activando DNS personalizado...",

View File

@@ -24,7 +24,11 @@
"game_count": "{0} ({1} jeux)",
"game_filter": "Filtre actif : {0}",
"game_search": "Filtrer : {0}",
"global_search_title": "Recherche globale : {0}",
"global_search_empty_query": "Saisissez un nom pour rechercher dans toutes les consoles",
"global_search_no_results": "Aucun resultat pour : {0}",
"game_header_name": "Nom",
"game_header_ext": "Ext",
"game_header_size": "Taille",
"history_title": "Téléchargements ({0})",
"history_empty": "Aucun téléchargement dans l'historique",
@@ -106,6 +110,7 @@
"controls_action_queue": "Mettre en file d'attente",
"controls_action_history": "Historique / Téléchargements",
"controls_action_close_history": "Fermer l'historique",
"history_column_folder": "Dossier",
"controls_action_delete": "Supprimer",
"controls_action_space": "Espace",
"controls_action_start": "Aide / Réglages",
@@ -138,6 +143,8 @@
"controls_confirm_select": "Confirmer/Sélectionner",
"controls_cancel_back": "Annuler/Retour",
"controls_filter_search": "Filtrer/Rechercher",
"controls_action_edit_search": "Modifier recherche",
"controls_action_show_results": "Voir resultats",
"network_download_failed": "Échec du téléchargement après {0} tentatives",
"network_api_error": "Erreur lors de la requête API, la clé est peut-être incorrecte: {0}",
"network_download_error": "Erreur téléchargement {0}: {1}",
@@ -234,6 +241,7 @@
"instruction_games_history": "Lister les téléchargements passés et leur statut",
"instruction_games_source_mode": "Basculer entre liste RGSX ou source personnalisée",
"instruction_games_update_cache": "Retélécharger & rafraîchir la liste des jeux",
"instruction_games_scan_owned": "Scanner les dossiers ROMs et marquer les jeux déjà possédés",
"instruction_settings_music": "Activer ou désactiver la lecture musicale",
"instruction_settings_symlink": "Basculer l'utilisation de symlinks pour l'installation",
"instruction_settings_auto_extract": "Activer/désactiver l'extraction automatique des archives après téléchargement",
@@ -298,6 +306,9 @@
"history_option_delete_game": "Supprimer le jeu",
"history_option_error_info": "Détails de l'erreur",
"history_option_retry": "Retenter le téléchargement",
"menu_scan_owned_roms": "Scanner les ROMs présentes",
"popup_scan_owned_roms_done": "Scan ROMs terminé : {0} jeux ajoutés sur {1} plateformes",
"popup_scan_owned_roms_error": "Erreur scan ROMs : {0}",
"history_option_back": "Retour",
"history_folder_path_label": "Chemin de destination :",
"history_scraper_not_implemented": "Scraper pas encore implémenté",

View File

@@ -24,7 +24,11 @@
"game_count": "{0} ({1} giochi)",
"game_filter": "Filtro attivo: {0}",
"game_search": "Filtro: {0}",
"global_search_title": "Ricerca globale: {0}",
"global_search_empty_query": "Digita un nome per cercare in tutte le console",
"global_search_no_results": "Nessun risultato per: {0}",
"game_header_name": "Nome",
"game_header_ext": "Ext",
"game_header_size": "Dimensione",
"history_title": "Download ({0})",
"history_empty": "Nessun download nella cronologia",
@@ -107,6 +111,7 @@
"controls_action_clear_history": "Cancella cronologia",
"controls_action_history": "Cronologia / Downloads",
"controls_action_close_history": "Chiudi Cronologia",
"history_column_folder": "Cartella",
"controls_action_delete": "Elimina",
"controls_action_space": "Spazio",
"controls_action_start": "Aiuto / Impostazioni",
@@ -164,6 +169,8 @@
"controls_confirm_select": "Conferma/Seleziona",
"controls_cancel_back": "Annulla/Indietro",
"controls_filter_search": "Filtro/Ricerca",
"controls_action_edit_search": "Modifica ricerca",
"controls_action_show_results": "Mostra risultati",
"games_source_rgsx": "RGSX",
"sources_mode_rgsx_select_info": "RGSX: aggiorna l'elenco dei giochi",
"games_source_custom": "Personalizzato",
@@ -227,6 +234,7 @@
"instruction_games_history": "Elencare download passati e stato",
"instruction_games_source_mode": "Passare tra elenco RGSX o sorgente personalizzata",
"instruction_games_update_cache": "Riscaria e aggiorna l'elenco dei giochi",
"instruction_games_scan_owned": "Scansiona le cartelle ROMs e segna i giochi gia posseduti",
"instruction_settings_music": "Abilitare o disabilitare musica di sottofondo",
"instruction_settings_symlink": "Abilitare/disabilitare uso symlink per installazioni",
"instruction_settings_auto_extract": "Attivare/disattivare estrazione automatica archivi dopo il download",
@@ -258,6 +266,9 @@
"settings_custom_dns_enabling": "Abilitazione DNS personalizzato...",
"settings_custom_dns_disabling": "Disabilitazione DNS personalizzato...",
"settings_custom_dns_success_enabled": "DNS personalizzato abilitato all'avvio (1.1.1.1)",
"menu_scan_owned_roms": "Scansiona ROM presenti",
"popup_scan_owned_roms_done": "Scansione ROM completata: {0} giochi aggiunti su {1} piattaforme",
"popup_scan_owned_roms_error": "Errore scansione ROM: {0}",
"settings_custom_dns_success_disabled": "DNS personalizzato disabilitato all'avvio",
"controls_desc_confirm": "Confermare (es. A/Croce)",
"controls_desc_cancel": "Annullare/Indietro (es. B/Cerchio)",

View File

@@ -24,7 +24,11 @@
"game_count": "{0} ({1} jogos)",
"game_filter": "Filtro ativo: {0}",
"game_search": "Filtro: {0}",
"global_search_title": "Busca global: {0}",
"global_search_empty_query": "Digite um nome para buscar em todos os consoles",
"global_search_no_results": "Nenhum resultado para: {0}",
"game_header_name": "Nome",
"game_header_ext": "Ext",
"game_header_size": "Tamanho",
"history_title": "Downloads ({0})",
"history_empty": "Nenhum download no histórico",
@@ -111,6 +115,7 @@
"controls_action_clear_history": "Limpar histórico",
"controls_action_history": "Histórico / Downloads",
"controls_action_close_history": "Fechar Histórico",
"history_column_folder": "Pasta",
"controls_action_delete": "Deletar",
"controls_action_space": "Espaço",
"controls_action_start": "Ajuda / Configurações",
@@ -167,6 +172,8 @@
"controls_confirm_select": "Confirmar/Selecionar",
"controls_cancel_back": "Cancelar/Voltar",
"controls_filter_search": "Filtrar/Buscar",
"controls_action_edit_search": "Editar busca",
"controls_action_show_results": "Ver resultados",
"symlink_option_enabled": "Opção de symlink ativada",
"symlink_option_disabled": "Opção de symlink desativada",
"menu_games_source_prefix": "Fonte de jogos",
@@ -233,6 +240,7 @@
"instruction_games_history": "Listar downloads anteriores e status",
"instruction_games_source_mode": "Alternar entre lista RGSX ou fonte personalizada",
"instruction_games_update_cache": "Baixar novamente e atualizar a lista de jogos",
"instruction_games_scan_owned": "Verificar as pastas ROMs e marcar os jogos ja existentes",
"instruction_settings_music": "Ativar ou desativar música de fundo",
"instruction_settings_symlink": "Ativar/desativar uso de symlinks para instalações",
"instruction_settings_auto_extract": "Ativar/desativar extração automática de arquivos após download",
@@ -258,6 +266,9 @@
"settings_web_service_success_enabled": "Serviço web ativado na inicialização",
"settings_web_service_success_disabled": "Serviço web desativado na inicialização",
"settings_web_service_error": "Erro: {0}",
"menu_scan_owned_roms": "Verificar ROMs existentes",
"popup_scan_owned_roms_done": "Verificacao de ROMs concluida: {0} jogos adicionados em {1} plataformas",
"popup_scan_owned_roms_error": "Erro ao verificar ROMs: {0}",
"settings_custom_dns": "DNS Personalizado na Inicialização",
"settings_custom_dns_enabled": "Ativado",
"settings_custom_dns_disabled": "Desativado",

View File

@@ -1,3 +1,3 @@
{
"version": "2.5.0.6"
"version": "2.6.0.0"
}