Compare commits

..

7 Commits

Author SHA1 Message Date
skymike03
55231bb823 v2.3.2.5 (2025.11.18)
- bugs in menu solved and display tweak on filter systems
2025-11-18 16:16:41 +01:00
skymike03
d9c1ca6794 Merge branch 'main' of https://github.com/RetroGameSets/RGSX 2025-11-16 17:14:22 +01:00
skymike03
6613b43264 v2.3.2.4
- update README_FR.md with new features and installation instructions.
- Refactor controls.py for improved input handling specially on page up/ down release repeat
2025-11-16 17:14:18 +01:00
RGS
d60dc31291 Add donation link to README
Added a donation link to support the project.
2025-11-16 16:39:56 +01:00
skymike03
ace6ec876f v2.3.2.3
- correct bug when using both keyboard and controller mixed that cause a repeat key holdind
2025-11-16 14:05:21 +01:00
RGS
9f759c1928 Enhance README with platform and interface images
Added images to enhance the README presentation.
2025-11-16 13:39:45 +01:00
skymike03
db287e33d7 v2.3.2.2 (2025.11.16)
- now keyboard works everytime even when a controller is plugged to be able to reconfigure mapping or navigate
2025-11-16 13:11:33 +01:00
6 changed files with 372 additions and 357 deletions

View File

