Compare commits

...

6 Commits

Author SHA1 Message Date
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
skymike03
5c7fa0484f v2.3.2.9 (2025.11.23)
- Enhance UI with modern effects and improve PSVita game handling (auto extract and create .psvita file for batocera)
- add text file viewer for game txt informations (windows)
2025-11-23 01:25:15 +01:00
skymike03
814861e9ee - add text file viewer for game txt informations 2025-11-21 00:28:46 +01:00
14 changed files with 935 additions and 217 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

@@ -687,6 +687,7 @@ async def main():
"history_error_details",
"history_confirm_delete",
"history_extract_archive",
"text_file_viewer", # Visualiseur de fichiers texte
# Menus filtrage avancé
"filter_menu_choice",
"filter_advanced",
@@ -1115,6 +1116,9 @@ async def main():
elif config.menu_state == "history_error_details":
from display import draw_history_error_details
draw_history_error_details(screen)
elif config.menu_state == "text_file_viewer":
from display import draw_text_file_viewer
draw_text_file_viewer(screen)
elif config.menu_state == "history_confirm_delete":
from display import draw_history_confirm_delete
draw_history_confirm_delete(screen)

View File

@@ -13,7 +13,7 @@ except Exception:
pygame = None # type: ignore
# Version actuelle de l'application
app_version = "2.3.2.8"
app_version = "2.3.3.1"
def get_application_root():

View File

@@ -59,6 +59,7 @@ VALID_STATES = [
"history_error_details", # détails de l'erreur
"history_confirm_delete", # confirmation suppression jeu
"history_extract_archive", # extraction d'archive
"text_file_viewer", # visualiseur de fichiers texte
# Nouveaux menus filtrage avancé
"filter_menu_choice", # menu de choix entre recherche et filtrage avancé
"filter_search", # recherche par nom (existant, mais renommé)
@@ -1008,6 +1009,8 @@ def handle_controls(event, sources, joystick, screen):
ext = os.path.splitext(actual_filename)[1].lower()
if ext in ['.zip', '.rar']:
options.append("extract_archive")
elif ext == '.txt':
options.append("open_file")
elif status in ["Erreur", "Error", "Canceled"]:
options.append("error_info")
options.append("retry")
@@ -1050,6 +1053,30 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
logger.debug(f"Affichage du dossier de téléchargement pour {game_name}")
elif selected_option == "open_file":
# Ouvrir le fichier texte
if actual_path and os.path.exists(actual_path):
try:
with open(actual_path, 'r', encoding='utf-8', errors='replace') as f:
content = f.read()
config.text_file_content = content
config.text_file_name = actual_filename
config.text_file_scroll_offset = 0
config.previous_menu_state = "history_game_options"
config.menu_state = "text_file_viewer"
config.needs_redraw = True
logger.debug(f"Ouverture du fichier texte: {actual_filename}")
except Exception as e:
logger.error(f"Erreur lors de l'ouverture du fichier texte: {e}")
config.menu_state = "error"
config.error_message = f"Erreur lors de l'ouverture du fichier: {str(e)}"
config.needs_redraw = True
else:
logger.error(f"Fichier texte introuvable: {actual_path}")
config.menu_state = "error"
config.error_message = "Fichier introuvable"
config.needs_redraw = True
elif selected_option == "extract_archive":
# L'option n'apparaît que si le fichier existe, pas besoin de re-vérifier
config.previous_menu_state = "history_game_options"
@@ -1201,6 +1228,128 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
# Affichage détails erreur
# Visualiseur de fichiers texte
elif config.menu_state == "text_file_viewer":
content = getattr(config, 'text_file_content', '')
if content:
lines = content.split('\n')
line_height = config.small_font.get_height() + 2
# Calculer le nombre de lignes visibles (approximation)
controls_y = config.screen_height - int(config.screen_height * 0.037)
margin = 40
header_height = 60
content_area_height = controls_y - 2 * margin - 10 - header_height - 20
visible_lines = int(content_area_height / line_height)
scroll_offset = getattr(config, 'text_file_scroll_offset', 0)
max_scroll = max(0, len(lines) - visible_lines)
if is_input_matched(event, "up"):
config.text_file_scroll_offset = max(0, scroll_offset - 1)
update_key_state("up", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "down"):
config.text_file_scroll_offset = min(max_scroll, scroll_offset + 1)
update_key_state("down", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "page_up"):
config.text_file_scroll_offset = max(0, scroll_offset - visible_lines)
update_key_state("page_up", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "page_down"):
config.text_file_scroll_offset = min(max_scroll, scroll_offset + visible_lines)
update_key_state("page_down", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "cancel") or is_input_matched(event, "confirm"):
config.menu_state = validate_menu_state(config.previous_menu_state)
config.needs_redraw = True
else:
# Si pas de contenu, retourner au menu précédent
if is_input_matched(event, "cancel") or is_input_matched(event, "confirm"):
config.menu_state = validate_menu_state(config.previous_menu_state)
config.needs_redraw = True
# Visualiseur de fichiers texte
elif config.menu_state == "text_file_viewer":
content = getattr(config, 'text_file_content', '')
if content:
from utils import wrap_text
# Calculer les dimensions
controls_y = config.screen_height - int(config.screen_height * 0.037)
margin = 40
header_height = 60
rect_width = config.screen_width - 2 * margin
content_area_height = controls_y - 2 * margin - 10 - header_height - 20
max_width = rect_width - 60
# Diviser le contenu en lignes et appliquer le word wrap
original_lines = content.split('\n')
wrapped_lines = []
for original_line in original_lines:
if original_line.strip(): # Si la ligne n'est pas vide
wrapped = wrap_text(original_line, config.small_font, max_width)
wrapped_lines.extend(wrapped)
else: # Ligne vide
wrapped_lines.append('')
line_height = config.small_font.get_height() + 2
visible_lines = int(content_area_height / line_height)
scroll_offset = getattr(config, 'text_file_scroll_offset', 0)
max_scroll = max(0, len(wrapped_lines) - visible_lines)
if is_input_matched(event, "up"):
config.text_file_scroll_offset = max(0, scroll_offset - 1)
update_key_state("up", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "down"):
config.text_file_scroll_offset = min(max_scroll, scroll_offset + 1)
update_key_state("down", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "page_up"):
config.text_file_scroll_offset = max(0, scroll_offset - visible_lines)
update_key_state("page_up", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "page_down"):
config.text_file_scroll_offset = min(max_scroll, scroll_offset + visible_lines)
update_key_state("page_down", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "cancel") or is_input_matched(event, "confirm"):
config.menu_state = validate_menu_state(config.previous_menu_state)
config.needs_redraw = True
else:
# Si pas de contenu, retourner au menu précédent
if is_input_matched(event, "cancel") or is_input_matched(event, "confirm"):
config.menu_state = validate_menu_state(config.previous_menu_state)
config.needs_redraw = True
elif config.menu_state == "history_error_details":
if is_input_matched(event, "confirm") or is_input_matched(event, "cancel"):
config.menu_state = validate_menu_state(config.previous_menu_state)
@@ -1333,8 +1482,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 []:
@@ -1346,7 +1502,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)
@@ -1356,11 +1514,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":
@@ -1380,38 +1545,36 @@ def handle_controls(event, sources, joystick, screen):
config.selected_option = min(total - 1, config.selected_option + 1)
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
@@ -1422,7 +1585,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)
@@ -1587,7 +1750,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
@@ -1595,14 +1758,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')
@@ -1617,11 +1785,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()
@@ -1660,11 +1823,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
@@ -1713,12 +1884,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

