Compare commits

..

14 Commits

Author SHA1 Message Date
skymike03
920914b374 v2.4.0.1 (2026.01.07 bis)
- remove windowed mode (useless)
2026-01-07 14:36:00 +01:00
skymike03
a326cb0b67 Merge branch 'main' of https://github.com/RetroGameSets/RGSX 2026-01-07 14:25:18 +01:00
skymike03
c9fdf93221 v2.4.0.0 (2026.01.07)
- add performance mode to disable some 3d effects for a better experience on low-end devices
- multi screen support (choose default screen in Pause>Display menu )
- update windows launcher for retrobat
- ignore some useless warnings
- Merge pull request [#34](https://github.com/RetroGameSets/RGSX/issues/34) from aaronson2012/start-menu-fix
(feat: enable circular navigation for pause menu options)
2026-01-07 14:25:15 +01:00
RGS
184a8c64fe Update troubleshooting steps for app crashes 2026-01-03 16:37:17 +01:00
RGS
9a2e4ce0db Merge pull request #34 from aaronson2012/start-menu-fix
feat: enable circular navigation for pause menu options
2025-12-06 13:06:31 +01:00
Jacob Christie
73eceeb777 feat: enable circular navigation for pause menu options 2025-12-06 05:26:16 -06:00
RGS
2fcc4ca6df Update README to include troubleshooting link
Added a link to the troubleshooting section in the README.
2025-11-25 19:54:03 +01:00
RGS
2ed889540b Enhance troubleshooting section in README
Updated troubleshooting solutions for controls and games visibility issues.
2025-11-25 19:50:20 +01:00
skymike03
e9a610b5dd v2.3.3.3
- Enhance download queue functionality to stop download, continue queue, remove games  and update related UI options
2025-11-25 19:21:12 +01:00
skymike03
bd3b885736 v2.3.3.2
- Fix French print statements for consistency in output messages
- improve filtering  in game list to be permanent, even in search mode, and more efficient for games that have foreign language on other region
2025-11-25 18:40:13 +01:00
skymike03
1592671ddc v2.3.3.1 (2025.11.24)
- correct menu control handling bug
- enhance UI elements for improved user experience
2025-11-24 22:31:53 +01:00
skymike03
4e029aabf1 Remove unnecessary files from RGSX package builds to streamline release process 2025-11-24 13:16:52 +01:00
skymike03
970fcaf197 Update installation instructions for clarity and add manual update notes 2025-11-24 13:14:30 +01:00
skymike03
ff30e6d297 v2.3.3.0 (2025.11.23)
- add a workaround to github update checking
2025-11-23 16:00:38 +01:00
18 changed files with 1518 additions and 446 deletions

View File

@@ -30,16 +30,9 @@ jobs:
zip -r "../../dist/RGSX_update_latest.zip" . \
-x "logs/*" \
"logs/**" \
"images/*" \
"images/**" \
"games/*" \
"games/**" \
"scripts/*" \
"scripts/**" \
"__pycache__/*" \
"__pycache__/**" \
"*.pyc" \
"sources.json" \
"*.log"
cd ../..
@@ -52,16 +45,9 @@ jobs:
zip -r "dist/RGSX_full_latest.zip" ports windows \
-x "ports/RGSX/logs/*" \
"ports/RGSX/logs/**" \
"ports/RGSX/images/*" \
"ports/RGSX/images/**" \
"ports/RGSX/games/*" \
"ports/RGSX/games/**" \
"ports/RGSX/scripts/*" \
"ports/RGSX/scripts/**" \
"ports/RGSX/__pycache__/*" \
"ports/RGSX/__pycache__/**" \
"ports/RGSX/*.pyc" \
"ports/RGSX/sources.json" \
"ports/RGSX/*.log" \
"windows/logs/*" \
"windows/*.xml" \
@@ -88,12 +74,12 @@ jobs:
## 📥 Automatic Installation (Only for batocera Knulli)
### ON PC :
### ON PC , NUC, SteamDeck or any x86_64 based:
1. Open File Manager (F1) then "Applications" and launch xterm
2. Use the command line `curl -L bit.ly/rgsx-install | sh`
3. Launch RGSX from "Ports" menu
### ON RASPBERRY/ARM SBC / HANDHELD :
### ON RASPBERRY/ ARM based SBC / HANDHELD :
1. Connect your device with SSH on a computer/smartphone connected to same network (ssh root@IPADDRESS , pass:linux)
2. Use the command line `curl -L bit.ly/rgsx-install | sh`
3. Launch RGSX from "Ports" menu
@@ -111,17 +97,12 @@ jobs:
3. Launch RGSX from system "Windows"
## 📥 Manual Update (you shouldn't need to do this as RGSX updates automatically on each start)
#### Batocera/Knulli
## 📥 Manual Update (only if automatic doesn't work for some obcure reason)
#### Batocera/Knulli/Retrobat
1. Download latest update : https://github.com/RetroGameSets/RGSX/releases/latest/download/RGSX_update_latest.zip
2. Extract only PORTS folder in `/userdata/roms/`
3. Launch RGSX from the Ports menu
#### Retrobat
1. Download latest update : https://github.com/RetroGameSets/RGSX/releases/latest/download/RGSX_update_latest.zip
2. Extract all folders in your Retrobat\roms folder
3. Launch RGSX from system "Windows"
2. Extract zip content on in `/userdata/roms/ports/RGSX`
3. Launch RGSX
### 📖 Documentation

View File

@@ -1,6 +1,6 @@
# 🎮 Retro Game Sets Xtra (RGSX)
**[Discord Support](https://discord.gg/Vph9jwg3VV)** • **[Installation](#-installation)** • **[French Documentation](https://github.com/RetroGameSets/RGSX/blob/main/README_FR.md)**
**[Discord Support](https://discord.gg/Vph9jwg3VV)** • **[Installation](#-installation)** • **[French Documentation](https://github.com/RetroGameSets/RGSX/blob/main/README_FR.md)****[Troubleshoot / Common Errors](https://github.com/RetroGameSets/RGSX#%EF%B8%8F-troubleshooting)** •
A free, user-friendly ROM downloader for Batocera, Knulli, and RetroBat with multi-source support.
@@ -201,11 +201,12 @@ RGSX includes a web interface that launched automatically when using RGSX for re
| Issue | Solution |
|-------|----------|
| Controls not working | Delete `/saves/ports/rgsx/controls.json` + restart |
| Games not showing | Pause Menu > Games > Update Game Cache |
| Download stuck | Check API keys in `/saves/ports/rgsx/` |
| App crashes | Check `/roms/ports/RGSX/logs/RGSX.log` |
| Controls not working | Delete `/saves/ports/rgsx/controls.json` + restart app, you can try delete /roms/ports/RGSX/assets/controls/xx.json too |
| No games ? | Pause Menu > Games > Update Game Cache |
| Missing systems on the list? | RGSX read es_systems.cfg to show only supported systems, if you want all systems : Pause Menu > Games > Show unsupported systems |
| App crashes | Check `/roms/ports/RGSX/logs/RGSX.log` or `/roms/windows/logs/Retrobat_RGSX_log.txt` |
| Layout change not applied | Restart RGSX after changing layout |
| Downloading BIOS file is ok but you can't download any games? | Activate custom DNS on Pause Menu> Settings and reboot , server can be blocked by your ISP. check any threat/website protection on your router too, especially on ASUS one|
**Need help?** Share logs from `/roms/ports/RGSX/logs/` on [Discord](https://discord.gg/Vph9jwg3VV).

View File

@@ -1,5 +1,11 @@
import os
import platform
import warnings
# Ignorer le warning de deprecation de pkg_resources dans pygame
warnings.filterwarnings("ignore", category=UserWarning, module="pygame.pkgdata")
warnings.filterwarnings("ignore", message="pkg_resources is deprecated")
# Ne pas forcer SDL_FBDEV ici; si déjà défini par l'environnement, on le garde
try:
if "SDL_FBDEV" in os.environ:
@@ -28,7 +34,7 @@ from display import (
draw_toast, show_toast, THEME_COLORS
)
from language import _
from network import test_internet, download_rom, is_1fichier_url, download_from_1fichier, check_for_updates, cancel_all_downloads
from network import test_internet, download_rom, is_1fichier_url, download_from_1fichier, check_for_updates, cancel_all_downloads, download_queue_worker
from controls import handle_controls, validate_menu_state, process_key_repeats, get_emergency_controls
from controls_mapper import map_controls, draw_controls_mapping, get_actions
from controls import load_controls_config
@@ -213,7 +219,7 @@ except Exception:
normalized_names = [n.lower() for n in joystick_names]
if not joystick_names:
joystick_names = ["Clavier"]
print("Aucun joystick détecté, utilisation du clavier par défaut")
print("Aucun joystick detecte, utilisation du clavier par defaut")
logger.debug("Aucun joystick détecté, utilisation du clavier par défaut.")
config.joystick = False
config.keyboard = True
@@ -438,6 +444,11 @@ async def main():
# Démarrer le serveur web en arrière-plan
start_web_server()
# Démarrer le worker de la queue de téléchargement
queue_worker_thread = threading.Thread(target=download_queue_worker, daemon=True)
queue_worker_thread.start()
logger.info("Worker de la queue de téléchargement démarré")
running = True
loading_step = "none"
sources = []
@@ -473,7 +484,7 @@ async def main():
config.needs_redraw = True
last_redraw_time = current_time
# Forcer redraw toutes les 100 ms dans history avec téléchargement actif
if config.menu_state == "history" and any(entry["status"] == "Téléchargement" for entry in config.history):
if config.menu_state == "history" and any(entry["status"] in ["Downloading", "Téléchargement"] for entry in config.history):
if current_time - last_redraw_time >= 100:
config.needs_redraw = True
last_redraw_time = current_time

View File

@@ -13,7 +13,7 @@ except Exception:
pygame = None # type: ignore
# Version actuelle de l'application
app_version = "2.3.2.9"
app_version = "2.4.0.1"
def get_application_root():
@@ -133,6 +133,7 @@ logger = logging.getLogger(__name__)
# File d'attente de téléchargements (jobs en attente)
download_queue = [] # Liste de dicts: {url, platform, game_name, ...}
pending_download_is_queue = False # Indique si pending_download doit être ajouté à la queue
# Indique si un téléchargement est en cours
download_active = False

View File

@@ -159,7 +159,7 @@ def load_controls_config(path=CONTROLS_CONFIG_PATH):
dev_field = preset.get('device') if isinstance(preset, dict) else None
if isinstance(dev_field, str) and _sanitize(dev_field) == target_norm:
logging.getLogger(__name__).info(f"Chargement préréglage (device) depuis le fichier: {fname}")
print(f"Chargement préréglage (device) depuis le fichier: {fname}")
print(f"Chargement prereglage (device) depuis le fichier: {fname}")
return preset
except Exception as e:
logging.getLogger(__name__).warning(f"Échec scan préréglages par device: {e}")
@@ -559,7 +559,11 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
elif is_input_matched(event, "confirm"):
config.search_query += keyboard_layout[row][col]
config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()]
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
base_games = config.games
if config.game_filter_obj and config.game_filter_obj.is_active():
base_games = config.game_filter_obj.apply_filters(config.games)
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
config.current_game = 0
config.scroll_offset = 0
config.needs_redraw = True
@@ -567,14 +571,22 @@ def handle_controls(event, sources, joystick, screen):
elif is_input_matched(event, "delete"):
if config.search_query:
config.search_query = config.search_query[:-1]
config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()]
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
base_games = config.games
if config.game_filter_obj and config.game_filter_obj.is_active():
base_games = config.game_filter_obj.apply_filters(config.games)
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
config.current_game = 0
config.scroll_offset = 0
config.needs_redraw = True
#logger.debug(f"Suppression caractère: query={config.search_query}, jeux filtrés={len(config.filtered_games)}")
elif is_input_matched(event, "space"):
config.search_query += " "
config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()]
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
base_games = config.games
if config.game_filter_obj and config.game_filter_obj.is_active():
base_games = config.game_filter_obj.apply_filters(config.games)
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
config.current_game = 0
config.scroll_offset = 0
config.needs_redraw = True
@@ -583,7 +595,13 @@ def handle_controls(event, sources, joystick, screen):
config.search_mode = False
config.search_query = ""
config.selected_key = (0, 0)
config.filtered_games = config.games
# Restaurer les jeux filtrés par les filtres avancés si actifs
if hasattr(config, 'game_filter_obj') and config.game_filter_obj and config.game_filter_obj.is_active():
config.filtered_games = config.game_filter_obj.apply_filters(config.games)
config.filter_active = True
else:
config.filtered_games = config.games
config.filter_active = False
config.current_game = 0
config.scroll_offset = 0
config.needs_redraw = True
@@ -605,8 +623,13 @@ def handle_controls(event, sources, joystick, screen):
elif is_input_matched(event, "cancel"):
config.search_mode = False
config.search_query = ""
config.filtered_games = config.games
config.filter_active = False
# Restaurer les jeux filtrés par les filtres avancés si actifs
if hasattr(config, 'game_filter_obj') and config.game_filter_obj and config.game_filter_obj.is_active():
config.filtered_games = config.game_filter_obj.apply_filters(config.games)
config.filter_active = True
else:
config.filtered_games = config.games
config.filter_active = False
config.current_game = 0
config.scroll_offset = 0
config.needs_redraw = True
@@ -615,7 +638,11 @@ def handle_controls(event, sources, joystick, screen):
# Saisie de texte alphanumérique
if event.unicode.isalnum() or event.unicode == ' ':
config.search_query += event.unicode
config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()]
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
base_games = config.games
if config.game_filter_obj and config.game_filter_obj.is_active():
base_games = config.game_filter_obj.apply_filters(config.games)
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
config.current_game = 0
config.scroll_offset = 0
config.needs_redraw = True
@@ -624,7 +651,11 @@ def handle_controls(event, sources, joystick, screen):
elif is_input_matched(event, "delete"):
if config.search_query:
config.search_query = config.search_query[:-1]
config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()]
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
base_games = config.games
if config.game_filter_obj and config.game_filter_obj.is_active():
base_games = config.game_filter_obj.apply_filters(config.games)
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
config.current_game = 0
config.scroll_offset = 0
config.needs_redraw = True
@@ -709,6 +740,7 @@ def handle_controls(event, sources, joystick, screen):
# Si extension non supportée ET pas en archive connu, afficher avertissement
if (not is_supported and not zip_ok) and not allow_unknown:
config.pending_download = pending_download
config.pending_download_is_queue = True # Marquer comme action queue
config.previous_menu_state = config.menu_state
config.menu_state = "extension_warning"
config.extension_confirm_selection = 0
@@ -782,29 +814,69 @@ def handle_controls(event, sources, joystick, screen):
if config.extension_confirm_selection == 0: # 0 = Oui, 1 = Non
if config.pending_download and len(config.pending_download) == 4:
url, platform, game_name, is_zip_non_supported = config.pending_download
if is_1fichier_url(url):
ensure_download_provider_keys(False)
# Avertissement si pas de clé (utilisation mode gratuit)
if missing_all_provider_keys():
logger.warning("Aucune clé API - Mode gratuit 1fichier sera utilisé (attente requise)")
# Vérifier si c'est une action queue
is_queue_action = getattr(config, 'pending_download_is_queue', False)
if is_queue_action:
# Ajouter à la queue au lieu de télécharger immédiatement
task_id = str(pygame.time.get_ticks())
task = asyncio.create_task(download_from_1fichier(url, platform, game_name, is_zip_non_supported, task_id))
queue_item = {
'url': url,
'platform': platform,
'game_name': game_name,
'is_zip_non_supported': is_zip_non_supported,
'is_1fichier': is_1fichier_url(url),
'task_id': task_id,
'status': 'Queued'
}
config.download_queue.append(queue_item)
# Ajouter une entrée à l'historique avec status "Queued"
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)
# Afficher un toast de notification
show_toast(f"{game_name}\n{_('download_queued')}")
# Le worker de la queue détectera automatiquement le nouvel élément
logger.debug(f"{game_name} ajouté à la file d'attente après confirmation. Queue size: {len(config.download_queue)}")
else:
task_id = str(pygame.time.get_ticks())
task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported, task_id))
config.download_tasks[task_id] = (task, url, game_name, platform)
# Afficher un toast de notification
show_toast(f"{_('download_started')}: {game_name}")
# Téléchargement immédiat
if is_1fichier_url(url):
ensure_download_provider_keys(False)
# Avertissement si pas de clé (utilisation mode gratuit)
if missing_all_provider_keys():
logger.warning("Aucune clé API - Mode gratuit 1fichier sera utilisé (attente requise)")
task_id = str(pygame.time.get_ticks())
task = asyncio.create_task(download_from_1fichier(url, platform, game_name, is_zip_non_supported, task_id))
else:
task_id = str(pygame.time.get_ticks())
task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported, task_id))
config.download_tasks[task_id] = (task, url, game_name, platform)
# Afficher un toast de notification
show_toast(f"{_('download_started')}: {game_name}")
logger.debug(f"[CONTROLS_EXT_WARNING] Téléchargement confirmé après avertissement: {game_name} pour {platform} depuis {url}, task_id={task_id}")
config.previous_menu_state = validate_menu_state(config.previous_menu_state)
config.needs_redraw = True
logger.debug(f"[CONTROLS_EXT_WARNING] Téléchargement confirmé après avertissement: {game_name} pour {platform} depuis {url}, task_id={task_id}")
config.pending_download = None
config.pending_download_is_queue = False
config.extension_confirm_selection = 0 # Réinitialiser la sélection
action = "download"
# Téléchargement simple - retourner au menu précédent
# Retourner au menu précédent
config.menu_state = config.previous_menu_state if config.previous_menu_state else "game"
logger.debug(f"[CONTROLS_EXT_WARNING] Retour au menu {config.menu_state} après confirmation")
else:
@@ -912,22 +984,35 @@ def handle_controls(event, sources, joystick, screen):
if is_input_matched(event, "confirm"):
if config.confirm_cancel_selection == 1: # Oui
entry = config.history[config.current_history_item]
task_id = entry.get("task_id")
url = entry.get("url")
# Annuler la tâche correspondante
for task_id, (task, task_url, game_name, platform) in list(config.download_tasks.items()):
if task_url == url:
game_name = entry.get("game_name", "Unknown")
# Annuler via cancel_events (pour les threads de téléchargement)
try:
request_cancel(task_id)
logger.debug(f"Signal d'annulation envoyé pour task_id={task_id}")
except Exception as e:
logger.debug(f"Erreur lors de l'envoi du signal d'annulation: {e}")
# Annuler aussi la tâche asyncio si elle existe (pour les téléchargements directs)
for tid, (task, task_url, tname, tplatform) in list(config.download_tasks.items()):
if tid == task_id or task_url == url:
try:
request_cancel(task_id)
except Exception:
pass
task.cancel()
del config.download_tasks[task_id]
entry["status"] = "Canceled"
entry["progress"] = 0
entry["message"] = _("download_canceled") if _ else "Download canceled"
save_history(config.history)
logger.debug(f"Téléchargement annulé: {game_name}")
task.cancel()
del config.download_tasks[tid]
logger.debug(f"Tâche asyncio annulée: {tname}")
except Exception as e:
logger.debug(f"Erreur lors de l'annulation de la tâche asyncio: {e}")
break
# Mettre à jour l'entrée historique
entry["status"] = "Canceled"
entry["progress"] = 0
entry["message"] = _("download_canceled") if _ else "Download canceled"
save_history(config.history)
logger.debug(f"Téléchargement annulé: {game_name}")
config.menu_state = "history"
config.needs_redraw = True
else: # Non
@@ -1003,7 +1088,13 @@ def handle_controls(event, sources, joystick, screen):
options.append("scraper")
# Options selon statut
if status == "Download_OK" or status == "Completed":
if status == "Queued":
# En attente dans la queue
options.append("remove_from_queue")
elif status in ["Downloading", "Téléchargement", "Extracting"]:
# Téléchargement en cours
options.append("cancel_download")
elif status == "Download_OK" or status == "Completed":
# Vérifier si c'est une archive ET si le fichier existe
if actual_filename and file_exists:
ext = os.path.splitext(actual_filename)[1].lower()
@@ -1046,7 +1137,37 @@ def handle_controls(event, sources, joystick, screen):
selected_option = options[sel]
logger.debug(f"history_game_options: CONFIRM option={selected_option}")
if selected_option == "download_folder":
if selected_option == "remove_from_queue":
# Retirer de la queue
task_id = entry.get("task_id")
url = entry.get("url")
# Chercher et retirer de la queue
for i, queue_item in enumerate(config.download_queue):
if queue_item.get("task_id") == task_id or queue_item.get("url") == url:
config.download_queue.pop(i)
logger.debug(f"Jeu retiré de la queue: {game_name}")
break
# Mettre à jour l'entrée historique avec status Canceled
entry["status"] = "Canceled"
entry["progress"] = 0
entry["message"] = _("download_canceled") if _ else "Download canceled"
save_history(config.history)
# Retour à l'historique
config.menu_state = "history"
config.needs_redraw = True
elif selected_option == "cancel_download":
# Rediriger vers le dialogue de confirmation (même que bouton cancel)
config.previous_menu_state = "history"
config.menu_state = "confirm_cancel_download"
config.confirm_cancel_selection = 0
config.needs_redraw = True
logger.debug("Redirection vers confirm_cancel_download depuis history_game_options")
elif selected_option == "download_folder":
# Afficher le chemin de destination
config.previous_menu_state = "history_game_options"
config.menu_state = "history_show_folder"
@@ -1482,8 +1603,15 @@ def handle_controls(event, sources, joystick, screen):
# Confirmation quitter
elif config.menu_state == "confirm_exit":
if is_input_matched(event, "confirm"):
if config.confirm_selection == 1:
# Sous-menu Quit: 0=Quit RGSX, 1=Restart RGSX, 2=Back
if is_input_matched(event, "up"):
config.confirm_selection = max(0, config.confirm_selection - 1)
config.needs_redraw = True
elif is_input_matched(event, "down"):
config.confirm_selection = min(2, config.confirm_selection + 1)
config.needs_redraw = True
elif is_input_matched(event, "confirm"):
if config.confirm_selection == 0: # Quit RGSX
# Mark all in-progress downloads as canceled in history
try:
for entry in getattr(config, 'history', []) or []:
@@ -1495,7 +1623,9 @@ def handle_controls(event, sources, joystick, screen):
except Exception:
pass
return "quit"
else:
elif config.confirm_selection == 1: # Restart RGSX
restart_application(2000)
elif config.confirm_selection == 2: # Back
# Retour à l'état capturé (confirm_exit_origin) sinon previous_menu_state sinon platform
target = getattr(config, 'confirm_exit_origin', getattr(config, 'previous_menu_state', 'platform'))
config.menu_state = validate_menu_state(target)
@@ -1505,11 +1635,18 @@ def handle_controls(event, sources, joystick, screen):
except Exception:
pass
config.needs_redraw = True
logger.debug(f"Retour à {config.menu_state} depuis confirm_exit (annulation)")
elif is_input_matched(event, "left") or is_input_matched(event, "right"):
config.confirm_selection = 1 - config.confirm_selection
logger.debug(f"Retour à {config.menu_state} depuis confirm_exit (back)")
elif is_input_matched(event, "cancel"):
# Retour à l'état capturé
target = getattr(config, 'confirm_exit_origin', getattr(config, 'previous_menu_state', 'platform'))
config.menu_state = validate_menu_state(target)
if hasattr(config, 'confirm_exit_origin'):
try:
delattr(config, 'confirm_exit_origin')
except Exception:
pass
config.needs_redraw = True
#logger.debug(f"Changement sélection confirm_exit: {config.confirm_selection}")
logger.debug(f"Retour à {config.menu_state} depuis confirm_exit (cancel)")
# Menu pause
elif config.menu_state == "pause_menu":
@@ -1521,46 +1658,46 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
logger.debug(f"Start: retour à {config.menu_state} depuis pause_menu")
elif is_input_matched(event, "up"):
config.selected_option = max(0, config.selected_option - 1)
# Menu racine hiérarchique: nombre dynamique (langue + catégories)
total = getattr(config, 'pause_menu_total_options', 7)
config.selected_option = (config.selected_option - 1) % total
config.needs_redraw = True
elif is_input_matched(event, "down"):
# Menu racine hiérarchique: nombre dynamique (langue + catégories)
total = getattr(config, 'pause_menu_total_options', 7)
config.selected_option = min(total - 1, config.selected_option + 1)
config.selected_option = (config.selected_option + 1) % total
config.needs_redraw = True
elif is_input_matched(event, "confirm"):
if config.selected_option == 0: # Language selector direct
if config.selected_option == 0: # Games submenu
config.menu_state = "pause_games_menu"
if not hasattr(config, 'pause_games_selection'):
config.pause_games_selection = 0
config.last_state_change_time = pygame.time.get_ticks()
config.needs_redraw = True
elif config.selected_option == 1: # Language selector direct
config.menu_state = "language_select"
config.previous_menu_state = "pause_menu"
config.last_state_change_time = pygame.time.get_ticks()
config.needs_redraw = True
elif config.selected_option == 1: # Controls submenu
elif config.selected_option == 2: # Controls submenu
config.menu_state = "pause_controls_menu"
if not hasattr(config, 'pause_controls_selection'):
config.pause_controls_selection = 0
config.last_state_change_time = pygame.time.get_ticks()
config.needs_redraw = True
elif config.selected_option == 2: # Display submenu
elif config.selected_option == 3: # Display submenu
config.menu_state = "pause_display_menu"
if not hasattr(config, 'pause_display_selection'):
config.pause_display_selection = 0
config.last_state_change_time = pygame.time.get_ticks()
config.needs_redraw = True
elif config.selected_option == 3: # Games submenu
config.menu_state = "pause_games_menu"
if not hasattr(config, 'pause_games_selection'):
config.pause_games_selection = 0
config.last_state_change_time = pygame.time.get_ticks()
config.needs_redraw = True
elif config.selected_option == 4: # Settings submenu
config.menu_state = "pause_settings_menu"
if not hasattr(config, 'pause_settings_selection'):
config.pause_settings_selection = 0
config.last_state_change_time = pygame.time.get_ticks()
config.needs_redraw = True
elif config.selected_option == 5: # Restart
restart_application(2000)
elif config.selected_option == 6: # Support
elif config.selected_option == 5: # Support
success, message, zip_path = generate_support_zip()
if success:
config.support_zip_path = zip_path
@@ -1571,7 +1708,7 @@ def handle_controls(event, sources, joystick, screen):
config.menu_state = "support_dialog"
config.last_state_change_time = pygame.time.get_ticks()
config.needs_redraw = True
elif config.selected_option == 7: # Quit
elif config.selected_option == 6: # Quit submenu
# Capturer l'origine pause_menu pour retour si annulation
config.confirm_exit_origin = "pause_menu"
config.previous_menu_state = validate_menu_state(config.previous_menu_state)
@@ -1618,7 +1755,8 @@ def handle_controls(event, sources, joystick, screen):
# Sous-menu Display
elif config.menu_state == "pause_display_menu":
sel = getattr(config, 'pause_display_selection', 0)
total = 6 # layout, font size, footer font size, font family, allow unknown extensions, back
# layout, font size, footer font size, font family, monitor, light, allow unknown extensions, back (8)
total = 8
if is_input_matched(event, "up"):
config.pause_display_selection = (sel - 1) % total
config.needs_redraw = True
@@ -1713,8 +1851,37 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur changement font family: {e}")
# 4 allow unknown extensions
elif sel == 4 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
# 4 monitor selection
elif sel == 4 and (is_input_matched(event, "left") or is_input_matched(event, "right")):
try:
from rgsx_settings import get_display_monitor, set_display_monitor, get_available_monitors
monitors = get_available_monitors()
num_monitors = len(monitors)
if num_monitors > 1:
current = get_display_monitor()
new_monitor = (current - 1) % num_monitors if is_input_matched(event, "left") else (current + 1) % num_monitors
set_display_monitor(new_monitor)
config.popup_message = _("display_monitor_restart_required") if _ else "Restart required to apply monitor change"
config.popup_timer = 3000
else:
config.popup_message = _("display_monitor_single_only") if _ else "Only one monitor detected"
config.popup_timer = 2000
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur changement moniteur: {e}")
# 5 light mode toggle
elif sel == 5 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
try:
from rgsx_settings import get_light_mode, set_light_mode
current = get_light_mode()
new_val = set_light_mode(not current)
config.popup_message = _("display_light_mode_enabled") if new_val else _("display_light_mode_disabled")
config.popup_timer = 2000
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur toggle light mode: {e}")
# 6 allow unknown extensions
elif sel == 6 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
try:
current = get_allow_unknown_extensions()
new_val = set_allow_unknown_extensions(not current)
@@ -1723,8 +1890,8 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur toggle allow_unknown_extensions: {e}")
# 5 back
elif sel == 5 and is_input_matched(event, "confirm"):
# 7 back
elif sel == 7 and is_input_matched(event, "confirm"):
config.menu_state = "pause_menu"
config.last_state_change_time = pygame.time.get_ticks()
config.needs_redraw = True
@@ -1736,7 +1903,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 # history, source, redownload, unsupported, hide premium, filter, back
total = 7 # update cache, history, source, unsupported, hide premium, filter, back
if is_input_matched(event, "up"):
config.pause_games_selection = (sel - 1) % total
config.needs_redraw = True
@@ -1744,14 +1911,19 @@ def handle_controls(event, sources, joystick, screen):
config.pause_games_selection = (sel + 1) % total
config.needs_redraw = True
elif is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right"):
if sel == 0 and is_input_matched(event, "confirm"): # history
if sel == 0 and is_input_matched(event, "confirm"): # update cache
config.previous_menu_state = "pause_games_menu"
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
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 == 1 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")):
elif sel == 2 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')
@@ -1766,11 +1938,6 @@ 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 == 2 and is_input_matched(event, "confirm"): # redownload cache
config.previous_menu_state = "pause_games_menu"
config.menu_state = "reload_games_data"
config.redownload_confirm_selection = 0
config.needs_redraw = True
elif sel == 3 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # unsupported toggle
try:
current = get_show_unsupported_platforms()
@@ -1809,11 +1976,19 @@ def handle_controls(event, sources, joystick, screen):
elif config.menu_state == "pause_settings_menu":
sel = getattr(config, 'pause_settings_selection', 0)
# Calculer le nombre total d'options selon le système
total = 4 # music, symlink, api keys, back
# Liste des options : music, symlink, [web_service], [custom_dns], api keys, back
total = 4 # music, symlink, api keys, back (Windows)
web_service_index = -1
custom_dns_index = -1
api_keys_index = 2
back_index = 3
if config.OPERATING_SYSTEM == "Linux":
total = 5 # music, symlink, web_service, api keys, back
total = 6 # music, symlink, web_service, custom_dns, api keys, back
web_service_index = 2
custom_dns_index = 3
api_keys_index = 4
back_index = 5
if is_input_matched(event, "up"):
config.pause_settings_selection = (sel - 1) % total
@@ -1862,12 +2037,31 @@ def handle_controls(event, sources, joystick, screen):
else:
logger.error(f"Erreur toggle service web: {message}")
threading.Thread(target=toggle_service, daemon=True).start()
# Option API Keys (index varie selon Linux ou pas)
elif sel == (web_service_index + 1 if web_service_index >= 0 else 2) and is_input_matched(event, "confirm"):
# Option 3: Custom DNS toggle (seulement si Linux)
elif sel == custom_dns_index and custom_dns_index >= 0 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")):
from utils import check_custom_dns_status, toggle_custom_dns_at_boot
current_status = check_custom_dns_status()
# Afficher un message de chargement
config.popup_message = _("settings_custom_dns_enabling") if not current_status else _("settings_custom_dns_disabling")
config.popup_timer = 1000
config.needs_redraw = True
# Exécuter en thread pour ne pas bloquer l'UI
def toggle_dns():
success, message = toggle_custom_dns_at_boot(not current_status)
config.popup_message = message
config.popup_timer = 5000 if success else 7000
config.needs_redraw = True
if success:
logger.info(f"Custom DNS {'activé' if not current_status else 'désactivé'} au démarrage")
else:
logger.error(f"Erreur toggle custom DNS: {message}")
threading.Thread(target=toggle_dns, daemon=True).start()
# Option API Keys
elif sel == api_keys_index and is_input_matched(event, "confirm"):
config.menu_state = "pause_api_keys_status"
config.needs_redraw = True
# Option Back (dernière option)
elif sel == (total - 1) and is_input_matched(event, "confirm"):
elif sel == back_index and is_input_matched(event, "confirm"):
config.menu_state = "pause_menu"
config.last_state_change_time = pygame.time.get_ticks()
config.needs_redraw = True
@@ -1889,14 +2083,15 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
logger.debug("Retour au menu pause depuis controls_help")
# Menu Affichage (layout, police, unsupported)
# Menu Affichage (layout, police, moniteur, mode écran, unsupported, extensions, filtres)
elif config.menu_state == "display_menu":
sel = getattr(config, 'display_menu_selection', 0)
num_options = 7 # Layout, Font, Monitor, Mode, Unsupported, Extensions, Filters
if is_input_matched(event, "up"):
config.display_menu_selection = (sel - 1) % 5
config.display_menu_selection = (sel - 1) % num_options
config.needs_redraw = True
elif is_input_matched(event, "down"):
config.display_menu_selection = (sel + 1) % 5
config.display_menu_selection = (sel + 1) % num_options
config.needs_redraw = True
elif is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm"):
# 0: layout change
@@ -1942,8 +2137,40 @@ def handle_controls(event, sources, joystick, screen):
except Exception as e:
logger.error(f"Erreur init polices: {e}")
config.needs_redraw = True
# 2: toggle unsupported
elif sel == 2 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
# 2: monitor selection (new)
elif sel == 2 and (is_input_matched(event, "left") or is_input_matched(event, "right")):
try:
from rgsx_settings import get_display_monitor, set_display_monitor, get_available_monitors
monitors = get_available_monitors()
num_monitors = len(monitors)
if num_monitors > 1:
current = get_display_monitor()
new_monitor = (current - 1) % num_monitors if is_input_matched(event, "left") else (current + 1) % num_monitors
set_display_monitor(new_monitor)
config.needs_redraw = True
# Informer l'utilisateur qu'un redémarrage est nécessaire
config.popup_message = _("display_monitor_restart_required")
config.popup_timer = 3000
else:
config.popup_message = _("display_monitor_single_only")
config.popup_timer = 2000
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur changement moniteur: {e}")
# 3: fullscreen/windowed toggle (new)
elif sel == 3 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
try:
from rgsx_settings import get_display_fullscreen, set_display_fullscreen
current = get_display_fullscreen()
new_val = set_display_fullscreen(not current)
config.needs_redraw = True
# Informer l'utilisateur qu'un redémarrage est nécessaire
config.popup_message = _("display_mode_restart_required")
config.popup_timer = 3000
except Exception as e:
logger.error(f"Erreur toggle fullscreen: {e}")
# 4: toggle unsupported (was 2)
elif sel == 4 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
try:
current = get_show_unsupported_platforms()
new_val = set_show_unsupported_platforms(not current)
@@ -1953,8 +2180,8 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur toggle unsupported: {e}")
# 3: toggle allow unknown extensions
elif sel == 3 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
# 5: toggle allow unknown extensions (was 3)
elif sel == 5 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
try:
current = get_allow_unknown_extensions()
new_val = set_allow_unknown_extensions(not current)
@@ -1963,8 +2190,8 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur toggle allow_unknown_extensions: {e}")
# 4: open filter platforms menu
elif sel == 4 and (is_input_matched(event, "confirm") or is_input_matched(event, "right")):
# 6: open filter platforms menu (was 4)
elif sel == 6 and (is_input_matched(event, "confirm") or is_input_matched(event, "right")):
# Remember return target so the filter menu can go back to display
config.filter_return_to = "display_menu"
config.menu_state = "filter_platforms"
@@ -2099,7 +2326,11 @@ def handle_controls(event, sources, joystick, screen):
# Recherche par nom (mode existant)
config.search_mode = True
config.search_query = ""
config.filtered_games = config.games
# Initialiser avec les jeux déjà filtrés par les filtres avancés si actifs
if hasattr(config, 'game_filter_obj') and config.game_filter_obj and config.game_filter_obj.is_active():
config.filtered_games = config.game_filter_obj.apply_filters(config.games)
else:
config.filtered_games = config.games
config.current_game = 0
config.scroll_offset = 0
config.selected_key = (0, 0)

