mirror of
https://github.com/RetroGameSets/RGSX.git
synced 2026-05-19 22:25:36 +02:00
Compare commits
8 Commits
v2.6.1.5.1
...
v2.6.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
579d0a1c28 | ||
|
|
60ca7bc375 | ||
|
|
c6a76f56d5 | ||
|
|
f835d00886 | ||
|
|
e84c7ae167 | ||
|
|
ce39722351 | ||
|
|
a7dad84108 | ||
|
|
c9f48d20dd |
@@ -2,6 +2,53 @@ import os
|
||||
import platform
|
||||
import warnings
|
||||
|
||||
|
||||
def _enable_windows_dpi_awareness_early():
|
||||
"""Enable DPI awareness before importing pygame so SDL sees physical monitor sizes."""
|
||||
if platform.system() != "Windows":
|
||||
return
|
||||
|
||||
try:
|
||||
os.environ.setdefault("SDL_WINDOWS_DPI_AWARENESS", "permonitorv2")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
import ctypes
|
||||
|
||||
user32 = ctypes.WinDLL("user32", use_last_error=True)
|
||||
if hasattr(user32, "SetProcessDpiAwarenessContext"):
|
||||
for awareness in (-4, -3):
|
||||
try:
|
||||
if user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(awareness)):
|
||||
return
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
import ctypes
|
||||
|
||||
shcore = ctypes.WinDLL("shcore", use_last_error=True)
|
||||
if hasattr(shcore, "SetProcessDpiAwareness"):
|
||||
shcore.SetProcessDpiAwareness(2)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
import ctypes
|
||||
|
||||
user32 = ctypes.WinDLL("user32", use_last_error=True)
|
||||
if hasattr(user32, "SetProcessDPIAware"):
|
||||
user32.SetProcessDPIAware()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
_enable_windows_dpi_awareness_early()
|
||||
|
||||
# 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")
|
||||
@@ -19,6 +66,7 @@ import logging
|
||||
import requests
|
||||
import queue
|
||||
import datetime
|
||||
from datetime import timezone
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
@@ -28,7 +76,7 @@ from display import (
|
||||
init_display, draw_loading_screen, draw_error_screen, draw_platform_grid,
|
||||
draw_progress_screen, draw_controls, draw_virtual_keyboard,
|
||||
draw_extension_warning, draw_pause_menu, draw_controls_help, draw_game_list,
|
||||
draw_global_search_list,
|
||||
draw_global_search_list, draw_global_sort_menu,
|
||||
draw_display_menu, draw_filter_menu_choice, draw_filter_advanced, draw_filter_priority_config,
|
||||
draw_history_list, draw_clear_history_dialog, draw_cancel_download_dialog,
|
||||
draw_confirm_dialog, draw_reload_games_data_dialog, draw_popup, draw_gradient,
|
||||
@@ -41,7 +89,7 @@ from controls_mapper import map_controls, draw_controls_mapping, get_actions
|
||||
from controls import load_controls_config
|
||||
from utils import (
|
||||
load_sources, check_extension_before_download, extract_data,
|
||||
play_random_music, load_music_config, load_api_keys
|
||||
play_random_music, load_music_config, load_api_keys, _refresh_loading_feedback, _format_size_bytes
|
||||
)
|
||||
from history import load_history, save_history, load_downloaded_games
|
||||
from config import OTA_data_ZIP
|
||||
@@ -99,6 +147,7 @@ _run_windows_gamelist_update()
|
||||
try:
|
||||
config.update_checked = False
|
||||
config.gamelist_update_prompted = False # Flag pour ne pas redemander la mise à jour plusieurs fois
|
||||
config.gamelist_refreshed_this_session = False
|
||||
config.pending_update_version = ""
|
||||
config.startup_update_confirmed = False
|
||||
config.text_file_mode = ""
|
||||
@@ -439,7 +488,8 @@ async def main():
|
||||
# Charger les filtres de jeux sauvegardés
|
||||
try:
|
||||
from game_filters import GameFilters
|
||||
from rgsx_settings import load_game_filters
|
||||
from rgsx_settings import get_global_sort_option, load_game_filters
|
||||
config.global_sort_option = get_global_sort_option()
|
||||
config.game_filter_obj = GameFilters()
|
||||
filter_dict = load_game_filters()
|
||||
if filter_dict:
|
||||
@@ -447,6 +497,7 @@ async def main():
|
||||
if config.game_filter_obj.is_active():
|
||||
config.filter_active = True
|
||||
logger.info("Filtres de jeux chargés et actifs")
|
||||
logger.info(f"Tri global chargé: {config.global_sort_option}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du chargement des filtres: {e}")
|
||||
config.game_filter_obj = None
|
||||
@@ -760,6 +811,7 @@ async def main():
|
||||
"filter_menu_choice",
|
||||
"filter_advanced",
|
||||
"filter_priority_config",
|
||||
"global_sort_menu",
|
||||
"platform_search",
|
||||
}
|
||||
if config.menu_state in SIMPLE_HANDLE_STATES:
|
||||
@@ -1199,6 +1251,8 @@ async def main():
|
||||
draw_filter_platforms_menu(screen)
|
||||
elif config.menu_state == "filter_menu_choice":
|
||||
draw_filter_menu_choice(screen)
|
||||
elif config.menu_state == "global_sort_menu":
|
||||
draw_global_sort_menu(screen)
|
||||
elif config.menu_state == "filter_advanced":
|
||||
draw_filter_advanced(screen)
|
||||
elif config.menu_state == "filter_priority_config":
|
||||
@@ -1440,6 +1494,10 @@ async def main():
|
||||
try:
|
||||
success, message = extract_data(local_zip, dest_dir, local_zip)
|
||||
if success:
|
||||
from rgsx_settings import set_last_gamelist_update
|
||||
|
||||
set_last_gamelist_update()
|
||||
config.gamelist_refreshed_this_session = True
|
||||
logger.debug(f"Extraction locale réussie : {message}")
|
||||
config.loading_progress = 70.0
|
||||
config.needs_redraw = True
|
||||
@@ -1460,17 +1518,28 @@ async def main():
|
||||
config.popup_timer = 5000
|
||||
else:
|
||||
try:
|
||||
_refresh_loading_feedback(
|
||||
current_system=_("loading_download_data"),
|
||||
progress=config.loading_progress,
|
||||
force=True,
|
||||
)
|
||||
with requests.get(sources_zip_url, stream=True, headers=headers, timeout=30) as response:
|
||||
response.raise_for_status()
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
logger.debug(f"Taille totale du ZIP : {total_size} octets")
|
||||
downloaded = 0
|
||||
download_started_at = time.time()
|
||||
last_loading_refresh = 0.0
|
||||
os.makedirs(os.path.dirname(zip_path), exist_ok=True)
|
||||
with open(zip_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
for chunk in response.iter_content(chunk_size=262144):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
downloaded += len(chunk)
|
||||
elapsed = max(0.001, time.time() - download_started_at)
|
||||
speed_bytes_per_second = downloaded / elapsed
|
||||
progress_detail = f"{_format_size_bytes(downloaded)} / {_format_size_bytes(total_size)}" if total_size > 0 else _format_size_bytes(downloaded)
|
||||
speed_detail = f"{speed_bytes_per_second / (1024 * 1024):.1f} MB/s"
|
||||
config.download_progress[sources_zip_url] = {
|
||||
"downloaded_size": downloaded,
|
||||
"total_size": total_size,
|
||||
@@ -1479,7 +1548,16 @@ async def main():
|
||||
}
|
||||
config.loading_progress = 15.0 + (35.0 * downloaded / total_size) if total_size > 0 else 15.0
|
||||
config.needs_redraw = True
|
||||
await asyncio.sleep(0)
|
||||
now = time.time()
|
||||
should_refresh = (now - last_loading_refresh) >= 0.12 or (total_size > 0 and downloaded >= total_size)
|
||||
if should_refresh:
|
||||
last_loading_refresh = now
|
||||
_refresh_loading_feedback(
|
||||
current_system=_("loading_download_data"),
|
||||
progress=config.loading_progress,
|
||||
detail_lines=[progress_detail, speed_detail],
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
logger.debug(f"ZIP téléchargé : {zip_path}")
|
||||
|
||||
config.current_loading_system = _("loading_extracting_data")
|
||||
@@ -1488,6 +1566,11 @@ async def main():
|
||||
dest_dir = config.SAVE_FOLDER
|
||||
success, message = extract_data(zip_path, dest_dir, sources_zip_url)
|
||||
if success:
|
||||
from rgsx_settings import get_remote_gamelist_timestamp, set_last_gamelist_update
|
||||
|
||||
remote_update_dt = get_remote_gamelist_timestamp(sources_zip_url)
|
||||
set_last_gamelist_update(remote_update_dt)
|
||||
config.gamelist_refreshed_this_session = True
|
||||
logger.debug(f"Extraction réussie : {message}")
|
||||
config.loading_progress = 70.0
|
||||
config.needs_redraw = True
|
||||
@@ -1526,34 +1609,76 @@ async def main():
|
||||
continue # Passer immédiatement à load_sources
|
||||
elif loading_step == "load_sources":
|
||||
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
|
||||
sources = load_sources()
|
||||
sources = load_sources(allow_torrent_manifest_fetch=True)
|
||||
config.loading_progress = 100.0
|
||||
config.current_loading_system = ""
|
||||
config.loading_detail_lines = []
|
||||
|
||||
# Vérifier si une mise à jour de la liste des jeux est nécessaire (seulement si pas déjà demandé)
|
||||
if not config.gamelist_update_prompted:
|
||||
from rgsx_settings import get_last_gamelist_update
|
||||
from config import GAMELIST_UPDATE_DAYS
|
||||
from datetime import datetime, timedelta
|
||||
if getattr(config, "gamelist_refreshed_this_session", False):
|
||||
logger.info("Liste des jeux déjà téléchargée/extraites pendant ce lancement, aucun prompt de mise à jour supplémentaire")
|
||||
config.menu_state = "platform"
|
||||
logger.debug(f"Fin chargement, passage à platform, progress={config.loading_progress}")
|
||||
continue
|
||||
|
||||
from rgsx_settings import (
|
||||
get_last_gamelist_update,
|
||||
get_last_gamelist_prompt_remote_update,
|
||||
parse_gamelist_update_timestamp,
|
||||
format_gamelist_update_display,
|
||||
get_remote_gamelist_timestamp,
|
||||
)
|
||||
|
||||
last_update = get_last_gamelist_update()
|
||||
last_prompted_remote_update = get_last_gamelist_prompt_remote_update()
|
||||
last_update_dt = parse_gamelist_update_timestamp(last_update)
|
||||
last_prompted_remote_update_dt = parse_gamelist_update_timestamp(last_prompted_remote_update)
|
||||
remote_sources_url = get_sources_zip_url(OTA_data_ZIP)
|
||||
remote_update_dt = get_remote_gamelist_timestamp(remote_sources_url)
|
||||
config.gamelist_local_update_display = format_gamelist_update_display(last_update)
|
||||
config.gamelist_remote_update_display = format_gamelist_update_display(remote_update_dt)
|
||||
config.gamelist_remote_update_timestamp = (
|
||||
remote_update_dt.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
if remote_update_dt is not None else None
|
||||
)
|
||||
should_prompt_update = False
|
||||
|
||||
if last_update is None:
|
||||
# Première utilisation, proposer la mise à jour
|
||||
logger.info("Première utilisation détectée, proposition de mise à jour de la liste des jeux")
|
||||
should_prompt_update = True
|
||||
else:
|
||||
try:
|
||||
last_update_date = datetime.strptime(last_update, "%Y-%m-%d")
|
||||
days_since_update = (datetime.now() - last_update_date).days
|
||||
logger.info(f"Dernière mise à jour de la liste des jeux: {last_update} ({days_since_update} jours)")
|
||||
|
||||
if days_since_update >= GAMELIST_UPDATE_DAYS:
|
||||
logger.info(f"Mise à jour de la liste des jeux recommandée (>{GAMELIST_UPDATE_DAYS} jours)")
|
||||
try:
|
||||
logger.info(
|
||||
f"Dernière mise à jour locale de la liste des jeux: {config.gamelist_local_update_display or last_update}"
|
||||
)
|
||||
if last_prompted_remote_update_dt is not None:
|
||||
logger.info(
|
||||
"Dernière date distante déjà proposée pour la liste des jeux: "
|
||||
f"{last_prompted_remote_update_dt.isoformat()}"
|
||||
)
|
||||
if remote_update_dt is not None:
|
||||
logger.info(
|
||||
f"Date distante détectée pour la liste des jeux: {config.gamelist_remote_update_display}"
|
||||
)
|
||||
|
||||
latest_seen_update_dt = None
|
||||
for candidate_dt in (last_update_dt, last_prompted_remote_update_dt):
|
||||
if candidate_dt is None:
|
||||
continue
|
||||
if latest_seen_update_dt is None or candidate_dt > latest_seen_update_dt:
|
||||
latest_seen_update_dt = candidate_dt
|
||||
|
||||
if remote_update_dt is not None:
|
||||
if latest_seen_update_dt is None:
|
||||
logger.info("Première vérification distante détectée, proposition de mise à jour de la liste des jeux")
|
||||
should_prompt_update = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la vérification de la date de mise à jour: {e}")
|
||||
elif remote_update_dt > latest_seen_update_dt:
|
||||
logger.info("Mise à jour de la liste des jeux recommandée (fichier distant plus récent)")
|
||||
should_prompt_update = True
|
||||
else:
|
||||
logger.info("Même version distante déjà appliquée ou déjà proposée, aucun prompt affiché")
|
||||
elif last_update is None and last_prompted_remote_update is None:
|
||||
logger.info("Première utilisation détectée sans date distante exploitable, proposition de mise à jour de la liste des jeux")
|
||||
should_prompt_update = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la vérification de la date de mise à jour: {e}")
|
||||
|
||||
if should_prompt_update:
|
||||
config.menu_state = "gamelist_update_prompt"
|
||||
|
||||
1
ports/RGSX/assets/images/archive.svg
Normal file
1
ports/RGSX/assets/images/archive.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height='86' viewBox='0 0 76 86' width='76' xmlns='http://www.w3.org/2000/svg'><path d='m76 82v4h-76l.00080851-4zm-3-6v5h-70v-5zm-62.6696277-54 .8344146.4217275.4176066 6.7436084.4176065 10.9576581v10.5383496l-.4176065 13.1364492-.0694681 8.8498268-1.1825531.3523804h-4.17367003l-1.25202116-.3523804-.48627608-8.8498268-.41840503-13.0662957v-10.5375432l.41840503-11.028618.38167482-6.7798947.87034634-.3854412zm60.0004653 0 .8353798.4217275.4168913 6.7436084.4168913 10.9576581v10.5383496l-.4168913 13.1364492-.0686832 8.8498268-1.1835879.3523804h-4.1737047l-1.2522712-.3523804-.4879704-8.8498268-.4168913-13.0662957v-10.5375432l.4168913-11.028618.3833483-6.7798947.8697215-.3854412zm-42.000632 0 .8344979.4217275.4176483 6.7436084.4176482 10.9576581v10.5383496l-.4176482 13.1364492-.0686764 8.8498268-1.1834698.3523804h-4.1740866l-1.2529447-.3523804-.4863246-8.8498268-.4168497-13.0662957v-10.5375432l.4168497-11.028618.38331-6.7798947.8688361-.3854412zm23 0 .8344979.4217275.4176483 6.7436084.4176482 10.9576581v10.5383496l-.4176482 13.1364492-.0686764 8.8498268-1.1834698.3523804h-4.1740866l-1.2521462-.3523804-.4871231-8.8498268-.4168497-13.0662957v-10.5375432l.4168497-11.028618.38331-6.7798947.8696347-.3854412zm21.6697944-9v7h-70v-7zm-35.7200748-13 36.7200748 8.4088317-1.4720205 2.5911683h-70.32799254l-2.19998696-2.10140371z' fill='#2c2c2c' fill-rule='evenodd'/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
18
ports/RGSX/assets/images/lolroms.svg
Normal file
18
ports/RGSX/assets/images/lolroms.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" aria-labelledby="title desc" role="img">
|
||||
<title id="title">LOL</title>
|
||||
<desc id="desc">Texte LOL style gaming retro</desc>
|
||||
|
||||
<!-- Lettre L (gauche) - rose/violet -->
|
||||
<path d="M35 45 L35 155 L65 155 L65 135 L50 135 L50 45 Z" fill="#ff00ff"/>
|
||||
|
||||
<!-- Lettre O (milieu) - cyan -->
|
||||
<ellipse cx="105" cy="100" rx="29" ry="48" fill="none" stroke="#00ffff" stroke-width="19"/>
|
||||
|
||||
<!-- Lettre L (droite) - rose/violet -->
|
||||
<path d="M145 45 L145 155 L175 155 L175 135 L160 135 L160 45 Z" fill="#ff00ff"/>
|
||||
|
||||
<!-- Effet néon plus prononcé -->
|
||||
<path d="M35 45 L35 155 L65 155 L65 135 L50 135 L50 45 Z" fill="none" stroke="#ff00ff" stroke-width="9" opacity="0.85"/>
|
||||
<ellipse cx="105" cy="100" rx="29" ry="48" fill="none" stroke="#00ffff" stroke-width="9" opacity="0.85"/>
|
||||
<path d="M145 45 L145 155 L175 155 L175 135 L160 135 L160 45 Z" fill="none" stroke="#ff00ff" stroke-width="9" opacity="0.85"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1006 B |
63
ports/RGSX/assets/images/torrent.svg
Normal file
63
ports/RGSX/assets/images/torrent.svg
Normal file
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="500"
|
||||
height="498"
|
||||
viewBox="0 0 500 497.99999"
|
||||
version="1.1"
|
||||
id="svg20"
|
||||
sodipodi:docname="μTorrent logo with title.svg"
|
||||
inkscape:version="1.1.2 (b8e25be833, 2022-02-05)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs24" />
|
||||
<sodipodi:namedview
|
||||
id="namedview22"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.40844424"
|
||||
inkscape:cx="634.11348"
|
||||
inkscape:cy="73.449437"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="974"
|
||||
inkscape:window-x="-11"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg20" />
|
||||
<!-- Generator: Sketch 56.3 (81716) - https://sketch.com -->
|
||||
<title
|
||||
id="title2">Group</title>
|
||||
<desc
|
||||
id="desc4">Created with Sketch.</desc>
|
||||
<g
|
||||
id="g186"
|
||||
transform="scale(2.0742981)">
|
||||
<path
|
||||
d="m 137.38383,239.99418 c 44.64832,-6.2824 81.43169,-37.04615 96.37094,-78.28306 -1.17843,0.43717 -2.61663,0.95348 -4.33344,1.46978 -20.0859,5.93191 -34.17804,-4.68447 -37.02056,-6.51981 -2.84253,-1.81651 -5.88835,-4.82769 -6.45686,-4.6694 -1.83729,11.38519 -12.22849,27.19481 -35.94756,34.36661 -12.98901,3.93827 -26.97196,4.02872 -36.94903,-1.56777 l 3.29055,8.26095 c 1.31773,3.29382 3.52774,8.69811 4.89817,11.97687 0,0 8.61793,20.53931 16.14779,34.96583"
|
||||
id="Fill-18"
|
||||
style="fill:#76b83f;fill-rule:evenodd;stroke:none;stroke-width:5.02241" />
|
||||
<path
|
||||
d="M 27.337163,71.292153 62.88564,64.663041 c 3.237841,-0.591683 6.82582,1.247434 7.962829,4.085251 l 24.423106,61.011188 c 3.324434,6.36154 4.002122,7.76725 6.166955,10.48071 0,0 16.86313,24.09695 42.57008,18.21027 17.33374,-3.96466 25.42834,-16.98922 25.97802,-26.25641 0.5685,-2.97349 -0.32002,-6.71579 -1.86364,-10.20936 L 138.86947,55.784027 c -1.17465,-2.668226 0.21837,-5.28746 3.07219,-5.833919 l 29.64506,-5.528656 c 2.71828,-0.48616 5.90718,1.202209 7.09313,3.783756 l 32.401,69.437962 c 1.30643,2.77752 3.95318,7.15673 5.89964,9.70812 0,0 6.73923,9.68928 17.53705,7.85017 2.66934,0 5.7114,-1.44718 5.7114,-1.44718 0.47438,-4.32267 0.72663,-8.70565 0.72663,-13.16023 C 240.95557,53.986366 187.01165,0 120.46273,0 53.932634,0 0,53.986366 0,120.59405 c 0,53.44744 34.739017,98.7583 82.851068,114.57923 -3.249135,-6.69318 -6.53592,-14.0911 -9.227845,-21.31567 L 23.067731,77.872271 c -1.110654,-2.984796 0.835815,-5.935674 4.269432,-6.580118"
|
||||
id="Fill-19"
|
||||
style="fill:#76b83f;fill-rule:evenodd;stroke:none;stroke-width:5.02241" />
|
||||
</g>
|
||||
<metadata
|
||||
id="metadata26">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:title>Group</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
388
ports/RGSX/build_embedded_caches.py
Normal file
388
ports/RGSX/build_embedded_caches.py
Normal file
@@ -0,0 +1,388 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
_TORRENT_DOWNLOAD_SCHEME = "rgsx+torrent"
|
||||
|
||||
|
||||
def _format_size_bytes(size_bytes: int) -> str:
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes} B"
|
||||
if size_bytes < 1024 * 1024:
|
||||
return f"{size_bytes / 1024:.1f} KB"
|
||||
if size_bytes < 1024 * 1024 * 1024:
|
||||
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
||||
return f"{size_bytes / (1024 * 1024 * 1024):.2f} GB"
|
||||
|
||||
|
||||
def build_torrent_download_url(source_url: str, file_index: int, relative_path: str, size_bytes: int | None = None) -> str:
|
||||
params = {
|
||||
"source": source_url,
|
||||
"index": str(max(1, int(file_index))),
|
||||
"path": relative_path,
|
||||
}
|
||||
if isinstance(size_bytes, int) and size_bytes > 0:
|
||||
params["size"] = str(size_bytes)
|
||||
return f"{_TORRENT_DOWNLOAD_SCHEME}://download?{urllib.parse.urlencode(params, quote_via=urllib.parse.quote)}"
|
||||
|
||||
|
||||
def get_clean_display_name(raw_name, platform_id=None):
|
||||
text = str(raw_name or "").strip()
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
normalized = text.replace("\\", "/")
|
||||
leaf_name = normalized.rsplit("/", 1)[-1]
|
||||
display_name = Path(leaf_name).stem.strip()
|
||||
|
||||
prefixes = []
|
||||
if platform_id:
|
||||
prefixes.append(str(platform_id).strip())
|
||||
|
||||
for prefix in prefixes:
|
||||
if not prefix:
|
||||
continue
|
||||
pattern = rf"^{re.escape(prefix)}[\s\-_:]+"
|
||||
updated_name = re.sub(pattern, "", display_name, flags=re.IGNORECASE).strip()
|
||||
if updated_name:
|
||||
display_name = updated_name
|
||||
|
||||
return display_name.strip(" -_/")
|
||||
|
||||
|
||||
def _decode_bencode_text(value) -> str:
|
||||
if isinstance(value, bytes):
|
||||
return value.decode("utf-8", errors="replace")
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
return str(value or "")
|
||||
|
||||
|
||||
def _bdecode(data: bytes, index: int = 0):
|
||||
token = data[index:index + 1]
|
||||
if token == b"i":
|
||||
end = data.index(b"e", index)
|
||||
return int(data[index + 1:end]), end + 1
|
||||
if token == b"l":
|
||||
items = []
|
||||
index += 1
|
||||
while data[index:index + 1] != b"e":
|
||||
value, index = _bdecode(data, index)
|
||||
items.append(value)
|
||||
return items, index + 1
|
||||
if token == b"d":
|
||||
values = {}
|
||||
index += 1
|
||||
while data[index:index + 1] != b"e":
|
||||
key, index = _bdecode(data, index)
|
||||
value, index = _bdecode(data, index)
|
||||
values[key] = value
|
||||
return values, index + 1
|
||||
if token.isdigit():
|
||||
sep = data.index(b":", index)
|
||||
length = int(data[index:sep])
|
||||
start = sep + 1
|
||||
end = start + length
|
||||
return data[start:end], end
|
||||
raise ValueError(f"Invalid bencode token at offset {index}: {token!r}")
|
||||
|
||||
|
||||
def is_torrent_manifest_url(url: str | None) -> bool:
|
||||
if not url or not isinstance(url, str):
|
||||
return False
|
||||
try:
|
||||
parsed = urllib.parse.urlparse(url.strip())
|
||||
except Exception:
|
||||
return False
|
||||
return (parsed.path or "").lower().endswith(".torrent")
|
||||
|
||||
|
||||
def _extract_torrent_source(item) -> tuple[str, str] | None:
|
||||
if isinstance(item, (list, tuple)):
|
||||
if len(item) < 2:
|
||||
return None
|
||||
source_name = str(item[0] or "").strip()
|
||||
source_url = item[1] if isinstance(item[1], str) else None
|
||||
if source_url and is_torrent_manifest_url(source_url):
|
||||
return source_name, source_url.strip()
|
||||
return None
|
||||
|
||||
if isinstance(item, dict):
|
||||
source_url = item.get("torrent_url") or item.get("url") or item.get("download") or item.get("link")
|
||||
if not isinstance(source_url, str) or not source_url.strip():
|
||||
return None
|
||||
source_type = str(item.get("type") or item.get("source_type") or item.get("source") or "").strip().lower()
|
||||
if source_type == "torrent" or is_torrent_manifest_url(source_url):
|
||||
source_name = item.get("game_name") or item.get("name") or item.get("title") or item.get("game") or item.get("label")
|
||||
if not source_name:
|
||||
parsed = urllib.parse.urlparse(source_url)
|
||||
source_name = urllib.parse.unquote(Path(parsed.path).name)
|
||||
return str(source_name or "").strip(), source_url.strip()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _extract_torrent_entries_from_bytes(payload: bytes, source_url: str) -> list[dict[str, str | int]]:
|
||||
torrent_data, _next_index = _bdecode(payload)
|
||||
if not isinstance(torrent_data, dict):
|
||||
raise ValueError("Torrent root metadata is not a dictionary")
|
||||
|
||||
info = torrent_data.get(b"info")
|
||||
if not isinstance(info, dict):
|
||||
raise ValueError("Torrent metadata does not contain an info dictionary")
|
||||
|
||||
entries: list[dict[str, str | int]] = []
|
||||
files = info.get(b"files")
|
||||
root_name = _decode_bencode_text(info.get(b"name.utf-8") or info.get(b"name") or "").strip()
|
||||
if isinstance(files, list):
|
||||
for file_index, file_entry in enumerate(files, start=1):
|
||||
if not isinstance(file_entry, dict):
|
||||
continue
|
||||
path_parts = file_entry.get(b"path.utf-8") or file_entry.get(b"path") or []
|
||||
if not isinstance(path_parts, list):
|
||||
continue
|
||||
parts = [_decode_bencode_text(part).strip() for part in path_parts]
|
||||
parts = [part for part in parts if part]
|
||||
if not parts:
|
||||
continue
|
||||
full_path = "/".join(parts)
|
||||
download_path = "/".join([part for part in [root_name, full_path] if part])
|
||||
entries.append(
|
||||
{
|
||||
"name": parts[-1],
|
||||
"path": full_path,
|
||||
"download_path": download_path or full_path,
|
||||
"index": file_index,
|
||||
"size_bytes": int(file_entry.get(b"length") or 0),
|
||||
"source_url": source_url,
|
||||
}
|
||||
)
|
||||
else:
|
||||
if root_name:
|
||||
entries.append(
|
||||
{
|
||||
"name": root_name,
|
||||
"path": root_name,
|
||||
"download_path": root_name,
|
||||
"index": 1,
|
||||
"size_bytes": int(info.get(b"length") or 0),
|
||||
"source_url": source_url,
|
||||
}
|
||||
)
|
||||
|
||||
duplicate_names: dict[str, int] = {}
|
||||
for entry in entries:
|
||||
name = str(entry["name"])
|
||||
duplicate_names[name] = duplicate_names.get(name, 0) + 1
|
||||
|
||||
for entry in entries:
|
||||
if duplicate_names.get(str(entry["name"]), 0) > 1:
|
||||
entry["name"] = str(entry["path"])
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def _fetch_torrent_entries(source_url: str) -> list[dict[str, str | int]]:
|
||||
request = urllib.request.Request(
|
||||
source_url,
|
||||
headers={
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36",
|
||||
"Accept": "*/*",
|
||||
},
|
||||
)
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
payload = response.read()
|
||||
return _extract_torrent_entries_from_bytes(payload, source_url)
|
||||
|
||||
|
||||
def _iter_game_rows(data):
|
||||
if isinstance(data, dict) and "games" in data:
|
||||
data = data["games"]
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
if isinstance(data, dict):
|
||||
return [data]
|
||||
return []
|
||||
|
||||
|
||||
def _build_platform_search_entries(
|
||||
rows,
|
||||
torrent_manifest_cache: dict[str, list[dict[str, str | int]]],
|
||||
warnings: list[str],
|
||||
platform_id: str,
|
||||
) -> list[dict[str, str | int]]:
|
||||
indexed_entries: list[dict[str, str | int]] = []
|
||||
for item in rows:
|
||||
torrent_source = _extract_torrent_source(item)
|
||||
if torrent_source is not None:
|
||||
source_name, source_url = torrent_source
|
||||
entries = torrent_manifest_cache.get(source_url)
|
||||
if entries is None:
|
||||
try:
|
||||
entries = _fetch_torrent_entries(source_url)
|
||||
torrent_manifest_cache[source_url] = entries
|
||||
except Exception as exc:
|
||||
warnings.append(f"{platform_id}: failed to build torrent cache for {source_name or source_url}: {exc}")
|
||||
entries = []
|
||||
for entry in entries:
|
||||
game_name = str(entry.get("name") or "").strip()
|
||||
if not game_name:
|
||||
continue
|
||||
size_bytes = int(entry.get("size_bytes") or 0)
|
||||
file_index = int(entry.get("index") or 1)
|
||||
relative_path = str(entry.get("download_path") or entry.get("path") or game_name)
|
||||
indexed_entries.append(
|
||||
{
|
||||
"platform_id": platform_id,
|
||||
"game_name": game_name,
|
||||
"display_name": get_clean_display_name(game_name, platform_id),
|
||||
"url": build_torrent_download_url(source_url, file_index, relative_path, size_bytes),
|
||||
"size": _format_size_bytes(size_bytes) if size_bytes > 0 else "",
|
||||
"size_bytes": size_bytes,
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
if isinstance(item, dict):
|
||||
name = item.get("game_name") or item.get("name") or item.get("title") or item.get("game")
|
||||
if name:
|
||||
size = item.get("size") or item.get("filesize") or item.get("length") or ""
|
||||
indexed_entries.append(
|
||||
{
|
||||
"platform_id": platform_id,
|
||||
"game_name": str(name),
|
||||
"display_name": get_clean_display_name(name, platform_id),
|
||||
"url": str(item.get("url") or item.get("download") or item.get("link") or item.get("href") or ""),
|
||||
"size": str(size) if size else "",
|
||||
"size_bytes": 0,
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
if isinstance(item, (list, tuple)):
|
||||
if len(item) > 0 and str(item[0] or "").strip():
|
||||
size = item[2] if len(item) > 2 and item[2] is not None else ""
|
||||
url = item[1] if len(item) > 1 and isinstance(item[1], str) else ""
|
||||
indexed_entries.append(
|
||||
{
|
||||
"platform_id": platform_id,
|
||||
"game_name": str(item[0]),
|
||||
"display_name": get_clean_display_name(item[0], platform_id),
|
||||
"url": url,
|
||||
"size": str(size) if size else "",
|
||||
"size_bytes": 0,
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
if isinstance(item, str) and item.strip():
|
||||
indexed_entries.append(
|
||||
{
|
||||
"platform_id": platform_id,
|
||||
"game_name": item.strip(),
|
||||
"display_name": get_clean_display_name(item, platform_id),
|
||||
"url": "",
|
||||
"size": "",
|
||||
"size_bytes": 0,
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
if item is not None:
|
||||
item_text = str(item)
|
||||
indexed_entries.append(
|
||||
{
|
||||
"platform_id": platform_id,
|
||||
"game_name": item_text,
|
||||
"display_name": get_clean_display_name(item_text, platform_id),
|
||||
"url": "",
|
||||
"size": "",
|
||||
"size_bytes": 0,
|
||||
}
|
||||
)
|
||||
|
||||
return indexed_entries
|
||||
|
||||
|
||||
def build_caches(games_dir: Path) -> tuple[dict[str, list[dict[str, str | int]]], dict[str, dict[str, str | int]], list[dict[str, str | int]], list[str]]:
|
||||
torrent_manifest_cache: dict[str, list[dict[str, str | int]]] = {}
|
||||
platform_count_cache: dict[str, dict[str, str | int]] = {}
|
||||
global_search_index: list[dict[str, str | int]] = []
|
||||
warnings: list[str] = []
|
||||
|
||||
for game_file in sorted(games_dir.glob("*.json")):
|
||||
with game_file.open("r", encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
rows = _iter_game_rows(data)
|
||||
platform_id = game_file.stem
|
||||
platform_entries = _build_platform_search_entries(rows, torrent_manifest_cache, warnings, platform_id)
|
||||
count = len(platform_entries)
|
||||
platform_count_cache[platform_id] = {
|
||||
"path": "",
|
||||
"mtime_ns": 0,
|
||||
"file_name": game_file.name,
|
||||
"size_bytes": game_file.stat().st_size,
|
||||
"count": int(count),
|
||||
}
|
||||
global_search_index.extend(platform_entries)
|
||||
|
||||
return torrent_manifest_cache, platform_count_cache, global_search_index, warnings
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Build portable RGSX cache files from exported games JSONs.")
|
||||
parser.add_argument("--games-dir", required=True, help="Directory containing exported games/*.json files")
|
||||
parser.add_argument("--output-dir", required=True, help="Directory where cache JSON files will be written")
|
||||
args = parser.parse_args()
|
||||
|
||||
games_dir = Path(args.games_dir)
|
||||
output_dir = Path(args.output_dir)
|
||||
if not games_dir.is_dir():
|
||||
print(json.dumps({"ok": False, "error": f"games directory not found: {games_dir}"}), file=sys.stderr)
|
||||
return 2
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
torrent_manifest_cache, platform_count_cache, global_search_index, warnings = build_caches(games_dir)
|
||||
except Exception as exc:
|
||||
print(json.dumps({"ok": False, "error": str(exc)}), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
torrent_cache_path = output_dir / "torrent_manifest_cache.json"
|
||||
platform_count_cache_path = output_dir / "platform_games_count_cache.json"
|
||||
global_search_index_path = output_dir / "global_search_index.json"
|
||||
torrent_cache_payload = {"version": 1, "entries": torrent_manifest_cache}
|
||||
platform_count_payload = {"version": 2, "entries": platform_count_cache}
|
||||
global_search_payload = {"version": 1, "entries": global_search_index}
|
||||
|
||||
torrent_cache_path.write_text(json.dumps(torrent_cache_payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
platform_count_cache_path.write_text(json.dumps(platform_count_payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
global_search_index_path.write_text(json.dumps(global_search_payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"ok": True,
|
||||
"torrent_manifest_count": len(torrent_manifest_cache),
|
||||
"platform_count_entries": len(platform_count_cache),
|
||||
"global_search_entries": len(global_search_index),
|
||||
"torrent_cache_path": str(torrent_cache_path),
|
||||
"platform_count_cache_path": str(platform_count_cache_path),
|
||||
"global_search_index_path": str(global_search_index_path),
|
||||
"warnings": warnings,
|
||||
}
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -27,7 +27,7 @@ except Exception:
|
||||
pygame = None # type: ignore
|
||||
|
||||
# Version actuelle de l'application
|
||||
app_version = "2.6.1.5.1"
|
||||
app_version = "2.6.3.2"
|
||||
|
||||
# Nombre de jours avant de proposer la mise à jour de la liste des jeux
|
||||
GAMELIST_UPDATE_DAYS = 1
|
||||
@@ -195,6 +195,10 @@ PRECONF_CONTROLS_PATH = os.path.join(APP_FOLDER, "assets", "controls")
|
||||
CONTROLS_CONFIG_PATH = os.path.join(SAVE_FOLDER, "controls.json")
|
||||
HISTORY_PATH = os.path.join(SAVE_FOLDER, "history.json")
|
||||
DOWNLOADED_GAMES_PATH = os.path.join(SAVE_FOLDER, "downloaded_games.json")
|
||||
TORRENT_MANIFEST_CACHE_PATH = os.path.join(SAVE_FOLDER, "torrent_manifest_cache.json")
|
||||
PLATFORM_GAME_COUNT_CACHE_PATH = os.path.join(SAVE_FOLDER, "platform_games_count_cache.json")
|
||||
GLOBAL_SEARCH_INDEX_CACHE_PATH = os.path.join(SAVE_FOLDER, "global_search_index.json")
|
||||
PENDING_TORRENT_REFRESH_MARKER_PATH = os.path.join(SAVE_FOLDER, "pending_torrent_refresh.marker")
|
||||
RGSX_SETTINGS_PATH = os.path.join(SAVE_FOLDER, "rgsx_settings.json")
|
||||
API_KEY_1FICHIER_PATH = os.path.join(SAVE_FOLDER, "1FichierAPI.txt")
|
||||
API_KEY_ALLDEBRID_PATH = os.path.join(SAVE_FOLDER, "AllDebridAPI.txt")
|
||||
@@ -441,11 +445,20 @@ global_search_query = "" # Texte saisi pour la recherche globale
|
||||
global_search_selected = 0 # Index du resultat global selectionne
|
||||
global_search_scroll_offset = 0 # Offset de defilement des resultats globaux
|
||||
global_search_editing = False # True si le clavier virtuel est actif pour la recherche globale
|
||||
global_search_allow_empty = False # True pour afficher tous les resultats sans requete (filtre/tri globaux)
|
||||
global_search_title_override = "" # Titre alternatif pour la vue globale
|
||||
global_search_return_state = "platform" # Etat de retour depuis la vue globale
|
||||
global_sort_option = "name_asc" # Tri global courant
|
||||
|
||||
# Variables pour le filtrage avancé
|
||||
selected_filter_choice = 0 # Index dans le menu de choix de filtrage (recherche / avancé)
|
||||
selected_filter_option = 0 # Index dans le menu de filtrage avancé
|
||||
game_filter_obj = None # Objet GameFilters pour le filtrage avancé
|
||||
filter_menu_context = "platform" # Contexte d'ouverture du menu filtre unifie
|
||||
filter_menu_entries = [] # Entrees du menu filtre unifie
|
||||
filter_menu_return_state = "platform" # Etat de retour depuis le menu filtre unifie
|
||||
filter_target_scope = "local" # Portee du filtrage avance (local/global)
|
||||
global_sort_selected = 0 # Index selectionne dans le menu de tri global
|
||||
|
||||
# Gestion des états du menu
|
||||
needs_redraw = False # Indicateur si l'écran doit être redessiné
|
||||
|
||||
@@ -15,11 +15,17 @@ from utils import (
|
||||
load_games, check_extension_before_download, is_extension_supported,
|
||||
load_extensions_json, play_random_music, sanitize_filename,
|
||||
save_music_config, load_api_keys, _get_dest_folder_name,
|
||||
extract_zip, extract_rar, find_file_with_or_without_extension, find_matching_files, toggle_web_service_at_boot, check_web_service_status,
|
||||
extract_zip, extract_rar, extract_7z, find_file_with_or_without_extension, find_matching_files, toggle_web_service_at_boot, check_web_service_status,
|
||||
restart_application, generate_support_zip, load_sources,
|
||||
ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string,
|
||||
start_connection_status_check, get_clean_display_name, get_existing_history_matches,
|
||||
move_files_to_directory, parse_torrent_download_url
|
||||
start_connection_status_check, get_clean_display_name, get_existing_history_matches, remember_history_local_match,
|
||||
clear_torrent_manifest_cache,
|
||||
request_torrent_manifest_refresh,
|
||||
clear_platform_game_count_cache,
|
||||
move_files_to_directory, parse_torrent_download_url,
|
||||
_refresh_loading_feedback,
|
||||
parse_game_size_to_bytes,
|
||||
sort_games_list,
|
||||
)
|
||||
from history import load_history, clear_history, add_to_history, save_history, scan_roms_for_downloaded_games
|
||||
from language import _, get_available_languages, set_language
|
||||
@@ -27,7 +33,9 @@ from rgsx_settings import (
|
||||
get_allow_unknown_extensions, set_display_grid, get_font_family, set_font_family,
|
||||
get_show_unsupported_platforms, set_show_unsupported_platforms,
|
||||
set_allow_unknown_extensions, get_hide_premium_systems, set_hide_premium_systems,
|
||||
get_sources_mode, set_sources_mode, set_symlink_option, get_symlink_option, load_rgsx_settings, save_rgsx_settings
|
||||
get_sources_mode, set_sources_mode, set_symlink_option, get_symlink_option,
|
||||
get_global_sort_option, set_global_sort_option,
|
||||
load_rgsx_settings, save_rgsx_settings
|
||||
)
|
||||
from accessibility import save_accessibility_settings
|
||||
from scraper import get_game_metadata, download_image_to_surface
|
||||
@@ -39,6 +47,13 @@ logger = logging.getLogger(__name__)
|
||||
# Extensions d'archives pour lesquelles on ignore l'avertissement d'extension non supportée
|
||||
ARCHIVE_EXTENSIONS = {'.zip', '.7z', '.rar', '.tar', '.gz', '.xz', '.bz2'}
|
||||
|
||||
GLOBAL_SORT_OPTIONS = [
|
||||
("name_asc", lambda: _("web_sort_name_asc") or "A-Z (Name)"),
|
||||
("name_desc", lambda: _("web_sort_name_desc") or "Z-A (Name)"),
|
||||
("size_asc", lambda: _("web_sort_size_asc") or "Size -+ (Small first)"),
|
||||
("size_desc", lambda: _("web_sort_size_desc") or "Size +- (Large first)"),
|
||||
]
|
||||
|
||||
|
||||
def _notify_torrent_in_maintenance(game_name: str | None = None) -> None:
|
||||
try:
|
||||
@@ -69,6 +84,113 @@ def _wrap_index(current_index: int, delta: int, item_count: int) -> int:
|
||||
return (current_index + delta) % item_count
|
||||
|
||||
|
||||
def _sort_global_items(items: list[dict]) -> list[dict]:
|
||||
option = getattr(config, 'global_sort_option', 'name_asc') or 'name_asc'
|
||||
reverse = option in ('name_desc', 'size_desc')
|
||||
|
||||
if option.startswith('size_'):
|
||||
return sorted(
|
||||
items,
|
||||
key=lambda item: (
|
||||
int(item.get('size_bytes') or 0),
|
||||
str(item.get('display_name') or '').lower(),
|
||||
str(item.get('platform_label') or '').lower(),
|
||||
),
|
||||
reverse=reverse,
|
||||
)
|
||||
|
||||
return sorted(
|
||||
items,
|
||||
key=lambda item: (
|
||||
str(item.get('display_name') or '').lower(),
|
||||
str(item.get('platform_label') or '').lower(),
|
||||
int(item.get('size_bytes') or 0),
|
||||
),
|
||||
reverse=reverse,
|
||||
)
|
||||
|
||||
|
||||
def _get_global_sort_index(option: str | None = None) -> int:
|
||||
target = option or getattr(config, 'global_sort_option', 'name_asc')
|
||||
for index, (key, _) in enumerate(GLOBAL_SORT_OPTIONS):
|
||||
if key == target:
|
||||
return index
|
||||
return 0
|
||||
|
||||
|
||||
def _sort_local_games(items: list[Game]) -> list[Game]:
|
||||
option = getattr(config, 'global_sort_option', 'name_asc')
|
||||
return sort_games_list(items, option)
|
||||
|
||||
|
||||
def _apply_sorted_active_filters() -> list[Game]:
|
||||
if hasattr(config, 'game_filter_obj') and config.game_filter_obj and config.game_filter_obj.is_active():
|
||||
return _sort_local_games(config.game_filter_obj.apply_filters(config.games))
|
||||
return config.games
|
||||
|
||||
|
||||
def _build_filter_menu_entries(context: str) -> list[dict[str, str]]:
|
||||
global_search_label = 'Recherche globale' if (_ is None or _("global_search_title") == "global_search_title") else _("global_search_title").format("").replace(" : ", "").rstrip(': ')
|
||||
platform_search_label = 'Recherche sur cette plateforme' if (_ is None or _("platform_search_title") == "platform_search_title") else _("platform_search_title")
|
||||
advanced_filter_label = 'Filtrer' if (_ is None or _("filter_advanced") == "filter_advanced") else _("filter_advanced")
|
||||
sort_label = 'Trier' if (_ is None or _("web_sort") == "web_sort") else _("web_sort")
|
||||
back_label = 'Retour' if (_ is None or _("menu_back") == "menu_back") else _("menu_back")
|
||||
|
||||
entries = []
|
||||
if context == 'game':
|
||||
entries.extend([
|
||||
{
|
||||
'key': 'platform_search',
|
||||
'label': platform_search_label,
|
||||
},
|
||||
{
|
||||
'key': 'global_sort',
|
||||
'label': sort_label,
|
||||
},
|
||||
{
|
||||
'key': 'global_search',
|
||||
'label': global_search_label,
|
||||
},
|
||||
{
|
||||
'key': 'global_filter',
|
||||
'label': advanced_filter_label,
|
||||
},
|
||||
])
|
||||
else:
|
||||
entries.extend([
|
||||
{
|
||||
'key': 'global_search',
|
||||
'label': global_search_label,
|
||||
},
|
||||
{
|
||||
'key': 'global_filter',
|
||||
'label': advanced_filter_label,
|
||||
},
|
||||
{
|
||||
'key': 'global_sort',
|
||||
'label': sort_label,
|
||||
},
|
||||
])
|
||||
|
||||
entries.append({
|
||||
'key': 'back',
|
||||
'label': back_label,
|
||||
})
|
||||
return entries
|
||||
|
||||
|
||||
def open_unified_filter_menu(source_state: str) -> None:
|
||||
context = 'game' if source_state == 'game' else 'global'
|
||||
config.filter_menu_context = context
|
||||
config.filter_menu_entries = _build_filter_menu_entries(context)
|
||||
config.filter_menu_return_state = validate_menu_state(source_state)
|
||||
config.selected_filter_choice = 0
|
||||
config.previous_menu_state = source_state
|
||||
config.menu_state = 'filter_menu_choice'
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Ouverture du menu filtre unifie depuis {source_state}")
|
||||
|
||||
|
||||
# Variables globales pour la répétition
|
||||
key_states = {} # Dictionnaire pour suivre l'état des touches
|
||||
|
||||
@@ -102,6 +224,7 @@ VALID_STATES = [
|
||||
"filter_search", # recherche par nom (existant, mais renommé)
|
||||
"filter_advanced", # filtrage avancé par région, etc.
|
||||
"filter_priority_config", # configuration priorité régions pour one-rom-per-game
|
||||
"global_sort_menu", # menu de tri global
|
||||
"platform_search", # recherche globale inter-plateformes
|
||||
"platform_folder_config", # configuration du dossier personnalisé pour une plateforme
|
||||
"folder_browser", # navigateur de dossiers intégré
|
||||
@@ -446,8 +569,8 @@ def filter_games_by_search_query() -> list[Game]:
|
||||
game_name = game.display_name
|
||||
if config.search_query.lower() in game_name.lower():
|
||||
filtered_games.append(game)
|
||||
|
||||
return filtered_games
|
||||
|
||||
return _sort_local_games(filtered_games)
|
||||
|
||||
|
||||
GLOBAL_SEARCH_KEYBOARD_LAYOUT = [
|
||||
@@ -466,11 +589,33 @@ def _get_platform_label(platform_id: str) -> str:
|
||||
return config.platform_names.get(platform_id, platform_id)
|
||||
|
||||
|
||||
def _build_global_search_loading_title() -> str:
|
||||
fallback = "Loading..."
|
||||
if _ is None:
|
||||
return fallback
|
||||
try:
|
||||
text = _("global_search_title").format("").replace(" : ", "").rstrip(': ')
|
||||
except Exception:
|
||||
text = ""
|
||||
return text or fallback
|
||||
|
||||
|
||||
def build_global_search_index() -> list[dict]:
|
||||
indexed_games = []
|
||||
total_platforms = max(1, len(config.platforms))
|
||||
for platform_index, platform in enumerate(config.platforms):
|
||||
platform_id = _get_platform_id(platform)
|
||||
platform_label = _get_platform_label(platform_id)
|
||||
_refresh_loading_feedback(
|
||||
current_system=_build_global_search_loading_title(),
|
||||
progress=((platform_index / total_platforms) * 100.0),
|
||||
detail_lines=[
|
||||
_("loading_platform_counter").format(platform_index + 1, total_platforms) if _ else f"Platform {platform_index + 1}/{total_platforms}",
|
||||
_("loading_platform_name").format(platform_label) if _ else f"Platform: {platform_label}",
|
||||
_("loading_read_games_resolve_sources") if _ else "Reading games and resolving sources...",
|
||||
],
|
||||
force=True,
|
||||
)
|
||||
for game in load_games(platform_id):
|
||||
display_name = game.display_name or Path(game.name).stem
|
||||
indexed_games.append({
|
||||
@@ -482,21 +627,135 @@ def build_global_search_index() -> list[dict]:
|
||||
"search_name": display_name.lower(),
|
||||
"url": game.url,
|
||||
"size": game.size,
|
||||
"size_bytes": parse_game_size_to_bytes(game.size),
|
||||
"game_obj": game,
|
||||
})
|
||||
|
||||
indexed_games.sort(key=lambda item: (item["platform_label"].lower(), item["display_name"].lower()))
|
||||
return indexed_games
|
||||
_refresh_loading_feedback(
|
||||
current_system=_build_global_search_loading_title(),
|
||||
progress=100.0,
|
||||
detail_lines=[
|
||||
_("loading_platform_counter").format(total_platforms, total_platforms) if _ else f"Platform {total_platforms}/{total_platforms}",
|
||||
],
|
||||
force=True,
|
||||
)
|
||||
|
||||
return _sort_global_items(indexed_games)
|
||||
|
||||
|
||||
def _load_embedded_global_search_index() -> list[dict] | None:
|
||||
cache_path = getattr(config, 'GLOBAL_SEARCH_INDEX_CACHE_PATH', '')
|
||||
if not cache_path or not os.path.exists(cache_path):
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(cache_path, 'r', encoding='utf-8') as handle:
|
||||
payload = json.load(handle)
|
||||
except Exception as exc:
|
||||
logger.warning(f"Impossible de charger l'index global embarque: {exc}")
|
||||
return None
|
||||
|
||||
raw_entries = payload.get('entries') if isinstance(payload, dict) else None
|
||||
if not isinstance(raw_entries, list):
|
||||
return None
|
||||
|
||||
platform_order: dict[str, int] = {}
|
||||
for index, platform in enumerate(config.platforms):
|
||||
platform_order[_get_platform_id(platform)] = index
|
||||
|
||||
indexed_games = []
|
||||
for raw_entry in raw_entries:
|
||||
if not isinstance(raw_entry, dict):
|
||||
continue
|
||||
platform_id = str(raw_entry.get('platform_id') or '').strip()
|
||||
if not platform_id or platform_id not in platform_order:
|
||||
continue
|
||||
|
||||
game_name = str(raw_entry.get('game_name') or '').strip()
|
||||
if not game_name:
|
||||
continue
|
||||
|
||||
display_name = str(raw_entry.get('display_name') or '').strip() or Path(game_name).stem
|
||||
url = str(raw_entry.get('url') or '').strip() or None
|
||||
size = str(raw_entry.get('size') or '').strip() or None
|
||||
try:
|
||||
size_bytes = int(raw_entry.get('size_bytes') or 0)
|
||||
except (TypeError, ValueError):
|
||||
size_bytes = 0
|
||||
|
||||
game_obj = Game(name=game_name, url=url, size=size, display_name=display_name)
|
||||
indexed_games.append({
|
||||
'platform_id': platform_id,
|
||||
'platform_label': _get_platform_label(platform_id),
|
||||
'platform_index': platform_order[platform_id],
|
||||
'game_name': game_name,
|
||||
'display_name': display_name,
|
||||
'search_name': display_name.lower(),
|
||||
'url': url,
|
||||
'size': size,
|
||||
'size_bytes': size_bytes,
|
||||
'game_obj': game_obj,
|
||||
})
|
||||
|
||||
if indexed_games:
|
||||
logger.info(f"Index global charge depuis le cache embarque: {len(indexed_games)} jeux")
|
||||
return _sort_global_items(indexed_games)
|
||||
return None
|
||||
|
||||
|
||||
def _ensure_global_search_index(operation_title: str | None = None) -> None:
|
||||
index_signature = tuple(config.platforms)
|
||||
if getattr(config, 'global_search_index', None) and getattr(config, 'global_search_index_signature', None) == index_signature:
|
||||
return
|
||||
|
||||
embedded_index = _load_embedded_global_search_index()
|
||||
if embedded_index is not None:
|
||||
config.global_search_index = embedded_index
|
||||
config.global_search_index_signature = index_signature
|
||||
return
|
||||
|
||||
previous_menu_state = getattr(config, 'menu_state', 'platform')
|
||||
previous_loading_system = getattr(config, 'current_loading_system', '')
|
||||
previous_loading_progress = getattr(config, 'loading_progress', 0.0)
|
||||
previous_loading_detail_lines = list(getattr(config, 'loading_detail_lines', []) or [])
|
||||
|
||||
config.menu_state = "loading"
|
||||
config.current_loading_system = operation_title or _build_global_search_loading_title()
|
||||
config.loading_progress = 0.0
|
||||
config.loading_detail_lines = [config.current_loading_system]
|
||||
config.needs_redraw = True
|
||||
_refresh_loading_feedback(force=True)
|
||||
|
||||
try:
|
||||
config.global_search_index = build_global_search_index()
|
||||
config.global_search_index_signature = index_signature
|
||||
finally:
|
||||
config.menu_state = previous_menu_state
|
||||
config.current_loading_system = previous_loading_system
|
||||
config.loading_progress = previous_loading_progress
|
||||
config.loading_detail_lines = previous_loading_detail_lines
|
||||
config.needs_redraw = True
|
||||
|
||||
|
||||
def refresh_global_search_results(reset_selection: bool = True) -> None:
|
||||
query = (config.global_search_query or "").strip().lower()
|
||||
if not query:
|
||||
config.global_search_results = []
|
||||
else:
|
||||
config.global_search_results = [
|
||||
item for item in config.global_search_index
|
||||
items = list(getattr(config, 'global_search_index', []) or [])
|
||||
|
||||
filter_obj = getattr(config, 'game_filter_obj', None)
|
||||
if filter_obj and filter_obj.is_active():
|
||||
item_by_game = {id(item.get('game_obj')): item for item in items}
|
||||
filtered_games = filter_obj.apply_filters([item.get('game_obj') for item in items if item.get('game_obj') is not None])
|
||||
items = [item_by_game[id(game)] for game in filtered_games if id(game) in item_by_game]
|
||||
|
||||
if query:
|
||||
items = [
|
||||
item for item in items
|
||||
if query in item.get("search_name", item["display_name"].lower())
|
||||
]
|
||||
elif not getattr(config, 'global_search_allow_empty', False):
|
||||
items = []
|
||||
|
||||
config.global_search_results = _sort_global_items(items)
|
||||
|
||||
if reset_selection:
|
||||
config.global_search_selected = 0
|
||||
@@ -508,30 +767,58 @@ def refresh_global_search_results(reset_selection: bool = True) -> None:
|
||||
|
||||
|
||||
def enter_global_search() -> None:
|
||||
index_signature = tuple(config.platforms)
|
||||
if not getattr(config, 'global_search_index', None) or getattr(config, 'global_search_index_signature', None) != index_signature:
|
||||
config.global_search_index = build_global_search_index()
|
||||
config.global_search_index_signature = index_signature
|
||||
_ensure_global_search_index(_build_global_search_loading_title())
|
||||
config.global_search_query = ""
|
||||
config.global_search_results = []
|
||||
config.global_search_selected = 0
|
||||
config.global_search_scroll_offset = 0
|
||||
config.global_search_editing = bool(getattr(config, 'joystick', False))
|
||||
config.global_search_allow_empty = False
|
||||
config.global_search_title_override = _("global_search_title").format("").replace(" : ", "").rstrip(': ') if _ else 'Recherche globale'
|
||||
config.selected_key = (0, 0)
|
||||
config.previous_menu_state = "platform"
|
||||
config.menu_state = "platform_search"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Entree en recherche globale inter-plateformes")
|
||||
|
||||
|
||||
def enter_global_filtered_results() -> None:
|
||||
_ensure_global_search_index(_("filter_advanced") if _ else "Loading...")
|
||||
config.global_search_query = ""
|
||||
config.global_search_selected = 0
|
||||
config.global_search_scroll_offset = 0
|
||||
config.global_search_editing = False
|
||||
config.global_search_allow_empty = True
|
||||
config.global_search_title_override = _("filter_advanced") if _ else 'Filtrer'
|
||||
refresh_global_search_results(reset_selection=True)
|
||||
config.menu_state = "platform_search"
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Affichage des resultats globaux filtres: {len(config.global_search_results)}")
|
||||
|
||||
|
||||
def enter_global_sorted_results() -> None:
|
||||
_ensure_global_search_index(_("web_sort") if _ else "Loading...")
|
||||
config.global_search_query = ""
|
||||
config.global_search_selected = 0
|
||||
config.global_search_scroll_offset = 0
|
||||
config.global_search_editing = False
|
||||
config.global_search_allow_empty = True
|
||||
config.global_search_title_override = _("web_sort") if _ else 'Trier'
|
||||
refresh_global_search_results(reset_selection=True)
|
||||
config.menu_state = "platform_search"
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Affichage des resultats globaux tries ({config.global_sort_option}): {len(config.global_search_results)}")
|
||||
|
||||
|
||||
def exit_global_search() -> None:
|
||||
config.global_search_query = ""
|
||||
config.global_search_results = []
|
||||
config.global_search_selected = 0
|
||||
config.global_search_scroll_offset = 0
|
||||
config.global_search_editing = False
|
||||
config.global_search_allow_empty = False
|
||||
config.global_search_title_override = ""
|
||||
config.selected_key = (0, 0)
|
||||
config.menu_state = "platform"
|
||||
config.menu_state = validate_menu_state(getattr(config, 'global_search_return_state', None) or getattr(config, 'previous_menu_state', None))
|
||||
config.needs_redraw = True
|
||||
|
||||
|
||||
@@ -832,7 +1119,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
logger.debug("Ouverture history depuis platform")
|
||||
elif is_input_matched(event, "filter"):
|
||||
enter_global_search()
|
||||
open_unified_filter_menu("platform")
|
||||
elif is_input_matched(event, "confirm"):
|
||||
# Démarrer le chronomètre pour l'appui long - ne pas exécuter immédiatement
|
||||
# L'action sera exécutée au relâchement si appui court, ou config dossier si appui long
|
||||
@@ -1047,7 +1334,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.selected_key = (0, 0)
|
||||
# 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.filtered_games = _apply_sorted_active_filters()
|
||||
config.filter_active = True
|
||||
else:
|
||||
config.filtered_games = config.games
|
||||
@@ -1075,7 +1362,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.search_query = ""
|
||||
# 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.filtered_games = _apply_sorted_active_filters()
|
||||
config.filter_active = True
|
||||
else:
|
||||
config.filtered_games = config.games
|
||||
@@ -1152,12 +1439,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
event.value)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "filter"):
|
||||
# Afficher le menu de choix entre recherche et filtrage avancé
|
||||
config.menu_state = "filter_menu_choice"
|
||||
config.selected_filter_choice = 0
|
||||
config.previous_menu_state = "game"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Ouverture du menu de filtrage")
|
||||
open_unified_filter_menu("game")
|
||||
elif is_input_matched(event, "history"):
|
||||
config.history_origin = "game"
|
||||
config.menu_state = "history"
|
||||
@@ -1499,7 +1781,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
|
||||
# Dialogue fichier de support
|
||||
elif config.menu_state == "support_dialog":
|
||||
if is_input_matched(event, "confirm") or is_input_matched(event, "cancel"):
|
||||
if is_input_matched(event, "confirm") or is_input_matched(event, "cancel") or is_input_matched(event, "start"):
|
||||
# Retour au menu pause
|
||||
config.menu_state = "pause_menu"
|
||||
config.needs_redraw = True
|
||||
@@ -1526,11 +1808,29 @@ def handle_controls(event, sources, joystick, screen):
|
||||
base_path = os.path.join(config.ROMS_FOLDER, dest_folder)
|
||||
file_exists, actual_filename, actual_path = find_file_with_or_without_extension(base_path, game_name)
|
||||
actual_matches = find_matching_files(base_path, game_name)
|
||||
local_path = entry.get("local_path")
|
||||
local_filename = entry.get("local_filename")
|
||||
if not file_exists and local_path and os.path.isfile(local_path):
|
||||
actual_filename = os.path.basename(local_path)
|
||||
actual_path = local_path
|
||||
file_exists = True
|
||||
actual_matches = [(actual_filename, actual_path)]
|
||||
logger.debug("[HISTORY_OPTIONS] direct local_path match used: %s", actual_path)
|
||||
elif not file_exists and local_filename:
|
||||
local_filename_path = os.path.join(base_path, str(local_filename))
|
||||
if os.path.isfile(local_filename_path):
|
||||
actual_filename = os.path.basename(local_filename_path)
|
||||
actual_path = local_filename_path
|
||||
file_exists = True
|
||||
actual_matches = [(actual_filename, actual_path)]
|
||||
logger.debug("[HISTORY_OPTIONS] direct local_filename match used: %s", actual_path)
|
||||
if not actual_matches:
|
||||
actual_matches = get_existing_history_matches(entry)
|
||||
if actual_matches:
|
||||
actual_filename, actual_path = actual_matches[0]
|
||||
file_exists = True
|
||||
if file_exists and actual_path:
|
||||
remember_history_local_match(entry, actual_filename, actual_path)
|
||||
config.history_actual_matches = actual_matches
|
||||
|
||||
# Stocker les informations pour les autres handlers
|
||||
@@ -1555,7 +1855,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
# 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()
|
||||
if ext in ['.zip', '.rar']:
|
||||
if ext in ['.zip', '.rar', '.7z']:
|
||||
options.append("extract_archive")
|
||||
elif ext == '.txt':
|
||||
options.append("open_file")
|
||||
@@ -1570,6 +1870,31 @@ def handle_controls(event, sources, joystick, screen):
|
||||
|
||||
# Option commune: retour
|
||||
options.append("back")
|
||||
|
||||
diagnostics_signature = (
|
||||
entry.get("url", ""),
|
||||
status,
|
||||
file_exists,
|
||||
actual_filename or "",
|
||||
actual_path or "",
|
||||
tuple(options),
|
||||
)
|
||||
if getattr(config, 'history_options_diagnostics_signature', None) != diagnostics_signature:
|
||||
config.history_options_diagnostics_signature = diagnostics_signature
|
||||
logger.debug(
|
||||
"[HISTORY_OPTIONS] platform=%s game=%s status=%s dest_folder=%s base_path=%s file_exists=%s actual_filename=%s actual_path=%s local_path=%s moved_paths=%s options=%s",
|
||||
platform,
|
||||
game_name,
|
||||
status,
|
||||
dest_folder,
|
||||
base_path,
|
||||
file_exists,
|
||||
actual_filename,
|
||||
actual_path,
|
||||
entry.get("local_path"),
|
||||
entry.get("moved_paths"),
|
||||
options,
|
||||
)
|
||||
|
||||
total_options = len(options)
|
||||
sel = getattr(config, 'history_game_option_selection', 0)
|
||||
@@ -1766,6 +2091,9 @@ def handle_controls(event, sources, joystick, screen):
|
||||
is_zip_non_supported = pending_download[3] if len(pending_download) > 3 else False
|
||||
|
||||
if is_1fichier_url(url):
|
||||
ensure_download_provider_keys(False)
|
||||
if missing_all_provider_keys():
|
||||
logger.warning("Aucune clé API - Mode gratuit 1fichier sera utilisé (attente requise)")
|
||||
task = asyncio.create_task(download_from_1fichier(url, platform, game_name, is_zip_non_supported, task_id))
|
||||
else:
|
||||
task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported, task_id))
|
||||
@@ -2032,6 +2360,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
success, msg = extract_zip(file_path, dest_dir, url)
|
||||
elif ext == '.rar':
|
||||
success, msg = extract_rar(file_path, dest_dir, url)
|
||||
elif ext == '.7z':
|
||||
success, msg = extract_7z(file_path, dest_dir, url)
|
||||
else:
|
||||
success, msg = False, "Not an archive"
|
||||
|
||||
@@ -2877,9 +3207,16 @@ def handle_controls(event, sources, joystick, screen):
|
||||
shutil.rmtree(config.GAMES_FOLDER)
|
||||
if os.path.exists(config.IMAGES_FOLDER):
|
||||
shutil.rmtree(config.IMAGES_FOLDER)
|
||||
# Mettre à jour la date
|
||||
from rgsx_settings import set_last_gamelist_update
|
||||
set_last_gamelist_update()
|
||||
clear_torrent_manifest_cache()
|
||||
clear_platform_game_count_cache()
|
||||
request_torrent_manifest_refresh()
|
||||
# Mettre à jour la date et mémoriser la version distante déjà proposée
|
||||
from rgsx_settings import (
|
||||
set_last_gamelist_prompt_remote_update,
|
||||
set_last_gamelist_update,
|
||||
)
|
||||
set_last_gamelist_update(getattr(config, 'gamelist_remote_update_timestamp', None))
|
||||
set_last_gamelist_prompt_remote_update(getattr(config, 'gamelist_remote_update_timestamp', None))
|
||||
config.menu_state = "restart_popup"
|
||||
config.popup_message = _("popup_gamelist_updating") if _ else "Updating game list... Restarting..."
|
||||
config.popup_timer = 2000
|
||||
@@ -2890,18 +3227,25 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.menu_state = "loading"
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
# Pas de cache existant, juste mettre à jour la date et continuer
|
||||
from rgsx_settings import set_last_gamelist_update
|
||||
set_last_gamelist_update()
|
||||
# Pas de cache existant, juste mettre à jour la date et mémoriser la version distante déjà proposée
|
||||
from rgsx_settings import (
|
||||
set_last_gamelist_prompt_remote_update,
|
||||
set_last_gamelist_update,
|
||||
)
|
||||
set_last_gamelist_update(getattr(config, 'gamelist_remote_update_timestamp', None))
|
||||
set_last_gamelist_prompt_remote_update(getattr(config, 'gamelist_remote_update_timestamp', None))
|
||||
config.menu_state = "loading"
|
||||
config.needs_redraw = True
|
||||
else: # Non
|
||||
logger.info("Utilisateur a refusé la mise à jour de la liste des jeux")
|
||||
# Ne pas mettre à jour la date pour redemander plus tard
|
||||
from rgsx_settings import set_last_gamelist_prompt_remote_update
|
||||
set_last_gamelist_prompt_remote_update(getattr(config, 'gamelist_remote_update_timestamp', None))
|
||||
config.menu_state = "platform"
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "cancel"):
|
||||
logger.info("Utilisateur a annulé le prompt de mise à jour")
|
||||
from rgsx_settings import set_last_gamelist_prompt_remote_update
|
||||
set_last_gamelist_prompt_remote_update(getattr(config, 'gamelist_remote_update_timestamp', None))
|
||||
config.menu_state = "platform"
|
||||
config.needs_redraw = True
|
||||
|
||||
@@ -3222,9 +3566,12 @@ def handle_controls(event, sources, joystick, screen):
|
||||
if os.path.exists(config.IMAGES_FOLDER):
|
||||
shutil.rmtree(config.IMAGES_FOLDER)
|
||||
logger.debug("Dossier images supprimé avec succès")
|
||||
clear_torrent_manifest_cache()
|
||||
clear_platform_game_count_cache()
|
||||
request_torrent_manifest_refresh()
|
||||
# Mettre à jour la date de dernière mise à jour
|
||||
from rgsx_settings import set_last_gamelist_update
|
||||
set_last_gamelist_update()
|
||||
set_last_gamelist_update(getattr(config, 'gamelist_remote_update_timestamp', None))
|
||||
config.menu_state = "restart_popup"
|
||||
config.popup_message = _("popup_redownload_success")
|
||||
config.popup_timer = 2000 # bref message
|
||||
@@ -3306,20 +3653,26 @@ def handle_controls(event, sources, joystick, screen):
|
||||
|
||||
# Menu de choix filtrage
|
||||
elif config.menu_state == "filter_menu_choice":
|
||||
entries = getattr(config, 'filter_menu_entries', []) or _build_filter_menu_entries(getattr(config, 'filter_menu_context', 'global'))
|
||||
return_state = validate_menu_state(getattr(config, 'filter_menu_return_state', None))
|
||||
total_entries = max(1, len(entries))
|
||||
if is_input_matched(event, "up"):
|
||||
config.selected_filter_choice = (config.selected_filter_choice - 1) % 2
|
||||
config.selected_filter_choice = (config.selected_filter_choice - 1) % total_entries
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "down"):
|
||||
config.selected_filter_choice = (config.selected_filter_choice + 1) % 2
|
||||
config.selected_filter_choice = (config.selected_filter_choice + 1) % total_entries
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "confirm"):
|
||||
if config.selected_filter_choice == 0:
|
||||
# Recherche par nom (mode existant)
|
||||
selected_entry = entries[config.selected_filter_choice] if entries else {'key': 'back'}
|
||||
selected_key = selected_entry.get('key')
|
||||
if selected_key == 'global_search':
|
||||
config.global_search_return_state = return_state
|
||||
enter_global_search()
|
||||
elif selected_key == 'platform_search':
|
||||
config.search_mode = True
|
||||
config.search_query = ""
|
||||
# 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)
|
||||
config.filtered_games = _apply_sorted_active_filters()
|
||||
else:
|
||||
config.filtered_games = config.games
|
||||
config.current_game = 0
|
||||
@@ -3327,27 +3680,74 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.selected_key = (0, 0)
|
||||
config.menu_state = "game"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Entrée en mode recherche par nom")
|
||||
else:
|
||||
# Filtrage avancé
|
||||
logger.debug("Entrée en mode recherche sur cette plateforme")
|
||||
elif selected_key == 'global_filter':
|
||||
from game_filters import GameFilters
|
||||
from rgsx_settings import load_game_filters
|
||||
|
||||
# Initialiser le filtre
|
||||
|
||||
if not hasattr(config, 'game_filter_obj'):
|
||||
config.game_filter_obj = GameFilters()
|
||||
filter_dict = load_game_filters()
|
||||
if filter_dict:
|
||||
config.game_filter_obj.load_from_dict(filter_dict)
|
||||
|
||||
|
||||
config.filter_target_scope = 'local' if getattr(config, 'filter_menu_context', 'global') == 'game' else 'saved'
|
||||
config.global_search_return_state = return_state
|
||||
config.previous_menu_state = 'filter_menu_choice'
|
||||
config.menu_state = "filter_advanced"
|
||||
config.selected_filter_option = 0
|
||||
config.needs_redraw = True
|
||||
logger.debug("Entrée en filtrage avancé")
|
||||
logger.debug("Entrée en filtrage avancé global")
|
||||
elif selected_key == 'global_sort':
|
||||
config.global_search_return_state = return_state
|
||||
config.global_sort_selected = _get_global_sort_index()
|
||||
config.menu_state = 'global_sort_menu'
|
||||
config.previous_menu_state = 'filter_menu_choice'
|
||||
config.needs_redraw = True
|
||||
logger.debug("Ouverture du menu de tri global")
|
||||
else:
|
||||
config.menu_state = return_state
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "cancel"):
|
||||
config.menu_state = "game"
|
||||
config.menu_state = return_state
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Retour depuis menu filtre vers {config.menu_state}")
|
||||
|
||||
elif config.menu_state == 'global_sort_menu':
|
||||
total_items = len(GLOBAL_SORT_OPTIONS) + 1
|
||||
if is_input_matched(event, 'up'):
|
||||
config.global_sort_selected = (config.global_sort_selected - 1) % total_items
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, 'down'):
|
||||
config.global_sort_selected = (config.global_sort_selected + 1) % total_items
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, 'confirm'):
|
||||
if config.global_sort_selected < len(GLOBAL_SORT_OPTIONS):
|
||||
config.global_sort_option = set_global_sort_option(GLOBAL_SORT_OPTIONS[config.global_sort_selected][0])
|
||||
if getattr(config, 'filter_menu_context', 'global') == 'game':
|
||||
config.games = _sort_local_games(config.games)
|
||||
if config.search_query:
|
||||
config.filtered_games = filter_games_by_search_query()
|
||||
config.filter_active = True
|
||||
elif hasattr(config, 'game_filter_obj') and config.game_filter_obj and config.game_filter_obj.is_active():
|
||||
config.filtered_games = _sort_local_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.menu_state = validate_menu_state(getattr(config, 'filter_menu_return_state', None))
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Tri local applique sur la liste courante ({config.global_sort_option})")
|
||||
else:
|
||||
enter_global_sorted_results()
|
||||
else:
|
||||
config.menu_state = 'filter_menu_choice'
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, 'cancel'):
|
||||
config.menu_state = 'filter_menu_choice'
|
||||
config.needs_redraw = True
|
||||
logger.debug("Retour à la liste des jeux")
|
||||
|
||||
# Filtrage avancé
|
||||
elif config.menu_state == "filter_advanced":
|
||||
@@ -3479,38 +3879,60 @@ def handle_controls(event, sources, joystick, screen):
|
||||
if button_idx == 0:
|
||||
# Apply
|
||||
save_game_filters(config.game_filter_obj.to_dict())
|
||||
|
||||
# Appliquer aux jeux actuels
|
||||
if config.game_filter_obj.is_active():
|
||||
config.filtered_games = config.game_filter_obj.apply_filters(config.games)
|
||||
config.filter_active = True
|
||||
|
||||
if getattr(config, 'filter_target_scope', 'local') == 'global':
|
||||
enter_global_filtered_results()
|
||||
elif getattr(config, 'filter_target_scope', 'local') == 'saved':
|
||||
config.menu_state = validate_menu_state(getattr(config, 'filter_menu_return_state', None))
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
config.filtered_games = config.games
|
||||
config.filter_active = False
|
||||
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.menu_state = "game"
|
||||
config.needs_redraw = True
|
||||
if config.game_filter_obj.is_active():
|
||||
config.filtered_games = _apply_sorted_active_filters()
|
||||
config.filter_active = True
|
||||
else:
|
||||
config.filtered_games = config.games
|
||||
config.filter_active = False
|
||||
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.menu_state = "game"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Filtres appliqués")
|
||||
|
||||
elif button_idx == 1:
|
||||
# Reset
|
||||
config.game_filter_obj.reset()
|
||||
save_game_filters(config.game_filter_obj.to_dict())
|
||||
config.filtered_games = config.games
|
||||
config.filter_active = False
|
||||
config.needs_redraw = True
|
||||
if getattr(config, 'filter_target_scope', 'local') == 'global':
|
||||
config.needs_redraw = True
|
||||
elif getattr(config, 'filter_target_scope', 'local') == 'saved':
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
config.filtered_games = config.games
|
||||
config.filter_active = False
|
||||
config.needs_redraw = True
|
||||
logger.debug("Filtres réinitialisés")
|
||||
|
||||
elif button_idx == 2:
|
||||
# Back
|
||||
config.menu_state = "game"
|
||||
scope = getattr(config, 'filter_target_scope', 'local')
|
||||
if scope == 'global':
|
||||
config.menu_state = "filter_menu_choice"
|
||||
elif scope == 'saved':
|
||||
config.menu_state = validate_menu_state(getattr(config, 'filter_menu_return_state', None))
|
||||
else:
|
||||
config.menu_state = "game"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Retour sans appliquer les filtres")
|
||||
|
||||
elif is_input_matched(event, "cancel"):
|
||||
config.menu_state = "game"
|
||||
scope = getattr(config, 'filter_target_scope', 'local')
|
||||
if scope == 'global':
|
||||
config.menu_state = "filter_menu_choice"
|
||||
elif scope == 'saved':
|
||||
config.menu_state = validate_menu_state(getattr(config, 'filter_menu_return_state', None))
|
||||
else:
|
||||
config.menu_state = "game"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Annulation du filtrage avancé")
|
||||
|
||||
@@ -3813,7 +4235,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
|
||||
# Apply saved filters automatically if any
|
||||
if config.game_filter_obj and config.game_filter_obj.is_active():
|
||||
config.filtered_games = config.game_filter_obj.apply_filters(config.games)
|
||||
config.filtered_games = _apply_sorted_active_filters()
|
||||
config.filter_active = True
|
||||
else:
|
||||
config.filtered_games = config.games
|
||||
|
||||
@@ -12,7 +12,8 @@ from utils import (truncate_text_middle, wrap_text, load_system_image, truncate_
|
||||
check_web_service_status, check_custom_dns_status, load_api_keys,
|
||||
_get_dest_folder_name, find_file_with_or_without_extension, find_matching_files,
|
||||
get_connection_status_targets, get_connection_status_snapshot,
|
||||
get_clean_display_name, get_existing_history_matches)
|
||||
get_clean_display_name, get_existing_history_matches, remember_history_local_match,
|
||||
sort_games_list, get_platform_source_badge_key, get_platform_source_badge_surface)
|
||||
import logging
|
||||
import math
|
||||
from history import load_history, is_game_downloaded
|
||||
@@ -28,6 +29,93 @@ from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
import urllib.request
|
||||
|
||||
|
||||
def _get_windows_monitor_physical_sizes() -> list[tuple[int, int]]:
|
||||
"""Return physical monitor resolutions from Win32, bypassing DPI-scaled SDL values."""
|
||||
if platform.system() != "Windows":
|
||||
return []
|
||||
|
||||
try:
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
CCHDEVICENAME = 32
|
||||
ENUM_CURRENT_SETTINGS = -1
|
||||
|
||||
class MONITORINFOEXW(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("cbSize", wintypes.DWORD),
|
||||
("rcMonitor", wintypes.RECT),
|
||||
("rcWork", wintypes.RECT),
|
||||
("dwFlags", wintypes.DWORD),
|
||||
("szDevice", wintypes.WCHAR * CCHDEVICENAME),
|
||||
]
|
||||
|
||||
class DEVMODEW(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("dmDeviceName", wintypes.WCHAR * CCHDEVICENAME),
|
||||
("dmSpecVersion", wintypes.WORD),
|
||||
("dmDriverVersion", wintypes.WORD),
|
||||
("dmSize", wintypes.WORD),
|
||||
("dmDriverExtra", wintypes.WORD),
|
||||
("dmFields", wintypes.DWORD),
|
||||
("dmPositionX", wintypes.LONG),
|
||||
("dmPositionY", wintypes.LONG),
|
||||
("dmDisplayOrientation", wintypes.DWORD),
|
||||
("dmDisplayFixedOutput", wintypes.DWORD),
|
||||
("dmColor", wintypes.SHORT),
|
||||
("dmDuplex", wintypes.SHORT),
|
||||
("dmYResolution", wintypes.SHORT),
|
||||
("dmTTOption", wintypes.SHORT),
|
||||
("dmCollate", wintypes.SHORT),
|
||||
("dmFormName", wintypes.WCHAR * 32),
|
||||
("dmLogPixels", wintypes.WORD),
|
||||
("dmBitsPerPel", wintypes.DWORD),
|
||||
("dmPelsWidth", wintypes.DWORD),
|
||||
("dmPelsHeight", wintypes.DWORD),
|
||||
("dmDisplayFlags", wintypes.DWORD),
|
||||
("dmDisplayFrequency", wintypes.DWORD),
|
||||
("dmICMMethod", wintypes.DWORD),
|
||||
("dmICMIntent", wintypes.DWORD),
|
||||
("dmMediaType", wintypes.DWORD),
|
||||
("dmDitherType", wintypes.DWORD),
|
||||
("dmReserved1", wintypes.DWORD),
|
||||
("dmReserved2", wintypes.DWORD),
|
||||
("dmPanningWidth", wintypes.DWORD),
|
||||
("dmPanningHeight", wintypes.DWORD),
|
||||
]
|
||||
|
||||
user32 = ctypes.WinDLL("user32", use_last_error=True)
|
||||
monitors: list[tuple[int, int]] = []
|
||||
|
||||
monitor_enum_proc = ctypes.WINFUNCTYPE(
|
||||
wintypes.BOOL,
|
||||
wintypes.HMONITOR,
|
||||
wintypes.HDC,
|
||||
ctypes.POINTER(wintypes.RECT),
|
||||
wintypes.LPARAM,
|
||||
)
|
||||
|
||||
def _callback(hmonitor, hdc, lprect, lparam):
|
||||
monitor_info = MONITORINFOEXW()
|
||||
monitor_info.cbSize = ctypes.sizeof(MONITORINFOEXW)
|
||||
if user32.GetMonitorInfoW(hmonitor, ctypes.byref(monitor_info)):
|
||||
devmode = DEVMODEW()
|
||||
devmode.dmSize = ctypes.sizeof(DEVMODEW)
|
||||
if user32.EnumDisplaySettingsW(monitor_info.szDevice, ENUM_CURRENT_SETTINGS, ctypes.byref(devmode)):
|
||||
width = int(devmode.dmPelsWidth or 0)
|
||||
height = int(devmode.dmPelsHeight or 0)
|
||||
if width > 0 and height > 0:
|
||||
monitors.append((width, height))
|
||||
return True
|
||||
return True
|
||||
|
||||
user32.EnumDisplayMonitors(0, 0, monitor_enum_proc(_callback), 0)
|
||||
return monitors
|
||||
except Exception as e:
|
||||
logger.debug(f"Résolution physique Win32 indisponible: {e}")
|
||||
return []
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
OVERLAY = None # Initialisé dans init_display()
|
||||
@@ -470,7 +558,11 @@ def init_display():
|
||||
|
||||
# Obtenir la résolution du moniteur cible
|
||||
try:
|
||||
if hasattr(pygame.display, 'get_desktop_sizes') and num_displays > 1:
|
||||
win32_sizes = _get_windows_monitor_physical_sizes()
|
||||
if target_monitor < len(win32_sizes):
|
||||
screen_width, screen_height = win32_sizes[target_monitor]
|
||||
logger.debug(f"Résolution moniteur via Win32: {screen_width}x{screen_height} (monitor={target_monitor})")
|
||||
elif 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]
|
||||
@@ -821,7 +913,7 @@ def draw_loading_screen(screen):
|
||||
for detail_line in detail_lines:
|
||||
if not detail_line:
|
||||
continue
|
||||
rendered_lines.extend(wrap_text(str(detail_line), config.small_font, max_detail_width))
|
||||
rendered_lines.append(truncate_text_middle(str(detail_line), config.small_font, max_detail_width, is_filename=False))
|
||||
|
||||
for index, detail_line in enumerate(rendered_lines[:3]):
|
||||
detail_surface = config.small_font.render(detail_line, True, THEME_COLORS["title_text"])
|
||||
@@ -1225,6 +1317,22 @@ def get_display_resolution_line():
|
||||
return ""
|
||||
|
||||
|
||||
def draw_platform_source_badge(screen, platform_name, container_rect):
|
||||
source_key = get_platform_source_badge_key(platform_name)
|
||||
if not source_key:
|
||||
return
|
||||
|
||||
badge_size = max(20, min(int(min(container_rect.width, container_rect.height) * 0.24), 44))
|
||||
badge_surface = get_platform_source_badge_surface(source_key, badge_size)
|
||||
if badge_surface is None:
|
||||
return
|
||||
|
||||
inset = max(5, badge_size // 6)
|
||||
badge_x = container_rect.right - badge_size - inset
|
||||
badge_y = container_rect.top + inset
|
||||
screen.blit(badge_surface, (badge_x, badge_y))
|
||||
|
||||
|
||||
def draw_platform_header_info(screen, light_mode=False, badge_x=None, max_badge_width=None, include_details=True):
|
||||
"""Affiche version, controleur connecte et IP reseau dans un cartouche en haut a droite."""
|
||||
lines = get_platform_header_info_lines(max_badge_width, include_details=include_details)
|
||||
@@ -1305,10 +1413,10 @@ def draw_platform_grid(screen):
|
||||
try:
|
||||
if hasattr(config, 'games_count') and isinstance(config.games_count, dict):
|
||||
game_count = config.games_count.get(platform_name, 0)
|
||||
# Fallback dynamique si pas dans le cache (ex: plateformes modifiées à chaud)
|
||||
# Fallback local sans fetch réseau pour éviter un chargement implicite pendant la navigation.
|
||||
if game_count == 0 and hasattr(config, 'platform_dict_by_name'):
|
||||
from utils import load_games # import local pour éviter import circulaire global
|
||||
game_count = len(load_games(platform_name))
|
||||
from utils import get_platform_game_count # import local pour éviter import circulaire global
|
||||
game_count = get_platform_game_count(platform_name, allow_torrent_manifest_fetch=False)
|
||||
except Exception:
|
||||
game_count = 0
|
||||
title_text = f"{platform_name} ({game_count})" if game_count > 0 else f"{platform_name}"
|
||||
@@ -1704,6 +1812,8 @@ def draw_platform_grid(screen):
|
||||
screen.blit(temp_image, centered_image_rect)
|
||||
else:
|
||||
screen.blit(scaled_image, centered_image_rect)
|
||||
|
||||
draw_platform_source_badge(screen, display_name, border_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
|
||||
@@ -1781,7 +1891,10 @@ def draw_game_list(screen):
|
||||
...
|
||||
|
||||
if config.game_filter_obj and config.game_filter_obj.is_active() and not config.search_query:
|
||||
config.filtered_games = config.game_filter_obj.apply_filters(config.games)
|
||||
config.filtered_games = sort_games_list(
|
||||
config.game_filter_obj.apply_filters(config.games),
|
||||
getattr(config, 'global_sort_option', 'name_asc'),
|
||||
)
|
||||
|
||||
games = config.filtered_games if config.filter_active or config.search_mode else config.games
|
||||
game_count = len(games)
|
||||
@@ -2120,15 +2233,20 @@ def get_display_extension(file_name):
|
||||
|
||||
|
||||
def draw_global_search_list(screen):
|
||||
"""Affiche la recherche globale par nom sur toutes les plateformes."""
|
||||
"""Affiche la vue globale unifiée (recherche, filtre, tri)."""
|
||||
query = getattr(config, 'global_search_query', '') or ''
|
||||
results = getattr(config, 'global_search_results', []) or []
|
||||
keyboard_active = bool(getattr(config, 'joystick', False) and getattr(config, 'global_search_editing', False))
|
||||
allow_empty = bool(getattr(config, 'global_search_allow_empty', False))
|
||||
custom_title = (getattr(config, 'global_search_title_override', '') or '').strip()
|
||||
|
||||
screen.blit(OVERLAY, (0, 0))
|
||||
|
||||
title_query = query + "_" if (getattr(config, 'joystick', False) and getattr(config, 'global_search_editing', False)) or (not getattr(config, 'joystick', False)) else query
|
||||
title_text = _("global_search_title").format(title_query)
|
||||
if custom_title:
|
||||
title_text = custom_title if not title_query else f"{custom_title} : {title_query}"
|
||||
else:
|
||||
title_text = _("global_search_title").format(title_query)
|
||||
if results:
|
||||
title_text += f" ({len(results)})"
|
||||
|
||||
@@ -2167,7 +2285,7 @@ def draw_global_search_list(screen):
|
||||
message_zone_top = title_rect_inflated.bottom + 24
|
||||
message_zone_bottom = max(message_zone_top + 80, reserved_bottom)
|
||||
|
||||
if not query.strip():
|
||||
if not query.strip() and not allow_empty:
|
||||
message = _("global_search_empty_query")
|
||||
lines = wrap_text(message, config.font, config.screen_width - 80)
|
||||
line_height = config.font.get_height() + 5
|
||||
@@ -2497,14 +2615,16 @@ def draw_history_list(screen):
|
||||
folder_text = _get_dest_folder_name(platform)
|
||||
|
||||
# Correction du calcul de la taille
|
||||
size = entry.get("total_size", 0)
|
||||
color = THEME_COLORS["fond_lignes"] if i == current_history_item_inverted else THEME_COLORS["text"]
|
||||
size_text = format_size(size)
|
||||
|
||||
status = entry.get("status", "Inconnu")
|
||||
progress = entry.get("progress", 0)
|
||||
progress = max(0, min(100, progress)) # Clamp progress between 0 and 100
|
||||
|
||||
size = entry.get("total_size", 0)
|
||||
if (not size or int(size or 0) <= 0) and status in ["Téléchargement", "Downloading"]:
|
||||
size = entry.get("downloaded_size", 0)
|
||||
color = THEME_COLORS["fond_lignes"] if i == current_history_item_inverted else THEME_COLORS["text"]
|
||||
size_text = format_size(size)
|
||||
|
||||
# Precompute provider prefix once
|
||||
provider_prefix = entry.get("provider_prefix") or (entry.get("provider") + ":" if entry.get("provider") else "")
|
||||
|
||||
@@ -2512,10 +2632,14 @@ def draw_history_list(screen):
|
||||
if status in ["Téléchargement", "Downloading"]:
|
||||
# Vérifier si un message personnalisé existe (ex: mode gratuit avec attente)
|
||||
custom_message = entry.get('message', '')
|
||||
total_size_value = int(entry.get("total_size", 0) or 0)
|
||||
downloaded_size_value = int(entry.get("downloaded_size", 0) or 0)
|
||||
# Détecter les messages du mode gratuit (commencent par '[' dans toutes les langues)
|
||||
if custom_message and custom_message.strip().startswith('['):
|
||||
# Utiliser le message personnalisé pour le mode gratuit
|
||||
status_text = custom_message
|
||||
elif total_size_value <= 0 and downloaded_size_value > 0:
|
||||
status_text = str(status)
|
||||
else:
|
||||
# Comportement normal: afficher le pourcentage
|
||||
status_text = _("history_status_downloading").format(progress)
|
||||
@@ -2949,6 +3073,9 @@ def draw_controls(screen, menu_state, current_music_name=None, music_popup_start
|
||||
"pause_connection_status": [
|
||||
("cancel", _("controls_cancel_back")),
|
||||
],
|
||||
"support_dialog": [
|
||||
("start", _("controls_cancel_back")),
|
||||
],
|
||||
}
|
||||
|
||||
# Cas spécial : pause_settings_menu avec option roms_folder sélectionnée
|
||||
@@ -3807,7 +3934,7 @@ def draw_pause_games_menu(screen, selected_index):
|
||||
hide_premium_txt = f"{hide_premium_label}: < {status_hide_premium} >"
|
||||
|
||||
# Filter platforms
|
||||
filter_txt = _("submenu_display_filter_platforms") if _ else "Filter Platforms"
|
||||
filter_txt = _("submenu_display_filter_platforms") if _ else "Show/Hide Platforms"
|
||||
|
||||
back_txt = _("menu_back") if _ else "Back"
|
||||
options = [update_txt, scan_txt, history_txt, source_txt, unsupported_txt, hide_premium_txt, filter_txt, back_txt]
|
||||
@@ -4582,12 +4709,15 @@ def draw_gamelist_update_prompt(screen):
|
||||
|
||||
screen.blit(OVERLAY, (0, 0))
|
||||
|
||||
from config import GAMELIST_UPDATE_DAYS
|
||||
from rgsx_settings import get_last_gamelist_update
|
||||
from rgsx_settings import get_last_gamelist_update, format_gamelist_update_display
|
||||
|
||||
last_update = get_last_gamelist_update()
|
||||
if last_update:
|
||||
message = _("gamelist_update_prompt_with_date").format(GAMELIST_UPDATE_DAYS, last_update) if _ else f"Game list hasn't been updated for more than {GAMELIST_UPDATE_DAYS} days (last update: {last_update}). Download the latest version?"
|
||||
remote_update = getattr(config, 'gamelist_remote_update_display', '') or ''
|
||||
local_update = getattr(config, 'gamelist_local_update_display', '') or format_gamelist_update_display(last_update)
|
||||
if last_update and remote_update:
|
||||
message = _("gamelist_update_prompt_remote_newer").format(local_update, remote_update) if _ else f"A newer online game list is available (local: {local_update}, online: {remote_update}). Download the latest version?"
|
||||
elif last_update:
|
||||
message = _("gamelist_update_prompt_with_date").format(local_update) if _ else f"Local game list last update: {local_update}. Download the latest version?"
|
||||
else:
|
||||
message = _("gamelist_update_prompt_first_time") if _ else "Would you like to download the latest game list?"
|
||||
|
||||
@@ -4885,22 +5015,17 @@ def draw_support_dialog(screen):
|
||||
|
||||
screen.blit(OVERLAY, (0, 0))
|
||||
|
||||
# Récupérer le nom du bouton "cancel/back" depuis la configuration des contrôles
|
||||
cancel_key = "SELECT"
|
||||
try:
|
||||
from controls_mapper import get_mapped_button
|
||||
cancel_key = get_mapped_button("cancel") or "SELECT"
|
||||
except Exception:
|
||||
pass
|
||||
# Cet écran se ferme via l'action Start dans la navigation actuelle.
|
||||
return_key = get_control_display("start", "Start")
|
||||
|
||||
# Déterminer le message à afficher (succès ou erreur)
|
||||
if hasattr(config, 'support_zip_error') and config.support_zip_error:
|
||||
title = _("support_dialog_title")
|
||||
message = _("support_dialog_error").format(config.support_zip_error, cancel_key)
|
||||
message = _("support_dialog_error").format(config.support_zip_error, return_key)
|
||||
else:
|
||||
title = _("support_dialog_title")
|
||||
zip_path = getattr(config, 'support_zip_path', 'rgsx_support.zip')
|
||||
message = _("support_dialog_message").format(zip_path, cancel_key)
|
||||
message = _("support_dialog_message").format(zip_path, return_key)
|
||||
|
||||
# Diviser le message par les retours à la ligne puis wrapper chaque segment
|
||||
raw_segments = message.split('\n') if message else []
|
||||
@@ -5085,11 +5210,29 @@ def draw_history_game_options(screen):
|
||||
base_path = os.path.join(config.ROMS_FOLDER, dest_folder)
|
||||
file_exists, actual_filename, actual_path = find_file_with_or_without_extension(base_path, game_name)
|
||||
actual_matches = find_matching_files(base_path, game_name)
|
||||
local_path = entry.get("local_path")
|
||||
local_filename = entry.get("local_filename")
|
||||
if not file_exists and local_path and os.path.isfile(local_path):
|
||||
actual_filename = os.path.basename(local_path)
|
||||
actual_path = local_path
|
||||
file_exists = True
|
||||
actual_matches = [(actual_filename, actual_path)]
|
||||
logger.debug("[HISTORY_OPTIONS_RENDER] direct local_path match used: %s", actual_path)
|
||||
elif not file_exists and local_filename:
|
||||
local_filename_path = os.path.join(base_path, str(local_filename))
|
||||
if os.path.isfile(local_filename_path):
|
||||
actual_filename = os.path.basename(local_filename_path)
|
||||
actual_path = local_filename_path
|
||||
file_exists = True
|
||||
actual_matches = [(actual_filename, actual_path)]
|
||||
logger.debug("[HISTORY_OPTIONS_RENDER] direct local_filename match used: %s", actual_path)
|
||||
if not actual_matches:
|
||||
actual_matches = get_existing_history_matches(entry)
|
||||
if actual_matches:
|
||||
actual_filename, actual_path = actual_matches[0]
|
||||
file_exists = True
|
||||
if file_exists and actual_path:
|
||||
remember_history_local_match(entry, actual_filename, actual_path)
|
||||
|
||||
# Déterminer les options disponibles selon le statut
|
||||
options = []
|
||||
@@ -5119,7 +5262,7 @@ def draw_history_game_options(screen):
|
||||
# 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()
|
||||
if ext in ['.zip', '.rar']:
|
||||
if ext in ['.zip', '.rar', '.7z']:
|
||||
options.append("extract_archive")
|
||||
option_labels.append(_("history_option_extract_archive"))
|
||||
elif ext == '.txt':
|
||||
@@ -5139,6 +5282,31 @@ def draw_history_game_options(screen):
|
||||
option_labels.append(_("history_option_delete_game"))
|
||||
options.append("back")
|
||||
option_labels.append(_("history_option_back"))
|
||||
|
||||
diagnostics_signature = (
|
||||
entry.get("url", ""),
|
||||
status,
|
||||
file_exists,
|
||||
actual_filename or "",
|
||||
actual_path or "",
|
||||
tuple(options),
|
||||
)
|
||||
if getattr(config, 'history_options_render_signature', None) != diagnostics_signature:
|
||||
config.history_options_render_signature = diagnostics_signature
|
||||
logger.debug(
|
||||
"[HISTORY_OPTIONS_RENDER] platform=%s game=%s status=%s dest_folder=%s base_path=%s file_exists=%s actual_filename=%s actual_path=%s local_path=%s moved_paths=%s options=%s",
|
||||
platform,
|
||||
game_name,
|
||||
status,
|
||||
dest_folder,
|
||||
base_path,
|
||||
file_exists,
|
||||
actual_filename,
|
||||
actual_path,
|
||||
entry.get("local_path"),
|
||||
entry.get("moved_paths"),
|
||||
options,
|
||||
)
|
||||
|
||||
# Calculer dimensions
|
||||
title = _("history_game_options_title")
|
||||
@@ -5376,7 +5544,7 @@ def draw_history_confirm_delete(screen):
|
||||
|
||||
|
||||
def draw_history_extract_archive(screen):
|
||||
"""Affiche la confirmation d'extraction d'archive."""
|
||||
"""Affiche la confirmation d'extraction forcée d'archive."""
|
||||
screen.blit(OVERLAY, (0, 0))
|
||||
|
||||
if not config.history or config.current_history_item >= len(config.history):
|
||||
@@ -5385,7 +5553,8 @@ def draw_history_extract_archive(screen):
|
||||
entry = config.history[config.current_history_item]
|
||||
game_name = entry.get("game_name", "Unknown")
|
||||
|
||||
message = f"Extract archive: {game_name}?"
|
||||
prompt = _("history_extract_archive_confirm") if _ else "Force extract archive"
|
||||
message = f"{prompt}: {game_name}?"
|
||||
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
|
||||
@@ -5681,7 +5850,7 @@ def draw_scraper_screen(screen):
|
||||
|
||||
|
||||
def draw_filter_menu_choice(screen):
|
||||
"""Affiche le menu de choix entre recherche par nom et filtrage avancé"""
|
||||
"""Affiche le menu filtre unifie."""
|
||||
screen.blit(OVERLAY, (0, 0))
|
||||
|
||||
# Titre
|
||||
@@ -5691,10 +5860,8 @@ def draw_filter_menu_choice(screen):
|
||||
screen.blit(title_surface, title_rect)
|
||||
|
||||
# Options
|
||||
options = [
|
||||
_("filter_search_by_name"),
|
||||
_("filter_advanced")
|
||||
]
|
||||
entries = getattr(config, 'filter_menu_entries', []) or []
|
||||
options = [entry.get('label', '') for entry in entries]
|
||||
|
||||
# Calculer hauteur dynamique basée sur la taille de police
|
||||
sample_text = config.font.render("Sample", True, THEME_COLORS["text"])
|
||||
@@ -5746,6 +5913,49 @@ def draw_filter_menu_choice(screen):
|
||||
screen.blit(text_surface, text_rect)
|
||||
|
||||
|
||||
def draw_global_sort_menu(screen):
|
||||
screen.blit(OVERLAY, (0, 0))
|
||||
|
||||
title = _("web_sort") if _ else "Trier"
|
||||
title_surface = config.title_font.render(title, True, THEME_COLORS["text"])
|
||||
title_rect = title_surface.get_rect(center=(config.screen_width // 2, 60))
|
||||
screen.blit(title_surface, title_rect)
|
||||
|
||||
options = [
|
||||
_("web_sort_name_asc") if _ else "A-Z (Nom)",
|
||||
_("web_sort_name_desc") if _ else "Z-A (Nom)",
|
||||
_("web_sort_size_asc") if _ else "Taille -+ (Petit d'abord)",
|
||||
_("web_sort_size_desc") if _ else "Taille +- (Grand d'abord)",
|
||||
_("menu_back") if _ else "Retour",
|
||||
]
|
||||
|
||||
sample_text = config.font.render("Sample", True, THEME_COLORS["text"])
|
||||
font_height = sample_text.get_height()
|
||||
button_height = max(60, font_height + 30)
|
||||
max_text_width = 0
|
||||
for option in options:
|
||||
text_surface = config.font.render(option, True, THEME_COLORS["text"])
|
||||
max_text_width = max(max_text_width, text_surface.get_width())
|
||||
button_width = max(460, max_text_width + 80)
|
||||
menu_y = 150
|
||||
button_spacing = 20
|
||||
|
||||
for i, option in enumerate(options):
|
||||
y = menu_y + i * (button_height + button_spacing)
|
||||
x = (config.screen_width - button_width) // 2
|
||||
if i == getattr(config, 'global_sort_selected', 0):
|
||||
color = THEME_COLORS["button_selected"]
|
||||
border_color = THEME_COLORS["border_selected"]
|
||||
else:
|
||||
color = THEME_COLORS["button_idle"]
|
||||
border_color = THEME_COLORS["border"]
|
||||
pygame.draw.rect(screen, color, (x, y, button_width, button_height), border_radius=12)
|
||||
pygame.draw.rect(screen, border_color, (x, y, button_width, button_height), 3, border_radius=12)
|
||||
text_surface = config.font.render(option, True, THEME_COLORS["text"])
|
||||
text_rect = text_surface.get_rect(center=(config.screen_width // 2, y + button_height // 2))
|
||||
screen.blit(text_surface, text_rect)
|
||||
|
||||
|
||||
def draw_filter_advanced(screen):
|
||||
"""Affiche l'écran de filtrage avancé"""
|
||||
|
||||
|
||||
@@ -2,11 +2,41 @@ import json
|
||||
import os
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
import config
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _atomic_write_json(target_path, payload):
|
||||
temp_path = f"{target_path}.{os.getpid()}.{threading.get_ident()}.tmp"
|
||||
try:
|
||||
with open(temp_path, "w", encoding='utf-8') as f:
|
||||
json.dump(payload, f, indent=2, ensure_ascii=False)
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
|
||||
last_error = None
|
||||
for attempt in range(5):
|
||||
try:
|
||||
os.replace(temp_path, target_path)
|
||||
last_error = None
|
||||
break
|
||||
except PermissionError as e:
|
||||
last_error = e
|
||||
time.sleep(0.15 * (attempt + 1))
|
||||
|
||||
if last_error is not None:
|
||||
raise last_error
|
||||
finally:
|
||||
try:
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Chemin par défaut pour history.json
|
||||
|
||||
def init_history():
|
||||
@@ -77,24 +107,9 @@ def save_history(history):
|
||||
history_path = getattr(config, 'HISTORY_PATH')
|
||||
try:
|
||||
os.makedirs(os.path.dirname(history_path), exist_ok=True)
|
||||
|
||||
# Écriture atomique : écrire dans un fichier temporaire puis renommer
|
||||
temp_path = history_path + '.tmp'
|
||||
with open(temp_path, "w", encoding='utf-8') as f:
|
||||
json.dump(history, f, indent=2, ensure_ascii=False)
|
||||
f.flush() # Forcer l'écriture sur disque
|
||||
os.fsync(f.fileno()) # Synchroniser avec le système de fichiers
|
||||
|
||||
# Renommer atomiquement (remplace l'ancien fichier)
|
||||
os.replace(temp_path, history_path)
|
||||
_atomic_write_json(history_path, history)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'écriture de {history_path} : {e}")
|
||||
# Nettoyer le fichier temporaire en cas d'erreur
|
||||
try:
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
def add_to_history(platform, game_name, status, url=None, progress=0, message=None, timestamp=None):
|
||||
"""Ajoute une entrée à l'historique."""
|
||||
@@ -314,23 +329,10 @@ def save_downloaded_games(downloaded_games_dict):
|
||||
try:
|
||||
normalized_downloaded = _normalize_downloaded_games_dict(downloaded_games_dict)
|
||||
os.makedirs(os.path.dirname(downloaded_path), exist_ok=True)
|
||||
|
||||
# Écriture atomique
|
||||
temp_path = downloaded_path + '.tmp'
|
||||
with open(temp_path, "w", encoding='utf-8') as f:
|
||||
json.dump(normalized_downloaded, f, indent=2, ensure_ascii=False)
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
|
||||
os.replace(temp_path, downloaded_path)
|
||||
_atomic_write_json(downloaded_path, normalized_downloaded)
|
||||
logger.debug(f"Jeux téléchargés sauvegardés : {_count_downloaded_games(normalized_downloaded)} jeux")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'écriture de {downloaded_path} : {e}")
|
||||
try:
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def mark_game_as_downloaded(platform_name, game_name, file_size=None):
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
"loading_downloading_games_images": "Spiele und Bilder werden heruntergeladen...",
|
||||
"loading_extracting_data": "Der initiale Datenordner wird entpackt...",
|
||||
"loading_load_systems": "Systeme werden geladen...",
|
||||
"loading_platform_counter": "Plattform {0}/{1}",
|
||||
"loading_platform_name": "Plattform: {0}",
|
||||
"loading_game_entries_progress": "Spiele gescannt: {0}/{1}",
|
||||
"loading_read_games_resolve_sources": "Spiele werden gelesen und Quellen aufgelost...",
|
||||
"loading_torrent_manifest_analysis": "Torrent-Manifest wird analysiert: {0}",
|
||||
"loading_torrent_files_progress": "Torrent-Dateien: {0}/{1}",
|
||||
"error_extract_data_failed": "Herunterladen oder Entpacken des initialen Datenordners fehlgeschlagen.",
|
||||
"error_no_internet": "Keine Internetverbindung. Überprüfe dein Netzwerk.",
|
||||
"error_api_key": "Achtung, du musst deinen API-Schlüssel (nur Premium) in der Datei {0} eingeben",
|
||||
@@ -26,6 +32,7 @@
|
||||
"game_filter": "Aktiver Filter: {0}",
|
||||
"game_search": "Filtern: {0}",
|
||||
"global_search_title": "Globale Suche: {0}",
|
||||
"platform_search_title": "Diese Plattform durchsuchen",
|
||||
"global_search_empty_query": "Geben Sie einen Namen ein, um alle Systeme zu durchsuchen",
|
||||
"global_search_no_results": "Keine Ergebnisse fur: {0}",
|
||||
"game_header_name": "Name",
|
||||
@@ -47,6 +54,9 @@
|
||||
"free_mode_submitting": "[Kostenloser Modus] Formular wird gesendet...",
|
||||
"free_mode_link_found": "[Kostenloser Modus] Link gefunden: {0}...",
|
||||
"free_mode_completed": "[Kostenloser Modus] Abgeschlossen: {0}",
|
||||
"free_mode_guest_slots_unavailable": "1fichier: Der kostenlose Gast-Download ist vorübergehend nicht verfügbar (alle Slots sind belegt). Bitte versuchen Sie es später erneut.",
|
||||
"free_mode_unavailable_in_app": "1fichier: Dieser Download ist derzeit in der Anwendung nicht verfügbar. Bitte versuchen Sie es später erneut.",
|
||||
"free_mode_premium_advice": "Für unbegrenzte Downloads jederzeit und mit voller Geschwindigkeit benötigen Sie ein Premium-Konto oder einen Debrid-Dienst und müssen dessen API-Schlüssel in RGSX eintragen.",
|
||||
"download_status": "{0}: {1}",
|
||||
"download_canceled": "Download vom Benutzer abgebrochen.",
|
||||
"download_removed_from_queue": "Aus der Download-Warteschlange entfernt",
|
||||
@@ -57,7 +67,8 @@
|
||||
"confirm_exit_with_downloads": "Achtung: {0} Download(s) laufen. Trotzdem beenden?",
|
||||
"confirm_clear_history": "Verlauf löschen?",
|
||||
"confirm_redownload_cache": "Spieleliste aktualisieren?",
|
||||
"gamelist_update_prompt_with_date": "Die Spieleliste wurde seit mehr als {0} Tagen nicht aktualisiert (letzte Aktualisierung: {1}). Die neueste Version herunterladen?",
|
||||
"gamelist_update_prompt_with_date": "Letzte lokale Aktualisierung der Spieleliste: {0}. Neueste Version herunterladen?",
|
||||
"gamelist_update_prompt_remote_newer": "Online ist eine neuere Spieleliste verfügbar (lokal: {0}, online: {1}). Neueste Version herunterladen?",
|
||||
"gamelist_update_prompt_first_time": "Möchten Sie die neueste Spieleliste herunterladen?",
|
||||
"popup_redownload_success": "Cache gelöscht, bitte die Anwendung neu starten",
|
||||
"popup_no_cache": "Kein Cache gefunden.\nBitte starte die Anwendung neu, um die Spiele zu laden.",
|
||||
@@ -298,7 +309,7 @@
|
||||
"footer_joystick": "Joystick: {0}",
|
||||
"history_game_options_title": "Spiel Optionen",
|
||||
"history_option_download_folder": "Datei lokalisieren",
|
||||
"history_option_extract_archive": "Archiv extrahieren",
|
||||
"history_option_extract_archive": "Archivsextraktion erzwingen",
|
||||
"history_option_open_file": "Datei öffnen",
|
||||
"history_option_scraper": "Metadaten abrufen",
|
||||
"history_option_remove_from_queue": "Aus Warteschlange entfernen",
|
||||
@@ -314,6 +325,7 @@
|
||||
"history_scraper_not_implemented": "Scraper noch nicht implementiert",
|
||||
"history_confirm_delete": "Dieses Spiel von der Festplatte löschen?",
|
||||
"history_file_not_found": "Datei nicht gefunden",
|
||||
"history_extract_archive_confirm": "Archivsextraktion erzwingen",
|
||||
"history_extracting": "Extrahieren...",
|
||||
"history_extracted": "Extrahiert",
|
||||
"history_delete_success": "Spiel erfolgreich gelöscht",
|
||||
@@ -427,8 +439,8 @@
|
||||
"web_sort": "Sortieren nach",
|
||||
"web_sort_name_asc": "A-Z (Name)",
|
||||
"web_sort_name_desc": "Z-A (Name)",
|
||||
"web_sort_size_asc": "Größe +- (Klein zuerst)",
|
||||
"web_sort_size_desc": "Größe -+ (Groß zuerst)",
|
||||
"web_sort_size_asc": "Größe -+ (Klein zuerst)",
|
||||
"web_sort_size_desc": "Größe +- (Groß zuerst)",
|
||||
"download_already_present": " (bereits vorhanden)",
|
||||
"network_download_ok": "Download erfolgreich: {0}",
|
||||
"web_filter_region": "Region",
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
"loading_downloading_games_images": "Downloading games and images...",
|
||||
"loading_extracting_data": "Extracting initial Data folder...",
|
||||
"loading_load_systems": "Loading systems...",
|
||||
"loading_platform_counter": "Platform {0}/{1}",
|
||||
"loading_platform_name": "Platform: {0}",
|
||||
"loading_game_entries_progress": "Games scanned: {0}/{1}",
|
||||
"loading_read_games_resolve_sources": "Reading games and resolving sources...",
|
||||
"loading_torrent_manifest_analysis": "Analyzing torrent manifest: {0}",
|
||||
"loading_torrent_files_progress": "Torrent files: {0}/{1}",
|
||||
"error_extract_data_failed": "Failed to download or extract the initial Data folder.",
|
||||
"error_no_internet": "No Internet connection. Check your network.",
|
||||
"error_api_key": "Please enter your API key (premium only) in the file {0}",
|
||||
@@ -26,6 +32,7 @@
|
||||
"game_filter": "Active filter: {0}",
|
||||
"game_search": "Filter: {0}",
|
||||
"global_search_title": "Global search: {0}",
|
||||
"platform_search_title": "Search this platform",
|
||||
"global_search_empty_query": "Type a game name to search across all systems",
|
||||
"global_search_no_results": "No results for: {0}",
|
||||
"game_header_name": "Name",
|
||||
@@ -47,6 +54,9 @@
|
||||
"free_mode_submitting": "[Free mode] Submitting form...",
|
||||
"free_mode_link_found": "[Free mode] Link found: {0}...",
|
||||
"free_mode_completed": "[Free mode] Completed: {0}",
|
||||
"free_mode_guest_slots_unavailable": "1fichier: free guest download is temporarily unavailable (all slots are currently in use). Please try again later.",
|
||||
"free_mode_unavailable_in_app": "1fichier: this download is not available in the application right now. Please try again later.",
|
||||
"free_mode_premium_advice": "For unlimited, on-demand, full-speed downloads, you need a premium account or debrid service and must enter its API key in RGSX.",
|
||||
"download_status": "{0}: {1}",
|
||||
"download_canceled": "Download canceled by user.",
|
||||
"download_removed_from_queue": "Removed from download queue",
|
||||
@@ -57,7 +67,8 @@
|
||||
"confirm_exit_with_downloads": "Attention: {0} download(s) in progress. Quit anyway?",
|
||||
"confirm_clear_history": "Clear history?",
|
||||
"confirm_redownload_cache": "Update games list?",
|
||||
"gamelist_update_prompt_with_date": "Game list hasn't been updated for more than {0} days (last update: {1}). Download the latest version?",
|
||||
"gamelist_update_prompt_with_date": "Local game list last update: {0}. Download the latest version?",
|
||||
"gamelist_update_prompt_remote_newer": "A newer game list is available online (local: {0}, online: {1}). Download the latest version?",
|
||||
"gamelist_update_prompt_first_time": "Would you like to download the latest game list?",
|
||||
"popup_redownload_success": "Cache cleared, please restart the application",
|
||||
"popup_no_cache": "No cache found.\nPlease restart the application to load games.",
|
||||
@@ -190,7 +201,7 @@
|
||||
"submenu_display_font_size": "Font Size",
|
||||
"submenu_display_show_unsupported": "Show unsupported systems: {status}",
|
||||
"submenu_display_allow_unknown_ext": "Hide unknown ext warn: {status}",
|
||||
"submenu_display_filter_platforms": "Filter systems",
|
||||
"submenu_display_filter_platforms": "Show/Hide Platforms",
|
||||
"status_on": "On",
|
||||
"status_off": "Off",
|
||||
"status_present": "Present",
|
||||
@@ -297,7 +308,7 @@
|
||||
"footer_joystick": "Joystick: {0}",
|
||||
"history_game_options_title": "Game Options",
|
||||
"history_option_download_folder": "Locate file",
|
||||
"history_option_extract_archive": "Extract archive",
|
||||
"history_option_extract_archive": "Force extract archive",
|
||||
"history_option_open_file": "Open file",
|
||||
"history_option_scraper": "Scrape metadata",
|
||||
"history_option_remove_from_queue": "Remove from queue",
|
||||
@@ -316,6 +327,7 @@
|
||||
"history_scraper_not_implemented": "Scraper not yet implemented",
|
||||
"history_confirm_delete": "Delete this game from disk?",
|
||||
"history_file_not_found": "File not found",
|
||||
"history_extract_archive_confirm": "Force extract archive",
|
||||
"history_extracting": "Extracting...",
|
||||
"history_extracted": "Extracted",
|
||||
"history_delete_success": "Game deleted successfully",
|
||||
@@ -429,8 +441,8 @@
|
||||
"web_sort": "Sort by",
|
||||
"web_sort_name_asc": "A-Z (Name)",
|
||||
"web_sort_name_desc": "Z-A (Name)",
|
||||
"web_sort_size_asc": "Size +- (Small first)",
|
||||
"web_sort_size_desc": "Size -+ (Large first)",
|
||||
"web_sort_size_asc": "Size -+ (Small first)",
|
||||
"web_sort_size_desc": "Size +- (Large first)",
|
||||
"web_filter_region": "Region",
|
||||
"web_filter_hide_non_release": "Hide Demos/Betas/Protos",
|
||||
"web_filter_regex_mode": "Enable Regex Search",
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
"loading_downloading_games_images": "Descargando juegos e imágenes...",
|
||||
"loading_extracting_data": "Extrayendo la carpeta de datos inicial...",
|
||||
"loading_load_systems": "Cargando sistemas...",
|
||||
"loading_platform_counter": "Plataforma {0}/{1}",
|
||||
"loading_platform_name": "Plataforma: {0}",
|
||||
"loading_game_entries_progress": "Juegos analizados: {0}/{1}",
|
||||
"loading_read_games_resolve_sources": "Leyendo juegos y resolviendo fuentes...",
|
||||
"loading_torrent_manifest_analysis": "Analizando el manifiesto torrent: {0}",
|
||||
"loading_torrent_files_progress": "Archivos del torrent: {0}/{1}",
|
||||
"error_extract_data_failed": "Error al descargar o extraer la carpeta de datos inicial.",
|
||||
"error_no_internet": "Sin conexión a Internet. Verifica tu red.",
|
||||
"error_api_key": "Atención, debes ingresar tu clave API (solo premium) en el archivo {0}",
|
||||
@@ -26,6 +32,7 @@
|
||||
"game_filter": "Filtro activo: {0}",
|
||||
"game_search": "Filtrar: {0}",
|
||||
"global_search_title": "Busqueda global: {0}",
|
||||
"platform_search_title": "Buscar en esta plataforma",
|
||||
"global_search_empty_query": "Escribe un nombre para buscar en todas las consolas",
|
||||
"global_search_no_results": "Sin resultados para: {0}",
|
||||
"game_header_name": "Nombre",
|
||||
@@ -47,6 +54,9 @@
|
||||
"free_mode_submitting": "[Modo gratuito] Enviando formulario...",
|
||||
"free_mode_link_found": "[Modo gratuito] Enlace encontrado: {0}...",
|
||||
"free_mode_completed": "[Modo gratuito] Completado: {0}",
|
||||
"free_mode_guest_slots_unavailable": "1fichier: la descarga gratuita como invitado no está disponible temporalmente (todos los cupos están ocupados). Inténtelo de nuevo más tarde.",
|
||||
"free_mode_unavailable_in_app": "1fichier: esta descarga no está disponible en la aplicación en este momento. Inténtelo de nuevo más tarde.",
|
||||
"free_mode_premium_advice": "Para descargar de forma ilimitada, cuando quiera y a máxima velocidad, necesita una cuenta premium o un desbridizador y debe introducir su clave API en RGSX.",
|
||||
"download_status": "{0}: {1}",
|
||||
"download_canceled": "Descarga cancelada por el usuario.",
|
||||
"download_removed_from_queue": "Eliminado de la cola de descarga",
|
||||
@@ -56,7 +66,8 @@
|
||||
"confirm_exit": "¿Salir de la aplicación?",
|
||||
"confirm_exit_with_downloads": "Atención: {0} descarga(s) en curso. ¿Salir de todas formas?",
|
||||
"confirm_clear_history": "¿Vaciar el historial?",
|
||||
"confirm_redownload_cache": "¿Actualizar la lista de juegos?", "gamelist_update_prompt_with_date": "La lista de juegos no se ha actualizado durante más de {0} días (última actualización: {1}). ¿Descargar la última versión?",
|
||||
"confirm_redownload_cache": "¿Actualizar la lista de juegos?", "gamelist_update_prompt_with_date": "Última actualización local de la lista de juegos: {0}. ¿Descargar la última versión?",
|
||||
"gamelist_update_prompt_remote_newer": "Hay una lista de juegos más reciente disponible en línea (local: {0}, en línea: {1}). ¿Descargar la última versión?",
|
||||
"gamelist_update_prompt_first_time": "¿Desea descargar la última lista de juegos?", "popup_redownload_success": "Caché borrada, por favor reinicia la aplicación",
|
||||
"popup_no_cache": "No se encontró caché.\nPor favor, reinicia la aplicación para cargar los juegos.",
|
||||
"popup_countdown": "Este mensaje se cerrará en {0} segundo{1}",
|
||||
@@ -298,7 +309,7 @@
|
||||
"footer_joystick": "Joystick: {0}",
|
||||
"history_game_options_title": "Opciones del juego",
|
||||
"history_option_download_folder": "Localizar archivo",
|
||||
"history_option_extract_archive": "Extraer archivo",
|
||||
"history_option_extract_archive": "Forzar extraccion del archivo",
|
||||
"history_option_open_file": "Abrir archivo",
|
||||
"history_option_scraper": "Obtener metadatos",
|
||||
"history_option_remove_from_queue": "Quitar de la cola",
|
||||
@@ -314,6 +325,7 @@
|
||||
"history_scraper_not_implemented": "Scraper aún no implementado",
|
||||
"history_confirm_delete": "¿Eliminar este juego del disco?",
|
||||
"history_file_not_found": "Archivo no encontrado",
|
||||
"history_extract_archive_confirm": "Forzar extraccion del archivo",
|
||||
"history_extracting": "Extrayendo...",
|
||||
"history_extracted": "Extraído",
|
||||
"history_delete_success": "Juego eliminado con éxito",
|
||||
@@ -427,8 +439,8 @@
|
||||
"web_sort": "Ordenar por",
|
||||
"web_sort_name_asc": "A-Z (Nombre)",
|
||||
"web_sort_name_desc": "Z-A (Nombre)",
|
||||
"web_sort_size_asc": "Tamaño +- (Menor primero)",
|
||||
"web_sort_size_desc": "Tamaño -+ (Mayor primero)",
|
||||
"web_sort_size_asc": "Tamaño -+ (Menor primero)",
|
||||
"web_sort_size_desc": "Tamaño +- (Mayor primero)",
|
||||
"web_filter_region": "Región",
|
||||
"web_filter_hide_non_release": "Ocultar Demos/Betas/Protos",
|
||||
"web_filter_regex_mode": "Activar búsqueda Regex",
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
"loading_downloading_games_images": "Téléchargement des jeux et images...",
|
||||
"loading_extracting_data": "Extraction du dossier de données initial...",
|
||||
"loading_load_systems": "Chargement des systèmes...",
|
||||
"loading_platform_counter": "Plateforme {0}/{1}",
|
||||
"loading_platform_name": "Plateforme : {0}",
|
||||
"loading_game_entries_progress": "Jeux analyses : {0}/{1}",
|
||||
"loading_read_games_resolve_sources": "Lecture des jeux et résolution des sources...",
|
||||
"loading_torrent_manifest_analysis": "Analyse du manifest torrent : {0}",
|
||||
"loading_torrent_files_progress": "Fichiers du torrent : {0}/{1}",
|
||||
"error_extract_data_failed": "Échec du téléchargement ou de l'extraction du dossier de données initial.",
|
||||
"error_no_internet": "Pas de connexion Internet. Vérifiez votre réseau.",
|
||||
"error_api_key": "Attention il faut renseigner sa clé API (premium only) dans le fichier {0}",
|
||||
@@ -26,6 +32,7 @@
|
||||
"game_filter": "Filtre actif : {0}",
|
||||
"game_search": "Filtrer : {0}",
|
||||
"global_search_title": "Recherche globale : {0}",
|
||||
"platform_search_title": "Recherche sur cette plateforme",
|
||||
"global_search_empty_query": "Saisissez un nom pour rechercher dans toutes les consoles",
|
||||
"global_search_no_results": "Aucun resultat pour : {0}",
|
||||
"game_header_name": "Nom",
|
||||
@@ -47,6 +54,9 @@
|
||||
"free_mode_submitting": "[Mode gratuit] Soumission formulaire...",
|
||||
"free_mode_link_found": "[Mode gratuit] Lien trouvé: {0}...",
|
||||
"free_mode_completed": "[Mode gratuit] Terminé: {0}",
|
||||
"free_mode_guest_slots_unavailable": "1fichier : le téléchargement gratuit invité est temporairement indisponible (tous les créneaux sont occupés). Réessayez plus tard.",
|
||||
"free_mode_unavailable_in_app": "1fichier : ce téléchargement n'est pas disponible dans l'application pour le moment. Réessayez plus tard.",
|
||||
"free_mode_premium_advice": "Pour télécharger de manière illimitée, quand vous voulez et à pleine vitesse, vous devez obtenir un compte premium ou un débrideur et entrer votre clé API dans RGSX.",
|
||||
"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",
|
||||
@@ -57,7 +67,8 @@
|
||||
"confirm_exit_with_downloads": "Attention : {0} téléchargement(s) en cours. Quitter quand même ?",
|
||||
"confirm_clear_history": "Vider l'historique ?",
|
||||
"confirm_redownload_cache": "Mettre à jour la liste des jeux ?",
|
||||
"gamelist_update_prompt_with_date": "La liste des jeux n'a pas été mise à jour depuis plus de {0} jours (dernière mise à jour : {1}). Télécharger la dernière version ?",
|
||||
"gamelist_update_prompt_with_date": "Dernière mise à jour locale de la liste des jeux : {0}. Télécharger la dernière version ?",
|
||||
"gamelist_update_prompt_remote_newer": "Une liste des jeux plus récente est disponible en ligne (locale : {0}, en ligne : {1}). Télécharger la dernière version ?",
|
||||
"gamelist_update_prompt_first_time": "Souhaitez-vous télécharger la dernière liste des jeux ?",
|
||||
"popup_redownload_success": "Le cache a été effacé, merci de relancer l'application",
|
||||
"popup_no_cache": "Aucun cache trouvé.\nVeuillez redémarrer l'application pour charger les jeux.",
|
||||
@@ -187,7 +198,7 @@
|
||||
"submenu_display_font_size": "Taille Police",
|
||||
"submenu_display_show_unsupported": "Afficher systèmes non supportés : {status}",
|
||||
"submenu_display_allow_unknown_ext": "Masquer avert. ext inconnue : {status}",
|
||||
"submenu_display_filter_platforms": "Filtrer systèmes",
|
||||
"submenu_display_filter_platforms": "Afficher/Masquer plateformes",
|
||||
"status_on": "Oui",
|
||||
"status_off": "Non",
|
||||
"status_present": "Présente",
|
||||
@@ -297,7 +308,7 @@
|
||||
"footer_joystick": "Joystick : {0}",
|
||||
"history_game_options_title": "Options du jeu",
|
||||
"history_option_download_folder": "Localiser le fichier",
|
||||
"history_option_extract_archive": "Extraire l'archive",
|
||||
"history_option_extract_archive": "Forcer l'extraction",
|
||||
"history_option_open_file": "Ouvrir le fichier",
|
||||
"history_option_scraper": "Récupérer métadonnées",
|
||||
"history_option_remove_from_queue": "Retirer de la file d'attente",
|
||||
@@ -316,6 +327,7 @@
|
||||
"history_scraper_not_implemented": "Scraper pas encore implémenté",
|
||||
"history_confirm_delete": "Supprimer ce jeu du disque ?",
|
||||
"history_file_not_found": "Fichier introuvable",
|
||||
"history_extract_archive_confirm": "Forcer l'extraction de l'archive",
|
||||
"history_extracting": "Extraction en cours...",
|
||||
"history_extracted": "Extrait",
|
||||
"history_delete_success": "Jeu supprimé avec succès",
|
||||
@@ -429,8 +441,8 @@
|
||||
"web_sort": "Trier par",
|
||||
"web_sort_name_asc": "A-Z (Nom)",
|
||||
"web_sort_name_desc": "Z-A (Nom)",
|
||||
"web_sort_size_asc": "Taille +- (Petit d'abord)",
|
||||
"web_sort_size_desc": "Taille -+ (Grand d'abord)",
|
||||
"web_sort_size_asc": "Taille -+ (Petit d'abord)",
|
||||
"web_sort_size_desc": "Taille +- (Grand d'abord)",
|
||||
"web_filter_region": "Région",
|
||||
"web_filter_hide_non_release": "Masquer Démos/Betas/Protos",
|
||||
"web_filter_regex_mode": "Activer recherche Regex",
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
"loading_downloading_games_images": "Download giochi e immagini...",
|
||||
"loading_extracting_data": "Estrazione cartella Dati iniziale...",
|
||||
"loading_load_systems": "Caricamento sistemi...",
|
||||
"loading_platform_counter": "Piattaforma {0}/{1}",
|
||||
"loading_platform_name": "Piattaforma: {0}",
|
||||
"loading_game_entries_progress": "Giochi analizzati: {0}/{1}",
|
||||
"loading_read_games_resolve_sources": "Lettura dei giochi e risoluzione delle sorgenti...",
|
||||
"loading_torrent_manifest_analysis": "Analisi del manifest torrent: {0}",
|
||||
"loading_torrent_files_progress": "File del torrent: {0}/{1}",
|
||||
"error_extract_data_failed": "Errore nel download o nell'estrazione della cartella Dati iniziale.",
|
||||
"error_no_internet": "Nessuna connessione Internet. Controlla la rete.",
|
||||
"error_api_key": "Inserisci la tua API key (solo premium) nel file {0}",
|
||||
@@ -26,6 +32,7 @@
|
||||
"game_filter": "Filtro attivo: {0}",
|
||||
"game_search": "Filtro: {0}",
|
||||
"global_search_title": "Ricerca globale: {0}",
|
||||
"platform_search_title": "Cerca in questa piattaforma",
|
||||
"global_search_empty_query": "Digita un nome per cercare in tutte le console",
|
||||
"global_search_no_results": "Nessun risultato per: {0}",
|
||||
"game_header_name": "Nome",
|
||||
@@ -47,6 +54,9 @@
|
||||
"free_mode_submitting": "[Modalità gratuita] Invio modulo...",
|
||||
"free_mode_link_found": "[Modalità gratuita] Link trovato: {0}...",
|
||||
"free_mode_completed": "[Modalità gratuita] Completato: {0}",
|
||||
"free_mode_guest_slots_unavailable": "1fichier: il download gratuito come ospite non è temporaneamente disponibile (tutti gli slot sono occupati). Riprova più tardi.",
|
||||
"free_mode_unavailable_in_app": "1fichier: questo download non è disponibile nell'applicazione in questo momento. Riprova più tardi.",
|
||||
"free_mode_premium_advice": "Per scaricare senza limiti, quando vuoi e alla massima velocità, hai bisogno di un account premium o di un servizio debrid e devi inserire la sua chiave API in RGSX.",
|
||||
"download_status": "{0}: {1}",
|
||||
"download_canceled": "Download annullato dall'utente.",
|
||||
"download_removed_from_queue": "Rimosso dalla coda di download",
|
||||
@@ -56,7 +66,8 @@
|
||||
"confirm_exit": "Uscire dall'applicazione?",
|
||||
"confirm_exit_with_downloads": "Attenzione: {0} download in corso. Uscire comunque?",
|
||||
"confirm_clear_history": "Cancellare la cronologia?",
|
||||
"confirm_redownload_cache": "Aggiornare l'elenco dei giochi?", "gamelist_update_prompt_with_date": "L'elenco dei giochi non è stato aggiornato da più di {0} giorni (ultimo aggiornamento: {1}). Scaricare l'ultima versione?",
|
||||
"confirm_redownload_cache": "Aggiornare l'elenco dei giochi?", "gamelist_update_prompt_with_date": "Ultimo aggiornamento locale dell'elenco dei giochi: {0}. Scaricare l'ultima versione?",
|
||||
"gamelist_update_prompt_remote_newer": "È disponibile online un elenco dei giochi più recente (locale: {0}, online: {1}). Scaricare l'ultima versione?",
|
||||
"gamelist_update_prompt_first_time": "Vuoi scaricare l'ultimo elenco dei giochi?", "popup_redownload_success": "Cache pulita, riavvia l'applicazione",
|
||||
"popup_no_cache": "Nessuna cache trovata.\nRiavvia l'applicazione per caricare i giochi.",
|
||||
"popup_countdown": "Questo messaggio si chiuderà tra {0} secondo{1}",
|
||||
@@ -293,7 +304,7 @@
|
||||
"footer_joystick": "Joystick: {0}",
|
||||
"history_game_options_title": "Opzioni gioco",
|
||||
"history_option_download_folder": "Localizza file",
|
||||
"history_option_extract_archive": "Estrai archivio",
|
||||
"history_option_extract_archive": "Forza estrazione archivio",
|
||||
"history_option_open_file": "Apri file",
|
||||
"history_option_scraper": "Scraper metadati",
|
||||
"history_option_remove_from_queue": "Rimuovi dalla coda",
|
||||
@@ -309,6 +320,7 @@
|
||||
"history_scraper_not_implemented": "Scraper non ancora implementato",
|
||||
"history_confirm_delete": "Eliminare questo gioco dal disco?",
|
||||
"history_file_not_found": "File non trovato",
|
||||
"history_extract_archive_confirm": "Forza estrazione archivio",
|
||||
"history_extracting": "Estrazione in corso...",
|
||||
"history_extracted": "Estratto",
|
||||
"history_delete_success": "Gioco eliminato con successo",
|
||||
@@ -422,8 +434,8 @@
|
||||
"web_sort": "Ordina per",
|
||||
"web_sort_name_asc": "A-Z (Nome)",
|
||||
"web_sort_name_desc": "Z-A (Nome)",
|
||||
"web_sort_size_asc": "Dimensione +- (Piccolo primo)",
|
||||
"web_sort_size_desc": "Dimensione -+ (Grande primo)",
|
||||
"web_sort_size_asc": "Dimensione -+ (Piccolo primo)",
|
||||
"web_sort_size_desc": "Dimensione +- (Grande primo)",
|
||||
"accessibility_font_size": "Dimensione carattere: {0}",
|
||||
"confirm_cancel_download": "Annullare il download corrente?",
|
||||
"controls_help_title": "Guida ai controlli",
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
"loading_downloading_games_images": "Baixando jogos e imagens...",
|
||||
"loading_extracting_data": "Extraindo pasta inicial Data...",
|
||||
"loading_load_systems": "Carregando sistemas...",
|
||||
"loading_platform_counter": "Plataforma {0}/{1}",
|
||||
"loading_platform_name": "Plataforma: {0}",
|
||||
"loading_game_entries_progress": "Jogos analisados: {0}/{1}",
|
||||
"loading_read_games_resolve_sources": "Lendo jogos e resolvendo fontes...",
|
||||
"loading_torrent_manifest_analysis": "Analisando o manifesto torrent: {0}",
|
||||
"loading_torrent_files_progress": "Arquivos do torrent: {0}/{1}",
|
||||
"error_extract_data_failed": "Falha ao baixar ou extrair a pasta inicial Data.",
|
||||
"error_no_internet": "Sem conexão com a Internet. Verifique sua rede.",
|
||||
"error_api_key": "Insira sua chave API (somente premium) no arquivo {0}",
|
||||
@@ -26,6 +32,7 @@
|
||||
"game_filter": "Filtro ativo: {0}",
|
||||
"game_search": "Filtro: {0}",
|
||||
"global_search_title": "Busca global: {0}",
|
||||
"platform_search_title": "Pesquisar nesta plataforma",
|
||||
"global_search_empty_query": "Digite um nome para buscar em todos os consoles",
|
||||
"global_search_no_results": "Nenhum resultado para: {0}",
|
||||
"game_header_name": "Nome",
|
||||
@@ -47,6 +54,9 @@
|
||||
"free_mode_submitting": "[Modo gratuito] Enviando formulário...",
|
||||
"free_mode_link_found": "[Modo gratuito] Link encontrado: {0}...",
|
||||
"free_mode_completed": "[Modo gratuito] Concluído: {0}",
|
||||
"free_mode_guest_slots_unavailable": "1fichier: o download gratuito como convidado está temporariamente indisponível (todos os slots estão ocupados). Tente novamente mais tarde.",
|
||||
"free_mode_unavailable_in_app": "1fichier: este download não está disponível no aplicativo no momento. Tente novamente mais tarde.",
|
||||
"free_mode_premium_advice": "Para baixar sem limites, quando quiser e em velocidade máxima, você precisa de uma conta premium ou de um serviço debrid e deve inserir a chave API no RGSX.",
|
||||
"download_status": "{0}: {1}",
|
||||
"download_canceled": "Download cancelado pelo usuário.",
|
||||
"download_removed_from_queue": "Removido da fila de download",
|
||||
@@ -57,7 +67,8 @@
|
||||
"confirm_exit_with_downloads": "Atenção: {0} download(s) em andamento. Sair mesmo assim?",
|
||||
"confirm_clear_history": "Limpar histórico?",
|
||||
"confirm_redownload_cache": "Atualizar lista de jogos?",
|
||||
"gamelist_update_prompt_with_date": "A lista de jogos não foi atualizada há mais de {0} dias (última atualização: {1}). Baixar a versão mais recente?",
|
||||
"gamelist_update_prompt_with_date": "Última atualização local da lista de jogos: {0}. Baixar a versão mais recente?",
|
||||
"gamelist_update_prompt_remote_newer": "Há uma lista de jogos mais recente disponível online (local: {0}, online: {1}). Baixar a versão mais recente?",
|
||||
"gamelist_update_prompt_first_time": "Gostaria de baixar a última lista de jogos?",
|
||||
"popup_redownload_success": "Cache limpo, reinicie a aplicação",
|
||||
"popup_no_cache": "Nenhum cache encontrado.\nReinicie a aplicação para carregar os jogos.",
|
||||
@@ -299,7 +310,7 @@
|
||||
"footer_joystick": "Joystick: {0}",
|
||||
"history_game_options_title": "Opções do jogo",
|
||||
"history_option_download_folder": "Localizar arquivo",
|
||||
"history_option_extract_archive": "Extrair arquivo",
|
||||
"history_option_extract_archive": "Forcar extracao do arquivo",
|
||||
"history_option_open_file": "Abrir arquivo",
|
||||
"history_option_scraper": "Obter metadados",
|
||||
"history_option_remove_from_queue": "Remover da fila",
|
||||
@@ -315,6 +326,7 @@
|
||||
"history_scraper_not_implemented": "Scraper ainda não implementado",
|
||||
"history_confirm_delete": "Excluir este jogo do disco?",
|
||||
"history_file_not_found": "Arquivo não encontrado",
|
||||
"history_extract_archive_confirm": "Forcar extracao do arquivo",
|
||||
"history_extracting": "Extraindo...",
|
||||
"history_extracted": "Extraído",
|
||||
"history_delete_success": "Jogo excluído com sucesso",
|
||||
@@ -428,8 +440,8 @@
|
||||
"web_sort": "Ordenar por",
|
||||
"web_sort_name_asc": "A-Z (Nome)",
|
||||
"web_sort_name_desc": "Z-A (Nome)",
|
||||
"web_sort_size_asc": "Tamanho +- (Menor primeiro)",
|
||||
"web_sort_size_desc": "Tamanho -+ (Maior primeiro)",
|
||||
"web_sort_size_asc": "Tamanho -+ (Menor primeiro)",
|
||||
"web_sort_size_desc": "Tamanho +- (Maior primeiro)",
|
||||
"accessibility_font_size": "Tamanho da fonte: {0}",
|
||||
"web_filter_region": "Região",
|
||||
"web_filter_hide_non_release": "Ocultar Demos/Betas/Protos",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,11 @@
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from email.utils import parsedate_to_datetime
|
||||
|
||||
import requests
|
||||
|
||||
import config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -79,6 +84,8 @@ def load_rgsx_settings():
|
||||
"roms_folder": "",
|
||||
"web_service_at_boot": False,
|
||||
"last_gamelist_update": None,
|
||||
"last_gamelist_prompt_remote_update": None,
|
||||
"global_sort_option": "name_asc",
|
||||
"platform_custom_paths": {} # Chemins personnalisés par plateforme
|
||||
}
|
||||
|
||||
@@ -120,18 +127,117 @@ def get_last_gamelist_update(settings=None):
|
||||
return settings.get("last_gamelist_update", None)
|
||||
|
||||
|
||||
def get_last_gamelist_prompt_remote_update(settings=None):
|
||||
"""Récupère la dernière date distante déjà proposée pour la liste des jeux."""
|
||||
if settings is None:
|
||||
settings = load_rgsx_settings()
|
||||
return settings.get("last_gamelist_prompt_remote_update", None)
|
||||
|
||||
|
||||
def parse_gamelist_update_timestamp(value):
|
||||
"""Parse legacy dates, ISO timestamps, or HTTP dates into UTC datetimes."""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, datetime):
|
||||
return value.astimezone(timezone.utc) if value.tzinfo else value.replace(tzinfo=timezone.utc)
|
||||
|
||||
text = str(value).strip()
|
||||
if not text:
|
||||
return None
|
||||
|
||||
try:
|
||||
normalized = text.replace("Z", "+00:00") if text.endswith("Z") else text
|
||||
dt = datetime.fromisoformat(normalized)
|
||||
return dt.astimezone(timezone.utc) if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for fmt in ("%Y-%m-%d", "%Y-%m-%d %H:%M:%S"):
|
||||
try:
|
||||
return datetime.strptime(text, fmt).replace(tzinfo=timezone.utc)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
dt = parsedate_to_datetime(text)
|
||||
return dt.astimezone(timezone.utc) if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def format_gamelist_update_display(value):
|
||||
"""Return a stable YYYY-MM-DD string for UI display."""
|
||||
dt = parse_gamelist_update_timestamp(value)
|
||||
if dt is not None:
|
||||
return dt.strftime("%Y-%m-%d")
|
||||
return str(value).strip() if value is not None else ""
|
||||
|
||||
|
||||
def get_remote_gamelist_timestamp(url, timeout=15):
|
||||
"""Fetch the remote last modification date of the gamelist archive."""
|
||||
if not url:
|
||||
return None
|
||||
|
||||
headers = {
|
||||
"User-Agent": "RGSX/1.0",
|
||||
"Accept": "*/*",
|
||||
}
|
||||
|
||||
try:
|
||||
with requests.Session() as session:
|
||||
response = session.head(url, allow_redirects=True, timeout=timeout, headers=headers)
|
||||
if response.status_code >= 400 or not response.headers.get("Last-Modified"):
|
||||
response.close()
|
||||
response = session.get(url, stream=True, allow_redirects=True, timeout=timeout, headers=headers)
|
||||
|
||||
header_value = response.headers.get("Last-Modified")
|
||||
remote_dt = parse_gamelist_update_timestamp(header_value)
|
||||
response.close()
|
||||
|
||||
if remote_dt is not None:
|
||||
logger.info(f"Date distante gamelist détectée: {remote_dt.isoformat()} ({url})")
|
||||
else:
|
||||
logger.warning(f"Impossible de lire l'en-tête Last-Modified de la gamelist pour {url}")
|
||||
return remote_dt
|
||||
except Exception as e:
|
||||
logger.warning(f"Erreur détection date distante gamelist pour {url}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def set_last_gamelist_update(date_string=None):
|
||||
"""Définit la date de dernière mise à jour de la liste des jeux.
|
||||
Si date_string est None, utilise la date actuelle.
|
||||
"""
|
||||
from datetime import datetime
|
||||
settings = load_rgsx_settings()
|
||||
if date_string is None:
|
||||
date_string = datetime.now().strftime("%Y-%m-%d")
|
||||
settings["last_gamelist_update"] = date_string
|
||||
parsed_value = parse_gamelist_update_timestamp(date_string)
|
||||
if parsed_value is None:
|
||||
parsed_value = datetime.now(timezone.utc)
|
||||
normalized = parsed_value.isoformat().replace("+00:00", "Z")
|
||||
settings["last_gamelist_update"] = normalized
|
||||
save_rgsx_settings(settings)
|
||||
logger.info(f"Date de dernière mise à jour de la liste des jeux: {date_string}")
|
||||
return date_string
|
||||
logger.info(f"Date de dernière mise à jour de la liste des jeux: {normalized}")
|
||||
return normalized
|
||||
|
||||
|
||||
def set_last_gamelist_prompt_remote_update(date_string=None):
|
||||
"""Mémorise la dernière date distante déjà proposée pour la liste des jeux.
|
||||
Si date_string est None ou invalide, efface la valeur mémorisée.
|
||||
"""
|
||||
settings = load_rgsx_settings()
|
||||
parsed_value = parse_gamelist_update_timestamp(date_string)
|
||||
normalized = (
|
||||
parsed_value.isoformat().replace("+00:00", "Z")
|
||||
if parsed_value is not None else None
|
||||
)
|
||||
settings["last_gamelist_prompt_remote_update"] = normalized
|
||||
save_rgsx_settings(settings)
|
||||
|
||||
if normalized is not None:
|
||||
logger.info(f"Dernière date distante déjà proposée pour la liste des jeux: {normalized}")
|
||||
else:
|
||||
logger.info("Réinitialisation de la dernière date distante déjà proposée pour la liste des jeux")
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
|
||||
@@ -499,6 +605,29 @@ def save_game_filters(filters_dict):
|
||||
return False
|
||||
|
||||
|
||||
def get_global_sort_option(settings=None):
|
||||
"""Retourne l'option de tri globale sauvegardée."""
|
||||
allowed = {"name_asc", "name_desc", "size_asc", "size_desc"}
|
||||
if settings is None:
|
||||
settings = load_rgsx_settings()
|
||||
value = str(settings.get("global_sort_option", "name_asc") or "name_asc")
|
||||
return value if value in allowed else "name_asc"
|
||||
|
||||
|
||||
def set_global_sort_option(option):
|
||||
"""Sauvegarde l'option de tri globale."""
|
||||
allowed = {"name_asc", "name_desc", "size_asc", "size_desc"}
|
||||
normalized = str(option or "name_asc")
|
||||
if normalized not in allowed:
|
||||
normalized = "name_asc"
|
||||
|
||||
settings = load_rgsx_settings()
|
||||
settings["global_sort_option"] = normalized
|
||||
save_rgsx_settings(settings)
|
||||
logger.info(f"Option de tri globale sauvegardée: {normalized}")
|
||||
return normalized
|
||||
|
||||
|
||||
def get_platform_custom_path(platform_name):
|
||||
"""Récupère le chemin personnalisé pour une plateforme."""
|
||||
try:
|
||||
|
||||
@@ -21,7 +21,7 @@ from datetime import datetime, timezone
|
||||
from email.utils import formatdate, parsedate_to_datetime
|
||||
import config
|
||||
from history import load_history, save_history
|
||||
from utils import load_sources, load_games, extract_data, get_clean_display_name, parse_torrent_download_url
|
||||
from utils import load_sources, load_games, extract_data, get_clean_display_name, parse_torrent_download_url, request_torrent_manifest_refresh, _resolve_platform_image_path
|
||||
from network import download_rom, download_from_1fichier
|
||||
from pathlib import Path
|
||||
from rgsx_settings import get_language
|
||||
@@ -464,8 +464,12 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
self._set_headers('application/json', status, etag=etag, last_modified=cached_dt)
|
||||
self.wfile.write(json.dumps(data, ensure_ascii=False).encode('utf-8'))
|
||||
try:
|
||||
self._set_headers('application/json', status, etag=etag, last_modified=cached_dt)
|
||||
self.wfile.write(json.dumps(data, ensure_ascii=False).encode('utf-8'))
|
||||
except (ConnectionAbortedError, BrokenPipeError) as e:
|
||||
logger.debug(f"Connexion fermée par le client pendant l'envoi JSON: {e}")
|
||||
return
|
||||
|
||||
def _send_html(self, html, status=200, etag=None, last_modified=None):
|
||||
"""Envoie une réponse HTML"""
|
||||
@@ -972,13 +976,18 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
os.remove(zip_path)
|
||||
|
||||
if success:
|
||||
from rgsx_settings import get_remote_gamelist_timestamp, set_last_gamelist_update
|
||||
|
||||
remote_update_dt = get_remote_gamelist_timestamp(games_zip_url)
|
||||
set_last_gamelist_update(remote_update_dt)
|
||||
logger.info(f"✅ Extraction réussie: {message}")
|
||||
deleted.append(f'extracted: {message}')
|
||||
|
||||
# Maintenant charger les sources
|
||||
invalidate_all_caches(reason='update-cache refresh')
|
||||
logger.info("🔄 Chargement des plateformes...")
|
||||
refreshed_sources = load_sources()
|
||||
request_torrent_manifest_refresh()
|
||||
refreshed_sources = load_sources(allow_torrent_manifest_fetch=True)
|
||||
if refreshed_sources is not None:
|
||||
with cache_lock:
|
||||
source_cache.update({
|
||||
@@ -1777,86 +1786,28 @@ DO NOT share this file publicly as it may contain sensitive information.
|
||||
platform_dict = pd
|
||||
break
|
||||
|
||||
# Dossiers où chercher les images
|
||||
image_folders = [
|
||||
config.IMAGES_FOLDER, # Dossier utilisateur (saves/ports/rgsx/images)
|
||||
os.path.join(config.APP_FOLDER, 'assets', 'images') # Dossier app
|
||||
]
|
||||
|
||||
# Extensions possibles
|
||||
extensions = ['.png', '.jpg', '.jpeg', '.gif']
|
||||
|
||||
# Construire la liste des noms de fichiers à chercher (ordre de priorité)
|
||||
candidates = []
|
||||
|
||||
if platform_dict:
|
||||
# 1. platform_image explicite (priorité max)
|
||||
platform_image_field = (platform_dict.get('platform_image') or '').strip()
|
||||
if platform_image_field:
|
||||
candidates.append(platform_image_field)
|
||||
|
||||
# 2. platform_name.png
|
||||
candidates.append(platform_name)
|
||||
|
||||
# 3. folder.png si disponible
|
||||
folder_name = platform_dict.get('folder')
|
||||
if folder_name:
|
||||
candidates.append(folder_name)
|
||||
else:
|
||||
# Pas de platform_dict trouvé, juste essayer le nom
|
||||
candidates.append(platform_name)
|
||||
|
||||
# Chercher le fichier image
|
||||
image_path = None
|
||||
for candidate in candidates:
|
||||
# Retirer l'extension si déjà présente
|
||||
candidate_base = os.path.splitext(candidate)[0]
|
||||
|
||||
for folder in image_folders:
|
||||
if not os.path.exists(folder):
|
||||
continue
|
||||
|
||||
# Essayer avec chaque extension
|
||||
for ext in extensions:
|
||||
test_path = os.path.join(folder, candidate_base + ext)
|
||||
if os.path.exists(test_path):
|
||||
image_path = test_path
|
||||
break
|
||||
|
||||
if image_path:
|
||||
break
|
||||
|
||||
if image_path:
|
||||
break
|
||||
|
||||
# Si pas trouvé, chercher default.png
|
||||
if not image_path:
|
||||
for folder in image_folders:
|
||||
default_path = os.path.join(folder, 'default.png')
|
||||
if os.path.exists(default_path):
|
||||
image_path = default_path
|
||||
break
|
||||
|
||||
# Envoyer l'image
|
||||
payload_platform = platform_dict or {'platform_name': platform_name}
|
||||
image_path = _resolve_platform_image_path(payload_platform)
|
||||
|
||||
if image_path and os.path.exists(image_path):
|
||||
# Déterminer le type MIME
|
||||
ext = os.path.splitext(image_path)[1].lower()
|
||||
mime_types = {
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif'
|
||||
'.gif': 'image/gif',
|
||||
'.webp': 'image/webp',
|
||||
}
|
||||
content_type = mime_types.get(ext, 'image/png')
|
||||
|
||||
# Lire et envoyer l'image avec headers de cache
|
||||
with open(image_path, 'rb') as f:
|
||||
image_data = f.read()
|
||||
|
||||
|
||||
# Ajouter les headers de cache (1 heure)
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', content_type)
|
||||
self.send_header('Cache-Control', 'public, max-age=3600') # 1 heure
|
||||
self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
self.send_header('Pragma', 'no-cache')
|
||||
self.send_header('Expires', '0')
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
self.wfile.write(image_data)
|
||||
@@ -1865,7 +1816,9 @@ DO NOT share this file publicly as it may contain sensitive information.
|
||||
logger.warning(f"Aucune image trouvée pour {platform_name}, envoi PNG transparent")
|
||||
self.send_response(404)
|
||||
self.send_header('Content-type', 'image/png')
|
||||
self.send_header('Cache-Control', 'public, max-age=3600')
|
||||
self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
self.send_header('Pragma', 'no-cache')
|
||||
self.send_header('Expires', '0')
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
# PNG transparent 1x1 pixel
|
||||
@@ -1876,7 +1829,9 @@ DO NOT share this file publicly as it may contain sensitive information.
|
||||
logger.error(f"Erreur lors du chargement de l'image {platform_name}: {e}", exc_info=True)
|
||||
self.send_response(500)
|
||||
self.send_header('Content-type', 'image/png')
|
||||
self.send_header('Cache-Control', 'public, max-age=60') # Cache court pour les erreurs
|
||||
self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
self.send_header('Pragma', 'no-cache')
|
||||
self.send_header('Expires', '0')
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
# PNG transparent en cas d'erreur
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
let currentPlatform = null;
|
||||
let currentGameSort = 'name_asc'; // Type de tri actuel: 'name_asc', 'name_desc', 'size_asc', 'size_desc'
|
||||
let currentGames = []; // Stocke les jeux actuels pour le tri
|
||||
const loggedUnparsedSizeTexts = new Set();
|
||||
let lastProgressUpdate = Date.now();
|
||||
let autoRefreshTimeout = null;
|
||||
let progressInterval = null;
|
||||
@@ -209,6 +210,74 @@
|
||||
}
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
function parseSizeToBytes(sizeText) {
|
||||
if (!sizeText) return 0;
|
||||
|
||||
const rawText = String(sizeText).trim();
|
||||
let normalized = rawText.replace(/octets?/gi, 'B');
|
||||
|
||||
if (normalized.includes(',') && normalized.includes('.')) {
|
||||
normalized = normalized.replace(/,/g, '');
|
||||
} else if (normalized.includes(',')) {
|
||||
normalized = normalized.replace(',', '.');
|
||||
}
|
||||
|
||||
const match = normalized.match(/^([0-9]+(?:\.[0-9]+)?)\s*([a-zA-Z]+)/);
|
||||
if (!match) {
|
||||
if (!loggedUnparsedSizeTexts.has(rawText)) {
|
||||
loggedUnparsedSizeTexts.add(rawText);
|
||||
console.warn('[RGSX][sort] Taille non interpretable:', rawText);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const value = parseFloat(match[1]);
|
||||
if (Number.isNaN(value)) return 0;
|
||||
|
||||
const unit = match[2].toLowerCase();
|
||||
const multipliers = {
|
||||
b: 1,
|
||||
byte: 1,
|
||||
bytes: 1,
|
||||
o: 1,
|
||||
k: 1024,
|
||||
ko: 1024,
|
||||
kb: 1024,
|
||||
kib: 1024,
|
||||
kio: 1024,
|
||||
m: 1024 ** 2,
|
||||
mo: 1024 ** 2,
|
||||
mb: 1024 ** 2,
|
||||
mib: 1024 ** 2,
|
||||
mio: 1024 ** 2,
|
||||
g: 1024 ** 3,
|
||||
go: 1024 ** 3,
|
||||
gb: 1024 ** 3,
|
||||
gib: 1024 ** 3,
|
||||
gio: 1024 ** 3,
|
||||
t: 1024 ** 4,
|
||||
to: 1024 ** 4,
|
||||
tb: 1024 ** 4,
|
||||
tib: 1024 ** 4,
|
||||
tio: 1024 ** 4,
|
||||
p: 1024 ** 5,
|
||||
po: 1024 ** 5,
|
||||
pb: 1024 ** 5,
|
||||
pib: 1024 ** 5,
|
||||
pio: 1024 ** 5,
|
||||
};
|
||||
|
||||
if (!multipliers[unit]) {
|
||||
if (!loggedUnparsedSizeTexts.has(rawText)) {
|
||||
loggedUnparsedSizeTexts.add(rawText);
|
||||
console.warn('[RGSX][sort] Unite de taille non supportee:', rawText, '->', unit);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.round(value * multipliers[unit]);
|
||||
}
|
||||
|
||||
// Appliquer les traductions à tous les éléments marqués
|
||||
function applyTranslations() {
|
||||
@@ -1071,6 +1140,38 @@
|
||||
currentGameSort = sortType;
|
||||
const items = Array.from(document.querySelectorAll('.game-item'));
|
||||
const gamesList = document.querySelector('.games-list');
|
||||
|
||||
if (!gamesList) {
|
||||
console.warn('[RGSX][sort] .games-list introuvable pour le tri', sortType);
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldLogSizeSort = sortType === 'size_asc' || sortType === 'size_desc';
|
||||
const getSizeInBytes = (sizeElem) => {
|
||||
if (!sizeElem) return 0;
|
||||
return parseSizeToBytes(sizeElem.textContent);
|
||||
};
|
||||
|
||||
if (shouldLogSizeSort) {
|
||||
const previewBefore = items.slice(0, 5).map(item => {
|
||||
const sizeText = item.querySelector('.game-size')?.textContent?.trim() || '';
|
||||
return {
|
||||
name: item.querySelector('.game-name')?.textContent?.trim() || '',
|
||||
sizeText,
|
||||
sizeBytes: getSizeInBytes(item.querySelector('.game-size')),
|
||||
};
|
||||
});
|
||||
const zeroSizedCount = items.filter(item => {
|
||||
const sizeElem = item.querySelector('.game-size');
|
||||
return sizeElem && getSizeInBytes(sizeElem) === 0;
|
||||
}).length;
|
||||
console.debug('[RGSX][sort] Debut tri taille', {
|
||||
sortType,
|
||||
totalItems: items.length,
|
||||
zeroSizedCount,
|
||||
previewBefore,
|
||||
});
|
||||
}
|
||||
|
||||
// Trier les éléments
|
||||
items.sort((a, b) => {
|
||||
@@ -1079,41 +1180,15 @@
|
||||
const sizeElemA = a.querySelector('.game-size');
|
||||
const sizeElemB = b.querySelector('.game-size');
|
||||
|
||||
// Extraire la taille en Mo (normalisée)
|
||||
const getSizeInMo = (sizeElem) => {
|
||||
if (!sizeElem) return 0;
|
||||
const text = sizeElem.textContent;
|
||||
// Support des formats: "100 Mo", "2.5 Go" (français) et "100 MB", "2.5 GB" (anglais)
|
||||
// Plus Ko/KB, o/B, To/TB
|
||||
const match = text.match(/([0-9.]+)\s*(o|B|Ko|KB|Mo|MB|Go|GB|To|TB)/i);
|
||||
if (!match) return 0;
|
||||
let size = parseFloat(match[1]);
|
||||
const unit = match[2].toUpperCase();
|
||||
|
||||
// Convertir tout en Mo
|
||||
if (unit === 'O' || unit === 'B') {
|
||||
size /= (1024 * 1024); // octets/bytes vers Mo
|
||||
} else if (unit === 'KO' || unit === 'KB') {
|
||||
size /= 1024; // Ko vers Mo
|
||||
} else if (unit === 'MO' || unit === 'MB') {
|
||||
// Déjà en Mo
|
||||
} else if (unit === 'GO' || unit === 'GB') {
|
||||
size *= 1024; // Go vers Mo
|
||||
} else if (unit === 'TO' || unit === 'TB') {
|
||||
size *= 1024 * 1024; // To vers Mo
|
||||
}
|
||||
return size;
|
||||
};
|
||||
|
||||
switch(sortType) {
|
||||
case 'name_asc':
|
||||
return nameA.localeCompare(nameB);
|
||||
case 'name_desc':
|
||||
return nameB.localeCompare(nameA);
|
||||
case 'size_asc':
|
||||
return getSizeInMo(sizeElemA) - getSizeInMo(sizeElemB);
|
||||
return getSizeInBytes(sizeElemA) - getSizeInBytes(sizeElemB);
|
||||
case 'size_desc':
|
||||
return getSizeInMo(sizeElemB) - getSizeInMo(sizeElemA);
|
||||
return getSizeInBytes(sizeElemB) - getSizeInBytes(sizeElemA);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
@@ -1124,6 +1199,18 @@
|
||||
items.forEach(item => {
|
||||
gamesList.appendChild(item);
|
||||
});
|
||||
|
||||
if (shouldLogSizeSort) {
|
||||
const previewAfter = items.slice(0, 5).map(item => ({
|
||||
name: item.querySelector('.game-name')?.textContent?.trim() || '',
|
||||
sizeText: item.querySelector('.game-size')?.textContent?.trim() || '',
|
||||
sizeBytes: getSizeInBytes(item.querySelector('.game-size')),
|
||||
}));
|
||||
console.debug('[RGSX][sort] Fin tri taille', {
|
||||
sortType,
|
||||
previewAfter,
|
||||
});
|
||||
}
|
||||
|
||||
// Mettre à jour les boutons de tri
|
||||
document.querySelectorAll('.sort-btn').forEach(btn => {
|
||||
@@ -1150,6 +1237,7 @@
|
||||
|
||||
// Construire le HTML avec les traductions
|
||||
let searchPlaceholder = t('web_search_platform');
|
||||
const platformImageCacheBuster = Date.now();
|
||||
let html = `
|
||||
<div class="search-box">
|
||||
<input type="text" id="platform-search" placeholder="🔍 ${searchPlaceholder}"
|
||||
@@ -1164,9 +1252,9 @@
|
||||
let gameCountText = t('web_game_count', '📦', p.games_count || 0);
|
||||
html += `
|
||||
<div class="platform-card" onclick='loadGames("${p.platform_name.replace(/"/g, """).replace(/'/g, "'")}")'>
|
||||
<img src="/api/image/${encodeURIComponent(p.platform_name)}"
|
||||
<img src="/api/image/${encodeURIComponent(p.platform_name)}?v=${platformImageCacheBuster}"
|
||||
alt="${p.platform_name}"
|
||||
onerror="this.src='/api/image/default'">
|
||||
onerror="this.src='/api/image/default?v=${platformImageCacheBuster}'">
|
||||
<h3>${p.platform_name}</h3>
|
||||
<div class="count">${gameCountText}</div>
|
||||
</div>
|
||||
|
||||
1275
ports/RGSX/utils.py
1275
ports/RGSX/utils.py
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2.6.1.5.1"
|
||||
"version": "2.6.3.2"
|
||||
}
|
||||
Reference in New Issue
Block a user