View File

@@ -189,32 +189,38 @@ THEME_COLORS = {
# Néon image grille des systèmes
"neon": (0, 134, 179), # bleu
# Dégradé sombre pour le fond
"background_top": (30, 40, 50),
"background_bottom": (60, 80, 100), # noir vers bleu foncé
"background_top": (20, 25, 35),
"background_bottom": (45, 55, 75), # noir vers bleu foncé
# Fond des cadres
"button_idle": (50, 50, 70, 150), # Bleu sombre métal
"button_idle": (45, 50, 65, 180), # Bleu sombre métal avec plus d'opacité
# Fond des boutons sélectionnés
"button_selected": (70, 70, 100, 200), # Bleu plus clair
"button_selected": (70, 80, 110, 220), # Bleu plus clair
# Fond des boutons hover dans les popups ou menu
"button_hover": (255, 0, 255, 220), # Rose
"button_hover": (255, 0, 255, 240), # Rose vif
# Générique
"text": (255, 255, 255), # blanc
# Texte sélectionné (alias pour compatibilité)
"text_selected": (0, 255, 0), # utilise le même vert que fond_lignes
# Erreur
"error_text": (255, 0, 0), # rouge
"error_text": (255, 60, 60), # rouge vif
# Succès
"success_text": (0, 255, 0), # vert
"success_text": (0, 255, 150), # vert cyan
# Avertissement
"warning_text": (255, 100, 0), # orange
"warning_text": (255, 150, 0), # orange vif
# Titres
"title_text": (200, 200, 200), # gris clair
"title_text": (220, 220, 230), # gris très clair
# Bordures
"border": (150, 150, 150), # Bordures grises subtiles
"border_selected": (0, 255, 0), # Bordure verte pour sélection
"border": (100, 120, 150), # Bordures bleutées
"border_selected": (0, 255, 150), # Bordure verte cyan pour sélection
# Couleurs pour filtres
"green": (0, 255, 0), # vert
"red": (255, 0, 0), # rouge
# Nouvelles couleurs pour effets modernes
"shadow": (0, 0, 0, 100), # Ombre portée
"glow": (100, 180, 255, 40), # Effet glow bleu doux
"highlight": (255, 255, 255, 20), # Reflet subtil
"accent_gradient_start": (80, 120, 200), # Début dégradé accent
"accent_gradient_end": (120, 80, 200), # Fin dégradé accent
}
# Général, résolution, overlay
@@ -228,34 +234,104 @@ def init_display():
screen = pygame.display.set_mode((screen_width, screen_height))
config.screen_width = screen_width
config.screen_height = screen_height
# Initialisation de OVERLAY
# Initialisation de OVERLAY avec effet glassmorphism
OVERLAY = pygame.Surface((screen_width, screen_height), pygame.SRCALPHA)
OVERLAY.fill((0, 0, 0, 150)) # Transparence augmentée
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}")
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."""
"""Dessine un fond dégradé vertical avec des couleurs vibrantes et texture de grain."""
height = screen.get_height()
width = screen.get_width()
top_color = pygame.Color(*top_color)
bottom_color = pygame.Color(*bottom_color)
# Dégradé principal
for y in range(height):
ratio = y / height
color = top_color.lerp(bottom_color, ratio)
pygame.draw.line(screen, color, (0, y), (screen.get_width(), y))
pygame.draw.line(screen, color, (0, y), (width, y))
# Ajouter une texture de grain subtile pour plus de profondeur
grain_surface = pygame.Surface((width, height), pygame.SRCALPHA)
import random
random.seed(42) # Seed fixe pour cohérence
for _ in range(width * height // 200): # Réduire la densité pour performance
x = random.randint(0, width - 1)
y = random.randint(0, height - 1)
alpha = random.randint(5, 20)
grain_surface.set_at((x, y), (255, 255, 255, alpha))
screen.blit(grain_surface, (0, 0))
def draw_shadow(surface, rect, offset=6, alpha=120):
"""Dessine une ombre portée pour un rectangle."""
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."""
glow = pygame.Surface((rect.width + size * 2, rect.height + size * 2), pygame.SRCALPHA)
for i in range(size):
alpha = int(intensity * (1 - i / size))
pygame.draw.rect(glow, (*color[:3], alpha),
(i, i, rect.width + (size - i) * 2, rect.height + (size - i) * 2),
border_radius=15)
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 et bordure arrondie."""
button_surface = pygame.Surface((width, height), pygame.SRCALPHA)
"""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))
button_color = THEME_COLORS["button_hover"] if selected else THEME_COLORS["button_idle"]
pygame.draw.rect(button_surface, button_color, (0, 0, width, height), border_radius=12)
pygame.draw.rect(button_surface, THEME_COLORS["border"], (0, 0, width, height), 2, border_radius=12)
button_surface = pygame.Surface((width, height), pygame.SRCALPHA)
# Fond avec dégradé subtil pour bouton sélectionné
if selected:
glow_surface = pygame.Surface((width + 10, height + 10), pygame.SRCALPHA)
pygame.draw.rect(glow_surface, THEME_COLORS["fond_lignes"] + (50,), (5, 5, width, height), border_radius=12)
screen.blit(glow_surface, (x - 5, y - 5))
# 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
@@ -636,14 +712,35 @@ def draw_platform_grid(screen):
# Effet de pulsation subtil pour le titre - calculé une seule fois par frame
current_time = pygame.time.get_ticks()
pulse_factor = 0.05 * (1 + math.sin(current_time / 500))
title_glow = pygame.Surface((title_rect_inflated.width + 10, title_rect_inflated.height + 10), pygame.SRCALPHA)
pygame.draw.rect(title_glow, THEME_COLORS["neon"] + (int(40 * pulse_factor),),
title_glow.get_rect(), border_radius=14)
screen.blit(title_glow, (title_rect_inflated.left - 5, title_rect_inflated.top - 5))
pulse_factor = 0.08 * (1 + math.sin(current_time / 400))
pygame.draw.rect(screen, THEME_COLORS["button_idle"], title_rect_inflated, border_radius=12)
pygame.draw.rect(screen, THEME_COLORS["border"], title_rect_inflated, 2, border_radius=12)
# Ombre portée pour le titre
shadow_surf = pygame.Surface((title_rect_inflated.width + 12, title_rect_inflated.height + 12), pygame.SRCALPHA)
pygame.draw.rect(shadow_surf, (0, 0, 0, 140), (6, 6, title_rect_inflated.width, title_rect_inflated.height), border_radius=16)
screen.blit(shadow_surf, (title_rect_inflated.left - 6, title_rect_inflated.top - 6))
# Glow multicouche pour le titre
for i in range(2):
glow_size = title_rect_inflated.inflate(15 + i * 8, 15 + i * 8)
title_glow = pygame.Surface((glow_size.width, glow_size.height), pygame.SRCALPHA)
alpha = int((30 + 20 * pulse_factor) * (1 - i / 2))
pygame.draw.rect(title_glow, (*THEME_COLORS["neon"][:3], alpha),
title_glow.get_rect(), border_radius=16 + i * 2)
screen.blit(title_glow, (title_rect_inflated.left - 8 - i * 4, title_rect_inflated.top - 8 - i * 4)) # Fond du titre avec dégradé
title_bg = pygame.Surface((title_rect_inflated.width, title_rect_inflated.height), pygame.SRCALPHA)
for i in range(title_rect_inflated.height):
ratio = i / title_rect_inflated.height
alpha = int(THEME_COLORS["button_idle"][3] * (1 + ratio * 0.1))
pygame.draw.line(title_bg, (*THEME_COLORS["button_idle"][:3], alpha),
(0, i), (title_rect_inflated.width, i))
screen.blit(title_bg, title_rect_inflated.topleft)
# Reflet en haut du titre
highlight = pygame.Surface((title_rect_inflated.width - 8, title_rect_inflated.height // 3), pygame.SRCALPHA)
highlight.fill((255, 255, 255, 25))
screen.blit(highlight, (title_rect_inflated.left + 4, title_rect_inflated.top + 4))
pygame.draw.rect(screen, THEME_COLORS["border"], title_rect_inflated, 2, border_radius=14)
screen.blit(title_surface, title_rect)
# Configuration de la grille - calculée une seule fois
@@ -658,8 +755,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)]
@@ -689,14 +796,16 @@ def draw_platform_grid(screen):
if total_pages > 1:
page_indicator_text = _("platform_page").format(config.current_page + 1, total_pages)
page_indicator = config.small_font.render(page_indicator_text, True, THEME_COLORS["text"])
# Positionner au-dessus du footer (réserver ~80px pour le footer)
footer_reserved_height = 80
page_y = config.screen_height - footer_reserved_height - page_indicator.get_height() - 10
# Position fixe : 5px au-dessus du footer
# Le footer commence à screen_height - rect_height - 5 (voir draw_controls)
# On estime la hauteur du footer à environ 50-60px selon le contenu
# Pour être sûr, on positionne à screen_height - 60px (hauteur footer) - 5px (marge) - hauteur du texte
page_y = config.screen_height - 65 - page_indicator.get_height()
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
@@ -710,9 +819,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
@@ -731,62 +840,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
@@ -860,6 +1027,17 @@ def draw_game_list(screen):
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)
# Ombre pour le titre de recherche
shadow = pygame.Surface((title_rect_inflated.width + 10, title_rect_inflated.height + 10), pygame.SRCALPHA)
pygame.draw.rect(shadow, (0, 0, 0, 120), (5, 5, title_rect_inflated.width, title_rect_inflated.height), border_radius=14)
screen.blit(shadow, (title_rect_inflated.left - 5, title_rect_inflated.top - 5))
# Glow pour recherche active
glow = pygame.Surface((title_rect_inflated.width + 20, title_rect_inflated.height + 20), pygame.SRCALPHA)
pygame.draw.rect(glow, (*THEME_COLORS["glow"][:3], 60), glow.get_rect(), border_radius=16)
screen.blit(glow, (title_rect_inflated.left - 10, title_rect_inflated.top - 10))
pygame.draw.rect(screen, THEME_COLORS["button_idle"], title_rect_inflated, border_radius=12)
pygame.draw.rect(screen, THEME_COLORS["border"], title_rect_inflated, 2, border_radius=12)
screen.blit(title_surface, title_rect)
@@ -884,11 +1062,30 @@ def draw_game_list(screen):
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)
# Ombre et glow pour titre normal
shadow = pygame.Surface((title_rect_inflated.width + 10, title_rect_inflated.height + 10), pygame.SRCALPHA)
pygame.draw.rect(shadow, (0, 0, 0, 120), (5, 5, title_rect_inflated.width, title_rect_inflated.height), border_radius=14)
screen.blit(shadow, (title_rect_inflated.left - 5, title_rect_inflated.top - 5))
pygame.draw.rect(screen, THEME_COLORS["button_idle"], title_rect_inflated, border_radius=12)
pygame.draw.rect(screen, THEME_COLORS["border"], title_rect_inflated, 2, border_radius=12)
screen.blit(title_surface, title_rect)
# Ombre portée pour le cadre principal
shadow_rect = pygame.Rect(rect_x + 6, rect_y + 6, rect_width, rect_height)
shadow_surf = pygame.Surface((rect_width + 8, rect_height + 8), pygame.SRCALPHA)
pygame.draw.rect(shadow_surf, (0, 0, 0, 100), (4, 4, rect_width, rect_height), border_radius=14)
screen.blit(shadow_surf, (rect_x - 4, rect_y - 4))
# Fond du cadre avec légère transparence glassmorphism
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (rect_x, rect_y, rect_width, rect_height), border_radius=12)
# Reflet en haut du cadre
highlight = pygame.Surface((rect_width - 8, 40), pygame.SRCALPHA)
highlight.fill((255, 255, 255, 15))
screen.blit(highlight, (rect_x + 4, rect_y + 4))
pygame.draw.rect(screen, THEME_COLORS["border"], (rect_x, rect_y, rect_width, rect_height), 2, border_radius=12)
# Largeur colonne taille (15%) mini 120px, reste pour nom
@@ -947,10 +1144,29 @@ def draw_game_list(screen):
size_rect.midright = (rect_x + rect_width - 20, row_center_y)
if i == config.current_game:
glow_width = rect_width - 40
glow_height = name_rect.height + 10
glow_surface = pygame.Surface((glow_width, glow_height), pygame.SRCALPHA)
pygame.draw.rect(glow_surface, THEME_COLORS["fond_lignes"] + (50,), (0, 0, glow_width, glow_height), border_radius=8)
screen.blit(glow_surface, (rect_x + 20, row_center_y - glow_height // 2))
glow_height = name_rect.height + 12
# Effet de glow plus doux pour la sélection
glow_surface = pygame.Surface((glow_width + 6, glow_height + 6), pygame.SRCALPHA)
alpha = 50
pygame.draw.rect(glow_surface, (*THEME_COLORS["fond_lignes"][:3], alpha),
(3, 3, glow_width, glow_height),
border_radius=8)
screen.blit(glow_surface, (rect_x + 17, row_center_y - glow_height // 2 - 3))
# Fond principal de la sélection avec dégradé subtil
selection_bg = pygame.Surface((glow_width, glow_height), pygame.SRCALPHA)
for j in range(glow_height):
ratio = j / glow_height
alpha = int(60 + 20 * ratio)
pygame.draw.line(selection_bg, (*THEME_COLORS["fond_lignes"][:3], alpha),
(0, j), (glow_width, j))
screen.blit(selection_bg, (rect_x + 20, row_center_y - glow_height // 2))
# Bordure lumineuse plus subtile
border_rect = pygame.Rect(rect_x + 20, row_center_y - glow_height // 2, glow_width, glow_height)
pygame.draw.rect(screen, (*THEME_COLORS["fond_lignes"][:3], 120), border_rect, width=1, border_radius=8)
screen.blit(name_surface, name_rect)
screen.blit(size_surface, size_rect)
@@ -1777,9 +1993,9 @@ def draw_language_menu(screen):
# Instructions (placer juste au-dessus du footer sans chevauchement)
instruction_text = _("language_select_instruction")
instruction_surface = config.small_font.render(instruction_text, True, THEME_COLORS["text"])
footer_reserved = 72 # hauteur approximative footer (barre bas) + marge
bottom_margin = 12
instruction_y = config.screen_height - footer_reserved - bottom_margin
# Position fixe : 5px au-dessus du footer
footer_height = 70 # Hauteur estimée du footer
instruction_y = config.screen_height - footer_height - instruction_surface.get_height() - 5
# Empêcher un chevauchement avec les derniers boutons si espace réduit
last_button_bottom = start_y + (len(available_languages) - 1) * (button_height + button_spacing) + button_height
min_gap = 16
@@ -1799,9 +2015,10 @@ def draw_menu_instruction(screen, instruction_text, last_button_bottom=None):
return
try:
instruction_surface = config.small_font.render(instruction_text, True, THEME_COLORS["text"])
footer_reserved = 72
bottom_margin = 12
instruction_y = config.screen_height - footer_reserved - bottom_margin
# Position fixe : 5px au-dessus du footer
footer_height = 70 # Hauteur estimée du footer
instruction_y = config.screen_height - footer_height - instruction_surface.get_height() - 5
# Empêcher chevauchement avec le dernier bouton
min_gap = 16
if last_button_bottom is not None and instruction_y - last_button_bottom < min_gap:
instruction_y = last_button_bottom + min_gap
@@ -1876,25 +2093,27 @@ def draw_display_menu(screen):
selected=(i == selected)
)
# Aide en bas de l'écran
# Aide en bas de l'écran - Position fixe au-dessus du footer
instruction_text = _("language_select_instruction")
instruction_surface = config.small_font.render(instruction_text, True, THEME_COLORS["text"])
instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, config.screen_height - 50))
# Calculer la position en fonction de la hauteur du footer (environ 60-70px) + marge de 5px
footer_height = 70 # Hauteur estimée du footer
instruction_y = config.screen_height - footer_height - instruction_surface.get_height() - 5
instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, instruction_y))
screen.blit(instruction_surface, instruction_rect)
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"])
@@ -1931,12 +2150,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",
]
@@ -2144,12 +2362,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",
@@ -2653,57 +2871,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):
@@ -2975,6 +3165,9 @@ def draw_history_game_options(screen):
if ext in ['.zip', '.rar']:
options.append("extract_archive")
option_labels.append(_("history_option_extract_archive"))
elif ext == '.txt':
options.append("open_file")
option_labels.append(_("history_option_open_file"))
elif status in ["Erreur", "Error", "Canceled"]:
options.append("error_info")
option_labels.append(_("history_option_error_info"))
@@ -3250,6 +3443,96 @@ def draw_history_extract_archive(screen):
draw_stylized_button(screen, _("button_OK"), rect_x + (rect_width - button_width) // 2, rect_y + text_height + margin_top_bottom, button_width, button_height, selected=True)
def draw_text_file_viewer(screen):
"""Affiche le contenu d'un fichier texte avec défilement."""
screen.blit(OVERLAY, (0, 0))
# Récupérer les données du fichier texte
content = getattr(config, 'text_file_content', '')
filename = getattr(config, 'text_file_name', 'Unknown')
scroll_offset = getattr(config, 'text_file_scroll_offset', 0)
# Dimensions
margin = 40
header_height = 60
controls_y = config.screen_height - int(config.screen_height * 0.037)
bottom_margin = 10
rect_width = config.screen_width - 2 * margin
rect_height = controls_y - 2 * margin - bottom_margin
rect_x = margin
rect_y = margin
content_area_y = rect_y + header_height
content_area_height = rect_height - header_height - 20
# Fond principal
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)
# Titre/nom du fichier
title_text = f"{filename}"
title_surface = config.font.render(title_text, True, THEME_COLORS["text"])
title_rect = title_surface.get_rect(center=(config.screen_width // 2, rect_y + 30))
screen.blit(title_surface, title_rect)
# Séparateur
pygame.draw.line(screen, THEME_COLORS["border"], (rect_x + 20, content_area_y - 10), (rect_x + rect_width - 20, content_area_y - 10), 2)
# Contenu du fichier
if content:
# Diviser le contenu en lignes et appliquer le word wrap
original_lines = content.split('\n')
wrapped_lines = []
max_width = rect_width - 60
# Appliquer wrap_text à chaque ligne originale
for original_line in original_lines:
if original_line.strip(): # Si la ligne n'est pas vide
wrapped = wrap_text(original_line, config.small_font, max_width)
wrapped_lines.extend(wrapped)
else: # Ligne vide
wrapped_lines.append('')
line_height = config.small_font.get_height() + 2
# Calculer le nombre de lignes visibles
visible_lines = int(content_area_height / line_height)
# Appliquer le scroll
start_line = scroll_offset
end_line = min(start_line + visible_lines, len(wrapped_lines))
# Afficher les lignes visibles
for i, line_index in enumerate(range(start_line, end_line)):
if line_index < len(wrapped_lines):
line = wrapped_lines[line_index]
line_surface = config.small_font.render(line, True, THEME_COLORS["text"])
line_rect = line_surface.get_rect(left=rect_x + 30, top=content_area_y + i * line_height)
screen.blit(line_surface, line_rect)
# Scrollbar si nécessaire
if len(wrapped_lines) > visible_lines:
scrollbar_height = int((visible_lines / len(wrapped_lines)) * content_area_height)
scrollbar_y = content_area_y + int((scroll_offset / len(wrapped_lines)) * content_area_height)
scrollbar_x = rect_x + rect_width - 15
# Fond de la scrollbar
pygame.draw.rect(screen, THEME_COLORS["border"], (scrollbar_x, content_area_y, 8, content_area_height), border_radius=4)
# Barre de défilement
pygame.draw.rect(screen, THEME_COLORS["button_hover"], (scrollbar_x, scrollbar_y, 8, scrollbar_height), border_radius=4)
# Indicateur de position
position_text = f"{scroll_offset + 1}-{end_line}/{len(wrapped_lines)}"
position_surface = config.small_font.render(position_text, True, THEME_COLORS["title_text"])
position_rect = position_surface.get_rect(right=rect_x + rect_width - 30, bottom=rect_y + rect_height - 10)
screen.blit(position_surface, position_rect)
else:
# Aucun contenu
no_content_text = "Empty file"
no_content_surface = config.font.render(no_content_text, True, THEME_COLORS["title_text"])
no_content_rect = no_content_surface.get_rect(center=(config.screen_width // 2, content_area_y + content_area_height // 2))
screen.blit(no_content_surface, no_content_rect)
def draw_scraper_screen(screen):

View File

@@ -67,7 +67,7 @@
"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 +80,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 +185,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",
@@ -242,7 +245,8 @@
"history_game_options_title": "Spiel Optionen",
"history_option_download_folder": "Datei lokalisieren",
"history_option_extract_archive": "Archiv extrahieren",
"history_option_scraper": "Metadaten scrapen",
"history_option_open_file": "Datei öffnen",
"history_option_scraper": "Metadaten abrufen",
"history_option_delete_game": "Spiel löschen",
"history_option_error_info": "Fehlerdetails",
"history_option_retry": "Download wiederholen",

View File

@@ -67,7 +67,7 @@
"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 +80,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 +187,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",
@@ -244,6 +247,7 @@
"history_game_options_title": "Game Options",
"history_option_download_folder": "Locate file",
"history_option_extract_archive": "Extract archive",
"history_option_open_file": "Open file",
"history_option_scraper": "Scrape metadata",
"history_option_delete_game": "Delete game",
"history_option_error_info": "Error details",

View File

@@ -67,7 +67,7 @@
"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 +80,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 +187,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",
@@ -244,7 +247,8 @@
"history_game_options_title": "Opciones del juego",
"history_option_download_folder": "Localizar archivo",
"history_option_extract_archive": "Extraer archivo",
"history_option_scraper": "Scraper metadatos",
"history_option_open_file": "Abrir archivo",
"history_option_scraper": "Obtener metadatos",
"history_option_delete_game": "Eliminar juego",
"history_option_error_info": "Detalles del error",
"history_option_retry": "Reintentar descarga",

View File

@@ -67,9 +67,10 @@
"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 +187,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",
@@ -244,10 +247,11 @@
"history_game_options_title": "Options du jeu",
"history_option_download_folder": "Localiser le fichier",
"history_option_extract_archive": "Extraire l'archive",
"history_option_scraper": "Scraper métadonnées",
"history_option_open_file": "Ouvrir le fichier",
"history_option_scraper": "Récupérer métadonnées",
"history_option_delete_game": "Supprimer le jeu",
"history_option_error_info": "Détails de l'erreur",
"history_option_retry": "Réessayer le téléchargement",
"history_option_retry": "Retenter le téléchargement",
"history_option_back": "Retour",
"history_folder_path_label": "Chemin de destination :",
"history_scraper_not_implemented": "Scraper pas encore implémenté",

View File

@@ -67,7 +67,7 @@
"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 +80,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 +184,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",
@@ -241,6 +244,7 @@
"history_game_options_title": "Opzioni gioco",
"history_option_download_folder": "Localizza file",
"history_option_extract_archive": "Estrai archivio",
"history_option_open_file": "Apri file",
"history_option_scraper": "Scraper metadati",
"history_option_delete_game": "Elimina gioco",
"history_option_error_info": "Dettagli errore",

View File

@@ -67,7 +67,7 @@
"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 +80,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 +186,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",
@@ -243,7 +246,8 @@
"history_game_options_title": "Opções do jogo",
"history_option_download_folder": "Localizar arquivo",
"history_option_extract_archive": "Extrair arquivo",
"history_option_scraper": "Scraper metadados",
"history_option_open_file": "Abrir arquivo",
"history_option_scraper": "Obter metadados",
"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", "")

View File

@@ -1294,6 +1294,18 @@ def _handle_special_platforms(dest_dir, archive_path, before_dirs, iso_before=No
if not success:
return False, error_msg
# PSVita: extraction dans ux0/app + création fichier .psvita
psvita_dir_normal = os.path.join(config.ROMS_FOLDER, "psvita")
psvita_dir_symlink = os.path.join(config.ROMS_FOLDER, "psvita", "psvita")
is_psvita = (dest_dir == psvita_dir_normal or dest_dir == psvita_dir_symlink)
if is_psvita:
expected_base = os.path.splitext(os.path.basename(archive_path))[0]
items_before = before_items if before_items is not None else before_dirs
success, error_msg = handle_psvita(dest_dir, items_before, extracted_basename=expected_base)
if not success:
return False, error_msg
return True, None
def extract_zip(zip_path, dest_dir, url):
@@ -1959,6 +1971,136 @@ def handle_scummvm(dest_dir, before_items, extracted_basename=None):
return False, error_msg
def handle_psvita(dest_dir, before_items, extracted_basename=None):
"""Gère l'organisation spécifique des jeux PSVita extraits.
Structure attendue:
- Archive RAR extraite → Dossier "Nom du jeu"/
- Dans ce dossier → Fichier "IDJeu.zip" (ex: PCSE00890.zip)
- Ce ZIP contient → Dossier "IDJeu" (ex: PCSE00890/)
Actions:
1. Créer fichier "Nom du jeu [IDJeu].psvita" dans dest_dir
2. Extraire IDJeu.zip dans config.SAVE_FOLDER/psvita/ux0/app/
3. Supprimer le dossier temporaire "Nom du jeu"/
Args:
dest_dir: Dossier de destination (psvita ou psvita/psvita)
before_items: Set des éléments présents avant extraction
extracted_basename: Nom de base de l'archive extraite (sans extension)
"""
logger.debug(f"Traitement spécifique PSVita dans: {dest_dir}")
time.sleep(2) # Petite latence post-extraction
try:
after_items = set(os.listdir(dest_dir))
except Exception:
after_items = set()
ignore_names = {"psvita", "images", "videos", "manuals", "media"}
# Filtrer les nouveaux éléments (fichiers ou dossiers)
new_items = [item for item in (after_items - before_items)
if item not in ignore_names and not item.endswith('.psvita')]
if not new_items:
logger.warning("PSVita: Aucun nouveau dossier détecté après extraction")
return True, None
if not extracted_basename:
extracted_basename = new_items[0] if new_items else "game"
# Chercher le dossier du jeu (normalement il n'y en a qu'un)
game_folder = None
for item in new_items:
item_path = os.path.join(dest_dir, item)
if os.path.isdir(item_path):
game_folder = item
game_folder_path = item_path
break
if not game_folder:
logger.error("PSVita: Aucun dossier de jeu trouvé après extraction")
return False, "PSVita: Aucun dossier de jeu trouvé"
logger.debug(f"PSVita: Dossier de jeu trouvé: {game_folder}")
# Chercher le fichier ZIP à l'intérieur (IDJeu.zip)
try:
contents = os.listdir(game_folder_path)
zip_files = [f for f in contents if f.lower().endswith('.zip')]
if not zip_files:
logger.error(f"PSVita: Aucun fichier ZIP trouvé dans {game_folder}")
return False, f"PSVita: Aucun ZIP trouvé dans {game_folder}"
# Prendre le premier ZIP trouvé
zip_filename = zip_files[0]
zip_path = os.path.join(game_folder_path, zip_filename)
# Extraire l'ID du jeu (nom du ZIP sans extension)
game_id = os.path.splitext(zip_filename)[0]
logger.debug(f"PSVita: ZIP trouvé: {zip_filename}, ID du jeu: {game_id}")
# 1. Créer le fichier .psvita dans dest_dir
psvita_filename = f"{game_folder} [{game_id}].psvita"
psvita_file_path = os.path.join(dest_dir, psvita_filename)
try:
# Créer un fichier vide .psvita
with open(psvita_file_path, 'w', encoding='utf-8') as f:
f.write(f"# PSVita Game\n")
f.write(f"# Game: {game_folder}\n")
f.write(f"# ID: {game_id}\n")
logger.info(f"PSVita: Fichier .psvita créé: {psvita_filename}")
except Exception as e:
logger.error(f"PSVita: Erreur création fichier .psvita: {e}")
return False, f"Erreur création {psvita_filename}: {e}"
# 2. Extraire le ZIP dans le dossier parent de config.SAVE_FOLDER/psvita/ux0/app/
save_parent2 = os.path.dirname(config.SAVE_FOLDER)
save_parent = os.path.dirname(save_parent2)
ux0_app_dir = os.path.join(save_parent, "psvita", "ux0", "app")
os.makedirs(ux0_app_dir, exist_ok=True)
logger.debug(f"PSVita: Extraction de {zip_filename} dans {ux0_app_dir}")
try:
import zipfile
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(ux0_app_dir)
logger.info(f"PSVita: ZIP extrait avec succès dans {ux0_app_dir}")
# Vérifier que le dossier game_id existe bien
game_id_path = os.path.join(ux0_app_dir, game_id)
if not os.path.exists(game_id_path):
logger.warning(f"PSVita: Le dossier {game_id} n'a pas été trouvé dans l'extraction")
else:
logger.info(f"PSVita: Dossier {game_id} confirmé dans ux0/app/")
except zipfile.BadZipFile as e:
logger.error(f"PSVita: Fichier ZIP corrompu: {e}")
return False, f"ZIP corrompu: {zip_filename}"
except Exception as e:
logger.error(f"PSVita: Erreur extraction ZIP: {e}")
return False, f"Erreur extraction {zip_filename}: {e}"
# 3. Supprimer le dossier temporaire du jeu
try:
import shutil
shutil.rmtree(game_folder_path)
logger.info(f"PSVita: Dossier temporaire supprimé: {game_folder}")
except Exception as e:
logger.warning(f"PSVita: Impossible de supprimer {game_folder}: {e}")
# Ne pas échouer pour ça, le jeu est quand même installé
logger.info(f"PSVita: Traitement terminé avec succès - {psvita_filename} créé, {game_id} installé dans ux0/app/")
return True, None
except Exception as e:
logger.error(f"PSVita: Erreur générale: {e}", exc_info=True)
return False, f"Erreur PSVita: {str(e)}"
def handle_xbox(dest_dir, iso_files, url=None):
"""Gère la conversion des fichiers Xbox extraits et met à jour l'UI (Converting)."""
logger.debug(f"Traitement spécifique Xbox dans: {dest_dir}")

View File

@@ -1,3 +1,3 @@
{
"version": "2.3.2.8"
"version": "2.3.3.1"
}