mirror of
https://github.com/RetroGameSets/RGSX.git
synced 2026-03-20 08:45:41 +01:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbb1a2aa68 |
@@ -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é
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"cancel": {
|
||||
"type": "key",
|
||||
"key": 27,
|
||||
"display": "\u00c9chap"
|
||||
"display": "Esc/Echap"
|
||||
},
|
||||
"up": {
|
||||
"type": "key",
|
||||
|
||||
@@ -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é)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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-",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, {})
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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é",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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",
|
||||
|
||||
BIN
snes/3-ji no Wide Shou - 5 Gatsugou (Japan) (Magazine)[!].bs
Normal file
BIN
snes/3-ji no Wide Shou - 5 Gatsugou (Japan) (Magazine)[!].bs
Normal file
Binary file not shown.
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2.5.0.6"
|
||||
"version": "2.6.0.0"
|
||||
}
|
||||
Reference in New Issue
Block a user