@@ -4,6 +4,16 @@
A free, user-friendly ROM downloader for Batocera, Knulli, and RetroBat with multi-source support.
<p align="center">
<img width="69%" alt="platform menu" src="https://github.com/user-attachments/assets/4464b57b-06a8-45e9-a411-cc12b421545a" />
<img width="30%" alt="controls help" src="https://github.com/user-attachments/assets/38cac7e6-14f2-4e83-91da-0679669822ee" />
</p>
<p align="center">
<img width="49%" alt="web interface" src="https://github.com/user-attachments/assets/71f8bd39-5901-45a9-82b2-91426b3c31a7" />
<img width="49%" alt="api menu" src="https://github.com/user-attachments/assets/5bae018d-b7d9-4a95-9f1b-77db751ff24f" />
</p>
---
## 🚀 Installation
@@ -145,22 +155,13 @@ RGSX includes a web interface that launched automatically when using RGSX for re
### Enable/Disable Web Service at Boot, without the need to launch RGSX
**Method 1: From RGSX Menu**
**From RGSX Menu**
1. Open **Pause Menu** (Start/ALTGr)
2. Navigate to **Settings > Web Service**
3. Toggle **Enable at Boot**
4. Restart your device
**Method 2: Manual Configuration**
Edit `/saves/ports/rgsx/rgsx_settings.json`:
```json
{
"web_service": {
"enabled_at_boot": true
}
}
```
**Port Configuration**: The web service runs on port `5000` by default. Ensure this port is not blocked by firewall rules.
---
@@ -229,6 +230,7 @@ Free and open-source software. Use, modify, and distribute freely.
## Thanks to all contributors, and followers of this app
**If you want to support my project you can buy me a beer : https://bit.ly/donate-to-rgsx**
[![Stargazers over time](https://starchart.cc/RetroGameSets/RGSX.svg?variant=adaptive)](https://starchart.cc/RetroGameSets/RGSX)
**Developed with ❤️ for the retro gaming community.**
**Developed with ❤️ for the retro gaming community.**

View File

@@ -1,268 +1,236 @@
# 🎮 Retro Game Sets Xtra (RGSX)
## SUPPORT / HELP : https://discord.gg/Vph9jwg3VV
**[Support / Aide Discord](https://discord.gg/Vph9jwg3VV)** • **[Installation](#-installation)** • **[Documentation anglaise](https://github.com/RetroGameSets/RGSX/blob/main/README.md)**
RGSX est une application développée en Python basée sur Pygame pour la partie graphique pour la communauté par RetroGameSets. Elle est entièrement gratuite.
Un téléchargeur de ROMs gratuit et facile à utiliser pour Batocera, Knulli et RetroBat avec support multi-sources.
L'application prend en charge plusieurs sources comme myrient, 1fichier (avec support de débridage via AllDebrid en option). Ces sources pourront être mises à jour fréquemment.
<p align="center">
<img width="69%" alt="menu plateformes" src="https://github.com/user-attachments/assets/4464b57b-06a8-45e9-a411-cc12b421545a" />
<img width="30%" alt="aide contrôles" src="https://github.com/user-attachments/assets/38cac7e6-14f2-4e83-91da-0679669822ee" />
</p>
<p align="center">
<img width="49%" alt="interface web" src="https://github.com/user-attachments/assets/71f8bd39-5901-45a9-82b2-91426b3c31a7" />
<img width="49%" alt="menu API" src="https://github.com/user-attachments/assets/5bae018d-b7d9-4a95-9f1b-77db751ff24f" />
</p>
## INSTALLATION : https://github.com/RetroGameSets/RGSX/blob/main/README_FR.md#-installation
## ✨ Fonctionnalités
- **Téléchargement de jeux** : Prise en charge des fichiers ZIP et gestion des extensions non supportées à partir du fichier `es_systems.cfg` d'EmulationStation (et des `es_systems_*.cfg` personnalisés sur Batocera). RGSX lit les extensions autorisées par système depuis ces configurations et extrait automatiquement les archives si le système ne les supporte pas.
- Les téléchargements ne nécessitent aucune authentification ni compte pour la plupart.
- Les systèmes notés `(1fichier)` dans le nom ne seront accessibles que si vous renseignez votre clé API (1Fichier,AllDebrid, Real-Debrid)
---
> ## IMPORTANT (1Fichier / AllDebrid / Real-Debdrid)
> Pour télécharger depuis des liens 1Fichier, vous pouvez utiliser soit votre clé API 1Fichier, soit votre clé API AllDebrid (fallback automatique si 1Fichier est absent).
>
> Où coller votre clé API (le fichier doit contenir uniquement la clé) :
> - `/saves/ports/rgsx/1FichierAPI.txt` (clé API 1Fichier)
> - `/saves/ports/rgsx/AllDebridAPI.txt` (clé API AllDebrid)
> - `/saves/ports/rgsx/RealDebridAPI.txt` (clé API Real-Debrid)
>
> Ne créez PAS ces fichiers manuellement. Lancez une première fois l'application RGSX : elle créera automatiquement les fichiers vides sils sont absents. Ensuite, ouvrez le fichier correspondant et collez votre clé.
---
**🧰 Utilisation en ligne de commande (CLI)**
RGSX propose aussi une interface en ligne de commande (sans interface graphique) pour lister les plateformes/jeux et télécharger des ROMs :
- Guide FR: voir `https://github.com/RetroGameSets/RGSX/blob/main/README_CLI.md`
- **Historique des téléchargements** : Consultez la liste de tous les téléchargements actuels et anciens.
- **Téléchargements multi-sélection** : Marquez plusieurs jeux dans la liste avec la touche associée à Vider Historique (par défaut X) pour préparer un lot. Appuyez ensuite sur Confirmer pour lancer les téléchargements en séquence.
- **Personnalisation des contrôles** : Remappez les touches du clavier ou de la manette à votre convenance, par defaut certaines manettes sont automatiquement configurées
- **Grille des plateformes** : Possibilité de modifier la disposition de la grille des plateformes (3x3, 3x4, 4x3, 4x4)
- **Afficher/Masquer plateformes non supportées** : masquage automatique des systèmes dont le dossier ROM est absent selon `es_systems.cfg`, avec un interrupteur dans le menu Affichage.
- **Changement de police et de taille** : Si vous trouvez les écritures trop petites/trop grosses, pas assez lisibles, vous pouvez le changer dans le menu.
- **Mode recherche / Filtre** : Filtrez les jeux par nom pour une navigation rapide avec clavier virtuel sur manette.
- **Support multilingue** : Interface disponible en plusieurs langues. Vous pourrez choisir la langue dans le menu.
- **Interface adaptative** : L'interface s'adapte à toutes résolutions de 800x600 à 4K (non testé au-delà de 1920x1080).
- **Mise à jour automatique** : l'application se relance automatiquement après une mise à jour.
- **Systèmes et Extensions des fichiers** : à la première utilisation, RGSX lit `es_systems.cfg` (RetroBat/Batocera) et génère `/saves/ports/rgsx/rom_extensions.json` avec les extensions autorisées par système. Ainsi que la liste des plateformes prises en charge par le système.
---
## 🖥️ Prérequis
### Système d'exploitation
- Batocera / Knulli ou Retrobat
### Matériel
- PC, Raspberry, console portable...
- Manette (optionnelle, mais recommandée pour une expérience optimale) ou Clavier.
- Connexion internet active
### Espace disque
- 100 Mo pour l'application.
---
## 🚀 Installation
### Méthode Automatique : BATOCERA / KNULLI
### Installation rapide (Batocera / Knulli)
- Sur un PC lancer un terminal XTERM depuis le menu F1>Applications
- Depuis un autre équipement sur le réseau avec application Putty ou autre logiciel prenant en charge le SSH (connectez vous à l'IP user=root pass=linux)
**Accès SSH ou Terminal requis :**
```bash
curl -L bit.ly/rgsx-install | sh
```
**Entrez la commande :**
**`curl -L bit.ly/rgsx-install | sh`**
Patientez et regardez le retour à l'écran ou sur la commande.
Après l'installation :
1. Mettez à jour les listes de jeux : `Menu > Paramètres des jeux > Mettre à jour la liste des jeux`
2. Trouvez RGSX dans **PORTS** ou **Jeux amateurs et portages**
Vous trouverez RGSX dans le système "PORTS" ou "Jeux Amateurs et portages" (et physiquement dans `/roms/ports/RGSX` et `/roms/windows/rgsx` pour Retrobat.
### Installation manuelle (Tous systèmes)
1. **Télécharger** : [RGSX_full_latest.zip](https://github.com/RetroGameSets/RGSX/releases/latest/download/RGSX_full_latest.zip)
2. **Extraire** :
- **Batocera/Knulli** : extraire le dossier `ports` dans `/roms/`
- **RetroBat** : extraire les dossiers `ports` et `windows` dans `/roms/`
3. **Rafraîchir** : `Menu > Paramètres des jeux > Mettre à jour la liste des jeux`
Mettez à jour la liste des jeux via : `Menu > Paramètres de jeux > Mettre à jour la liste des jeux` si l'application n'apparaît pas !
### Mise à jour manuelle (si la mise à jour automatique a échoué)
Téléchargez la dernière version : [RGSX_update_latest.zip](https://github.com/RetroGameSets/RGSX/releases/latest/download/RGSX_full_latest.zip)
**Chemins d'installation :**
- `/roms/ports/RGSX` (tous systèmes)
- `/roms/windows/RGSX` (RetroBat uniquement)
---
### Méthode manuelle (Retrobat / Batocera)
## 🎮 Utilisation
- Téléchargez le contenu du dépôt en zip : https://github.com/RetroGameSets/RGSX/archive/refs/heads/main.zip
- Extraire le fichier zip dans le dossier ROMS de votre installation (pour Batocera, seulement le dossier PORTS, pour Retrobat : PORTS et WINDOWS)
- Vous aurez donc les dossiers `/roms/ports/RGSX` et `/roms/windows/rgsx`
- Mettez à jour la liste des jeux via : `Menu > Paramètres de jeux > Mettre à jour la liste des jeux` si l'application n'apparaît pas !
### Premier lancement
- Téléchargement automatique des images systèmes et des listes de jeux
- Configuration automatique des contrôles si votre manette est reconnue
- **Contrôles cassés ?** Supprimez `/saves/ports/rgsx/controls.json` puis relancez
**Mode clavier** : lorsqu'aucune manette n'est détectée, les contrôles s'affichent sous forme de `[Touche]` au lieu d'icônes.
### Structure du menu pause
**Contrôles**
- Voir l'aide des contrôles
- Remapper les contrôles
**Affichage**
- Disposition (3×3, 3×4, 4×3, 4×4)
- Taille de police (UI générale)
- Taille de police du footer (texte des contrôles/version)
- Famille de police (polices pixel)
- Masquer l'avertissement d'extension inconnue
**Jeux**
- Historique des téléchargements
+- Mode des sources (RGSX / Personnalisé)
- Mettre à jour le cache des jeux
- Afficher les plateformes non supportées
- Masquer les systèmes premium
- Filtrer les plateformes
**Paramètres**
- Musique de fond (on/off)
- Options de symlink (Batocera)
- Service web (Batocera)
- Gestion des clés API
- Sélection de la langue
---
## 🏁 Premier démarrage
## ✨ Fonctionnalités
- Vous trouverez RGSX dans le système "WINDOWS" sur Retrobat et dans "PORTS" ou "Jeux Amateurs et portages"
- Au premier lancement, l'application importera automatiquement la configuration des contrôles depuis des fichiers pré-configurés dans /roms/ports/RGSX/assets/controls si votre manette est reconnue
- L'application téléchargera toutes les données nécessaires automatiquement ensuite (images des systèmes, liste des jeux, etc.)
- 🎯 **Détection intelligente des systèmes** Découverte automatique des systèmes supportés depuis `es_systems.cfg`
- 📦 **Gestion intelligente des archives** Extraction automatique quand un système ne supporte pas les fichiers ZIP
- 🔑 **Débloquage premium** API 1Fichier + fallback AllDebrid/Real-Debrid pour des téléchargements illimités
- 🎨 **Entièrement personnalisable** Disposition (3×3 à 4×4), polices, tailles de police (UI + footer), langues (EN/FR/DE/ES/IT/PT)
- 🎮 **Pensé manette d'abord** Auto-mapping pour les manettes populaires + remapping personnalisé
- 🔍 **Filtrage avancé** Recherche par nom, affichage/masquage des systèmes non supportés, filtre de plateformes
- 📊 **Gestion des téléchargements** File d'attente, historique, notifications de progression
- 🌐 **Sources personnalisées** Utilisez vos propres URLs de dépôt de jeux
-**Accessibilité** Échelles de police séparées pour l'UI et le footer, support du mode clavier seul
INFO : pour retrobat au premier lancement, l'application téléchargera Python dans le dossier /system/tools/python qui est nécessaire pour faire fonctionner l'application. Le fichier fait environ 50 Mo et va assez vite à télécharger mais il n'y a aucun retour visuel à l'écran, qui va rester figé sur le chargement de RGSX pendant quelques secondes. Vous trouvez le log d'installation dans `/roms/ports/RGSX-INSTALL.log` à fournir en cas de problème.
> ### 🔑 Configuration des clés API
> Pour des téléchargements 1Fichier illimités, ajoutez vos clés API dans `/saves/ports/rgsx/` :
> - `1FichierAPI.txt` Clé API 1Fichier (recommandé)
> - `AllDebridAPI.txt` Fallback AllDebrid (optionnel)
> - `RealDebridAPI.txt` Fallback Real-Debrid (optionnel)
>
> **Chaque fichier ne doit contenir QUE la clé, sans texte supplémentaire.**
---
### Télécharger des jeux
## 🕹️ Utilisation
1. Parcourez les plateformes → sélectionnez un jeu
2. **Téléchargement direct** : appuyez sur `Confirmer`
3. **Ajout à la file d'attente** : appuyez sur `X` (bouton Ouest)
4. Suivez la progression dans le menu **Historique** ou via les popups de notification
### Navigation dans les menus
### Sources de jeux personnalisées
- Utilisez les touches directionnelles (D-Pad, flèches du clavier) pour naviguer entre les plateformes, jeux et options.
- Appuyez sur la touche configurée comme start (par défaut, **P** ou bouton Start sur la manette) pour ouvrir le menu pause. Depuis ce menu, accédez à toute la configuration de l'application.
- Vous pouvez aussi, depuis le menu, régénérer le cache de la liste des systèmes/jeux/images pour être sûr d'avoir les dernières mises à jour.
Basculez vers les sources personnalisées via **Menu pause > Jeux > Mode des sources**.
---
#### Menu Affichage
- Disposition: basculez la grille des plateformes entre 3x3, 3x4, 4x3, 4x4.
- Taille de police: ajustez léchelle du texte (accessibilité).
- Afficher plateformes non supportées: afficher/masquer les systèmes dont le dossier ROM est absent.
- Filtrer les systèmes: afficher/masquer rapidement des plateformes par nom (persistant).
---
### Téléchargement
- Sélectionnez une plateforme, puis un jeu.
- Appuyez sur la touche configurée confirm (par défaut, **Entrée** ou bouton **A**) pour lancer le téléchargement.
- Option : appuyez sur la touche Vider Historique (par défaut **X**) sur plusieurs jeux pour activer/désactiver leur sélection (marqueur [X]). Puis validez pour lancer un lot de téléchargements.
- Suivez la progression dans le menu `HISTORIQUE`.
---
### Personnalisation des contrôles
- Dans le menu pause, sélectionnez **Reconfigurer controles**.
- Suivez les instructions à l'écran pour mapper chaque action en maintenant la touche ou le bouton pendant 3 secondes.
- Les noms des boutons s'affichent automatiquement selon votre manette (A, B, X, Y, LB, RB, LT, RT, etc.).
- La configuration est compatible avec toutes les manettes supportées par EmulationStation.
- En cas de problème de contrôles ou configuration corrompue, supprimez le fichier : `/saves/ports/rgsx/controls.json` s'il existe puis redémarrez l'application (il sera recréé automatiquement).
---
### Historique
- Accédez à l'historique des téléchargements via le menu pause ou en appuyant sur la touche historique (par défaut, **H**).
- Sélectionnez un jeu pour le retélécharger si nécessaire en cas d'erreur ou annulation.
- Videz tout l'historique via le bouton **EFFACER** dans le menu historique. Les jeux ne sont pas effacés seulement la liste.
- Annulez un téléchargement avec le bouton **RETOUR**
---
### Logs
Les logs sont enregistrés dans `/roms/ports/RGSX/logs/RGSX.log` sur batocera et sur Retrobat pour diagnostiquer les problèmes et seront à partager pour tout support.
---
## 🔄 Journal des modifications
Toutes les infos sur discord ou sur les commit github.
---
## 🌐 Sources de jeux personnalisées
Vous pouvez changer la source dans le menu pause (Source des jeux : RGSX / Personnalisée).
Le mode personnalisé attend une URL ZIP (HTTP/HTTPS) pointant vers une archive des sources avec la même structure que celle par défaut. À configurer dans :
`{chemin rgsx_settings}` → clé : `sources.custom_url`
Comportement :
- Si mode personnalisé sélectionné et URL vide/invalide → liste vide + popup (aucun fallback)
- Corrigez lURL puis utilisez "Mettre à jour la liste des jeux" et redémarrez si nécessaire
Exemple dans rgsx_settings.json :
Configurez dans `/saves/ports/rgsx/rgsx_settings.json` :
```json
"sources": {
"mode": "custom",
"custom_url": "https://exemple.com/mes-sources.zip"
{
"sources": {
"mode": "custom",
"custom_url": "https://example.com/my-sources.zip"
}
}
```
Revenez au mode RGSX à tout moment via le menu pause.
**Note** : si le mode personnalisé est activé mais que l'URL est invalide/vide = utilisation de `/saves/ports/rgsx/games.zip`. Vous devez mettre à jour le cache des jeux dans le menu RGSX après avoir corrigé l'URL.
---
## 📁 Structure du projet
## 🌐 Interface web (Batocera/Knulli uniquement)
RGSX inclut une interface web qui se lance automatiquement avec RGSX pour parcourir et télécharger des jeux à distance depuis n'importe quel appareil de votre réseau.
### Accéder à l'interface web
1. **Trouvez l'adresse IP de votre Batocera** :
- Dans le menu Batocera : `Paramètres réseau`
- Ou depuis un terminal : `ip addr show`
2. **Ouvrez dans un navigateur** : `http://[IP_BATO]:5000` ou `http://BATOCERA:5000`
- Exemple : `http://192.168.1.100:5000`
3. **Accessible depuis n'importe quel appareil** : téléphone, tablette, PC sur le même réseau
### Fonctionnalités de l'interface web
- 📱 **Compatible mobile** Design responsive qui fonctionne sur tous les écrans
- 🔍 **Parcourir tous les systèmes** Voir toutes les plateformes et les jeux
- ⬇️ **Téléchargements à distance** Ajouter des téléchargements directement sur votre Batocera
- 📊 **Statut en temps réel** Voir les téléchargements actifs et l'historique
- 🎮 **Même liste de jeux** Utilise les mêmes sources que l'application principale
### Activer/Désactiver le service web au démarrage, sans lancer RGSX
**Depuis le menu RGSX**
1. Ouvrez le **menu pause** (Start/ALTGr)
2. Allez dans **Paramètres > Service web**
3. Basculez sur **Activer au démarrage**
4. Redémarrez votre appareil
**Configuration du port** : le service web utilise le port `5000` par défaut. Assurez-vous qu'il n'est pas bloqué par un pare-feu.
---
## 📁 Structure des fichiers
```
/roms/windows/RGSX
├── RGSX Retrobat.bat # Raccourci pour lancer l'application RGSX pour retrobat uniquement, non nécessaire pour batocera/knulli
/roms/ports/RGSX/
├── __main__.py # Point d'entrée
├── controls.py # Gestion des entrées
├── display.py # Moteur de rendu
├── network.py # Gestionnaire de téléchargements
├── rgsx_settings.py # Gestionnaire de paramètres
├── assets/controls/ # Profils de manettes
├── languages/ # Traductions (EN/FR/DE/ES/IT/PT)
└── logs/RGSX.log # Logs d'exécution
/roms/ports/
── RGSX-INSTALL.log # LOG d'installation uniquement
└── RGSX/
│ └──── __main__.py # Point d'entrée principal de l'application.
│ ├──── controls.py # Gestion des événements de navigation dans les menus.
│ ├──── controls_mapper.py # Configuration des contrôles
│ ├──── display.py # Rendu des interfaces graphiques avec Pygame.
│ ├──── config.py # Configuration globale (chemins, paramètres, etc.).
│ ├──── rgsx_settings.py # Gestion unifiée des paramètres de l'application.
│ ├──── network.py # Gestion des téléchargements de jeux.
│ ├──── history.py # Gestion de l'historique des téléchargements.
│ ├──── language.py # Gestion du support multilingue.
│ ├──── accessibility.py # Gestion des paramètres d'accessibilité.
│ ├──── utils.py # Fonctions utilitaires (wrap du texte, troncage etc.).
│ ├──── update_gamelist.py # Mise à jour de la liste des jeux (Batocera/Knulli).
│ └──── update_gamelist_windows.py # MAJ gamelist retrobat au lancement.
└────logs/
│ └──── RGSX.log # Fichier de logs.
└── assets/ # Ressources de l'application (polices, exécutables, musique).
└──── controls/ # Fichiers de configuration des contrôles pré-définis
└──── languages/ # Fichiers de traduction
/roms/windows/RGSX/
── RGSX Retrobat.bat # Lanceur RetroBat
/saves/ports/RGSX/
├── systems_list.json # Liste des systèmes / dossiers / images.
├── games/ # Liens des jeux / plateformes
├── images/ # Images des plateformes.
├── rgsx_settings.json # Fichier de configuration des paramètres.
├── controls.json # Fichier de mappage des contrôles manuel
├── history.json # Base de données de l'historique de téléchargements
├── rom_extensions.json # Généré depuis es_systems.cfg : extensions autorisées
── 1FichierAPI.txt # Clé API 1fichier
└── AllDebridAPI.txt # Clé API AllDebrid
/saves/ports/rgsx/
├── rgsx_settings.json # Préférences utilisateur
├── controls.json # Mappage des contrôles
├── history.json # Historique des téléchargements
├── rom_extensions.json # Cache des extensions supportées
├── systems_list.json # Systèmes détectés
├── games/ # Bases de données de jeux (par plateforme)
├── images/ # Images des plateformes
├── 1FichierAPI.txt # Clé API 1Fichier
├── AllDebridAPI.txt # Clé API AllDebrid
── RealDebridAPI.txt # Clé API Real-Debrid
```
---
## 🛠️ Dépannage
| Problème | Solution |
|----------|----------|
| Contrôles qui ne répondent plus | Supprimer `/saves/ports/rgsx/controls.json` + redémarrer |
| Jeux non affichés | Menu pause > Jeux > Mettre à jour le cache des jeux |
| Téléchargement bloqué | Vérifier les clés API dans `/saves/ports/rgsx/` |
| Crash de l'application | Vérifier `/roms/ports/RGSX/logs/RGSX.log` |
| Changement de layout non pris en compte | Redémarrer RGSX après modification du layout |
**Besoin d'aide ?** Partagez les logs depuis `/roms/ports/RGSX/logs/` sur [Discord](https://discord.gg/Vph9jwg3VV).
---
## 🤝 Contribution
### Signaler un bug
1. Consultez les logs dans `/roms/ports/RGSX/logs/RGSX.log`.
2. Envoyez un message sur le discord avec le log complet et une description du problème.
- Lien Discord : https://discord.gg/Vph9jwg3VV
### Proposer une fonctionnalité
- Discutez de votre idée sur le discord pour obtenir des retours.
- Soumettez une issue avec une description claire de la fonctionnalité proposée.
- Expliquez comment elle s'intègre dans l'application.
### Contribuer au code
1. Forkez le dépôt et créez une branche pour votre fonctionnalité ou correction :
```bash
git checkout -b feature/nom-de-votre-fonctionnalité
```
2. Testez vos modifications sur Batocera.
3. Soumettez une pull request avec une description détaillée.
---
## ⚠️ Problèmes connus
- (Aucun listé actuellement)
- **Rapports de bugs** : ouvrez une issue GitHub avec les logs ou postez sur Discord
- **Demandes de fonctionnalités** : discutez d'abord sur Discord, puis ouvrez une issue
- **Contributions de code** :
```bash
git checkout -b feature/your-feature
# Testez sur Batocera/RetroBat
# Soumettez une Pull Request
```
---
## 📝 Licence
Ce projet est libre. Vous êtes libre de l'utiliser, le modifier et le distribuer selon les termes de cette licence.
Logiciel libre et open-source. Utilisation, modification et distribution autorisées librement.
## Merci à tous les contributeurs et aux personnes qui suivent l'application
[![Stargazers over time](https://starchart.cc/RetroGameSets/RGSX.svg?variant=adaptive)](https://starchart.cc/RetroGameSets/RGSX)
**Développé avec ❤️ pour la communauté du retrogaming.**
Développé avec ❤️ pour les amateurs de jeux rétro.

View File

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

View File

@@ -15,14 +15,12 @@ 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,
toggle_web_service_at_boot, check_web_service_status,
toggle_custom_dns_at_boot, check_custom_dns_status,
extract_zip, extract_rar, find_file_with_or_without_extension, 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
)
from history import load_history, clear_history, add_to_history, save_history
from language import _, get_available_languages, set_language
from language import _, get_available_languages, set_language
from rgsx_settings import (
get_allow_unknown_extensions, set_display_grid, get_font_family, set_font_family,
get_show_unsupported_platforms, set_show_unsupported_platforms,
@@ -198,7 +196,21 @@ def is_input_matched(event, action_name):
elif input_type == "axis" and event.type == pygame.JOYAXISMOTION:
axis = mapping.get("axis")
direction = mapping.get("direction")
return event.axis == axis and abs(event.value) > 0.5 and (1 if event.value > 0 else -1) == direction
threshold = 0.5
# Pour les triggers Xbox (axes 4 et 5), la position de repos est -1.0
# Il faut inverser la détection : direction -1 = trigger appuyé (vers +1.0)
if axis in [4, 5]:
# Triggers Xbox: repos à -1.0, appuyé vers +1.0
# On inverse la direction configurée
if direction == -1:
# Direction -1 configurée = détecter quand trigger appuyé (valeur positive)
return event.axis == axis and event.value > threshold
else:
# Direction +1 configurée = détecter aussi quand trigger appuyé
return event.axis == axis and event.value > threshold
else:
# Autres axes: logique normale
return event.axis == axis and abs(event.value) > threshold and (1 if event.value > 0 else -1) == direction
elif input_type == "hat" and event.type == pygame.JOYHATMOTION:
hat_value = mapping.get("value")
if isinstance(hat_value, list):
@@ -206,6 +218,28 @@ def is_input_matched(event, action_name):
return event.value == hat_value
elif input_type == "mouse" and event.type == pygame.MOUSEBUTTONDOWN:
return event.button == mapping.get("button")
# Fallback clavier pour dépannage (fonctionne toujours même avec manette configurée)
if event.type == pygame.KEYDOWN:
keyboard_fallback = {
"up": pygame.K_UP,
"down": pygame.K_DOWN,
"left": pygame.K_LEFT,
"right": pygame.K_RIGHT,
"confirm": pygame.K_RETURN,
"cancel": pygame.K_ESCAPE,
"start": pygame.K_RALT,
"filter": pygame.K_f,
"history": pygame.K_h,
"clear_history": pygame.K_DELETE,
"delete": pygame.K_d,
"space": pygame.K_SPACE,
"page_up": pygame.K_PAGEUP,
"page_down": pygame.K_PAGEDOWN,
}
if action_name in keyboard_fallback:
return event.key == keyboard_fallback[action_name]
return False
def _launch_next_queued_download():
@@ -1417,7 +1451,7 @@ def handle_controls(event, sources, joystick, screen):
# Sous-menu Display
elif config.menu_state == "pause_display_menu":
sel = getattr(config, 'pause_display_selection', 0)
total = 6 # layout, font size, footer font size, font family, unknown, back
total = 6 # layout, font size, footer font size, font family, allow unknown extensions, back
if is_input_matched(event, "up"):
config.pause_display_selection = (sel - 1) % total
config.needs_redraw = True
@@ -1425,7 +1459,6 @@ def handle_controls(event, sources, joystick, screen):
config.pause_display_selection = (sel + 1) % total
config.needs_redraw = True
elif is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm"):
sel = getattr(config, 'pause_display_selection', 0)
# 0 layout cycle
if sel == 0 and (is_input_matched(event, "left") or is_input_matched(event, "right")):
layouts = [(3,3),(3,4),(4,3),(4,4)]
@@ -1441,12 +1474,12 @@ def handle_controls(event, sources, joystick, screen):
logger.error(f"Erreur set_display_grid: {e}")
config.GRID_COLS = new_cols
config.GRID_ROWS = new_rows
# Afficher popup au lieu de redémarrer
# Afficher un popup indiquant que le changement sera effectif après redémarrage
try:
restart_msg = _("popup_layout_changed_restart").format(new_cols, new_rows) if _ else f"Layout changed to {new_cols}x{new_rows}. Please restart the app to apply changes."
show_toast(restart_msg, duration=3000)
config.popup_message = _("popup_layout_changed_restart_required") if _ else "Layout changed. Restart required to apply."
config.popup_timer = 3000
except Exception as e:
logger.error(f"Erreur toast après layout: {e}")
logger.error(f"Erreur popup layout: {e}")
config.needs_redraw = True
# 1 font size
elif sel == 1 and (is_input_matched(event, "left") or is_input_matched(event, "right")):
@@ -1468,23 +1501,16 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
# 2 footer font size
elif sel == 2 and (is_input_matched(event, "left") or is_input_matched(event, "right")):
opts = getattr(config, 'footer_font_scale_options', [0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0])
from accessibility import update_footer_font_scale
footer_opts = getattr(config, 'footer_font_scale_options', [0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0])
idx = getattr(config, 'current_footer_font_scale_index', 3)
idx = max(0, idx-1) if is_input_matched(event, "left") else min(len(opts)-1, idx+1)
idx = max(0, idx-1) if is_input_matched(event, "left") else min(len(footer_opts)-1, idx+1)
if idx != getattr(config, 'current_footer_font_scale_index', 3):
config.current_footer_font_scale_index = idx
scale = opts[idx]
config.accessibility_settings["footer_font_scale"] = scale
try:
save_accessibility_settings(config.accessibility_settings)
update_footer_font_scale()
except Exception as e:
logger.error(f"Erreur sauvegarde footer font scale: {e}")
try:
init_footer_font_func = getattr(config, 'init_footer_font', None)
if callable(init_footer_font_func):
init_footer_font_func()
except Exception as e:
logger.error(f"Erreur init footer font: {e}")
logger.error(f"Erreur update footer font scale: {e}")
config.needs_redraw = True
# 3 font family cycle
elif sel == 3 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
@@ -1531,7 +1557,7 @@ def handle_controls(event, sources, joystick, screen):
except Exception as e:
logger.error(f"Erreur toggle allow_unknown_extensions: {e}")
# 5 back
elif sel == 5 and (is_input_matched(event, "confirm")):
elif sel == 5 and is_input_matched(event, "confirm"):
config.menu_state = "pause_menu"
config.last_state_change_time = pygame.time.get_ticks()
config.needs_redraw = True
@@ -1551,7 +1577,6 @@ def handle_controls(event, sources, joystick, screen):
config.pause_games_selection = (sel + 1) % total
config.needs_redraw = True
elif is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right"):
sel = getattr(config, 'pause_games_selection', 0)
if sel == 0 and is_input_matched(event, "confirm"): # history
config.history = load_history()
config.current_history_item = 0
@@ -1579,8 +1604,7 @@ def handle_controls(event, sources, joystick, screen):
config.menu_state = "reload_games_data"
config.redownload_confirm_selection = 0
config.needs_redraw = True
# 3 unsupported toggle
elif sel == 3 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
elif sel == 3 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # unsupported toggle
try:
current = get_show_unsupported_platforms()
new_val = set_show_unsupported_platforms(not current)
@@ -1590,8 +1614,7 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur toggle unsupported: {e}")
# 4 hide premium systems
elif sel == 4 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")):
elif sel == 4 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # hide premium
try:
cur = get_hide_premium_systems()
new_val = set_hide_premium_systems(not cur)
@@ -1600,8 +1623,7 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur toggle hide_premium_systems: {e}")
# 5 filter platforms
elif sel == 5 and (is_input_matched(event, "confirm") or is_input_matched(event, "right")):
elif sel == 5 and is_input_matched(event, "confirm"): # filter platforms
config.filter_return_to = "pause_games_menu"
config.menu_state = "filter_platforms"
config.selected_filter_index = 0
@@ -1622,11 +1644,9 @@ def handle_controls(event, sources, joystick, screen):
# Calculer le nombre total d'options selon le système
total = 4 # music, symlink, api keys, back
web_service_index = -1
custom_dns_index = -1
if config.OPERATING_SYSTEM == "Linux":
total = 6 # music, symlink, web_service, custom_dns, api keys, back
total = 5 # music, symlink, web_service, api keys, back
web_service_index = 2
custom_dns_index = 3
if is_input_matched(event, "up"):
config.pause_settings_selection = (sel - 1) % total
@@ -1635,7 +1655,6 @@ def handle_controls(event, sources, joystick, screen):
config.pause_settings_selection = (sel + 1) % total
config.needs_redraw = True
elif is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right"):
sel = getattr(config, 'pause_settings_selection', 0)
# Option 0: Music toggle
if sel == 0 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")):
config.music_enabled = not config.music_enabled
@@ -1646,11 +1665,7 @@ def handle_controls(event, sources, joystick, screen):
if music_files and music_folder:
config.current_music = play_random_music(music_files, music_folder, getattr(config, "current_music", None))
else:
try:
if pygame.mixer.get_init() is not None:
pygame.mixer.music.stop()
except (AttributeError, NotImplementedError):
pass
pygame.mixer.music.stop()
config.needs_redraw = True
logger.info(f"Musique {'activée' if config.music_enabled else 'désactivée'} via settings")
# Option 1: Symlink toggle
@@ -1663,6 +1678,7 @@ def handle_controls(event, sources, joystick, screen):
logger.info(f"Symlink option {'activée' if not current_status else 'désactivée'} via settings")
# Option 2: Web Service toggle (seulement si Linux)
elif sel == web_service_index and web_service_index >= 0 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")):
current_status = check_web_service_status()
# Afficher un message de chargement
config.popup_message = _("settings_web_service_enabling") if not current_status else _("settings_web_service_disabling")
@@ -1679,26 +1695,8 @@ def handle_controls(event, sources, joystick, screen):
else:
logger.error(f"Erreur toggle service web: {message}")
threading.Thread(target=toggle_service, daemon=True).start()
# Option 3: Custom DNS toggle (seulement si Linux)
elif sel == custom_dns_index and custom_dns_index >= 0 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")):
current_status = check_custom_dns_status()
# Afficher un message de chargement
config.popup_message = _("settings_custom_dns_enabling") if not current_status else _("settings_custom_dns_disabling")
config.popup_timer = 1000
config.needs_redraw = True
# Exécuter en thread pour ne pas bloquer l'UI
def toggle_dns():
success, message = toggle_custom_dns_at_boot(not current_status)
config.popup_message = message
config.popup_timer = 5000 if success else 7000
config.needs_redraw = True
if success:
logger.info(f"Service custom DNS {'activé' if not current_status else 'désactivé'} au démarrage")
else:
logger.error(f"Erreur toggle service custom DNS: {message}")
threading.Thread(target=toggle_dns, daemon=True).start()
# Option API Keys (index varie selon Linux ou pas)
elif sel == (custom_dns_index + 1 if custom_dns_index >= 0 else 2) and is_input_matched(event, "confirm"):
elif sel == (web_service_index + 1 if web_service_index >= 0 else 2) and is_input_matched(event, "confirm"):
config.menu_state = "pause_api_keys_status"
config.needs_redraw = True
# Option Back (dernière option)
@@ -1734,7 +1732,6 @@ def handle_controls(event, sources, joystick, screen):
config.display_menu_selection = (sel + 1) % 5
config.needs_redraw = True
elif is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm"):
sel = getattr(config, 'display_menu_selection', 0)
# 0: layout change
if sel == 0 and (is_input_matched(event, "left") or is_input_matched(event, "right")):
layouts = [(3,3),(3,4),(4,3),(4,4)]
@@ -1926,44 +1923,53 @@ def handle_controls(event, sources, joystick, screen):
elif config.menu_state == "filter_platforms":
total_items = len(config.filter_platforms_selection)
action_buttons = 4
extended_max = total_items + action_buttons - 1
# Indices: 0-3 = boutons, 4+ = liste des systèmes
extended_max = action_buttons + total_items - 1
if is_input_matched(event, "up"):
if config.selected_filter_index > 0:
config.selected_filter_index -= 1
config.needs_redraw = True
else:
# Wrap vers les boutons (premier bouton) depuis le haut
if total_items > 0:
config.selected_filter_index = total_items
config.needs_redraw = True
# Wrap vers le bas (dernière ligne de la liste)
config.selected_filter_index = extended_max
config.needs_redraw = True
# Activer la répétition automatique
update_key_state("up", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
elif is_input_matched(event, "down"):
if config.selected_filter_index < extended_max:
config.selected_filter_index += 1
config.needs_redraw = True
else:
# Wrap retour en haut de la liste
# Wrap retour en haut (premier bouton)
config.selected_filter_index = 0
config.needs_redraw = True
# Activer la répétition automatique
update_key_state("down", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
elif is_input_matched(event, "left"):
if config.selected_filter_index >= total_items:
if config.selected_filter_index > total_items:
# Navigation gauche/droite uniquement pour les boutons (indices 0-3)
if config.selected_filter_index < action_buttons:
if config.selected_filter_index > 0:
config.selected_filter_index -= 1
config.needs_redraw = True
# sinon ignorer
# sinon ignorer (dans la liste)
elif is_input_matched(event, "right"):
if config.selected_filter_index >= total_items:
if config.selected_filter_index < extended_max:
# Navigation gauche/droite uniquement pour les boutons (indices 0-3)
if config.selected_filter_index < action_buttons:
if config.selected_filter_index < action_buttons - 1:
config.selected_filter_index += 1
config.needs_redraw = True
# sinon ignorer
# sinon ignorer (dans la liste)
elif is_input_matched(event, "confirm"):
if config.selected_filter_index < total_items:
name, hidden = config.filter_platforms_selection[config.selected_filter_index]
config.filter_platforms_selection[config.selected_filter_index] = (name, not hidden)
config.filter_platforms_dirty = True
config.needs_redraw = True
else:
btn_idx = config.selected_filter_index - total_items
# Indices 0-3 = boutons, 4+ = liste
if config.selected_filter_index < action_buttons:
# Action sur un bouton
btn_idx = config.selected_filter_index
settings = load_rgsx_settings()
if btn_idx == 0: # all visible
config.filter_platforms_selection = [(n, False) for n, _ in config.filter_platforms_selection]
@@ -2009,6 +2015,14 @@ def handle_controls(event, sources, joystick, screen):
config.selected_option = 5
config.filter_return_to = None
config.needs_redraw = True
else:
# Action sur un élément de la liste (indices >= action_buttons)
list_index = config.selected_filter_index - action_buttons
if list_index < total_items:
name, hidden = config.filter_platforms_selection[list_index]
config.filter_platforms_selection[list_index] = (name, not hidden)
config.filter_platforms_dirty = True
config.needs_redraw = True
elif is_input_matched(event, "cancel"):
target = getattr(config, 'filter_return_to', 'pause_menu')
config.menu_state = target
@@ -2025,13 +2039,32 @@ def handle_controls(event, sources, joystick, screen):
# Gestion des relâchements de touches
if event.type == pygame.KEYUP:
# Vérifier quelle touche a été relâchée
# Définir le mapping clavier (même que dans is_input_matched)
keyboard_fallback = {
"up": pygame.K_UP,
"down": pygame.K_DOWN,
"left": pygame.K_LEFT,
"right": pygame.K_RIGHT,
"confirm": pygame.K_RETURN,
"cancel": pygame.K_ESCAPE,
"page_up": pygame.K_PAGEUP,
"page_down": pygame.K_PAGEDOWN,
}
for action_name in ["up", "down", "left", "right", "page_up", "page_down", "confirm", "cancel"]:
if config.controls_config.get(action_name, {}).get("type") == "key" and \
# Vérifier d'abord le keyboard_fallback
if action_name in keyboard_fallback and keyboard_fallback[action_name] == event.key:
update_key_state(action_name, False)
# Sinon vérifier la config normale
elif config.controls_config.get(action_name, {}).get("type") == "key" and \
config.controls_config.get(action_name, {}).get("key") == event.key:
update_key_state(action_name, False)
# Gestion spéciale pour confirm dans le menu game
if action_name == "confirm" and config.menu_state == "game":
# Gestion spéciale pour confirm dans le menu game (ne dépend pas du key_state)
if action_name == "confirm" and config.menu_state == "game" and \
((action_name in keyboard_fallback and keyboard_fallback[action_name] == event.key) or \
(config.controls_config.get(action_name, {}).get("type") == "key" and \
config.controls_config.get(action_name, {}).get("key") == event.key)):
press_duration = current_time - config.confirm_press_start_time
# Si appui court (< 2 secondes) et pas déjà traité par l'appui long
if press_duration < config.confirm_long_press_threshold and not config.confirm_long_press_triggered:
@@ -2122,9 +2155,11 @@ def handle_controls(event, sources, joystick, screen):
for action_name in ["up", "down", "left", "right", "page_up", "page_down", "confirm", "cancel"]:
if config.controls_config.get(action_name, {}).get("type") == "button" and \
config.controls_config.get(action_name, {}).get("button") == event.button:
update_key_state(action_name, False)
# Vérifier que cette action était bien activée par un bouton gamepad
if action_name in key_states and key_states[action_name].get("event_type") == pygame.JOYBUTTONDOWN:
update_key_state(action_name, False)
# Gestion spéciale pour confirm dans le menu game
# Gestion spéciale pour confirm dans le menu game (ne dépend pas du key_state)
if action_name == "confirm" and config.menu_state == "game":
press_duration = current_time - config.confirm_press_start_time
# Si appui court (< 2 secondes) et pas déjà traité par l'appui long
@@ -2211,18 +2246,31 @@ def handle_controls(event, sources, joystick, screen):
config.confirm_press_start_time = 0
config.confirm_long_press_triggered = False
elif event.type == pygame.JOYAXISMOTION and abs(event.value) < 0.5:
# Vérifier quel axe a été relâché
for action_name in ["up", "down", "left", "right", "page_up", "page_down"]:
if config.controls_config.get(action_name, {}).get("type") == "axis" and \
config.controls_config.get(action_name, {}).get("axis") == event.axis:
update_key_state(action_name, False)
elif event.type == pygame.JOYAXISMOTION:
# Détection de relâchement d'axe
# Pour les triggers Xbox (axes 4 et 5), relâché = retour à -1.0
# Pour les autres axes, relâché = proche de 0
is_released = False
if event.axis in [4, 5]: # Triggers Xbox
is_released = event.value < 0.5 # Relâché si < 0.5 (pas appuyé)
else: # Autres axes
is_released = abs(event.value) < 0.5
if is_released:
for action_name in ["up", "down", "left", "right", "page_up", "page_down"]:
if config.controls_config.get(action_name, {}).get("type") == "axis" and \
config.controls_config.get(action_name, {}).get("axis") == event.axis:
# Vérifier que cette action était bien activée par cet axe
if action_name in key_states and key_states[action_name].get("event_type") == pygame.JOYAXISMOTION:
update_key_state(action_name, False)
elif event.type == pygame.JOYHATMOTION and event.value == (0, 0):
# Vérifier quel hat a été relâché
for action_name in ["up", "down", "left", "right", "page_up", "page_down"]:
if config.controls_config.get(action_name, {}).get("type") == "hat":
update_key_state(action_name, False)
# Vérifier que cette action était bien activée par un hat
if action_name in key_states and key_states[action_name].get("event_type") == pygame.JOYHATMOTION:
update_key_state(action_name, False)
return action
@@ -2234,11 +2282,9 @@ def update_key_state(action, pressed, event_type=None, event_value=None):
if pressed:
# La touche vient d'être pressée
if action not in key_states:
# Ajouter un délai initial pour éviter les doubles actions sur appui court
initial_debounce = REPEAT_ACTION_DEBOUNCE
key_states[action] = {
"pressed": True,
"first_press_time": current_time + initial_debounce, # Ajouter un délai initial
"first_press_time": current_time,
"last_repeat_time": current_time,
"event_type": event_type,
"event_value": event_value

View File

@@ -2014,12 +2014,14 @@ def draw_pause_display_menu(screen, selected_index):
fam_label = family_map.get(current_family, current_family)
font_family_txt = f"{_('submenu_display_font_family') if _ else 'Font'}: < {fam_label} >"
# Allow unknown extensions
allow_unknown = get_allow_unknown_extensions()
status_unknown = _('status_on') if allow_unknown else _('status_off')
raw_unknown_label = _('submenu_display_allow_unknown_ext') if _ else 'Hide unknown ext warn: {status}'
if '{status}' in raw_unknown_label:
raw_unknown_label = raw_unknown_label.split('{status}')[0].rstrip(' :')
unknown_txt = f"{raw_unknown_label}: < {status_unknown} >"
back_txt = _("menu_back") if _ else "Back"
options = [layout_txt, font_txt, footer_font_txt, font_family_txt, unknown_txt, back_txt]
_draw_submenu_generic(screen, _("menu_display"), options, selected_index)
@@ -2319,11 +2321,32 @@ def draw_filter_platforms_menu(screen):
pygame.draw.rect(screen, THEME_COLORS["border"], title_rect_inflated, 2, border_radius=12)
screen.blit(title_surface, title_rect)
# Zone liste
# Boutons d'action en haut (avant la liste)
btn_width = 220
btn_height = int(config.screen_height * 0.0463)
spacing = 30
buttons_y = title_rect_inflated.bottom + 20
center_x = config.screen_width // 2
actions = [
("filter_all", 0),
("filter_none", 1),
("filter_apply", 2),
("filter_back", 3)
]
total_items = len(config.filter_platforms_selection)
action_buttons = len(actions)
for idx, (key, btn_idx) in enumerate(actions):
btn_x = center_x - (len(actions) * (btn_width + spacing) - spacing) // 2 + idx * (btn_width + spacing)
is_selected = (config.selected_filter_index == btn_idx)
label = _(key)
draw_stylized_button(screen, label, btn_x, buttons_y, btn_width, btn_height, selected=is_selected)
# Zone liste (après les boutons)
list_width = int(config.screen_width * 0.7)
list_height = int(config.screen_height * 0.6)
list_height = int(config.screen_height * 0.5)
list_x = (config.screen_width - list_width) // 2
list_y = title_rect_inflated.bottom + 20
list_y = buttons_y + btn_height + 20
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (list_x, list_y, list_width, list_height), border_radius=12)
pygame.draw.rect(screen, THEME_COLORS["border"], (list_x, list_y, list_width, list_height), 2, border_radius=12)
@@ -2340,12 +2363,13 @@ def draw_filter_platforms_menu(screen):
elif config.selected_filter_index >= config.filter_platforms_scroll_offset + visible_items:
config.filter_platforms_scroll_offset = config.selected_filter_index - visible_items + 1
# Dessiner items
# Dessiner items (les indices de la liste commencent à action_buttons)
for i in range(config.filter_platforms_scroll_offset, min(config.filter_platforms_scroll_offset + visible_items, total_items)):
name, is_hidden = config.filter_platforms_selection[i]
idx_on_screen = i - config.filter_platforms_scroll_offset
y_center = list_y + 10 + idx_on_screen * line_height + line_height // 2
selected = (i == config.selected_filter_index)
# Les éléments de la liste ont des indices à partir de action_buttons
selected = (config.selected_filter_index == action_buttons + i)
checkbox = "[ ]" if is_hidden else "[X]" # inversé: coché signifie visible
# Correction: on veut [X] si visible => is_hidden False
checkbox = "[X]" if not is_hidden else "[ ]"
@@ -2365,37 +2389,12 @@ def draw_filter_platforms_menu(screen):
scroll_y = int((config.filter_platforms_scroll_offset / max(1, total_items - visible_items)) * (list_height - 20 - scroll_height))
pygame.draw.rect(screen, THEME_COLORS["fond_lignes"], (list_x + list_width - 25, list_y + 10 + scroll_y, 10, scroll_height), border_radius=4)
# Boutons d'action
btn_width = 220
btn_height = int(config.screen_height * 0.0463)
spacing = 30
buttons_y = list_y + list_height + 20
center_x = config.screen_width // 2
actions = [
("filter_all", -2),
("filter_none", -3),
("filter_apply", -4),
("filter_back", -5)
]
# Indice spécial sélection boutons quand selected_filter_index >= total_items
extra_index_base = total_items
# Ajuster selected_filter_index max pour inclure boutons
extended_max = total_items + len(actions) - 1
if config.selected_filter_index > extended_max:
config.selected_filter_index = extended_max
for idx, (key, offset) in enumerate(actions):
btn_x = center_x - (len(actions) * (btn_width + spacing) - spacing) // 2 + idx * (btn_width + spacing)
is_selected = (config.selected_filter_index == total_items + idx)
label = _(key)
draw_stylized_button(screen, label, btn_x, buttons_y, btn_width, btn_height, selected=is_selected)
# Infos bas
hidden_count = sum(1 for _, h in config.filter_platforms_selection if h)
visible_count = total_items - hidden_count
info_text = _("filter_platforms_info").format(visible_count, hidden_count, total_items)
info_surface = config.small_font.render(info_text, True, THEME_COLORS["text"])
info_rect = info_surface.get_rect(center=(config.screen_width // 2, buttons_y + btn_height + 30))
info_rect = info_surface.get_rect(center=(config.screen_width // 2, list_y + list_height + 20))
screen.blit(info_surface, info_rect)
if config.filter_platforms_dirty:

View File

@@ -1,3 +1,3 @@
{
"version": "2.3.2.1"
"version": "2.3.2.5"
}