View File

@@ -225,26 +225,125 @@ THEME_COLORS = {
# Général, résolution, overlay
def init_display():
"""Initialise l'écran et les ressources globales."""
"""Initialise l'écran et les ressources globales.
Supporte la sélection de moniteur et le mode fenêtré/plein écran.
Compatible Windows et Linux (Batocera).
"""
global OVERLAY
import platform
import os
from rgsx_settings import get_display_monitor, get_display_fullscreen, load_rgsx_settings
logger.debug("Initialisation de l'écran")
display_info = pygame.display.Info()
screen_width = display_info.current_w
screen_height = display_info.current_h
screen = pygame.display.set_mode((screen_width, screen_height))
# Charger les paramètres d'affichage avec debug
settings = load_rgsx_settings()
logger.debug(f"Settings chargés: display={settings.get('display', {})}")
target_monitor = settings.get("display", {}).get("monitor", 0)
fullscreen = settings.get("display", {}).get("fullscreen", True)
logger.debug(f"Paramètres lus: monitor={target_monitor}, fullscreen={fullscreen}")
# Vérifier les variables d'environnement (priorité sur les settings)
env_display = os.environ.get("RGSX_DISPLAY")
env_windowed = os.environ.get("RGSX_WINDOWED")
if env_display is not None:
try:
target_monitor = int(env_display)
logger.debug(f"Override par RGSX_DISPLAY: monitor={target_monitor}")
except ValueError:
pass
if env_windowed == "1":
fullscreen = False
logger.debug("Override par RGSX_WINDOWED: fullscreen=False")
logger.debug(f"Configuration finale: monitor={target_monitor}, fullscreen={fullscreen}")
# Configurer SDL pour utiliser le bon moniteur
# Cette variable d'environnement doit être définie AVANT la création de la fenêtre
os.environ["SDL_VIDEO_FULLSCREEN_HEAD"] = str(target_monitor)
# Obtenir les informations d'affichage
num_displays = 1
try:
num_displays = pygame.display.get_num_displays()
except Exception:
pass
# S'assurer que le moniteur cible existe
if target_monitor >= num_displays:
logger.warning(f"Monitor {target_monitor} not available, using monitor 0")
target_monitor = 0
# Obtenir la résolution du moniteur cible
try:
if hasattr(pygame.display, 'get_desktop_sizes') and num_displays > 1:
desktop_sizes = pygame.display.get_desktop_sizes()
if target_monitor < len(desktop_sizes):
screen_width, screen_height = desktop_sizes[target_monitor]
else:
display_info = pygame.display.Info()
screen_width = display_info.current_w
screen_height = display_info.current_h
else:
display_info = pygame.display.Info()
screen_width = display_info.current_w
screen_height = display_info.current_h
except Exception as e:
logger.error(f"Error getting display info: {e}")
display_info = pygame.display.Info()
screen_width = display_info.current_w
screen_height = display_info.current_h
# Créer la fenêtre
flags = 0
if fullscreen:
flags = pygame.FULLSCREEN
# Sur certains systèmes, NOFRAME aide pour le multi-écran
if platform.system() == "Windows":
flags |= pygame.NOFRAME
try:
screen = pygame.display.set_mode((screen_width, screen_height), flags, display=target_monitor)
except TypeError:
# Anciennes versions de pygame ne supportent pas le paramètre display=
screen = pygame.display.set_mode((screen_width, screen_height), flags)
except Exception as e:
logger.error(f"Error creating display on monitor {target_monitor}: {e}")
screen = pygame.display.set_mode((screen_width, screen_height), flags)
config.screen_width = screen_width
config.screen_height = screen_height
config.current_monitor = target_monitor
config.is_fullscreen = fullscreen
# Initialisation de OVERLAY avec effet glassmorphism
OVERLAY = pygame.Surface((screen_width, screen_height), pygame.SRCALPHA)
OVERLAY.fill((5, 10, 20, 160)) # Bleu très foncé semi-transparent pour effet verre
logger.debug(f"Écran initialisé avec résolution : {screen_width}x{screen_height}")
logger.debug(f"Écran initialisé: {screen_width}x{screen_height} sur moniteur {target_monitor} (fullscreen={fullscreen})")
return screen
# Fond d'écran dégradé
def draw_gradient(screen, top_color, bottom_color):
"""Dessine un fond dégradé vertical avec des couleurs vibrantes et texture de grain."""
def draw_gradient(screen, top_color, bottom_color, light_mode=None):
"""Dessine un fond dégradé vertical avec des couleurs vibrantes et texture de grain.
En mode light, utilise une couleur unie pour de meilleures performances."""
from rgsx_settings import get_light_mode
if light_mode is None:
light_mode = get_light_mode()
height = screen.get_height()
width = screen.get_width()
if light_mode:
# Mode light: couleur unie (moyenne des deux couleurs)
avg_color = (
(top_color[0] + bottom_color[0]) // 2,
(top_color[1] + bottom_color[1]) // 2,
(top_color[2] + bottom_color[2]) // 2
)
screen.fill(avg_color)
return
top_color = pygame.Color(*top_color)
bottom_color = pygame.Color(*bottom_color)
@@ -266,15 +365,25 @@ def draw_gradient(screen, top_color, bottom_color):
screen.blit(grain_surface, (0, 0))
def draw_shadow(surface, rect, offset=6, alpha=120):
"""Dessine une ombre portée pour un rectangle."""
def draw_shadow(surface, rect, offset=6, alpha=120, light_mode=None):
"""Dessine une ombre portée pour un rectangle. Désactivé en mode light."""
from rgsx_settings import get_light_mode
if light_mode is None:
light_mode = get_light_mode()
if light_mode:
return None # Pas d'ombre en mode light
shadow = pygame.Surface((rect.width + offset, rect.height + offset), pygame.SRCALPHA)
pygame.draw.rect(shadow, (0, 0, 0, alpha), (0, 0, rect.width + offset, rect.height + offset), border_radius=15)
return shadow
def draw_glow_effect(screen, rect, color, intensity=80, size=10):
"""Dessine un effet de glow autour d'un rectangle."""
def draw_glow_effect(screen, rect, color, intensity=80, size=10, light_mode=None):
"""Dessine un effet de glow autour d'un rectangle. Désactivé en mode light."""
from rgsx_settings import get_light_mode
if light_mode is None:
light_mode = get_light_mode()
if light_mode:
return # Pas de glow en mode light
glow = pygame.Surface((rect.width + size * 2, rect.height + size * 2), pygame.SRCALPHA)
for i in range(size):
alpha = int(intensity * (1 - i / size))
@@ -284,55 +393,68 @@ def draw_glow_effect(screen, rect, color, intensity=80, size=10):
screen.blit(glow, (rect.x - size, rect.y - size))
# Nouvelle fonction pour dessiner un bouton stylisé
def draw_stylized_button(screen, text, x, y, width, height, selected=False):
"""Dessine un bouton moderne avec effet de survol, ombre et bordure arrondie."""
# Ombre portée subtile
shadow_surf = pygame.Surface((width + 6, height + 6), pygame.SRCALPHA)
pygame.draw.rect(shadow_surf, THEME_COLORS["shadow"], (3, 3, width, height), border_radius=12)
screen.blit(shadow_surf, (x - 3, y - 3))
def draw_stylized_button(screen, text, x, y, width, height, selected=False, light_mode=None):
"""Dessine un bouton moderne avec effet de survol, ombre et bordure arrondie.
En mode light, utilise un style simplifié pour de meilleures performances."""
from rgsx_settings import get_light_mode
if light_mode is None:
light_mode = get_light_mode()
button_color = THEME_COLORS["button_hover"] if selected else THEME_COLORS["button_idle"]
button_surface = pygame.Surface((width, height), pygame.SRCALPHA)
# Fond avec dégradé subtil pour bouton sélectionné
if selected:
# Créer le dégradé
for i in range(height):
ratio = i / height
brightness = 1 + 0.2 * ratio
r = min(255, int(button_color[0] * brightness))
g = min(255, int(button_color[1] * brightness))
b = min(255, int(button_color[2] * brightness))
alpha = button_color[3] if len(button_color) > 3 else 255
rect = pygame.Rect(0, i, width, 1)
pygame.draw.rect(button_surface, (r, g, b, alpha), rect)
# Appliquer les coins arrondis avec un masque
mask_surface = pygame.Surface((width, height), pygame.SRCALPHA)
pygame.draw.rect(mask_surface, (255, 255, 255, 255), (0, 0, width, height), border_radius=12)
button_surface.blit(mask_surface, (0, 0), special_flags=pygame.BLEND_RGBA_MIN)
if light_mode:
# Mode light: bouton simple sans effets
pygame.draw.rect(screen, button_color[:3], (x, y, width, height), border_radius=8)
if selected:
# Bordure simple pour indiquer la sélection
pygame.draw.rect(screen, THEME_COLORS["neon"], (x, y, width, height), width=2, border_radius=8)
else:
pygame.draw.rect(button_surface, button_color, (0, 0, width, height), border_radius=12)
# Reflet en haut
highlight = pygame.Surface((width - 4, height // 3), pygame.SRCALPHA)
highlight.fill(THEME_COLORS["highlight"])
button_surface.blit(highlight, (2, 2))
# Bordure
pygame.draw.rect(button_surface, THEME_COLORS["border"], (0, 0, width, height), 2, border_radius=12)
if selected:
# Effet glow doux pour sélection
glow_surface = pygame.Surface((width + 16, height + 16), pygame.SRCALPHA)
for i in range(6):
alpha = int(40 * (1 - i / 6))
pygame.draw.rect(glow_surface, (*THEME_COLORS["glow"][:3], alpha),
(i, i, width + 16 - i*2, height + 16 - i*2), border_radius=15)
screen.blit(glow_surface, (x - 8, y - 8))
screen.blit(button_surface, (x, y))
# Mode normal avec tous les effets
# Ombre portée subtile
shadow_surf = pygame.Surface((width + 6, height + 6), pygame.SRCALPHA)
pygame.draw.rect(shadow_surf, THEME_COLORS["shadow"], (3, 3, width, height), border_radius=12)
screen.blit(shadow_surf, (x - 3, y - 3))
button_surface = pygame.Surface((width, height), pygame.SRCALPHA)
# Fond avec dégradé subtil pour bouton sélectionné
if selected:
# Créer le dégradé
for i in range(height):
ratio = i / height
brightness = 1 + 0.2 * ratio
r = min(255, int(button_color[0] * brightness))
g = min(255, int(button_color[1] * brightness))
b = min(255, int(button_color[2] * brightness))
alpha = button_color[3] if len(button_color) > 3 else 255
rect = pygame.Rect(0, i, width, 1)
pygame.draw.rect(button_surface, (r, g, b, alpha), rect)
# Appliquer les coins arrondis avec un masque
mask_surface = pygame.Surface((width, height), pygame.SRCALPHA)
pygame.draw.rect(mask_surface, (255, 255, 255, 255), (0, 0, width, height), border_radius=12)
button_surface.blit(mask_surface, (0, 0), special_flags=pygame.BLEND_RGBA_MIN)
else:
pygame.draw.rect(button_surface, button_color, (0, 0, width, height), border_radius=12)
# Reflet en haut
highlight = pygame.Surface((width - 4, height // 3), pygame.SRCALPHA)
highlight.fill(THEME_COLORS["highlight"])
button_surface.blit(highlight, (2, 2))
# Bordure
pygame.draw.rect(button_surface, THEME_COLORS["border"], (0, 0, width, height), 2, border_radius=12)
if selected:
# Effet glow doux pour sélection
glow_surface = pygame.Surface((width + 16, height + 16), pygame.SRCALPHA)
for i in range(6):
alpha = int(40 * (1 - i / 6))
pygame.draw.rect(glow_surface, (*THEME_COLORS["glow"][:3], alpha),
(i, i, width + 16 - i*2, height + 16 - i*2), border_radius=15)
screen.blit(glow_surface, (x - 8, y - 8))
screen.blit(button_surface, (x, y))
# Vérifier si le texte dépasse la largeur disponible
text_surface = config.font.render(text, True, THEME_COLORS["text"])
@@ -755,8 +877,18 @@ def draw_platform_grid(screen):
available_width = config.screen_width - margin_left - margin_right
available_height = config.screen_height - margin_top - margin_bottom
# Calculer la taille des cellules en tenant compte de l'espace nécessaire pour le glow
# Réduire la taille effective pour laisser de l'espace entre les éléments
col_width = available_width // num_cols
row_height = available_height // num_rows
# Calculer la taille du container basée sur la cellule la plus petite
# avec marges pour éviter les chevauchements (20% de marge)
cell_size = min(col_width, row_height)
container_size = int(cell_size * 0.70) # 70% de la cellule pour laisser de l'espace
# Espacement entre les cellules pour éviter les chevauchements
cell_padding = int(cell_size * 0.15) # 15% d'espacement
x_positions = [margin_left + col_width * i + col_width // 2 for i in range(num_cols)]
y_positions = [margin_top + row_height * i + row_height // 2 for i in range(num_rows)]
@@ -794,8 +926,8 @@ def draw_platform_grid(screen):
page_rect = page_indicator.get_rect(center=(config.screen_width // 2, page_y))
screen.blit(page_indicator, page_rect)
# Calculer une seule fois la pulsation pour les éléments sélectionnés
pulse = 0.1 * math.sin(current_time / 300)
# Calculer une seule fois la pulsation pour les éléments sélectionnés (réduite)
pulse = 0.05 * math.sin(current_time / 300) # Réduit de 0.1 à 0.05
glow_intensity = 40 + int(30 * math.sin(current_time / 300))
# Pré-calcul des images pour optimiser le rendu
@@ -809,9 +941,9 @@ def draw_platform_grid(screen):
x = x_positions[col]
y = y_positions[row]
# Animation fluide pour l'item sélectionné
# Animation fluide pour l'item sélectionné (réduite pour éviter chevauchement)
is_selected = idx == config.selected_platform
scale_base = 1.5 if is_selected else 1.0
scale_base = 1.15 if is_selected else 1.0 # Réduit de 1.5 à 1.15
scale = scale_base + pulse if is_selected else scale_base
# Récupération robuste du dict via nom
@@ -830,62 +962,120 @@ def draw_platform_grid(screen):
platform_id = platform_dict.get("platform_name") or platform_dict.get("platform") or display_name
# Utiliser le cache d'images pour éviter de recharger/redimensionner à chaque frame
cache_key = f"{platform_id}_{scale:.2f}"
cache_key = f"{platform_id}_{scale:.2f}_{container_size}"
if cache_key not in platform_images_cache:
image = load_system_image(platform_dict)
if image:
orig_width, orig_height = image.get_width(), image.get_height()
max_size = int(min(col_width, row_height) * scale * 1.1) # Légèrement plus grand que la cellule
ratio = min(max_size / orig_width, max_size / orig_height)
# Taille normalisée basée sur container_size calculé en fonction de la grille
# Le scale affecte uniquement l'item sélectionné
# Adapter la largeur en fonction du nombre de colonnes pour occuper ~25-30% de l'écran
if num_cols == 3:
# En 3 colonnes, augmenter significativement la largeur (15% de l'écran par carte)
actual_container_width = int(config.screen_width * 0.15 * scale)
elif num_cols == 4:
# En 4 colonnes, largeur plus modérée (10% de l'écran par carte)
actual_container_width = int(config.screen_width * 0.15 * scale)
else:
# Par défaut, utiliser container_size * 1.3
actual_container_width = int(container_size * scale * 1.3)
actual_container_height = int(container_size * scale) # Hauteur normale
# Calculer le ratio pour fit dans le container en gardant l'aspect ratio
ratio = min(actual_container_width / orig_width, actual_container_height / orig_height)
new_width = int(orig_width * ratio)
new_height = int(orig_height * ratio)
scaled_image = pygame.transform.smoothscale(image, (new_width, new_height))
platform_images_cache[cache_key] = {
"image": scaled_image,
"width": new_width,
"height": new_height,
"container_width": actual_container_width,
"container_height": actual_container_height,
"last_used": current_time
}
else:
continue
else:
# Mettre à jour le timestamp de dernière utilisation
# Récupérer les données du cache (que ce soit nouveau ou existant)
if cache_key in platform_images_cache:
platform_images_cache[cache_key]["last_used"] = current_time
scaled_image = platform_images_cache[cache_key]["image"]
new_width = platform_images_cache[cache_key]["width"]
new_height = platform_images_cache[cache_key]["height"]
container_width = platform_images_cache[cache_key]["container_width"]
container_height = platform_images_cache[cache_key]["container_height"]
else:
continue
image_rect = scaled_image.get_rect(center=(x, y))
# Effet visuel amélioré pour l'item sélectionné
# Effet visuel moderne similaire au titre pour toutes les images
border_radius = 12
padding = 12
# Utiliser la taille du container normalisé au lieu de la taille variable de l'image
rect_width = container_width + 2 * padding
rect_height = container_height + 2 * padding
# Centrer le conteneur sur la position (x, y)
container_left = x - rect_width // 2
container_top = y - rect_height // 2
# Ombre portée
shadow_surf = pygame.Surface((rect_width + 12, rect_height + 12), pygame.SRCALPHA)
pygame.draw.rect(shadow_surf, (0, 0, 0, 160), (6, 6, rect_width, rect_height), border_radius=border_radius + 4)
screen.blit(shadow_surf, (container_left - 6, container_top - 6))
# Effet de glow multicouche pour l'item sélectionné
if is_selected:
neon_color = THEME_COLORS["neon"]
border_radius = 12
padding = 12
rect_width = image_rect.width + 2 * padding
rect_height = image_rect.height + 2 * padding
# Effet de glow dynamique
neon_surface = pygame.Surface((rect_width, rect_height), pygame.SRCALPHA)
pygame.draw.rect(neon_surface, neon_color + (glow_intensity,), neon_surface.get_rect(), border_radius=border_radius)
pygame.draw.rect(neon_surface, neon_color + (100,), neon_surface.get_rect().inflate(-10, -10), border_radius=border_radius)
pygame.draw.rect(neon_surface, neon_color + (200,), neon_surface.get_rect().inflate(-20, -20), width=1, border_radius=border_radius)
screen.blit(neon_surface, (image_rect.left - padding, image_rect.top - padding), special_flags=pygame.BLEND_RGBA_ADD)
# Fond pour toutes les images
background_surface = pygame.Surface((image_rect.width + 10, image_rect.height + 10), pygame.SRCALPHA)
bg_alpha = 220 if is_selected else 180 # Plus opaque pour l'item sélectionné
pygame.draw.rect(background_surface, THEME_COLORS["fond_image"] + (bg_alpha,), background_surface.get_rect(), border_radius=12)
screen.blit(background_surface, (image_rect.left - 5, image_rect.top - 5))
# Glow multicouche (2 couches pour effet profondeur)
for i in range(2):
glow_size = (rect_width + 15 + i * 8, rect_height + 15 + i * 8)
glow_surf = pygame.Surface(glow_size, pygame.SRCALPHA)
alpha = int((glow_intensity + 40) * (1 - i / 2))
pygame.draw.rect(glow_surf, neon_color + (alpha,), glow_surf.get_rect(), border_radius=border_radius + i * 2)
screen.blit(glow_surf, (container_left - 8 - i * 4, container_top - 8 - i * 4))
# Fond avec dégradé vertical (similaire au titre)
bg_surface = pygame.Surface((rect_width, rect_height), pygame.SRCALPHA)
base_color = THEME_COLORS["button_idle"] if is_selected else THEME_COLORS["fond_image"]
for i in range(rect_height):
ratio = i / rect_height
# Dégradé du haut (plus clair) vers le bas (plus foncé)
alpha = int(base_color[3] * (1 + ratio * 0.15)) if len(base_color) > 3 else int(200 * (1 + ratio * 0.15))
color = (*base_color[:3], min(255, alpha))
pygame.draw.line(bg_surface, color, (0, i), (rect_width, i))
screen.blit(bg_surface, (container_left, container_top))
# Reflet en haut (highlight pour effet glossy)
highlight_height = rect_height // 3
highlight = pygame.Surface((rect_width - 8, highlight_height), pygame.SRCALPHA)
highlight.fill((255, 255, 255, 35 if is_selected else 20))
screen.blit(highlight, (container_left + 4, container_top + 4))
# Bordure avec effet 3D
border_color = THEME_COLORS["neon"] if is_selected else THEME_COLORS["border"]
border_rect = pygame.Rect(container_left, container_top, rect_width, rect_height)
pygame.draw.rect(screen, border_color, border_rect, 2, border_radius=border_radius)
# Centrer l'image dans le container (l'image peut être plus petite que le container)
centered_image_rect = scaled_image.get_rect(center=(x, y))
# Affichage de l'image avec un léger effet de transparence pour les items non sélectionnés
if not is_selected:
# Appliquer la transparence seulement si nécessaire
temp_image = scaled_image.copy()
temp_image.set_alpha(220)
screen.blit(temp_image, image_rect)
screen.blit(temp_image, centered_image_rect)
else:
screen.blit(scaled_image, image_rect)
screen.blit(scaled_image, centered_image_rect)
# Nettoyer le cache périodiquement (garder seulement les images utilisées récemment)
if len(platform_images_cache) > 50: # Limite arbitraire pour éviter une croissance excessive
@@ -974,14 +1164,14 @@ def draw_game_list(screen):
pygame.draw.rect(screen, THEME_COLORS["border"], title_rect_inflated, 2, border_radius=12)
screen.blit(title_surface, title_rect)
elif config.filter_active:
# Display filter active indicator with count
if hasattr(config, 'game_filter_obj') and config.game_filter_obj and config.game_filter_obj.is_active():
total_games = len(config.games)
filtered_count = len(games)
filter_text = _("filter_games_shown").format(filtered_count, total_games)
else:
filter_text = _("game_filter").format(config.search_query)
title_surface = config.font.render(filter_text, True, THEME_COLORS["green"])
# Afficher le nom de la plateforme avec indicateur de filtre actif
filter_indicator = " (Active Filter)"
if config.search_query:
# Si recherche par nom active, afficher aussi la recherche
filter_indicator = f" - {_('game_filter').format(config.search_query)}"
title_text = _("game_count").format(platform_name, game_count) + filter_indicator
title_surface = config.title_font.render(title_text, True, THEME_COLORS["green"])
title_rect = title_surface.get_rect(center=(config.screen_width // 2, title_surface.get_height() // 2 + 20))
title_rect_inflated = title_rect.inflate(60, 30)
title_rect_inflated.topleft = ((config.screen_width - title_rect_inflated.width) // 2, 10)
@@ -989,7 +1179,12 @@ def draw_game_list(screen):
pygame.draw.rect(screen, THEME_COLORS["border_selected"], title_rect_inflated, 3, border_radius=12)
screen.blit(title_surface, title_rect)
else:
title_text = _("game_count").format(platform_name, game_count)
# Ajouter indicateur de filtre actif si filtres avancés sont actifs
filter_indicator = ""
if hasattr(config, 'game_filter_obj') and config.game_filter_obj and config.game_filter_obj.is_active():
filter_indicator = " (Active Filter)"
title_text = _("game_count").format(platform_name, game_count) + filter_indicator
title_surface = config.title_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)
@@ -1960,15 +2155,32 @@ def draw_menu_instruction(screen, instruction_text, last_button_bottom=None):
logger.error(f"Erreur draw_menu_instruction: {e}")
def draw_display_menu(screen):
"""Affiche le sous-menu Affichage (layout, taille de police, systèmes non supportés)."""
"""Affiche le sous-menu Affichage (layout, taille de police, systèmes non supportés, moniteur)."""
screen.blit(OVERLAY, (0, 0))
# États actuels
layout_str = f"{getattr(config, 'GRID_COLS', 3)}x{getattr(config, 'GRID_ROWS', 4)}"
font_scale = config.accessibility_settings.get("font_scale", 1.0)
from rgsx_settings import get_show_unsupported_platforms, get_allow_unknown_extensions
from rgsx_settings import (get_show_unsupported_platforms, get_allow_unknown_extensions,
get_display_monitor, get_display_fullscreen, get_available_monitors)
show_unsupported = get_show_unsupported_platforms()
allow_unknown = get_allow_unknown_extensions()
# Monitor info
current_monitor = get_display_monitor()
is_fullscreen = get_display_fullscreen()
monitors = get_available_monitors()
num_monitors = len(monitors)
# Construire le label du moniteur
if num_monitors > 1:
monitor_info = monitors[current_monitor] if current_monitor < num_monitors else monitors[0]
monitor_label = f"{_('display_monitor')}: {monitor_info['name']} ({monitor_info['resolution']})"
else:
monitor_label = f"{_('display_monitor')}: {_('display_monitor_single')}"
# Label mode écran
fullscreen_label = f"{_('display_mode')}: {_('display_fullscreen') if is_fullscreen else _('display_windowed')}"
# Compter les systèmes non supportés actuellement masqués
unsupported_list = getattr(config, "unsupported_platforms", []) or []
@@ -1981,11 +2193,13 @@ def draw_display_menu(screen):
else:
unsupported_label = _("menu_show_unsupported_all_displayed")
# Libellés
# Libellés - ajout des options moniteur et mode écran
options = [
f"{_('display_layout')}: {layout_str}",
_("accessibility_font_size").format(f"{font_scale:.1f}"),
unsupported_label,
monitor_label,
fullscreen_label,
unsupported_label,
_("menu_allow_unknown_ext_on") if allow_unknown else _("menu_allow_unknown_ext_off"),
_("menu_filter_platforms"),
]
@@ -2037,16 +2251,15 @@ def draw_display_menu(screen):
def draw_pause_menu(screen, selected_option):
"""Dessine le menu pause racine (catégories)."""
screen.blit(OVERLAY, (0, 0))
# Nouvel ordre: Language / Controls / Display / Games / Settings / Restart / Support / Quit
# Nouvel ordre: Games / Language / Controls / Display / Settings / Support / Quit
options = [
_("menu_language") if _ else "Language", # 0 -> sélecteur de langue direct
_("menu_controls"), # 1 -> sous-menu controls
_("menu_display"), # 2 -> sous-menu display
_("menu_games") if _ else "Games", # 3 -> sous-menu games (history + sources + update)
_("menu_settings_category") if _ else "Settings", # 4 -> sous-menu settings
_("menu_restart"), # 5 -> reboot
_("menu_support"), # 6 -> support
_("menu_quit") # 7 -> quit
_("menu_games") if _ else "Games", # 0 -> sous-menu games (history + sources + update)
_("menu_language") if _ else "Language", # 1 -> sélecteur de langue direct
_("menu_controls"), # 2 -> sous-menu controls
_("menu_display"), # 3 -> sous-menu display
_("menu_settings_category") if _ else "Settings", # 4 -> sous-menu settings
_("menu_support"), # 5 -> support
_("menu_quit") # 6 -> sous-menu quit (quit + restart)
]
# Calculer hauteur dynamique basée sur la taille de police
sample_text = config.font.render("Sample", True, THEME_COLORS["text"])
@@ -2083,12 +2296,11 @@ def draw_pause_menu(screen, selected_option):
# Instruction contextuelle pour l'option sélectionnée
# Mapping des clés i18n parallèles à la liste options (même ordre)
instruction_keys = [
"instruction_pause_games",
"instruction_pause_language",
"instruction_pause_controls",
"instruction_pause_display",
"instruction_pause_games",
"instruction_pause_settings",
"instruction_pause_restart",
"instruction_pause_support",
"instruction_pause_quit",
]
@@ -2208,7 +2420,11 @@ def draw_pause_controls_menu(screen, selected_index):
def draw_pause_display_menu(screen, selected_index):
from rgsx_settings import (
get_allow_unknown_extensions,
get_font_family
get_font_family,
get_display_monitor,
get_display_fullscreen,
get_available_monitors,
get_light_mode
)
# Layout label
layouts = [(3,3),(3,4),(4,3),(4,4)]
@@ -2238,6 +2454,17 @@ def draw_pause_display_menu(screen, selected_index):
fam_label = family_map.get(current_family, current_family)
font_family_txt = f"{_('submenu_display_font_family') if _ else 'Font'}: < {fam_label} >"
# Monitor selection
current_monitor = get_display_monitor()
monitors = get_available_monitors()
num_monitors = len(monitors)
if num_monitors > 1:
monitor_info = monitors[current_monitor] if current_monitor < num_monitors else monitors[0]
monitor_value = f"{monitor_info['name']} ({monitor_info['resolution']})"
else:
monitor_value = _('display_monitor_single') if _ else "Single monitor"
monitor_txt = f"{_('display_monitor') if _ else 'Monitor'}: < {monitor_value} >"
# Allow unknown extensions
allow_unknown = get_allow_unknown_extensions()
status_unknown = _('status_on') if allow_unknown else _('status_off')
@@ -2246,17 +2473,28 @@ def draw_pause_display_menu(screen, selected_index):
raw_unknown_label = raw_unknown_label.split('{status}')[0].rstrip(' :')
unknown_txt = f"{raw_unknown_label}: < {status_unknown} >"
# Light mode (performance)
light_mode = get_light_mode()
light_status = _('status_on') if light_mode else _('status_off')
light_txt = f"{_('display_light_mode') if _ else 'Light mode'}: < {light_status} >"
back_txt = _("menu_back") if _ else "Back"
options = [layout_txt, font_txt, footer_font_txt, font_family_txt, unknown_txt, back_txt]
_draw_submenu_generic(screen, _("menu_display"), options, selected_index)
# Build options list - same for all platforms
# layout, font, footer, family, monitor, light, unknown, back (8)
options = [layout_txt, font_txt, footer_font_txt, font_family_txt, monitor_txt, light_txt, unknown_txt, back_txt]
instruction_keys = [
"instruction_display_layout",
"instruction_display_font_size",
"instruction_display_footer_font_size",
"instruction_display_font_family",
"instruction_display_monitor",
"instruction_display_light_mode",
"instruction_display_unknown_ext",
"instruction_generic_back",
]
_draw_submenu_generic(screen, _("menu_display"), options, selected_index)
key = instruction_keys[selected_index] if 0 <= selected_index < len(instruction_keys) else None
if key:
button_height = int(config.screen_height * 0.045)
@@ -2296,12 +2534,12 @@ 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 = [history_txt, source_txt, update_txt, unsupported_txt, hide_premium_txt, filter_txt, back_txt]
options = [update_txt, history_txt, source_txt, unsupported_txt, hide_premium_txt, filter_txt, back_txt]
_draw_submenu_generic(screen, _("menu_games") if _ else "Games", options, selected_index)
instruction_keys = [
"instruction_games_update_cache",
"instruction_games_history",
"instruction_games_source_mode",
"instruction_games_update_cache",
"instruction_display_show_unsupported",
"instruction_display_hide_premium",
"instruction_display_filter_platforms",
@@ -2805,57 +3043,29 @@ def draw_controls_help(screen, previous_state):
# Menu Quitter Appli
def draw_confirm_dialog(screen):
"""Affiche la boîte de dialogue de confirmation pour quitter."""
global OVERLAY
if OVERLAY is None or OVERLAY.get_size() != (config.screen_width, config.screen_height):
OVERLAY = pygame.Surface((config.screen_width, config.screen_height), pygame.SRCALPHA)
OVERLAY.fill((0, 0, 0, 150))
logger.debug("OVERLAY recréé dans draw_confirm_dialog")
screen.blit(OVERLAY, (0, 0))
# Dynamic message: warn when downloads are active
active_downloads = 0
try:
active_downloads = len(getattr(config, 'download_tasks', {}) or {})
queued_downloads = len(getattr(config, 'download_queue', []) or [])
total_downloads = active_downloads + queued_downloads
except Exception:
total_downloads = 0
if total_downloads > 0:
# Try translated key if it exists; otherwise fallback to generic message
try:
warn_tpl = _("confirm_exit_with_downloads") # optional key
# If untranslated key returns the same string, still format
message = warn_tpl.format(total_downloads)
except Exception:
message = f"Attention: {total_downloads} téléchargement(s) en cours. Quitter quand même ?"
else:
message = _("confirm_exit")
wrapped_message = wrap_text(message, config.font, config.screen_width - 80)
line_height = config.font.get_height() + 5
text_height = len(wrapped_message) * line_height
# Adapter hauteur bouton en fonction de la taille de police
sample_text = config.font.render("Sample", True, THEME_COLORS["text"])
font_height = sample_text.get_height()
button_height = max(int(config.screen_height * 0.0463), font_height + 15)
margin_top_bottom = 20
rect_height = text_height + button_height + 2 * margin_top_bottom
max_text_width = max([config.font.size(line)[0] for line in wrapped_message], default=300)
rect_width = max_text_width + 150
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(wrapped_message):
text = config.font.render(line, True, THEME_COLORS["text"])
text_rect = text.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + i * line_height + line_height // 2))
screen.blit(text, text_rect)
button_width = min(160, (rect_width - 60) // 2)
draw_stylized_button(screen, _("button_yes"), rect_x + rect_width // 2 - button_width - 10, rect_y + text_height + margin_top_bottom, button_width, button_height, selected=config.confirm_selection == 1)
draw_stylized_button(screen, _("button_no"), rect_x + rect_width // 2 + 10, rect_y + text_height + margin_top_bottom, button_width, button_height, selected=config.confirm_selection == 0)
"""Affiche le sous-menu Quit avec les options Quit et Restart."""
options = [
_("menu_quit_app") if _ else "Quit RGSX",
_("menu_restart") if _ else "Restart RGSX",
_("menu_back") if _ else "Back"
]
_draw_submenu_generic(screen, _("menu_quit") if _ else "Quit", options, config.confirm_selection)
instruction_keys = [
"instruction_quit_app",
"instruction_quit_restart",
"instruction_generic_back",
]
key = instruction_keys[config.confirm_selection] if 0 <= config.confirm_selection < len(instruction_keys) else None
if key:
button_height = int(config.screen_height * 0.045)
margin_top_bottom = 26
menu_height = (len(options)+1) * (button_height + 10) + 2 * margin_top_bottom
menu_y = (config.screen_height - menu_height) // 2
title_surface = config.font.render("X", True, THEME_COLORS["text"])
title_rect_height = title_surface.get_height()
start_y = menu_y + margin_top_bottom//2 + title_rect_height + 10 + 10
last_button_bottom = start_y + (len(options)-1) * (button_height + 10) + button_height
draw_menu_instruction(screen, _(key), last_button_bottom)
def draw_reload_games_data_dialog(screen):
@@ -3120,7 +3330,15 @@ def draw_history_game_options(screen):
option_labels.append(_("history_option_scraper"))
# Options selon statut
if status == "Download_OK" or status == "Completed":
if status == "Queued":
# En attente dans la queue
options.append("remove_from_queue")
option_labels.append(_("history_option_remove_from_queue"))
elif status in ["Downloading", "Téléchargement", "Extracting"]:
# Téléchargement en cours
options.append("cancel_download")
option_labels.append(_("history_option_cancel_download"))
elif status == "Download_OK" or status == "Completed":
# Vérifier si c'est une archive ET si le fichier existe
if actual_filename and file_exists:
ext = os.path.splitext(actual_filename)[1].lower()

View File

@@ -69,30 +69,67 @@ class GameFilters:
name = game_name.upper()
regions = []
# Patterns de région communs
if 'USA' in name or 'US)' in name:
regions.append('USA')
# Patterns de région communs - chercher les codes entre parenthèses d'abord
# Codes de région/langue dans les parenthèses (Ex: (Fr,De) ou (En,Nl))
paren_content = re.findall(r'\(([^)]+)\)', name)
for content in paren_content:
# Codes de langue/région séparés par virgules
codes = [c.strip() for c in content.split(',')]
for code in codes:
if code in ['FR', 'FRA']:
if 'France' not in regions:
regions.append('France')
elif code in ['DE', 'GER', 'DEU']:
if 'Germany' not in regions:
regions.append('Germany')
elif code in ['EN', 'ENG'] or code.startswith('EN-'):
# EN peut être USA, Europe ou autre - on vérifie le contexte
if 'EU' in codes or 'EUR' in codes:
if 'Europe' not in regions:
regions.append('Europe')
elif code in ['ES', 'ESP', 'SPA']:
if 'Other' not in regions:
regions.append('Other')
elif code in ['IT', 'ITA']:
if 'Other' not in regions:
regions.append('Other')
elif code in ['NL', 'NLD', 'DU', 'DUT']:
if 'Europe' not in regions:
regions.append('Europe')
elif code in ['PT', 'POR']:
if 'Other' not in regions:
regions.append('Other')
# Patterns de région complets (mots entiers)
if 'USA' in name or 'US)' in name or re.search(r'\bUS\b', name):
if 'USA' not in regions:
regions.append('USA')
if 'CANADA' in name or 'CA)' in name:
regions.append('Canada')
if 'EUROPE' in name or 'EU)' in name:
regions.append('Europe')
if 'Canada' not in regions:
regions.append('Canada')
if 'EUROPE' in name or 'EU)' in name or re.search(r'\bEU\b', name):
if 'Europe' not in regions:
regions.append('Europe')
if 'FRANCE' in name or 'FR)' in name:
regions.append('France')
if 'France' not in regions:
regions.append('France')
if 'GERMANY' in name or 'DE)' in name or 'GER)' in name:
regions.append('Germany')
if 'JAPAN' in name or 'JP)' in name or 'JPN)' in name:
regions.append('Japan')
if 'Germany' not in regions:
regions.append('Germany')
if 'JAPAN' in name or 'JP)' in name or 'JPN)' in name or re.search(r'\bJP\b', name):
if 'Japan' not in regions:
regions.append('Japan')
if 'KOREA' in name or 'KR)' in name or 'KOR)' in name:
regions.append('Korea')
if 'Korea' not in regions:
regions.append('Korea')
if 'WORLD' in name:
regions.append('World')
if 'World' not in regions:
regions.append('World')
# Autres régions
if re.search(r'\b(AUSTRALIA|ASIA|KOREA|BRAZIL|CHINA|RUSSIA|SCANDINAVIA|'
r'SPAIN|FRANCE|GERMANY|ITALY|CANADA)\b', name):
if 'CANADA' in name:
regions.append('Canada')
else:
if re.search(r'\b(AUSTRALIA|ASIA|BRAZIL|CHINA|RUSSIA|SCANDINAVIA|'
r'SPAIN|ITALY)\b', name):
if 'Other' not in regions:
regions.append('Other')
# Si aucune région trouvée
@@ -157,14 +194,22 @@ class GameFilters:
def get_region_priority(self, game_name: str) -> int:
"""Obtient la priorité de région pour un jeu (pour one-rom-per-game)"""
name = game_name.upper()
# Utiliser la fonction de détection de régions pour être cohérent
game_regions = self.get_game_regions(game_name)
for i, region in enumerate(self.region_priority):
region_upper = region.upper()
if region_upper in name:
return i
# 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
return len(self.region_priority) # Autres régions (priorité la plus basse)
for region in game_regions:
try:
priority = self.region_priority.index(region)
if priority < best_priority:
best_priority = priority
except ValueError:
# La région n'est pas dans la liste de priorité
continue
return best_priority
def apply_filters(self, games: List[Tuple]) -> List[Tuple]:
"""

View File

@@ -44,6 +44,7 @@
"free_mode_completed": "[Kostenloser Modus] Abgeschlossen: {0}",
"download_status": "{0}: {1}",
"download_canceled": "Download vom Benutzer abgebrochen.",
"download_removed_from_queue": "Aus der Download-Warteschlange entfernt",
"extension_warning_zip": "Die Datei '{0}' ist ein Archiv und Batocera unterstützt keine Archive für dieses System. Die automatische Extraktion der Datei erfolgt nach dem Download, fortfahren?",
"extension_warning_unsupported": "Die Dateierweiterung für '{0}' wird laut der Konfiguration es_systems.cfg von Batocera nicht unterstützt. Möchtest du fortfahren?",
"extension_warning_enable_unknown_hint": "\nUm diese Meldung auszublenden: \"Warnung bei unbekannter Erweiterung ausblenden\" in Pausenmenü > Anzeige aktivieren",
@@ -64,10 +65,21 @@
"menu_accessibility": "Barrierefreiheit",
"menu_display": "Anzeige",
"display_layout": "Anzeigelayout",
"display_monitor": "Monitor",
"display_monitor_single": "Einzelner Monitor",
"display_monitor_single_only": "Nur ein Monitor erkannt",
"display_monitor_restart_required": "Neustart erforderlich um Monitor zu ändern",
"display_mode": "Anzeigemodus",
"display_fullscreen": "Vollbild",
"display_windowed": "Fenster",
"display_mode_restart_required": "Neustart erforderlich für Modusänderung",
"display_light_mode": "Performance-Modus",
"display_light_mode_enabled": "Performance-Modus aktiviert - Effekte deaktiviert",
"display_light_mode_disabled": "Performance-Modus deaktiviert - Effekte aktiviert",
"menu_redownload_cache": "Spieleliste aktualisieren",
"menu_music_enabled": "Musik aktiviert: {0}",
"menu_music_disabled": "Musik deaktiviert",
"menu_restart": "Neustart",
"menu_restart": "RGSX neu starten",
"menu_support": "Unterstützung",
"menu_filter_platforms": "Systeme filtern",
"filter_platforms_title": "Systemsichtbarkeit",
@@ -80,6 +92,7 @@
"menu_allow_unknown_ext_enabled": "Ausblenden der Warnung bei unbekannter Erweiterung aktiviert",
"menu_allow_unknown_ext_disabled": "Ausblenden der Warnung bei unbekannter Erweiterung deaktiviert",
"menu_quit": "Beenden",
"menu_quit_app": "RGSX beenden",
"support_dialog_title": "Support-Datei",
"support_dialog_message": "Eine Support-Datei wurde mit allen Ihren Konfigurations- und Protokolldateien erstellt.\n\nDatei: {0}\n\nUm Hilfe zu erhalten:\n1. Treten Sie dem RGSX Discord-Server bei\n2. Beschreiben Sie Ihr Problem\n3. Teilen Sie diese ZIP-Datei\n\nDrücken Sie {1}, um zum Menü zurückzukehren.",
"support_dialog_error": "Fehler beim Erstellen der Support-Datei:\n{0}\n\nDrücken Sie {1}, um zum Menü zurückzukehren.",
@@ -184,7 +197,9 @@
"instruction_pause_settings": "Musik, Symlink-Option & API-Schlüsselstatus",
"instruction_pause_restart": "RGSX neu starten um Konfiguration neu zu laden",
"instruction_pause_support": "Eine Diagnose-ZIP-Datei für den Support erstellen",
"instruction_pause_quit": "RGSX Anwendung beenden",
"instruction_pause_quit": "Menü für Beenden oder Neustart aufrufen",
"instruction_quit_app": "RGSX Anwendung beenden",
"instruction_quit_restart": "RGSX Anwendung neu starten",
"instruction_controls_help": "Komplette Referenz für Controller & Tastatur anzeigen",
"instruction_controls_remap": "Tasten / Buttons neu zuordnen",
"instruction_generic_back": "Zum vorherigen Menü zurückkehren",
@@ -192,6 +207,9 @@
"instruction_display_font_size": "Schriftgröße für bessere Lesbarkeit anpassen",
"instruction_display_footer_font_size": "Fußzeilen-Textgröße anpassen (Version & Steuerelemente)",
"instruction_display_font_family": "Zwischen verfügbaren Schriftarten wechseln",
"instruction_display_monitor": "Monitor für RGSX-Anzeige auswählen",
"instruction_display_mode": "Zwischen Vollbild und Fenstermodus wechseln",
"instruction_display_light_mode": "Performance-Modus für bessere FPS aktivieren",
"instruction_display_show_unsupported": "Nicht in es_systems.cfg definierte Systeme anzeigen/ausblenden",
"instruction_display_unknown_ext": "Warnung für in es_systems.cfg fehlende Dateiendungen an-/abschalten",
"instruction_display_hide_premium": "Systeme ausblenden, die Premiumzugang erfordern über API: {providers}",
@@ -244,6 +262,8 @@
"history_option_extract_archive": "Archiv extrahieren",
"history_option_open_file": "Datei öffnen",
"history_option_scraper": "Metadaten abrufen",
"history_option_remove_from_queue": "Aus Warteschlange entfernen",
"history_option_cancel_download": "Download abbrechen",
"history_option_delete_game": "Spiel löschen",
"history_option_error_info": "Fehlerdetails",
"history_option_retry": "Download wiederholen",

View File

@@ -44,6 +44,7 @@
"free_mode_completed": "[Free mode] Completed: {0}",
"download_status": "{0}: {1}",
"download_canceled": "Download canceled by user.",
"download_removed_from_queue": "Removed from download queue",
"extension_warning_zip": "The file '{0}' is an archive and Batocera does not support archives for this system. Automatic extraction will occur after download, continue?",
"extension_warning_unsupported": "The file extension for '{0}' is not supported by Batocera according to the es_systems.cfg configuration. Do you want to continue?",
"extension_warning_enable_unknown_hint": "\nTo hide this message: enable \"Hide unknown extension warning\" in Pause Menu > Display",
@@ -64,10 +65,21 @@
"menu_accessibility": "Accessibility",
"menu_display": "Display",
"display_layout": "Display layout",
"display_monitor": "Monitor",
"display_monitor_single": "Single monitor",
"display_monitor_single_only": "Only one monitor detected",
"display_monitor_restart_required": "Restart required to apply monitor change",
"display_mode": "Screen mode",
"display_fullscreen": "Fullscreen",
"display_windowed": "Windowed",
"display_mode_restart_required": "Restart required to apply screen mode",
"display_light_mode": "Performance mode",
"display_light_mode_enabled": "Performance mode enabled - effects disabled",
"display_light_mode_disabled": "Performance mode disabled - effects enabled",
"menu_redownload_cache": "Update games list",
"menu_music_enabled": "Music enabled: {0}",
"menu_music_disabled": "Music disabled",
"menu_restart": "Restart",
"menu_restart": "Restart RGSX",
"menu_filter_platforms": "Filter systems",
"filter_platforms_title": "Systems visibility",
"filter_platforms_info": "Visible: {0} | Hidden: {1} / Total: {2}",
@@ -80,6 +92,7 @@
"menu_allow_unknown_ext_disabled": "Hide unknown extension warning disabled",
"menu_support": "Support",
"menu_quit": "Quit",
"menu_quit_app": "Quit RGSX",
"button_yes": "Yes",
"button_no": "No",
"button_OK": "OK",
@@ -186,7 +199,9 @@
"instruction_pause_settings": "Music, symlink option & API keys status",
"instruction_pause_restart": "Restart RGSX to reload configuration",
"instruction_pause_support": "Generate a diagnostic ZIP file for support",
"instruction_pause_quit": "Exit the RGSX application",
"instruction_pause_quit": "Access menu to quit or restart",
"instruction_quit_app": "Exit the RGSX application",
"instruction_quit_restart": "Restart the RGSX application",
"instruction_controls_help": "Show full controller & keyboard reference",
"instruction_controls_remap": "Change button / key bindings",
"instruction_generic_back": "Return to the previous menu",
@@ -194,6 +209,9 @@
"instruction_display_font_size": "Adjust text scale for readability",
"instruction_display_footer_font_size": "Adjust footer text scale (version & controls display)",
"instruction_display_font_family": "Switch between available font families",
"instruction_display_monitor": "Select which monitor to display RGSX on",
"instruction_display_mode": "Toggle between fullscreen and windowed mode",
"instruction_display_light_mode": "Enable performance mode for better FPS on low-end devices",
"instruction_display_show_unsupported": "Show/hide systems not defined in es_systems.cfg",
"instruction_display_unknown_ext": "Enable/disable warning for file extensions absent from es_systems.cfg",
"instruction_display_hide_premium": "Hide systems requiring premium access via API: {providers}",
@@ -246,6 +264,8 @@
"history_option_extract_archive": "Extract archive",
"history_option_open_file": "Open file",
"history_option_scraper": "Scrape metadata",
"history_option_remove_from_queue": "Remove from queue",
"history_option_cancel_download": "Cancel download",
"history_option_delete_game": "Delete game",
"history_option_error_info": "Error details",
"history_option_retry": "Retry download",

View File

@@ -44,6 +44,7 @@
"free_mode_completed": "[Modo gratuito] Completado: {0}",
"download_status": "{0}: {1}",
"download_canceled": "Descarga cancelada por el usuario.",
"download_removed_from_queue": "Eliminado de la cola de descarga",
"extension_warning_zip": "El archivo '{0}' es un archivo comprimido y Batocera no soporta archivos comprimidos para este sistema. La extracción automática del archivo se realizará después de la descarga, ¿continuar?",
"extension_warning_unsupported": "La extensión del archivo '{0}' no está soportada por Batocera según la configuración es_systems.cfg. ¿Deseas continuar?",
"extension_warning_enable_unknown_hint": "\nPara no mostrar este mensaje: activa \"Ocultar aviso de extensión desconocida\" en Menú de pausa > Pantalla",
@@ -64,10 +65,21 @@
"menu_accessibility": "Accesibilidad",
"menu_display": "Pantalla",
"display_layout": "Distribución",
"display_monitor": "Monitor",
"display_monitor_single": "Monitor único",
"display_monitor_single_only": "Solo un monitor detectado",
"display_monitor_restart_required": "Reinicio necesario para cambiar de monitor",
"display_mode": "Modo de pantalla",
"display_fullscreen": "Pantalla completa",
"display_windowed": "Ventana",
"display_mode_restart_required": "Reinicio necesario para cambiar el modo",
"display_light_mode": "Modo rendimiento",
"display_light_mode_enabled": "Modo rendimiento activado - efectos desactivados",
"display_light_mode_disabled": "Modo rendimiento desactivado - efectos activados",
"menu_redownload_cache": "Actualizar lista de juegos",
"menu_music_enabled": "Música activada: {0}",
"menu_music_disabled": "Música desactivada",
"menu_restart": "Reiniciar",
"menu_restart": "Reiniciar RGSX",
"menu_support": "Soporte",
"menu_filter_platforms": "Filtrar sistemas",
"filter_platforms_title": "Visibilidad de sistemas",
@@ -80,6 +92,7 @@
"menu_allow_unknown_ext_enabled": "Aviso de extensión desconocida oculto (activado)",
"menu_allow_unknown_ext_disabled": "Aviso de extensión desconocida visible (desactivado)",
"menu_quit": "Salir",
"menu_quit_app": "Salir de RGSX",
"support_dialog_title": "Archivo de soporte",
"support_dialog_message": "Se ha creado un archivo de soporte con todos sus archivos de configuración y registros.\n\nArchivo: {0}\n\nPara obtener ayuda:\n1. Únete al servidor Discord de RGSX\n2. Describe tu problema\n3. Comparte este archivo ZIP\n\nPresiona {1} para volver al menú.",
"support_dialog_error": "Error al generar el archivo de soporte:\n{0}\n\nPresiona {1} para volver al menú.",
@@ -186,7 +199,9 @@
"instruction_pause_settings": "Música, opción symlink y estado de claves API",
"instruction_pause_restart": "Reiniciar RGSX para recargar configuración",
"instruction_pause_support": "Generar un archivo ZIP de diagnóstico para soporte",
"instruction_pause_quit": "Salir de la aplicación RGSX",
"instruction_pause_quit": "Acceder al menú para salir o reiniciar",
"instruction_quit_app": "Salir de la aplicación RGSX",
"instruction_quit_restart": "Reiniciar la aplicación RGSX",
"instruction_controls_help": "Mostrar referencia completa de mando y teclado",
"instruction_controls_remap": "Cambiar asignación de botones / teclas",
"instruction_generic_back": "Volver al menú anterior",
@@ -194,6 +209,9 @@
"instruction_display_font_size": "Ajustar tamaño del texto para mejor legibilidad",
"instruction_display_footer_font_size": "Ajustar el tamaño del texto del pie de página (versión y controles)",
"instruction_display_font_family": "Cambiar entre familias de fuentes disponibles",
"instruction_display_monitor": "Seleccionar monitor para mostrar RGSX",
"instruction_display_mode": "Alternar entre pantalla completa y ventana",
"instruction_display_light_mode": "Activar modo rendimiento para mejores FPS",
"instruction_display_show_unsupported": "Mostrar/ocultar sistemas no definidos en es_systems.cfg",
"instruction_display_unknown_ext": "Activar/desactivar aviso para extensiones no presentes en es_systems.cfg",
"instruction_display_hide_premium": "Ocultar sistemas que requieren acceso premium vía API: {providers}",
@@ -246,6 +264,8 @@
"history_option_extract_archive": "Extraer archivo",
"history_option_open_file": "Abrir archivo",
"history_option_scraper": "Obtener metadatos",
"history_option_remove_from_queue": "Quitar de la cola",
"history_option_cancel_download": "Cancelar descarga",
"history_option_delete_game": "Eliminar juego",
"history_option_error_info": "Detalles del error",
"history_option_retry": "Reintentar descarga",

View File

@@ -44,6 +44,7 @@
"free_mode_completed": "[Mode gratuit] Terminé: {0}",
"download_status": "{0} : {1}",
"download_canceled": "Téléchargement annulé par l'utilisateur.",
"download_removed_from_queue": "Retiré de la file de téléchargement",
"extension_warning_zip": "Le fichier '{0}' est une archive et Batocera ne prend pas en charge les archives pour ce système. L'extraction automatique du fichier aura lieu après le téléchargement, continuer ?",
"extension_warning_unsupported": "L'extension du fichier '{0}' n'est pas supportée par Batocera d'après la configuration es_systems.cfg. Voulez-vous continuer ?",
"extension_warning_enable_unknown_hint": "\nPour ne plus afficher ce messager : Activer l'option \"Masquer avertissement\" dans le Menu Pause>Display",
@@ -64,12 +65,24 @@
"menu_accessibility": "Accessibilité",
"menu_display": "Affichage",
"display_layout": "Disposition",
"display_monitor": "Écran",
"display_monitor_single": "Écran unique",
"display_monitor_single_only": "Un seul écran détecté",
"display_monitor_restart_required": "Redémarrage requis pour changer d'écran",
"display_mode": "Mode d'affichage",
"display_fullscreen": "Plein écran",
"display_windowed": "Fenêtré",
"display_mode_restart_required": "Redémarrage requis pour changer le mode",
"display_light_mode": "Mode performance",
"display_light_mode_enabled": "Mode performance activé - effets désactivés",
"display_light_mode_disabled": "Mode performance désactivé - effets activés",
"menu_redownload_cache": "Mettre à jour la liste des jeux",
"menu_support": "Support",
"menu_quit": "Quitter",
"menu_quit_app": "Quitter RGSX",
"menu_music_enabled": "Musique activée : {0}",
"menu_music_disabled": "Musique désactivée",
"menu_restart": "Redémarrer",
"menu_restart": "Redémarrer RGSX",
"menu_filter_platforms": "Filtrer les systèmes",
"filter_platforms_title": "Affichage des systèmes",
"filter_platforms_info": "Visibles: {0} | Masqués: {1} / Total: {2}",
@@ -186,7 +199,9 @@
"instruction_pause_settings": "Musique, option symlink & statut des clés API",
"instruction_pause_restart": "Redémarrer RGSX pour recharger la configuration",
"instruction_pause_support": "Générer un fichier ZIP de diagnostic pour l'assistance",
"instruction_pause_quit": "Quitter l'application RGSX",
"instruction_pause_quit": "Accéder au menu pour quitter ou redémarrer",
"instruction_quit_app": "Quitter l'application RGSX",
"instruction_quit_restart": "Redémarrer l'application RGSX",
"instruction_controls_help": "Afficher la référence complète manette & clavier",
"instruction_controls_remap": "Modifier l'association boutons / touches",
"instruction_generic_back": "Revenir au menu précédent",
@@ -194,6 +209,9 @@
"instruction_display_font_size": "Ajuster la taille du texte pour la lisibilité",
"instruction_display_footer_font_size": "Ajuster la taille du texte du pied de page (version et contrôles)",
"instruction_display_font_family": "Basculer entre les polices disponibles",
"instruction_display_monitor": "Sélectionner l'écran pour afficher RGSX",
"instruction_display_mode": "Basculer entre plein écran et fenêtré",
"instruction_display_light_mode": "Activer le mode performance pour de meilleurs FPS",
"instruction_display_show_unsupported": "Afficher/masquer systèmes absents de es_systems.cfg",
"instruction_display_unknown_ext": "Avertir ou non pour extensions absentes de es_systems.cfg",
"instruction_display_hide_premium": "Masquer les systèmes nécessitant un accès premium via API: {providers}",

View File

@@ -44,6 +44,7 @@
"free_mode_completed": "[Modalità gratuita] Completato: {0}",
"download_status": "{0}: {1}",
"download_canceled": "Download annullato dall'utente.",
"download_removed_from_queue": "Rimosso dalla coda di download",
"extension_warning_zip": "Il file '{0}' è un archivio e Batocera non supporta archivi per questo sistema. L'estrazione automatica avverrà dopo il download, continuare?",
"extension_warning_unsupported": "L'estensione del file '{0}' non è supportata da Batocera secondo la configurazione di es_systems.cfg. Vuoi continuare?",
"extension_warning_enable_unknown_hint": "\nPer non visualizzare questo messaggio: abilita \"Nascondi avviso estensione sconosciuta\" in Menu Pausa > Schermo",
@@ -64,10 +65,19 @@
"menu_accessibility": "Accessibilità",
"menu_display": "Schermo",
"display_layout": "Layout schermo",
"menu_redownload_cache": "Aggiorna elenco giochi",
"display_monitor": "Monitor",
"display_monitor_single": "Monitor singolo",
"display_monitor_single_only": "Rilevato un solo monitor",
"display_monitor_restart_required": "Riavvio necessario per cambiare monitor",
"display_mode": "Modalità schermo",
"display_fullscreen": "Schermo intero",
"display_windowed": "Finestra",
"display_mode_restart_required": "Riavvio necessario per cambiare modalità", "display_light_mode": "Modalità performance",
"display_light_mode_enabled": "Modalità performance attivata - effetti disattivati",
"display_light_mode_disabled": "Modalità performance disattivata - effetti attivati", "menu_redownload_cache": "Aggiorna elenco giochi",
"menu_music_enabled": "Musica attivata: {0}",
"menu_music_disabled": "Musica disattivata",
"menu_restart": "Riavvia",
"menu_restart": "Riavvia RGSX",
"menu_support": "Supporto",
"menu_filter_platforms": "Filtra sistemi",
"filter_platforms_title": "Visibilità sistemi",
@@ -80,6 +90,7 @@
"menu_allow_unknown_ext_enabled": "Nascondi avviso estensione sconosciuta abilitato",
"menu_allow_unknown_ext_disabled": "Nascondi avviso estensione sconosciuta disabilitato",
"menu_quit": "Esci",
"menu_quit_app": "Esci da RGSX",
"support_dialog_title": "File di supporto",
"support_dialog_message": "È stato creato un file di supporto con tutti i file di configurazione e di registro.\n\nFile: {0}\n\nPer ottenere aiuto:\n1. Unisciti al server Discord RGSX\n2. Descrivi il tuo problema\n3. Condividi questo file ZIP\n\nPremi {1} per tornare al menu.",
"support_dialog_error": "Errore durante la generazione del file di supporto:\n{0}\n\nPremi {1} per tornare al menu.",
@@ -183,7 +194,9 @@
"instruction_pause_settings": "Musica, opzione symlink e stato chiavi API",
"instruction_pause_restart": "Riavvia RGSX per ricaricare la configurazione",
"instruction_pause_support": "Genera un file ZIP diagnostico per il supporto",
"instruction_pause_quit": "Uscire dall'applicazione RGSX",
"instruction_pause_quit": "Accedere al menu per uscire o riavviare",
"instruction_quit_app": "Uscire dall'applicazione RGSX",
"instruction_quit_restart": "Riavviare l'applicazione RGSX",
"instruction_controls_help": "Mostrare riferimento completo controller & tastiera",
"instruction_controls_remap": "Modificare associazione pulsanti / tasti",
"instruction_generic_back": "Tornare al menu precedente",
@@ -191,6 +204,9 @@
"instruction_display_font_size": "Regolare dimensione testo per leggibilità",
"instruction_display_footer_font_size": "Regola dimensione testo piè di pagina (versione e controlli)",
"instruction_display_font_family": "Cambiare famiglia di font disponibile",
"instruction_display_monitor": "Selezionare monitor per visualizzare RGSX",
"instruction_display_mode": "Alternare tra schermo intero e finestra",
"instruction_display_light_mode": "Attivare modalità performance per FPS migliori",
"instruction_display_show_unsupported": "Mostrare/nascondere sistemi non definiti in es_systems.cfg",
"instruction_display_unknown_ext": "Attivare/disattivare avviso per estensioni assenti in es_systems.cfg",
"instruction_display_hide_premium": "Nascondere sistemi che richiedono accesso premium via API: {providers}",

View File

@@ -44,6 +44,7 @@
"free_mode_completed": "[Modo gratuito] Concluído: {0}",
"download_status": "{0}: {1}",
"download_canceled": "Download cancelado pelo usuário.",
"download_removed_from_queue": "Removido da fila de download",
"extension_warning_zip": "O arquivo '{0}' é um arquivo compactado e o Batocera não suporta arquivos compactados para este sistema. A extração automática ocorrerá após o download, continuar?",
"extension_warning_unsupported": "A extensão do arquivo '{0}' não é suportada pelo Batocera segundo a configuração es_systems.cfg. Deseja continuar?",
"extension_warning_enable_unknown_hint": "\nPara não ver esta mensagem: ative \"Ocultar aviso de extensão desconhecida\" em Menu de Pausa > Exibição",
@@ -64,10 +65,21 @@
"menu_accessibility": "Acessibilidade",
"menu_display": "Exibição",
"display_layout": "Layout de exibição",
"display_monitor": "Monitor",
"display_monitor_single": "Monitor único",
"display_monitor_single_only": "Apenas um monitor detectado",
"display_monitor_restart_required": "Reinício necessário para mudar de monitor",
"display_mode": "Modo de tela",
"display_fullscreen": "Tela cheia",
"display_windowed": "Janela",
"display_mode_restart_required": "Reinício necessário para mudar o modo",
"display_light_mode": "Modo performance",
"display_light_mode_enabled": "Modo performance ativado - efeitos desativados",
"display_light_mode_disabled": "Modo performance desativado - efeitos ativados",
"menu_redownload_cache": "Atualizar lista de jogos",
"menu_music_enabled": "Música ativada: {0}",
"menu_music_disabled": "Música desativada",
"menu_restart": "Reiniciar",
"menu_restart": "Reiniciar RGSX",
"menu_support": "Suporte",
"menu_filter_platforms": "Filtrar sistemas",
"filter_platforms_title": "Visibilidade dos sistemas",
@@ -80,6 +92,7 @@
"menu_allow_unknown_ext_enabled": "Aviso de extensão desconhecida oculto (ativado)",
"menu_allow_unknown_ext_disabled": "Aviso de extensão desconhecida visível (desativado)",
"menu_quit": "Sair",
"menu_quit_app": "Sair do RGSX",
"support_dialog_title": "Arquivo de suporte",
"support_dialog_message": "Foi criado um arquivo de suporte com todos os seus arquivos de configuração e logs.\n\nArquivo: {0}\n\nPara obter ajuda:\n1. Junte-se ao servidor Discord RGSX\n2. Descreva seu problema\n3. Compartilhe este arquivo ZIP\n\nPressione {1} para voltar ao menu.",
"support_dialog_error": "Erro ao gerar o arquivo de suporte:\n{0}\n\nPressione {1} para voltar ao menu.",
@@ -185,7 +198,9 @@
"instruction_pause_settings": "Música, opção symlink e status das chaves API",
"instruction_pause_restart": "Reiniciar RGSX para recarregar configuração",
"instruction_pause_support": "Gerar um arquivo ZIP de diagnóstico para suporte",
"instruction_pause_quit": "Sair da aplicação RGSX",
"instruction_pause_quit": "Acessar menu para sair ou reiniciar",
"instruction_quit_app": "Sair da aplicação RGSX",
"instruction_quit_restart": "Reiniciar a aplicação RGSX",
"instruction_controls_help": "Mostrar referência completa de controle e teclado",
"instruction_controls_remap": "Modificar associação de botões / teclas",
"instruction_generic_back": "Voltar ao menu anterior",
@@ -193,6 +208,9 @@
"instruction_display_font_size": "Ajustar tamanho do texto para legibilidade",
"instruction_display_footer_font_size": "Ajustar tamanho do texto do rodapé (versão e controles)",
"instruction_display_font_family": "Alternar entre famílias de fontes disponíveis",
"instruction_display_monitor": "Selecionar monitor para exibir RGSX",
"instruction_display_mode": "Alternar entre tela cheia e janela",
"instruction_display_light_mode": "Ativar modo performance para melhor FPS",
"instruction_display_show_unsupported": "Mostrar/ocultar sistemas não definidos em es_systems.cfg",
"instruction_display_unknown_ext": "Ativar/desativar aviso para extensões ausentes em es_systems.cfg",
"instruction_display_hide_premium": "Ocultar sistemas que exigem acesso premium via API: {providers}",
@@ -245,6 +263,8 @@
"history_option_extract_archive": "Extrair arquivo",
"history_option_open_file": "Abrir arquivo",
"history_option_scraper": "Obter metadados",
"history_option_remove_from_queue": "Remover da fila",
"history_option_cancel_download": "Cancelar download",
"history_option_delete_game": "Excluir jogo",
"history_option_error_info": "Detalhes do erro",
"history_option_retry": "Tentar novamente",

View File

@@ -453,8 +453,102 @@ async def check_for_updates():
config.loading_progress = 5.0
config.needs_redraw = True
response = requests.get(OTA_VERSION_ENDPOINT, timeout=5)
response.raise_for_status()
# Liste des endpoints à essayer (GitHub principal, puis fallback)
endpoints = [
OTA_VERSION_ENDPOINT,
"https://retrogamesets.fr/softs/version.json"
]
response = None
last_error = None
for endpoint_index, endpoint in enumerate(endpoints):
is_fallback = endpoint_index > 0
if is_fallback:
logger.info(f"Tentative sur endpoint de secours : {endpoint}")
# Gestion des erreurs de rate limit GitHub (429) avec retry
max_retries = 3 if not is_fallback else 1 # Moins de retries sur fallback
retry_count = 0
while retry_count < max_retries:
try:
response = requests.get(endpoint, timeout=10)
# Gestion spécifique des erreurs 429 (Too Many Requests) - surtout pour GitHub
if response.status_code == 429:
retry_after = response.headers.get('retry-after')
x_ratelimit_remaining = response.headers.get('x-ratelimit-remaining', '1')
x_ratelimit_reset = response.headers.get('x-ratelimit-reset')
if retry_after:
# En-tête retry-after présent : attendre le nombre de secondes spécifié
wait_time = int(retry_after)
logger.warning(f"Rate limit atteint (429) sur {endpoint}. Attente de {wait_time}s (retry-after header)")
elif x_ratelimit_remaining == '0' and x_ratelimit_reset:
# x-ratelimit-remaining est 0 : attendre jusqu'à x-ratelimit-reset
import time
reset_time = int(x_ratelimit_reset)
current_time = int(time.time())
wait_time = max(reset_time - current_time, 60) # Minimum 60s
logger.warning(f"Rate limit atteint (429) sur {endpoint}. Attente de {wait_time}s (x-ratelimit-reset)")
else:
# Pas d'en-têtes spécifiques : attendre au moins 60s
wait_time = 60
logger.warning(f"Rate limit atteint (429) sur {endpoint}. Attente de {wait_time}s par défaut")
if retry_count < max_retries - 1:
logger.info(f"Nouvelle tentative dans {wait_time}s... ({retry_count + 1}/{max_retries})")
await asyncio.sleep(wait_time)
retry_count += 1
continue
else:
# Si rate limit persistant et qu'on est sur GitHub, essayer le fallback
if not is_fallback:
logger.warning(f"Rate limit GitHub persistant, passage au serveur de secours")
break # Sortir de la boucle retry pour essayer le prochain endpoint
raise requests.exceptions.HTTPError(
f"Limite de débit atteinte (429). Veuillez réessayer plus tard."
)
response.raise_for_status()
# Succès, sortir de toutes les boucles
logger.debug(f"Version récupérée avec succès depuis : {endpoint}")
break
except requests.exceptions.HTTPError as e:
last_error = e
if response and response.status_code == 429:
# 429 géré au-dessus, continuer la boucle ou passer au fallback
retry_count += 1
if retry_count >= max_retries:
break # Passer au prochain endpoint
else:
# Erreur HTTP autre que 429
logger.warning(f"Erreur HTTP {response.status_code if response else 'inconnue'} sur {endpoint}")
break # Passer au prochain endpoint
except requests.exceptions.RequestException as e:
last_error = e
if retry_count < max_retries - 1:
# Erreur réseau, réessayer avec backoff exponentiel
wait_time = 2 ** retry_count # 1s, 2s, 4s
logger.warning(f"Erreur réseau sur {endpoint}. Nouvelle tentative dans {wait_time}s...")
await asyncio.sleep(wait_time)
retry_count += 1
else:
logger.warning(f"Erreur réseau persistante sur {endpoint} : {e}")
break # Passer au prochain endpoint
# Si on a une réponse valide, sortir de la boucle des endpoints
if response and response.status_code == 200:
break
# Si aucun endpoint n'a fonctionné
if not response or response.status_code != 200:
raise last_error if last_error else requests.exceptions.RequestException(
"Impossible de vérifier les mises à jour sur tous les serveurs"
)
# Accepter différents content-types (application/json, text/plain, text/html)
content_type = response.headers.get("content-type", "")
@@ -831,6 +925,16 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
logger.info(f"Le fichier {dest_path} existe déjà et la taille est correcte, téléchargement ignoré")
result[0] = True
result[1] = _("network_download_ok").format(game_name) + _("download_already_present")
# Mettre à jour l'historique
for entry in config.history:
if entry.get("url") == url:
entry["status"] = "Download_OK"
entry["progress"] = 100
entry["message"] = result[1]
save_history(config.history)
break
# Afficher un toast au lieu d'ouvrir l'historique
try:
show_toast(result[1])
@@ -839,6 +943,13 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
with urls_lock:
urls_in_progress.discard(url)
logger.debug(f"URL supprimée du set des téléchargements en cours: {url} (URLs restantes: {len(urls_in_progress)})")
# Libérer le slot de la queue
try:
notify_download_finished()
except Exception:
pass
return result[0], result[1]
file_found = True
@@ -881,6 +992,16 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
logger.info(f"Un fichier avec le même nom de base existe déjà: {existing_path}, téléchargement ignoré")
result[0] = True
result[1] = _("network_download_ok").format(game_name) + _("download_already_extracted")
# Mettre à jour l'historique
for entry in config.history:
if entry.get("url") == url:
entry["status"] = "Download_OK"
entry["progress"] = 100
entry["message"] = result[1]
save_history(config.history)
break
# Afficher un toast au lieu d'ouvrir l'historique
try:
show_toast(result[1])
@@ -889,6 +1010,13 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
with urls_lock:
urls_in_progress.discard(url)
logger.debug(f"URL supprimée du set des téléchargements en cours: {url} (URLs restantes: {len(urls_in_progress)})")
# Libérer le slot de la queue
try:
notify_download_finished()
except Exception:
pass
return result[0], result[1]
except Exception as e:
logger.debug(f"Erreur lors de la vérification des fichiers existants: {e}")
@@ -1115,6 +1243,11 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
# Si annulé, ne pas continuer avec extraction
if download_canceled:
# Libérer le slot de la queue
try:
notify_download_finished()
except Exception:
pass
return
os.chmod(dest_path, 0o644)
@@ -1336,6 +1469,12 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
if url in url_done_events:
url_done_events[url].set()
# Libérer le slot de la queue
try:
notify_download_finished()
except Exception:
pass
return result[0], result[1]
async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=False, task_id=None):

View File

@@ -29,7 +29,7 @@ def delete_old_files():
try:
if os.path.exists(file_path):
os.remove(file_path)
print(f"Ancien fichier supprimé : {file_path}")
print(f"Ancien fichier supprime : {file_path}")
logger.info(f"Ancien fichier supprimé : {file_path}")
except Exception as e:
print(f"Erreur lors de la suppression de {file_path} : {str(e)}")
@@ -39,7 +39,7 @@ def delete_old_files():
try:
if os.path.exists(file_path):
os.remove(file_path)
print(f"Ancien fichier supprimé : {file_path}")
print(f"Ancien fichier supprime : {file_path}")
logger.info(f"Ancien fichier supprimé : {file_path}")
except Exception as e:
print(f"Erreur lors de la suppression de {file_path} : {str(e)}")
@@ -49,6 +49,8 @@ def load_rgsx_settings():
"""Charge tous les paramètres depuis rgsx_settings.json."""
from config import RGSX_SETTINGS_PATH
logger.debug(f"Chargement des settings depuis: {RGSX_SETTINGS_PATH}")
default_settings = {
"language": "en",
"music_enabled": True,
@@ -58,7 +60,10 @@ def load_rgsx_settings():
},
"display": {
"grid": "3x4",
"font_family": "pixel"
"font_family": "pixel",
"monitor": 0,
"fullscreen": True,
"light_mode": False
},
"symlink": {
"enabled": False,
@@ -78,13 +83,17 @@ def load_rgsx_settings():
if os.path.exists(RGSX_SETTINGS_PATH):
with open(RGSX_SETTINGS_PATH, 'r', encoding='utf-8') as f:
settings = json.load(f)
logger.debug(f"Settings JSON chargé: display={settings.get('display', {})}")
# Fusionner avec les valeurs par défaut pour assurer la compatibilité
for key, value in default_settings.items():
if key not in settings:
settings[key] = value
return settings
else:
logger.warning(f"Fichier settings non trouvé: {RGSX_SETTINGS_PATH}")
except Exception as e:
print(f"Erreur lors du chargement de rgsx_settings.json: {str(e)}")
logger.error(f"Erreur chargement settings: {e}")
return default_settings
@@ -307,6 +316,92 @@ def set_display_grid(cols: int, rows: int):
save_rgsx_settings(settings)
return cols, rows
# ----------------------- Monitor/Display settings ----------------------- #
def get_display_monitor(settings=None):
"""Retourne l'index du moniteur configuré (par défaut 0 = principal)."""
if settings is None:
settings = load_rgsx_settings()
return settings.get("display", {}).get("monitor", 0)
def set_display_monitor(monitor_index: int):
"""Définit et sauvegarde l'index du moniteur à utiliser."""
settings = load_rgsx_settings()
disp = settings.setdefault("display", {})
disp["monitor"] = max(0, int(monitor_index))
save_rgsx_settings(settings)
return disp["monitor"]
def get_display_fullscreen(settings=None):
"""Retourne True si le mode plein écran est activé."""
if settings is None:
settings = load_rgsx_settings()
return settings.get("display", {}).get("fullscreen", True)
def set_display_fullscreen(fullscreen: bool):
"""Définit et sauvegarde le mode plein écran."""
settings = load_rgsx_settings()
disp = settings.setdefault("display", {})
disp["fullscreen"] = bool(fullscreen)
save_rgsx_settings(settings)
return disp["fullscreen"]
def get_light_mode(settings=None):
"""Retourne True si le mode léger (performance) est activé."""
if settings is None:
settings = load_rgsx_settings()
return settings.get("display", {}).get("light_mode", False)
def set_light_mode(enabled: bool):
"""Définit et sauvegarde le mode léger (performance)."""
settings = load_rgsx_settings()
disp = settings.setdefault("display", {})
disp["light_mode"] = bool(enabled)
save_rgsx_settings(settings)
return disp["light_mode"]
def get_available_monitors():
"""Retourne la liste des moniteurs disponibles avec leurs informations.
Compatible Windows, Linux (Batocera), et autres plateformes.
Retourne une liste de dicts: [{"index": 0, "name": "Monitor 1", "resolution": "1920x1080"}, ...]
"""
monitors = []
try:
import pygame
if not pygame.display.get_init():
pygame.display.init()
num_displays = pygame.display.get_num_displays()
for i in range(num_displays):
try:
# Essayer d'obtenir le mode desktop pour ce display
mode = pygame.display.get_desktop_sizes()[i] if hasattr(pygame.display, 'get_desktop_sizes') else None
if mode:
width, height = mode
else:
# Fallback: utiliser la résolution actuelle si disponible
info = pygame.display.Info()
width, height = info.current_w, info.current_h
monitors.append({
"index": i,
"name": f"Monitor {i + 1}",
"resolution": f"{width}x{height}"
})
except Exception as e:
# Si on ne peut pas obtenir les infos, ajouter quand même le moniteur
monitors.append({
"index": i,
"name": f"Monitor {i + 1}",
"resolution": "Unknown"
})
except Exception as e:
logger.error(f"Error getting monitors: {e}")
# Fallback: au moins un moniteur
monitors = [{"index": 0, "name": "Monitor 1 (Default)", "resolution": "Auto"}]
return monitors if monitors else [{"index": 0, "name": "Monitor 1 (Default)", "resolution": "Auto"}]
def get_font_family(settings=None):
if settings is None:
settings = load_rgsx_settings()

View File

@@ -364,7 +364,7 @@ try:
logger.info("Test d'écriture dans le fichier de log réussi")
except Exception as e:
logger.error(f"Erreur lors du test d'écriture : {e}")
print(f"ERREUR: Impossible d'écrire dans {config.log_file_web}: {e}", file=sys.stderr)
print(f"ERREUR: Impossible d'ecrire dans {config.log_file_web}: {e}", file=sys.stderr)
# Initialiser les données au démarrage
logger.info("Chargement initial des données...")
@@ -772,7 +772,7 @@ class RGSXHandler(BaseHTTPRequestHandler):
# Lire depuis history.json - filtrer seulement les téléchargements en cours
history = load_history() or []
print(f"\n[DEBUG PROGRESS] history.json chargé avec {len(history)} entrées totales")
print(f"\n[DEBUG PROGRESS] history.json charge avec {len(history)} entrees totales")
# Filtrer les entrées avec status "Downloading", "Téléchargement", "Connecting", "Try X/Y"
in_progress_statuses = ["Downloading", "Téléchargement", "Downloading", "Connecting", "Extracting"]
@@ -797,9 +797,9 @@ class RGSXHandler(BaseHTTPRequestHandler):
else:
# Debug: afficher les premiers status qui ne matchent pas
if len(downloads) < 3:
print(f" [DEBUG] Ignoré - Status: '{status}', Game: {entry.get('game_name', '')[:50]}")
print(f" [DEBUG] Ignore - Status: '{status}', Game: {entry.get('game_name', '')[:50]}")
print(f"[DEBUG PROGRESS] {len(downloads)} téléchargements en cours trouvés")
print(f"[DEBUG PROGRESS] {len(downloads)} telechargements en cours trouves")
if downloads:
for url, data in list(downloads.items())[:2]:
print(f" - URL: {url[:80]}...")
@@ -2088,7 +2088,7 @@ def run_server(host='0.0.0.0', port=5000):
if __name__ == '__main__':
print("="*60, flush=True)
print("Demarrage du serveur RGSX Web...", flush=True)
print(f"Fichier de log prévu: {config.log_file_web}", flush=True)
print(f"Fichier de log prevu: {config.log_file_web}", flush=True)
print("="*60, flush=True)
parser = argparse.ArgumentParser(description='RGSX Web Server')

View File

@@ -1,3 +1,3 @@
{
"version": "2.3.2.9"
"version": "2.4.0.1"
}

View File

@@ -1,149 +1,385 @@
@echo off
setlocal EnableDelayedExpansion
:: Fichier de log
if not exist "%CD%\logs" MD "%CD%\logs"
set "LOG_FILE=%CD%\logs\Retrobat_RGSX_log.txt"
:: Fichier de log (chemin absolu pour fiabilité)
:: Détecter la racine (ROOT_DIR) d'abord pour construire un chemin stable
set CURRENT_DIR=%CD%
pushd "%CURRENT_DIR%\..\.."
set "ROOT_DIR=%CD%"
popd
if not exist "%ROOT_DIR%\roms\windows\logs" MD "%ROOT_DIR%\roms\windows\logs"
set "LOG_FILE=%ROOT_DIR%\roms\windows\logs\Retrobat_RGSX_log.txt"
:: =============================================================================
:: RGSX Retrobat Launcher v1.3
:: =============================================================================
:: Usage: "RGSX Retrobat.bat" [options]
:: --display=N Launch on display N (0=primary, 1=secondary, etc.)
:: --windowed Launch in windowed mode instead of fullscreen
:: --help Show this help
:: =============================================================================
:: Ajouter un horodatage au début du log
echo [%DATE% %TIME%] Script start >> "%LOG_FILE%"
:: Configuration des couleurs (codes ANSI)
for /F "tokens=1,2 delims=#" %%a in ('"prompt #$H#$E# & echo on & for %%b in (1) do rem"') do (
set "ESC=%%b"
)
:: Afficher un message de démarrage
:: Couleurs
set "GREEN=[92m"
set "YELLOW=[93m"
set "RED=[91m"
set "CYAN=[96m"
set "RESET=[0m"
set "BOLD=[1m"
:: =============================================================================
:: Traitement des arguments
:: =============================================================================
set "DISPLAY_NUM="
set "WINDOWED_MODE="
set "CONFIG_FILE="
:parse_args
if "%~1"=="" goto :args_done
if /i "%~1"=="--help" goto :show_help
if /i "%~1"=="-h" goto :show_help
if /i "%~1"=="--windowed" (
set "WINDOWED_MODE=1"
shift
goto :parse_args
)
:: Check for --display=N format
echo %~1 | findstr /r "^--display=" >nul
if !ERRORLEVEL! EQU 0 (
for /f "tokens=2 delims==" %%a in ("%~1") do set "DISPLAY_NUM=%%a"
shift
goto :parse_args
)
shift
goto :parse_args
:show_help
echo.
echo %ESC%%CYAN%RGSX Retrobat Launcher - Help%ESC%%RESET%
echo.
echo Usage: "RGSX Retrobat.bat" [options]
echo.
echo Options:
echo --display=N Launch on display N (0=primary, 1=secondary, etc.)
echo --windowed Launch in windowed mode instead of fullscreen
echo --help, -h Show this help
echo.
echo Examples:
echo "RGSX Retrobat.bat" Launch on primary display
echo "RGSX Retrobat.bat" --display=1 Launch on secondary display (TV)
echo "RGSX Retrobat.bat" --windowed Launch in windowed mode
echo.
echo You can also create shortcuts with different display settings.
echo.
pause
exit /b 0
:args_done
:: URL de telechargement Python
set "PYTHON_ZIP_URL=https://github.com/RetroGameSets/RGSX/raw/main/windows/python.zip"
:: Obtenir le chemin du script de maniere fiable
set "SCRIPT_DIR=%~dp0"
set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
:: Detecter le repertoire racine
for %%I in ("%SCRIPT_DIR%\..\.." ) do set "ROOT_DIR=%%~fI"
:: Configuration des logs
set "LOG_DIR=%ROOT_DIR%\roms\windows\logs"
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
set "LOG_FILE=%LOG_DIR%\Retrobat_RGSX_log.txt"
set "LOG_BACKUP=%LOG_DIR%\Retrobat_RGSX_log.old.txt"
:: Rotation des logs avec backup
if exist "%LOG_FILE%" (
for %%A in ("%LOG_FILE%") do (
if %%~zA GTR 100000 (
if exist "%LOG_BACKUP%" del /q "%LOG_BACKUP%"
move /y "%LOG_FILE%" "%LOG_BACKUP%" >nul 2>&1
echo [%DATE% %TIME%] Log rotated - previous log saved as .old.txt > "%LOG_FILE%"
)
)
)
:: =============================================================================
:: Ecran d'accueil
:: =============================================================================
cls
echo Running __main__.py for RetroBat...
echo [%DATE% %TIME%] Running __main__.py for RetroBat >> "%LOG_FILE%"
echo.
echo %ESC%%CYAN% ____ ____ ______ __ %ESC%%RESET%
echo %ESC%%CYAN% ^| _ \ / ___^/ ___\ \/ / %ESC%%RESET%
echo %ESC%%CYAN% ^| ^|_) ^| ^| _\___ \\ / %ESC%%RESET%
echo %ESC%%CYAN% ^| _ ^<^| ^|_^| ^|___) / \ %ESC%%RESET%
echo %ESC%%CYAN% ^|_^| \_\\____^|____/_/\_\ %ESC%%RESET%
echo.
echo %ESC%%BOLD% RetroBat Launcher v1.3%ESC%%RESET%
echo --------------------------------
if "!DISPLAY_NUM!" NEQ "0" (
echo %ESC%%CYAN%Display: !DISPLAY_NUM!%ESC%%RESET%
)
if "!WINDOWED_MODE!"=="1" (
echo %ESC%%CYAN%Mode: Windowed%ESC%%RESET%
)
echo.
:: Définir les chemins relatifs et les convertir en absolus
set CURRENT_DIR=%CD%
set PYTHON_EXE=python.exe
:: Debut du log
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
echo [%DATE% %TIME%] RGSX Launcher v1.3 started >> "%LOG_FILE%"
echo [%DATE% %TIME%] Display: !DISPLAY_NUM!, Windowed: !WINDOWED_MODE! >> "%LOG_FILE%"
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
:: Détecter le répertoire racine en remontant de deux niveaux depuis le script
pushd "%CURRENT_DIR%\..\.."
set "ROOT_DIR=%CD%"
popd
:: Définir le chemin du script principal selon les spécifications
:: Configuration des chemins
set "PYTHON_DIR=%ROOT_DIR%\system\tools\Python"
set "PYTHON_EXE=%PYTHON_DIR%\python.exe"
set "MAIN_SCRIPT=%ROOT_DIR%\roms\ports\RGSX\__main__.py"
set "ZIP_FILE=%ROOT_DIR%\roms\windows\python.zip"
:: Definir le chemin du script de mise à jour de la gamelist Windows
set "UPDATE_GAMELIST_SCRIPT=%ROOT_DIR%\roms\ports\RGSX\update_gamelist_windows.py"
:: Exporter RGSX_ROOT pour le script Python
set "RGSX_ROOT=%ROOT_DIR%"
:: Convertir les chemins relatifs en absolus avec pushd/popd
pushd "%ROOT_DIR%\system\tools\Python"
set "PYTHON_EXE_FULL=%ROOT_DIR%\system\tools\Python\!PYTHON_EXE!"
set "PYTHONW_EXE_FULL=%ROOT_DIR%\system\tools\Python\pythonw.exe"
popd
:: Logger les chemins
echo [%DATE% %TIME%] System info: >> "%LOG_FILE%"
echo [%DATE% %TIME%] ROOT_DIR: %ROOT_DIR% >> "%LOG_FILE%"
echo [%DATE% %TIME%] PYTHON_EXE: %PYTHON_EXE% >> "%LOG_FILE%"
echo [%DATE% %TIME%] MAIN_SCRIPT: %MAIN_SCRIPT% >> "%LOG_FILE%"
echo [%DATE% %TIME%] RGSX_ROOT: %RGSX_ROOT% >> "%LOG_FILE%"
:: Afficher et logger les variables
:: =============================================================================
:: Verification Python
:: =============================================================================
echo %ESC%%YELLOW%[1/3]%ESC%%RESET% Checking Python environment...
echo [%DATE% %TIME%] Step 1/3: Checking Python >> "%LOG_FILE%"
echo ROOT_DIR : %ROOT_DIR% >> "%LOG_FILE%"
echo CURRENT_DIR : !CURRENT_DIR! >> "%LOG_FILE%"
echo ROOT_DIR : !ROOT_DIR! >> "%LOG_FILE%"
echo PYTHON_EXE_FULL : !PYTHON_EXE_FULL! >> "%LOG_FILE%"
echo MAIN_SCRIPT : !MAIN_SCRIPT! >> "%LOG_FILE%"
echo UPDATE_GAMELIST_SCRIPT : !UPDATE_GAMELIST_SCRIPT! >> "%LOG_FILE%"
:: Vérifier si l'exécutable Python existe
echo Checking python.exe...
echo [%DATE% %TIME%] Checking python.exe at !PYTHON_EXE_FULL! >> "%LOG_FILE%"
if not exist "!PYTHON_EXE_FULL!" (
echo python.exe not found in system/tools. Preparing to extract..
echo [%DATE% %TIME%] python.exe not found in system/tools. Preparing to extract.. >> "%LOG_FILE%"
if not exist "%PYTHON_EXE%" (
echo %ESC%%YELLOW%^> Python not found, installing...%ESC%%RESET%
echo [%DATE% %TIME%] Python not found, starting installation >> "%LOG_FILE%"
:: Créer le dossier Python s'il n'existe pas
set "TOOLS_FOLDER_FULL=!ROOT_DIR!\system\tools"
if not exist "!TOOLS_FOLDER_FULL!\Python" (
echo Creating folder !TOOLS_FOLDER_FULL!\Python...
echo [%DATE% %TIME%] Creating folder !TOOLS_FOLDER_FULL!\Python... >> "%LOG_FILE%"
mkdir "!TOOLS_FOLDER_FULL!\Python"
:: Creer le dossier Python
if not exist "%PYTHON_DIR%" (
mkdir "%PYTHON_DIR%" 2>nul
echo [%DATE% %TIME%] Created folder: %PYTHON_DIR% >> "%LOG_FILE%"
)
set "ZIP_FILE=%ROOT_DIR%\roms\windows\python.zip"
echo Extracting ZIP_FILE : !ZIP_FILE! in /system/tools/Python
echo [%DATE% %TIME%] ZIP_FILE : !ZIP_FILE! >> "%LOG_FILE%"
if exist "!ZIP_FILE!" (
echo [%DATE% %TIME%] Extracting python.zip to !TOOLS_FOLDER_FULL!... >> "%LOG_FILE%"
tar -xf "!ZIP_FILE!" -C "!TOOLS_FOLDER_FULL!\Python" --strip-components=0
echo Extraction finished.
echo [%DATE% %TIME%] Extraction finished. >> "%LOG_FILE%"
del /s /q "!ZIP_FILE!"
echo python.zip file deleted.
echo [%DATE% %TIME%] python.zip file deleted. >> "%LOG_FILE%"
) else (
echo Error: Error python.zip not found please download it from github and put in /roms/windows folder.
echo [%DATE% %TIME%] Error: Error python.zip not found please download it from github and put in /roms/windows folder >> "%LOG_FILE%"
:: Verifier si le ZIP existe, sinon le telecharger
if not exist "%ZIP_FILE%" (
echo %ESC%%YELLOW%^> python.zip not found, downloading from GitHub...%ESC%%RESET%
echo [%DATE% %TIME%] python.zip not found, attempting download >> "%LOG_FILE%"
echo [%DATE% %TIME%] Download URL: %PYTHON_ZIP_URL% >> "%LOG_FILE%"
:: Verifier si curl est disponible
where curl.exe >nul 2>&1
if !ERRORLEVEL! EQU 0 (
echo %ESC%%CYAN%^> Using curl to download...%ESC%%RESET%
echo [%DATE% %TIME%] Using curl.exe for download >> "%LOG_FILE%"
curl.exe -L -# -o "%ZIP_FILE%" "%PYTHON_ZIP_URL%"
set DOWNLOAD_RESULT=!ERRORLEVEL!
) else (
:: Fallback sur PowerShell
echo %ESC%%CYAN%^> Using PowerShell to download...%ESC%%RESET%
echo [%DATE% %TIME%] curl not found, using PowerShell >> "%LOG_FILE%"
powershell -NoProfile -ExecutionPolicy Bypass -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest -Uri '%PYTHON_ZIP_URL%' -OutFile '%ZIP_FILE%'"
set DOWNLOAD_RESULT=!ERRORLEVEL!
)
:: Verifier le resultat du telechargement
if !DOWNLOAD_RESULT! NEQ 0 (
echo.
echo %ESC%%RED% ERROR: Download failed!%ESC%%RESET%
echo.
echo Please download python.zip manually from:
echo %ESC%%CYAN%%PYTHON_ZIP_URL%%ESC%%RESET%
echo.
echo And place it in:
echo %ESC%%CYAN%%ROOT_DIR%\roms\windows\%ESC%%RESET%
echo.
echo [%DATE% %TIME%] ERROR: Download failed with code !DOWNLOAD_RESULT! >> "%LOG_FILE%"
goto :error
)
:: Verifier que le fichier a bien ete telecharge et n'est pas vide
if not exist "%ZIP_FILE%" (
echo.
echo %ESC%%RED% ERROR: Download failed - file not created!%ESC%%RESET%
echo [%DATE% %TIME%] ERROR: ZIP file not created after download >> "%LOG_FILE%"
goto :error
)
:: Verifier la taille du fichier (doit etre > 1MB pour etre valide)
for %%A in ("%ZIP_FILE%") do set ZIP_SIZE=%%~zA
if !ZIP_SIZE! LSS 1000000 (
echo.
echo %ESC%%RED% ERROR: Downloaded file appears invalid ^(too small^)!%ESC%%RESET%
echo [%DATE% %TIME%] ERROR: Downloaded file too small: !ZIP_SIZE! bytes >> "%LOG_FILE%"
del /q "%ZIP_FILE%" 2>nul
goto :error
)
echo %ESC%%GREEN%^> Download complete ^(!ZIP_SIZE! bytes^)%ESC%%RESET%
echo [%DATE% %TIME%] Download successful: !ZIP_SIZE! bytes >> "%LOG_FILE%"
)
:: Verifier que tar existe (Windows 10 1803+)
where tar >nul 2>&1
if !ERRORLEVEL! NEQ 0 (
echo.
echo %ESC%%RED% ERROR: tar command not available!%ESC%%RESET%
echo.
echo Please update Windows 10 or extract manually to:
echo %ESC%%CYAN%%PYTHON_DIR%%ESC%%RESET%
echo.
echo [%DATE% %TIME%] ERROR: tar command not found >> "%LOG_FILE%"
goto :error
)
:: Vérifier à nouveau si python.exe existe après extraction
if not exist "!PYTHON_EXE_FULL!" (
echo Error: python.exe not found after extraction at !PYTHON_EXE_FULL!.
echo [%DATE% %TIME%] Error: python.exe not found after extraction at !PYTHON_EXE_FULL! >> "%LOG_FILE%"
:: Extraction avec progression simulee
echo %ESC%%YELLOW%^> Extracting Python...%ESC%%RESET%
echo [%DATE% %TIME%] Extracting python.zip >> "%LOG_FILE%"
<nul set /p "= ["
tar -xf "%ZIP_FILE%" -C "%PYTHON_DIR%" --strip-components=0
set TAR_RESULT=!ERRORLEVEL!
echo %ESC%%GREEN%##########%ESC%%RESET%] Done
if !TAR_RESULT! NEQ 0 (
echo.
echo %ESC%%RED% ERROR: Extraction failed!%ESC%%RESET%
echo [%DATE% %TIME%] ERROR: tar extraction failed with code !TAR_RESULT! >> "%LOG_FILE%"
goto :error
)
echo [%DATE% %TIME%] Extraction completed >> "%LOG_FILE%"
:: Supprimer ZIP
del /q "%ZIP_FILE%" 2>nul
echo %ESC%%GREEN%^> python.zip cleaned up%ESC%%RESET%
echo [%DATE% %TIME%] python.zip deleted >> "%LOG_FILE%"
:: Verifier installation
if not exist "%PYTHON_EXE%" (
echo.
echo %ESC%%RED% ERROR: Python not found after extraction!%ESC%%RESET%
echo [%DATE% %TIME%] ERROR: python.exe not found after extraction >> "%LOG_FILE%"
goto :error
)
)
echo python.exe found.
echo [%DATE% %TIME%] python.exe found. >> "%LOG_FILE%"
:: Vérifier si le script Python existe
echo Checking __main__.py...
echo [%DATE% %TIME%] Checking __main__.py at !MAIN_SCRIPT! >> "%LOG_FILE%"
if not exist "!MAIN_SCRIPT!" (
echo Error: __main__.py not found at !MAIN_SCRIPT!.
echo [%DATE% %TIME%] Error: __main__.py not found at !MAIN_SCRIPT! >> "%LOG_FILE%"
:: Afficher et logger la version Python
for /f "tokens=*" %%v in ('"%PYTHON_EXE%" --version 2^>^&1') do set "PYTHON_VERSION=%%v"
echo %ESC%%GREEN%^> %PYTHON_VERSION% found%ESC%%RESET%
echo [%DATE% %TIME%] %PYTHON_VERSION% detected >> "%LOG_FILE%"
:: =============================================================================
:: Verification script principal
:: =============================================================================
echo %ESC%%YELLOW%[2/3]%ESC%%RESET% Checking RGSX application...
echo [%DATE% %TIME%] Step 2/3: Checking RGSX files >> "%LOG_FILE%"
if not exist "%MAIN_SCRIPT%" (
echo.
echo %ESC%%RED% ERROR: __main__.py not found!%ESC%%RESET%
echo.
echo Expected location:
echo %ESC%%CYAN%%MAIN_SCRIPT%%ESC%%RESET%
echo.
echo [%DATE% %TIME%] ERROR: __main__.py not found at %MAIN_SCRIPT% >> "%LOG_FILE%"
goto :error
)
echo __main__.py found.
echo [%DATE% %TIME%] __main__.py found. >> "%LOG_FILE%"
:: L'étape de mise à jour de la gamelist est désormais appelée depuis __main__.py
echo [%DATE% %TIME%] Skipping external gamelist update (handled in app). >> "%LOG_FILE%"
echo %ESC%%GREEN%^> RGSX files OK%ESC%%RESET%
echo [%DATE% %TIME%] RGSX files verified >> "%LOG_FILE%"
echo Launching __main__.py (attached)...
echo [%DATE% %TIME%] Preparing to launch main. >> "%LOG_FILE%"
:: =============================================================================
:: Lancement
:: =============================================================================
echo %ESC%%YELLOW%[3/3]%ESC%%RESET% Launching RGSX...
echo [%DATE% %TIME%] Step 3/3: Launching application >> "%LOG_FILE%"
:: Assurer le bon dossier de travail pour l'application
:: Changer le repertoire de travail
cd /d "%ROOT_DIR%\roms\ports\RGSX"
echo [%DATE% %TIME%] Working directory: %CD% >> "%LOG_FILE%"
:: Forcer les drivers SDL côté Windows et réduire le bruit console
:: Configuration SDL/Pygame
set PYGAME_HIDE_SUPPORT_PROMPT=1
set SDL_VIDEODRIVER=windows
set SDL_AUDIODRIVER=directsound
echo [%DATE% %TIME%] CWD before launch: %CD% >> "%LOG_FILE%"
set PYTHONWARNINGS=ignore::UserWarning:pygame.pkgdata
:: Lancer l'application dans la même console et attendre sa fin
:: Forcer python.exe pour capturer la sortie
set "PY_MAIN_EXE=!PYTHON_EXE_FULL!"
echo [%DATE% %TIME%] Using interpreter: !PY_MAIN_EXE! >> "%LOG_FILE%"
echo [%DATE% %TIME%] Launching "!MAIN_SCRIPT!" now... >> "%LOG_FILE%"
"!PY_MAIN_EXE!" "!MAIN_SCRIPT!" >> "%LOG_FILE%" 2>&1
set EXITCODE=!ERRORLEVEL!
echo [%DATE% %TIME%] __main__.py exit code: !EXITCODE! >> "%LOG_FILE%"
if "!EXITCODE!"=="0" (
echo Execution finished successfully.
echo [%DATE% %TIME%] Execution of __main__.py finished successfully. >> "%LOG_FILE%"
:: =============================================================================
:: Configuration multi-ecran
:: =============================================================================
:: SDL_VIDEO_FULLSCREEN_HEAD: Selectionne l'ecran pour le mode plein ecran
:: 0 = ecran principal, 1 = ecran secondaire, etc.
:: Ces variables ne sont definies que si --display=N ou --windowed est passe
:: Sinon, le script Python utilisera les parametres de rgsx_settings.json
echo [%DATE% %TIME%] Display configuration: >> "%LOG_FILE%"
if defined DISPLAY_NUM (
set SDL_VIDEO_FULLSCREEN_HEAD=!DISPLAY_NUM!
set RGSX_DISPLAY=!DISPLAY_NUM!
echo [%DATE% %TIME%] SDL_VIDEO_FULLSCREEN_HEAD=!DISPLAY_NUM! ^(from --display arg^) >> "%LOG_FILE%"
echo [%DATE% %TIME%] RGSX_DISPLAY=!DISPLAY_NUM! ^(from --display arg^) >> "%LOG_FILE%"
) else (
echo Error: Failed to execute __main__.py (code !EXITCODE!).
echo [%DATE% %TIME%] Error: Failed to execute __main__.py with error code !EXITCODE!. >> "%LOG_FILE%"
echo [%DATE% %TIME%] Display: using rgsx_settings.json config >> "%LOG_FILE%"
)
if defined WINDOWED_MODE (
set RGSX_WINDOWED=!WINDOWED_MODE!
echo [%DATE% %TIME%] RGSX_WINDOWED=!WINDOWED_MODE! ^(from --windowed arg^) >> "%LOG_FILE%"
) else (
echo [%DATE% %TIME%] Windowed: using rgsx_settings.json config >> "%LOG_FILE%"
)
:: Log environnement
echo [%DATE% %TIME%] Environment variables set: >> "%LOG_FILE%"
echo [%DATE% %TIME%] RGSX_ROOT=%RGSX_ROOT% >> "%LOG_FILE%"
echo [%DATE% %TIME%] SDL_VIDEODRIVER=%SDL_VIDEODRIVER% >> "%LOG_FILE%"
echo [%DATE% %TIME%] SDL_AUDIODRIVER=%SDL_AUDIODRIVER% >> "%LOG_FILE%"
echo.
if defined DISPLAY_NUM (
echo %ESC%%CYAN%Launching on display !DISPLAY_NUM!...%ESC%%RESET%
)
if defined WINDOWED_MODE (
echo %ESC%%CYAN%Windowed mode enabled%ESC%%RESET%
)
echo %ESC%%CYAN%Starting RGSX application...%ESC%%RESET%
echo %ESC%%BOLD%Press Ctrl+C to force quit if needed%ESC%%RESET%
echo.
echo [%DATE% %TIME%] Executing: "%PYTHON_EXE%" "%MAIN_SCRIPT%" >> "%LOG_FILE%"
echo [%DATE% %TIME%] --- Application output start --- >> "%LOG_FILE%"
"%PYTHON_EXE%" "%MAIN_SCRIPT%" >> "%LOG_FILE%" 2>&1
set EXITCODE=!ERRORLEVEL!
echo [%DATE% %TIME%] --- Application output end --- >> "%LOG_FILE%"
echo [%DATE% %TIME%] Exit code: !EXITCODE! >> "%LOG_FILE%"
if "!EXITCODE!"=="0" (
echo.
echo %ESC%%GREEN%RGSX closed successfully.%ESC%%RESET%
echo.
echo [%DATE% %TIME%] Application closed successfully >> "%LOG_FILE%"
) else (
echo.
echo %ESC%%RED%RGSX exited with error code !EXITCODE!%ESC%%RESET%
echo.
echo [%DATE% %TIME%] ERROR: Application exited with code !EXITCODE! >> "%LOG_FILE%"
goto :error
)
:end
echo Task completed.
echo [%DATE% %TIME%] Task completed successfully. >> "%LOG_FILE%"
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
echo [%DATE% %TIME%] Session ended normally >> "%LOG_FILE%"
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
timeout /t 2 >nul
exit /b 0
:error
echo An error occurred.
echo [%DATE% %TIME%] An error occurred. >> "%LOG_FILE%"
echo.
echo %ESC%%RED%An error occurred. Check the log file:%ESC%%RESET%
echo %ESC%%CYAN%%LOG_FILE%%ESC%%RESET%
echo.
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
echo [%DATE% %TIME%] Session ended with errors >> "%LOG_FILE%"
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
echo.
echo Press any key to close...
pause >nul
exit /b 1