mirror of
https://github.com/RetroGameSets/RGSX.git
synced 2026-03-19 16:26:00 +01:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05a8df5933 | ||
|
|
55231bb823 | ||
|
|
d9c1ca6794 | ||
|
|
6613b43264 | ||
|
|
d60dc31291 | ||
|
|
ace6ec876f | ||
|
|
9f759c1928 | ||
|
|
db287e33d7 | ||
|
|
217392dcd1 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -17,9 +17,9 @@ ports/RGSX.bat
|
||||
audit_i18n.py
|
||||
prune_i18n.py
|
||||
Info.txt
|
||||
pygame/
|
||||
|
||||
# Docker test data
|
||||
data/
|
||||
|
||||
docker-compose.test.yml
|
||||
config/
|
||||
|
||||
24
README.md
24
README.md
@@ -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**
|
||||
[](https://starchart.cc/RetroGameSets/RGSX)
|
||||
|
||||
**Developed with ❤️ for the retro gaming community.**
|
||||
**Developed with ❤️ for the retro gaming community.**
|
||||
|
||||
406
README_FR.md
406
README_FR.md
@@ -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 s’ils 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 l’URL 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
|
||||
|
||||
[](https://starchart.cc/RetroGameSets/RGSX)
|
||||
|
||||
**Développé avec ❤️ pour la communauté du retrogaming.**
|
||||
|
||||
Développé avec ❤️ pour les amateurs de jeux rétro.
|
||||
|
||||
@@ -238,11 +238,12 @@ except Exception as e:
|
||||
|
||||
# Initialisation du mixer Pygame (déférée/évitable si musique désactivée)
|
||||
if getattr(config, 'music_enabled', True):
|
||||
pygame.mixer.pre_init(44100, -16, 2, 4096)
|
||||
try:
|
||||
pygame.mixer.pre_init(44100, -16, 2, 4096)
|
||||
pygame.mixer.init()
|
||||
except Exception as e:
|
||||
logger.warning(f"Échec init mixer: {e}")
|
||||
except (NotImplementedError, AttributeError, Exception) as e:
|
||||
logger.warning(f"Mixer non disponible ou échec init: {e}")
|
||||
config.music_enabled = False # Désactiver la musique si mixer non disponible
|
||||
|
||||
# Dossier musique Batocera
|
||||
music_folder = os.path.join(config.APP_FOLDER, "assets", "music")
|
||||
@@ -1368,7 +1369,11 @@ async def main():
|
||||
clock.tick(60)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
pygame.mixer.music.stop()
|
||||
try:
|
||||
if pygame.mixer.get_init() is not None:
|
||||
pygame.mixer.music.stop()
|
||||
except (AttributeError, NotImplementedError):
|
||||
pass
|
||||
# Cancel any ongoing downloads to prevent lingering background threads
|
||||
try:
|
||||
cancel_all_downloads()
|
||||
|
||||
133
ports/RGSX/assets/progs/custom_dns
Normal file
133
ports/RGSX/assets/progs/custom_dns
Normal file
@@ -0,0 +1,133 @@
|
||||
#!/bin/bash
|
||||
# BATOCERA SERVICE
|
||||
# name: Custom DNS Service for RGSX
|
||||
# description: Force custom DNS servers (Cloudflare 1.1.1.1)
|
||||
# author: RetroGameSets
|
||||
# depends:
|
||||
# version: 1.0
|
||||
|
||||
RESOLV_CONF="/tmp/resolv.conf"
|
||||
PIDFILE="/var/run/custom_dns.pid"
|
||||
LOGFILE="/userdata/roms/ports/RGSX/logs/custom_dns_service.log"
|
||||
SERVICE_NAME="custom_dns"
|
||||
|
||||
# Fonction utilitaire : vérifie si le service est activé dans batocera-settings
|
||||
is_enabled() {
|
||||
local enabled_services
|
||||
enabled_services="$(/usr/bin/batocera-settings-get system.services 2>/dev/null)"
|
||||
for s in $enabled_services; do
|
||||
if [ "$s" = "$SERVICE_NAME" ]; then
|
||||
echo "enabled"
|
||||
return
|
||||
fi
|
||||
done
|
||||
echo "disabled"
|
||||
}
|
||||
|
||||
# Fonction pour appliquer les DNS personnalisés
|
||||
apply_custom_dns() {
|
||||
echo "[${SERVICE_NAME}] Applying custom DNS servers..."
|
||||
mkdir -p "$(dirname "$LOGFILE")"
|
||||
{
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - Applying custom DNS"
|
||||
# Retirer la protection si elle existe
|
||||
chattr -i "$RESOLV_CONF" 2>/dev/null || true
|
||||
# Écrire la nouvelle configuration DNS
|
||||
echo "# Generated by RGSX Custom DNS Service" > "$RESOLV_CONF"
|
||||
echo "nameserver 1.1.1.1" >> "$RESOLV_CONF"
|
||||
echo "nameserver 1.0.0.1" >> "$RESOLV_CONF"
|
||||
# Protéger le fichier contre les modifications
|
||||
chattr +i "$RESOLV_CONF" 2>/dev/null || true
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - Custom DNS applied successfully"
|
||||
} >> "$LOGFILE" 2>&1
|
||||
echo "[${SERVICE_NAME}] Custom DNS applied (1.1.1.1, 1.0.0.1)"
|
||||
}
|
||||
|
||||
# Fonction pour restaurer les DNS par défaut
|
||||
restore_default_dns() {
|
||||
echo "[${SERVICE_NAME}] Restoring default DNS..."
|
||||
mkdir -p "$(dirname "$LOGFILE")"
|
||||
{
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - Restoring default DNS"
|
||||
# Retirer la protection
|
||||
chattr -i "$RESOLV_CONF" 2>/dev/null || true
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - DNS protection removed"
|
||||
} >> "$LOGFILE" 2>&1
|
||||
echo "[${SERVICE_NAME}] Default DNS restored"
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
if [ -f "$PIDFILE" ]; then
|
||||
echo "[${SERVICE_NAME}] Already running (PID $(cat "$PIDFILE"))"
|
||||
exit 0
|
||||
fi
|
||||
apply_custom_dns
|
||||
echo $$ > "$PIDFILE"
|
||||
echo "[${SERVICE_NAME}] Started (PID $(cat "$PIDFILE"))"
|
||||
;;
|
||||
|
||||
stop)
|
||||
if [ -f "$PIDFILE" ]; then
|
||||
echo "[${SERVICE_NAME}] Stopping..."
|
||||
restore_default_dns
|
||||
rm -f "$PIDFILE"
|
||||
echo "[${SERVICE_NAME}] Stopped"
|
||||
else
|
||||
echo "[${SERVICE_NAME}] Not running"
|
||||
fi
|
||||
;;
|
||||
|
||||
restart)
|
||||
echo "[${SERVICE_NAME}] Restarting..."
|
||||
"$0" stop
|
||||
sleep 1
|
||||
"$0" start
|
||||
;;
|
||||
|
||||
status)
|
||||
ENABLE_STATE=$(is_enabled)
|
||||
if [ -f "$PIDFILE" ]; then
|
||||
if chattr -i "$RESOLV_CONF" 2>/dev/null; then
|
||||
chattr +i "$RESOLV_CONF" 2>/dev/null
|
||||
echo "[${SERVICE_NAME}] Running (DNS protected) - ${ENABLE_STATE} on boot"
|
||||
exit 0
|
||||
else
|
||||
echo "[${SERVICE_NAME}] Running (DNS not protected) - ${ENABLE_STATE} on boot"
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
echo "[${SERVICE_NAME}] Not running - ${ENABLE_STATE} on boot"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
enable)
|
||||
current=$(/usr/bin/batocera-settings-get system.services 2>/dev/null)
|
||||
if echo "$current" | grep -qw "$SERVICE_NAME"; then
|
||||
echo "[${SERVICE_NAME}] Already enabled on boot"
|
||||
else
|
||||
new_value="$current $SERVICE_NAME"
|
||||
/usr/bin/batocera-settings-set system.services "$new_value"
|
||||
echo "[${SERVICE_NAME}] Enabled on boot"
|
||||
fi
|
||||
;;
|
||||
|
||||
disable)
|
||||
current=$(/usr/bin/batocera-settings-get system.services 2>/dev/null)
|
||||
if echo "$current" | grep -qw "$SERVICE_NAME"; then
|
||||
new_value=$(echo "$current" | sed "s/\b$SERVICE_NAME\b//g" | xargs)
|
||||
/usr/bin/batocera-settings-set system.services "$new_value"
|
||||
echo "[${SERVICE_NAME}] Disabled on boot"
|
||||
else
|
||||
echo "[${SERVICE_NAME}] Already disabled"
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart|status|enable|disable}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
@@ -13,7 +13,7 @@ except Exception:
|
||||
pygame = None # type: ignore
|
||||
|
||||
# Version actuelle de l'application
|
||||
app_version = "2.3.2.0"
|
||||
app_version = "2.3.2.6"
|
||||
|
||||
|
||||
def get_application_root():
|
||||
|
||||
@@ -20,7 +20,7 @@ from utils import (
|
||||
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,
|
||||
@@ -196,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):
|
||||
@@ -204,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():
|
||||
@@ -593,31 +629,31 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "page_up"):
|
||||
config.current_game = max(0, config.current_game - config.visible_games)
|
||||
config.repeat_action = None
|
||||
config.repeat_key = None
|
||||
config.repeat_start_time = 0
|
||||
config.repeat_last_action = current_time
|
||||
update_key_state("page_up", True, event.type, event.key if event.type == pygame.KEYDOWN else
|
||||
event.button if event.type == pygame.JOYBUTTONDOWN else
|
||||
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
|
||||
event.value)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "left"):
|
||||
config.current_game = max(0, config.current_game - config.visible_games)
|
||||
config.repeat_action = None
|
||||
config.repeat_key = None
|
||||
config.repeat_start_time = 0
|
||||
config.repeat_last_action = current_time
|
||||
update_key_state("left", True, event.type, event.key if event.type == pygame.KEYDOWN else
|
||||
event.button if event.type == pygame.JOYBUTTONDOWN else
|
||||
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
|
||||
event.value)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "page_down"):
|
||||
config.current_game = min(len(games) - 1, config.current_game + config.visible_games)
|
||||
config.repeat_action = None
|
||||
config.repeat_key = None
|
||||
config.repeat_start_time = 0
|
||||
config.repeat_last_action = current_time
|
||||
update_key_state("page_down", True, event.type, event.key if event.type == pygame.KEYDOWN else
|
||||
event.button if event.type == pygame.JOYBUTTONDOWN else
|
||||
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
|
||||
event.value)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "right"):
|
||||
config.current_game = min(len(games) - 1, config.current_game + config.visible_games)
|
||||
config.repeat_action = None
|
||||
config.repeat_key = None
|
||||
config.repeat_start_time = 0
|
||||
config.repeat_last_action = current_time
|
||||
update_key_state("right", True, event.type, event.key if event.type == pygame.KEYDOWN else
|
||||
event.button if event.type == pygame.JOYBUTTONDOWN else
|
||||
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
|
||||
event.value)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "filter"):
|
||||
config.search_mode = True
|
||||
@@ -1415,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
|
||||
@@ -1423,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)]
|
||||
@@ -1439,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")):
|
||||
@@ -1466,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")):
|
||||
@@ -1529,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
|
||||
@@ -1549,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
|
||||
@@ -1577,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)
|
||||
@@ -1588,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)
|
||||
@@ -1598,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
|
||||
@@ -1631,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
|
||||
@@ -1709,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)]
|
||||
@@ -1901,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]
|
||||
@@ -1984,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
|
||||
@@ -2000,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
|
||||
for action_name in ["up", "down", "left", "right", "confirm", "cancel"]:
|
||||
if config.controls_config.get(action_name, {}).get("type") == "key" and \
|
||||
# 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"]:
|
||||
# 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:
|
||||
@@ -2094,12 +2152,14 @@ def handle_controls(event, sources, joystick, screen):
|
||||
|
||||
elif event.type == pygame.JOYBUTTONUP:
|
||||
# Vérifier quel bouton a été relâché
|
||||
for action_name in ["up", "down", "left", "right", "confirm", "cancel"]:
|
||||
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
|
||||
@@ -2186,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"]:
|
||||
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"]:
|
||||
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
|
||||
|
||||
@@ -2209,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
|
||||
|
||||
@@ -8,7 +8,7 @@ from utils import truncate_text_middle, wrap_text, load_system_image, truncate_t
|
||||
import logging
|
||||
import math
|
||||
from history import load_history, is_game_downloaded
|
||||
from language import _
|
||||
from language import _, get_size_units, get_speed_unit
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -956,15 +956,16 @@ def draw_game_scrollbar(screen, scroll_offset, total_items, visible_items, x, y,
|
||||
pygame.draw.rect(screen, THEME_COLORS["fond_lignes"], (x, scrollbar_y, 15, scrollbar_height), border_radius=4)
|
||||
|
||||
def format_size(size):
|
||||
"""Convertit une taille en octets en format lisible."""
|
||||
"""Convertit une taille en octets en format lisible avec unités adaptées à la langue."""
|
||||
if not isinstance(size, (int, float)) or size == 0:
|
||||
return "N/A"
|
||||
|
||||
for unit in ['o', 'Ko', 'Mo', 'Go', 'To']:
|
||||
units = get_size_units()
|
||||
for unit in units[:-1]: # Tous sauf le dernier (Po/PB)
|
||||
if size < 1024.0:
|
||||
return f"{size:.1f} {unit}"
|
||||
size /= 1024.0
|
||||
return f"{size:.1f} Po"
|
||||
return f"{size:.1f} {units[-1]}" # Dernier niveau (Po/PB)
|
||||
|
||||
|
||||
def draw_history_list(screen):
|
||||
@@ -991,7 +992,7 @@ def draw_history_list(screen):
|
||||
if entry.get("status") in ["Téléchargement", "Downloading"]:
|
||||
speed = entry.get("speed", 0.0)
|
||||
if speed and speed > 0:
|
||||
speed_str = f" - {speed:.2f} Mo/s"
|
||||
speed_str = f" - {speed:.2f} {get_speed_unit()}"
|
||||
break
|
||||
|
||||
screen.blit(OVERLAY, (0, 0))
|
||||
@@ -1036,7 +1037,7 @@ def draw_history_list(screen):
|
||||
if history and history[current_history_item_inverted].get("status") in ["Téléchargement", "Downloading"]:
|
||||
speed = history[current_history_item_inverted].get("speed", 0.0)
|
||||
if speed > 0:
|
||||
speed_str = f"{speed:.2f} Mo/s"
|
||||
speed_str = f"{speed:.2f} {get_speed_unit()}"
|
||||
title_text = _("history_title").format(history_count) + f" {speed_str}"
|
||||
else:
|
||||
title_text = _("history_title").format(history_count)
|
||||
@@ -2014,12 +2015,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)
|
||||
@@ -2119,7 +2122,7 @@ def draw_pause_games_menu(screen, selected_index):
|
||||
|
||||
def draw_pause_settings_menu(screen, selected_index):
|
||||
from rgsx_settings import get_symlink_option
|
||||
from utils import check_web_service_status
|
||||
from utils import check_web_service_status, check_custom_dns_status
|
||||
# Music
|
||||
if config.music_enabled:
|
||||
music_name = config.current_music_name or ""
|
||||
@@ -2140,10 +2143,16 @@ def draw_pause_settings_menu(screen, selected_index):
|
||||
|
||||
# Web Service at boot (only on Linux/Batocera)
|
||||
web_service_txt = ""
|
||||
custom_dns_txt = ""
|
||||
if config.OPERATING_SYSTEM == "Linux":
|
||||
web_service_enabled = check_web_service_status()
|
||||
web_service_status = _("settings_web_service_enabled") if web_service_enabled else _("settings_web_service_disabled")
|
||||
web_service_txt = f"{_('settings_web_service')} : < {web_service_status} >"
|
||||
|
||||
# Custom DNS at boot
|
||||
custom_dns_enabled = check_custom_dns_status()
|
||||
custom_dns_status = _("settings_custom_dns_enabled") if custom_dns_enabled else _("settings_custom_dns_disabled")
|
||||
custom_dns_txt = f"{_('settings_custom_dns')} : < {custom_dns_status} >"
|
||||
|
||||
api_keys_txt = _("menu_api_keys_status") if _ else "API Keys"
|
||||
back_txt = _("menu_back") if _ else "Back"
|
||||
@@ -2152,6 +2161,8 @@ def draw_pause_settings_menu(screen, selected_index):
|
||||
options = [music_option, symlink_option]
|
||||
if web_service_txt: # Ajouter seulement si Linux/Batocera
|
||||
options.append(web_service_txt)
|
||||
if custom_dns_txt: # Ajouter seulement si Linux/Batocera
|
||||
options.append(custom_dns_txt)
|
||||
options.extend([api_keys_txt, back_txt])
|
||||
|
||||
_draw_submenu_generic(screen, _("menu_settings_category") if _ else "Settings", options, selected_index)
|
||||
@@ -2161,6 +2172,8 @@ def draw_pause_settings_menu(screen, selected_index):
|
||||
]
|
||||
if web_service_txt:
|
||||
instruction_keys.append("instruction_settings_web_service")
|
||||
if custom_dns_txt:
|
||||
instruction_keys.append("instruction_settings_custom_dns")
|
||||
instruction_keys.extend([
|
||||
"instruction_settings_api_keys",
|
||||
"instruction_generic_back",
|
||||
@@ -2309,11 +2322,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)
|
||||
|
||||
@@ -2330,12 +2364,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 "[ ]"
|
||||
@@ -2355,37 +2390,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:
|
||||
|
||||
@@ -67,6 +67,28 @@ def load_language(lang_code=None):
|
||||
return load_language(DEFAULT_LANGUAGE)
|
||||
return False
|
||||
|
||||
def get_size_units():
|
||||
"""Retourne les unités de taille adaptées à la langue courante.
|
||||
|
||||
Français utilise l'octet (o, Ko, Mo, Go, To, Po)
|
||||
Autres langues utilisent byte (B, KB, MB, GB, TB, PB)
|
||||
"""
|
||||
if current_language == "fr":
|
||||
return ['o', 'Ko', 'Mo', 'Go', 'To', 'Po']
|
||||
else:
|
||||
return ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
|
||||
|
||||
def get_speed_unit():
|
||||
"""Retourne l'unité de vitesse adaptée à la langue courante.
|
||||
|
||||
Français utilise Mo/s
|
||||
Autres langues utilisent MB/s
|
||||
"""
|
||||
if current_language == "fr":
|
||||
return "Mo/s"
|
||||
else:
|
||||
return "MB/s"
|
||||
|
||||
def get_text(key, default=None):
|
||||
"""Récupère la traduction correspondant à la clé en garantissant une chaîne.
|
||||
|
||||
|
||||
@@ -203,6 +203,7 @@
|
||||
"instruction_settings_symlink": "Verwendung von Symlinks für Installationen umschalten",
|
||||
"instruction_settings_api_keys": "Gefundene Premium-API-Schlüssel ansehen",
|
||||
"instruction_settings_web_service": "Web-Dienst Autostart beim Booten aktivieren/deaktivieren",
|
||||
"instruction_settings_custom_dns": "Custom DNS (Cloudflare 1.1.1.1) beim Booten aktivieren/deaktivieren",
|
||||
"settings_web_service": "Web-Dienst beim Booten",
|
||||
"settings_web_service_enabled": "Aktiviert",
|
||||
"settings_web_service_disabled": "Deaktiviert",
|
||||
@@ -211,6 +212,13 @@
|
||||
"settings_web_service_success_enabled": "Web-Dienst beim Booten aktiviert",
|
||||
"settings_web_service_success_disabled": "Web-Dienst beim Booten deaktiviert",
|
||||
"settings_web_service_error": "Fehler: {0}",
|
||||
"settings_custom_dns": "Custom DNS beim Booten",
|
||||
"settings_custom_dns_enabled": "Aktiviert",
|
||||
"settings_custom_dns_disabled": "Deaktiviert",
|
||||
"settings_custom_dns_enabling": "Custom DNS wird aktiviert...",
|
||||
"settings_custom_dns_disabling": "Custom DNS wird deaktiviert...",
|
||||
"settings_custom_dns_success_enabled": "Custom DNS beim Booten aktiviert (1.1.1.1)",
|
||||
"settings_custom_dns_success_disabled": "Custom DNS beim Booten deaktiviert",
|
||||
"controls_desc_confirm": "Bestätigen (z.B. A/Kreuz)",
|
||||
"controls_desc_cancel": "Abbrechen/Zurück (z.B. B/Kreis)",
|
||||
"controls_desc_up": "UP ↑",
|
||||
@@ -363,6 +371,13 @@
|
||||
"filter_none": "Alles abwählen",
|
||||
"filter_apply": "Filter anwenden",
|
||||
"filter_back": "Zurück",
|
||||
"accessibility_footer_font_size": "Fußzeilen-Schriftgröße: {0}",
|
||||
"popup_layout_changed_restart": "Layout geändert auf {0}x{1}. Bitte starten Sie die App neu."
|
||||
"accessibility_footer_font_size": "Fußzeilenschriftgröße: {0}",
|
||||
"popup_layout_changed_restart": "Layout geändert auf {0}x{1}. Bitte starten Sie die App neu.",
|
||||
"web_started": "Gestartet",
|
||||
"web_downloading": "Download",
|
||||
"web_in_progress": "In Bearbeitung",
|
||||
"web_added_to_queue": "zur Warteschlange hinzugefügt",
|
||||
"web_download_success": "erfolgreich heruntergeladen!",
|
||||
"web_download_error_for": "Fehler beim Herunterladen von",
|
||||
"web_already_present": "war bereits vorhanden"
|
||||
}
|
||||
@@ -205,6 +205,7 @@
|
||||
"instruction_settings_symlink": "Toggle using filesystem symlinks for installs",
|
||||
"instruction_settings_api_keys": "See detected premium provider API keys",
|
||||
"instruction_settings_web_service": "Enable/disable web service auto-start at boot",
|
||||
"instruction_settings_custom_dns": "Enable/disable custom DNS (Cloudflare 1.1.1.1) at boot",
|
||||
"settings_web_service": "Web Service at Boot",
|
||||
"settings_web_service_enabled": "Enabled",
|
||||
"settings_web_service_disabled": "Disabled",
|
||||
@@ -213,6 +214,13 @@
|
||||
"settings_web_service_success_enabled": "Web service enabled at boot",
|
||||
"settings_web_service_success_disabled": "Web service disabled at boot",
|
||||
"settings_web_service_error": "Error: {0}",
|
||||
"settings_custom_dns": "Custom DNS at Boot",
|
||||
"settings_custom_dns_enabled": "Enabled",
|
||||
"settings_custom_dns_disabled": "Disabled",
|
||||
"settings_custom_dns_enabling": "Enabling custom DNS...",
|
||||
"settings_custom_dns_disabling": "Disabling custom DNS...",
|
||||
"settings_custom_dns_success_enabled": "Custom DNS enabled at boot (1.1.1.1)",
|
||||
"settings_custom_dns_success_disabled": "Custom DNS disabled at boot",
|
||||
"controls_desc_confirm": "Confirm (e.g. A/Cross)",
|
||||
"controls_desc_cancel": "Cancel/Back (e.g. B/Circle)",
|
||||
"controls_desc_up": "UP ↑",
|
||||
@@ -364,5 +372,12 @@
|
||||
"filter_apply": "Apply Filter",
|
||||
"filter_back": "Back",
|
||||
"accessibility_footer_font_size": "Footer font size: {0}",
|
||||
"popup_layout_changed_restart": "Layout changed to {0}x{1}. Please restart the app to apply."
|
||||
"popup_layout_changed_restart": "Layout changed to {0}x{1}. Please restart the app to apply.",
|
||||
"web_started": "Started",
|
||||
"web_downloading": "Download",
|
||||
"web_in_progress": "In progress",
|
||||
"web_added_to_queue": "added to queue",
|
||||
"web_download_success": "downloaded successfully!",
|
||||
"web_download_error_for": "Error downloading",
|
||||
"web_already_present": "was already present"
|
||||
}
|
||||
@@ -205,6 +205,7 @@
|
||||
"instruction_settings_symlink": "Alternar uso de symlinks en instalaciones",
|
||||
"instruction_settings_api_keys": "Ver claves API premium detectadas",
|
||||
"instruction_settings_web_service": "Activar/desactivar inicio automático del servicio web",
|
||||
"instruction_settings_custom_dns": "Activar/desactivar DNS personalizado (Cloudflare 1.1.1.1) al inicio",
|
||||
"settings_web_service": "Servicio Web al Inicio",
|
||||
"settings_web_service_enabled": "Activado",
|
||||
"settings_web_service_disabled": "Desactivado",
|
||||
@@ -213,6 +214,13 @@
|
||||
"settings_web_service_success_enabled": "Servicio web activado al inicio",
|
||||
"settings_web_service_success_disabled": "Servicio web desactivado al inicio",
|
||||
"settings_web_service_error": "Error: {0}",
|
||||
"settings_custom_dns": "DNS Personalizado al Inicio",
|
||||
"settings_custom_dns_enabled": "Activado",
|
||||
"settings_custom_dns_disabled": "Desactivado",
|
||||
"settings_custom_dns_enabling": "Activando DNS personalizado...",
|
||||
"settings_custom_dns_disabling": "Desactivando DNS personalizado...",
|
||||
"settings_custom_dns_success_enabled": "DNS personalizado activado al inicio (1.1.1.1)",
|
||||
"settings_custom_dns_success_disabled": "DNS personalizado desactivado al inicio",
|
||||
"controls_desc_confirm": "Confirmar (ej. A/Cruz)",
|
||||
"controls_desc_cancel": "Cancelar/Volver (ej. B/Círculo)",
|
||||
"controls_desc_up": "UP ↑",
|
||||
@@ -364,5 +372,12 @@
|
||||
"filter_apply": "Aplicar filtro",
|
||||
"filter_back": "Volver",
|
||||
"accessibility_footer_font_size": "Tamaño fuente pie de página: {0}",
|
||||
"popup_layout_changed_restart": "Diseño cambiado a {0}x{1}. Reinicie la app para aplicar."
|
||||
"popup_layout_changed_restart": "Diseño cambiado a {0}x{1}. Reinicie la app para aplicar.",
|
||||
"web_started": "Iniciado",
|
||||
"web_downloading": "Descarga",
|
||||
"web_in_progress": "En curso",
|
||||
"web_added_to_queue": "añadido a la cola",
|
||||
"web_download_success": "¡descargado con éxito!",
|
||||
"web_download_error_for": "Error al descargar",
|
||||
"web_already_present": "ya estaba presente"
|
||||
}
|
||||
@@ -205,6 +205,7 @@
|
||||
"instruction_settings_symlink": "Basculer l'utilisation de symlinks pour l'installation",
|
||||
"instruction_settings_api_keys": "Voir les clés API détectées des services premium",
|
||||
"instruction_settings_web_service": "Activer/désactiver le démarrage automatique du service web",
|
||||
"instruction_settings_custom_dns": "Activer/désactiver les DNS personnalisés (Cloudflare 1.1.1.1) au démarrage",
|
||||
"settings_web_service": "Service Web au démarrage",
|
||||
"settings_web_service_enabled": "Activé",
|
||||
"settings_web_service_disabled": "Désactivé",
|
||||
@@ -213,6 +214,13 @@
|
||||
"settings_web_service_success_enabled": "Service web activé au démarrage",
|
||||
"settings_web_service_success_disabled": "Service web désactivé au démarrage",
|
||||
"settings_web_service_error": "Erreur : {0}",
|
||||
"settings_custom_dns": "DNS Personnalisé au démarrage",
|
||||
"settings_custom_dns_enabled": "Activé",
|
||||
"settings_custom_dns_disabled": "Désactivé",
|
||||
"settings_custom_dns_enabling": "Activation du DNS personnalisé...",
|
||||
"settings_custom_dns_disabling": "Désactivation du DNS personnalisé...",
|
||||
"settings_custom_dns_success_enabled": "DNS personnalisé activé au démarrage (1.1.1.1)",
|
||||
"settings_custom_dns_success_disabled": "DNS personnalisé désactivé au démarrage",
|
||||
"controls_desc_confirm": "Valider (ex: A/Croix)",
|
||||
"controls_desc_cancel": "Annuler/Retour (ex: B/Rond)",
|
||||
"controls_desc_up": "UP ↑",
|
||||
@@ -364,5 +372,12 @@
|
||||
"filter_apply": "Appliquer filtre",
|
||||
"filter_back": "Retour",
|
||||
"accessibility_footer_font_size": "Taille police pied de page : {0}",
|
||||
"popup_layout_changed_restart": "Disposition changée en {0}x{1}. Veuillez redémarrer l'app pour appliquer."
|
||||
"popup_layout_changed_restart": "Disposition changée en {0}x{1}. Veuillez redémarrer l'app pour appliquer.",
|
||||
"web_started": "Démarré",
|
||||
"web_downloading": "Téléchargement",
|
||||
"web_in_progress": "En cours",
|
||||
"web_added_to_queue": "ajouté à la queue",
|
||||
"web_download_success": "téléchargé avec succès!",
|
||||
"web_download_error_for": "Erreur lors du téléchargement de",
|
||||
"web_already_present": "était déjà présent"
|
||||
}
|
||||
@@ -202,6 +202,7 @@
|
||||
"instruction_settings_symlink": "Abilitare/disabilitare uso symlink per installazioni",
|
||||
"instruction_settings_api_keys": "Mostrare chiavi API premium rilevate",
|
||||
"instruction_settings_web_service": "Attivare/disattivare avvio automatico servizio web all'avvio",
|
||||
"instruction_settings_custom_dns": "Attivare/disattivare DNS personalizzato (Cloudflare 1.1.1.1) all'avvio",
|
||||
"settings_web_service": "Servizio Web all'Avvio",
|
||||
"settings_web_service_enabled": "Abilitato",
|
||||
"settings_web_service_disabled": "Disabilitato",
|
||||
@@ -210,6 +211,13 @@
|
||||
"settings_web_service_success_enabled": "Servizio web abilitato all'avvio",
|
||||
"settings_web_service_success_disabled": "Servizio web disabilitato all'avvio",
|
||||
"settings_web_service_error": "Errore: {0}",
|
||||
"settings_custom_dns": "DNS Personalizzato all'Avvio",
|
||||
"settings_custom_dns_enabled": "Abilitato",
|
||||
"settings_custom_dns_disabled": "Disabilitato",
|
||||
"settings_custom_dns_enabling": "Abilitazione DNS personalizzato...",
|
||||
"settings_custom_dns_disabling": "Disabilitazione DNS personalizzato...",
|
||||
"settings_custom_dns_success_enabled": "DNS personalizzato abilitato all'avvio (1.1.1.1)",
|
||||
"settings_custom_dns_success_disabled": "DNS personalizzato disabilitato all'avvio",
|
||||
"controls_desc_confirm": "Confermare (es. A/Croce)",
|
||||
"controls_desc_cancel": "Annullare/Indietro (es. B/Cerchio)",
|
||||
"controls_desc_up": "UP ↑",
|
||||
@@ -364,5 +372,12 @@
|
||||
"filter_apply": "Applica filtro",
|
||||
"filter_back": "Indietro",
|
||||
"accessibility_footer_font_size": "Dimensione carattere piè di pagina: {0}",
|
||||
"popup_layout_changed_restart": "Layout cambiato in {0}x{1}. Riavviare l'app per applicare."
|
||||
"popup_layout_changed_restart": "Layout cambiato a {0}x{1}. Riavvia l'app per applicare.",
|
||||
"web_started": "Avviato",
|
||||
"web_downloading": "Download",
|
||||
"web_in_progress": "In corso",
|
||||
"web_added_to_queue": "aggiunto alla coda",
|
||||
"web_download_success": "scaricato con successo!",
|
||||
"web_download_error_for": "Errore durante il download di",
|
||||
"web_already_present": "era già presente"
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"instruction_settings_symlink": "Ativar/desativar uso de symlinks para instalações",
|
||||
"instruction_settings_api_keys": "Ver chaves API premium detectadas",
|
||||
"instruction_settings_web_service": "Ativar/desativar início automático do serviço web na inicialização",
|
||||
"instruction_settings_custom_dns": "Ativar/desativar DNS personalizado (Cloudflare 1.1.1.1) na inicialização",
|
||||
"settings_web_service": "Serviço Web na Inicialização",
|
||||
"settings_web_service_enabled": "Ativado",
|
||||
"settings_web_service_disabled": "Desativado",
|
||||
@@ -212,6 +213,13 @@
|
||||
"settings_web_service_success_enabled": "Serviço web ativado na inicialização",
|
||||
"settings_web_service_success_disabled": "Serviço web desativado na inicialização",
|
||||
"settings_web_service_error": "Erro: {0}",
|
||||
"settings_custom_dns": "DNS Personalizado na Inicialização",
|
||||
"settings_custom_dns_enabled": "Ativado",
|
||||
"settings_custom_dns_disabled": "Desativado",
|
||||
"settings_custom_dns_enabling": "Ativando DNS personalizado...",
|
||||
"settings_custom_dns_disabling": "Desativando DNS personalizado...",
|
||||
"settings_custom_dns_success_enabled": "DNS personalizado ativado na inicialização (1.1.1.1)",
|
||||
"settings_custom_dns_success_disabled": "DNS personalizado desativado na inicialização",
|
||||
"controls_desc_confirm": "Confirmar (ex. A/Cruz)",
|
||||
"controls_desc_cancel": "Cancelar/Voltar (ex. B/Círculo)",
|
||||
"controls_desc_up": "UP ↑",
|
||||
@@ -363,6 +371,13 @@
|
||||
"filter_none": "Desmarcar tudo",
|
||||
"filter_apply": "Aplicar filtro",
|
||||
"filter_back": "Voltar",
|
||||
"accessibility_footer_font_size": "Tamanho fonte rodapé: {0}",
|
||||
"popup_layout_changed_restart": "Layout alterado para {0}x{1}. Reinicie o app para aplicar."
|
||||
"accessibility_footer_font_size": "Tamanho da fonte do rodapé: {0}",
|
||||
"popup_layout_changed_restart": "Layout alterado para {0}x{1}. Reinicie o app para aplicar.",
|
||||
"web_started": "Iniciado",
|
||||
"web_downloading": "Download",
|
||||
"web_in_progress": "Em andamento",
|
||||
"web_added_to_queue": "adicionado à fila",
|
||||
"web_download_success": "baixado com sucesso!",
|
||||
"web_download_error_for": "Erro ao baixar",
|
||||
"web_already_present": "já estava presente"
|
||||
}
|
||||
@@ -460,8 +460,13 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
|
||||
def _send_html(self, html, status=200, etag=None, last_modified=None):
|
||||
"""Envoie une réponse HTML"""
|
||||
self._set_headers('text/html; charset=utf-8', status, etag=etag, last_modified=last_modified)
|
||||
self.wfile.write(html.encode('utf-8'))
|
||||
try:
|
||||
self._set_headers('text/html; charset=utf-8', status, etag=etag, last_modified=last_modified)
|
||||
self.wfile.write(html.encode('utf-8'))
|
||||
except (ConnectionAbortedError, BrokenPipeError) as e:
|
||||
# La connexion a été fermée par le client, ce n'est pas une erreur critique
|
||||
logger.debug(f"Connexion fermée par le client pendant l'envoi HTML: {e}")
|
||||
pass
|
||||
|
||||
def _send_not_found(self):
|
||||
"""Répond avec un 404 générique."""
|
||||
@@ -703,10 +708,13 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
|
||||
# Route: API - Traductions
|
||||
elif path == '/api/translations':
|
||||
# Ajouter le code de langue dans les traductions pour que JS puisse l'utiliser
|
||||
translations_with_lang = TRANSLATIONS.copy()
|
||||
translations_with_lang['_language'] = get_language()
|
||||
self._send_json({
|
||||
'success': True,
|
||||
'language': get_language(),
|
||||
'translations': TRANSLATIONS
|
||||
'translations': translations_with_lang
|
||||
})
|
||||
|
||||
# Route: API - Liste des jeux d'une plateforme
|
||||
|
||||
@@ -401,6 +401,55 @@ header p { opacity: 0.9; font-size: 1.1em; }
|
||||
}
|
||||
}
|
||||
|
||||
/* Amélioration de la lisibilité des settings */
|
||||
#settings-content label {
|
||||
display: block;
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
#settings-content select,
|
||||
#settings-content input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
background-color: #f8f8f8;
|
||||
color: #000;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
#settings-content select:focus,
|
||||
#settings-content input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
#settings-content input[type="checkbox"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#settings-content label.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#settings-content label.checkbox-label span {
|
||||
font-weight: bold;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#settings-content button {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
header h1 {
|
||||
font-size: 1.5em;
|
||||
|
||||
@@ -135,6 +135,34 @@
|
||||
return text;
|
||||
}
|
||||
|
||||
// Fonction pour obtenir les unités de taille selon la langue
|
||||
function getSizeUnits() {
|
||||
// Détecter la langue depuis les traductions chargées ou le navigateur
|
||||
const lang = translations['_language'] || navigator.language.substring(0, 2);
|
||||
// Français utilise o, Ko, Mo, Go, To
|
||||
// Autres langues utilisent B, KB, MB, GB, TB
|
||||
return lang === 'fr' ? ['o', 'Ko', 'Mo', 'Go', 'To', 'Po'] : ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
}
|
||||
|
||||
// Fonction pour obtenir l'unité de vitesse selon la langue
|
||||
function getSpeedUnit() {
|
||||
const lang = translations['_language'] || navigator.language.substring(0, 2);
|
||||
return lang === 'fr' ? 'Mo/s' : 'MB/s';
|
||||
}
|
||||
|
||||
// Fonction pour formater une taille en octets
|
||||
function formatSize(bytes) {
|
||||
if (!bytes || bytes === 0) return 'N/A';
|
||||
const units = getSizeUnits();
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
// Appliquer les traductions à tous les éléments marqués
|
||||
function applyTranslations() {
|
||||
// Mettre à jour le titre de la page
|
||||
@@ -1102,8 +1130,8 @@
|
||||
|
||||
// Afficher un toast de succès (pas de redirection de page)
|
||||
const toastMsg = mode === 'queue'
|
||||
? `📋 "${gameName}" ajouté à la queue`
|
||||
: `⬇️ Téléchargement de "${gameName}" lancé`;
|
||||
? `📋 "${gameName}" ${t('web_added_to_queue')}`
|
||||
: `⬇️ ${t('web_downloading')}: "${gameName}"`;
|
||||
showToast(toastMsg, 'success', 3000);
|
||||
|
||||
} else {
|
||||
@@ -1198,7 +1226,7 @@
|
||||
const speed = info.speed || 0;
|
||||
|
||||
// Utiliser game_name si disponible, sinon extraire de l'URL
|
||||
let fileName = info.game_name || 'Téléchargement';
|
||||
let fileName = info.game_name || t('web_downloading');
|
||||
if (!info.game_name) {
|
||||
try {
|
||||
fileName = decodeURIComponent(url.split('/').pop());
|
||||
@@ -1224,11 +1252,11 @@
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin-top: 5px; font-size: 0.9em;">
|
||||
<span>${status} - ${percent.toFixed(1)}%</span>
|
||||
<span>${speed > 0 ? speed.toFixed(2) + ' Mo/s' : ''}</span>
|
||||
<span>${speed > 0 ? speed.toFixed(2) + ' ' + getSpeedUnit() : ''}</span>
|
||||
</div>
|
||||
${total > 0 ? `<div style="font-size: 0.85em; color: #666;">${(downloaded / 1024 / 1024).toFixed(1)} Mo / ${(total / 1024 / 1024).toFixed(1)} Mo</div>` : ''}
|
||||
${total > 0 ? `<div style="font-size: 0.85em; color: #666;">${formatSize(downloaded)} / ${formatSize(total)}</div>` : ''}
|
||||
<div style="margin-top: 3px; font-size: 0.85em; color: #666;">
|
||||
📅 Démarré: ${info.timestamp || 'N/A'}
|
||||
📅 ${t('web_started')}: ${info.timestamp || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1264,10 +1292,10 @@
|
||||
const percent = info.progress_percent || 0;
|
||||
const downloaded = info.downloaded_size || 0;
|
||||
const total = info.total_size || 0;
|
||||
const status = info.status || 'En cours';
|
||||
const status = info.status || t('web_in_progress');
|
||||
const speed = info.speed || 0;
|
||||
|
||||
let fileName = info.game_name || 'Téléchargement';
|
||||
let fileName = info.game_name || t('web_downloading');
|
||||
if (!info.game_name) {
|
||||
try {
|
||||
fileName = decodeURIComponent(url.split('/').pop());
|
||||
@@ -1292,11 +1320,11 @@
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin-top: 5px; font-size: 0.9em;">
|
||||
<span>${status} - ${percent.toFixed(1)}%</span>
|
||||
<span>${speed > 0 ? speed.toFixed(2) + ' Mo/s' : ''}</span>
|
||||
<span>${speed > 0 ? speed.toFixed(2) + ' ' + getSpeedUnit() : ''}</span>
|
||||
</div>
|
||||
${total > 0 ? `<div style="font-size: 0.85em; color: #666;">${(downloaded / 1024 / 1024).toFixed(1)} Mo / ${(total / 1024 / 1024).toFixed(1)} Mo</div>` : ''}
|
||||
${total > 0 ? `<div style="font-size: 0.85em; color: #666;">${formatSize(downloaded)} / ${formatSize(total)}</div>` : ''}
|
||||
<div style="margin-top: 3px; font-size: 0.85em; color: #666;">
|
||||
📅 Démarré: ${info.timestamp || 'N/A'}
|
||||
📅 ${t('web_started')}: ${info.timestamp || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1446,13 +1474,13 @@
|
||||
// Si ce téléchargement n'était pas tracké et il est maintenant complété/erreur/etc
|
||||
if (!trackedDownloads[gameKey]) {
|
||||
if (status === 'Download_OK' || status === 'Completed') {
|
||||
showToast(`✅ "${entry.game_name}" téléchargé avec succès!`, 'success', 4000);
|
||||
showToast(`✅ "${entry.game_name}" ${t('web_download_success')}`, 'success', 4000);
|
||||
trackedDownloads[gameKey] = 'completed';
|
||||
} else if (status === 'Erreur' || status === 'error') {
|
||||
showToast(`❌ Erreur lors du téléchargement de "${entry.game_name}"`, 'error', 5000);
|
||||
showToast(`❌ ${t('web_download_error_for')} "${entry.game_name}"`, 'error', 5000);
|
||||
trackedDownloads[gameKey] = 'error';
|
||||
} else if (status === 'Already_Present') {
|
||||
showToast(`ℹ️ "${entry.game_name}" était déjà présent`, 'info', 3000);
|
||||
showToast(`ℹ️ "${entry.game_name}" ${t('web_already_present')}`, 'info', 3000);
|
||||
trackedDownloads[gameKey] = 'already_present';
|
||||
} else if (status === 'Canceled') {
|
||||
// Ne pas afficher de toast pour les téléchargements annulés
|
||||
@@ -1510,8 +1538,8 @@
|
||||
const isCanceled = status === 'Canceled';
|
||||
const isAlreadyPresent = status === 'Already_Present';
|
||||
const isQueued = status === 'Queued';
|
||||
const isDownloading = status === 'Downloading' || status === 'Téléchargement' || status === 'Downloading' ||
|
||||
status === 'Connecting' || status === 'Extracting' || status.startsWith('Try ');
|
||||
const isDownloading = status === 'Downloading' || status === 'Connecting' ||
|
||||
status === 'Extracting' || status.startsWith('Try ');
|
||||
const isSuccess = status === 'Download_OK' || status === 'Completed';
|
||||
|
||||
// Déterminer l'icône et la couleur
|
||||
@@ -1541,7 +1569,7 @@
|
||||
statusText = statusDownloading;
|
||||
}
|
||||
|
||||
const totalMo = h.total_size ? (h.total_size / 1024 / 1024).toFixed(1) : 'N/A';
|
||||
const sizeFormatted = h.total_size ? formatSize(h.total_size) : 'N/A';
|
||||
const platform = h.platform || 'N/A';
|
||||
const timestamp = h.timestamp || 'N/A';
|
||||
|
||||
@@ -1559,7 +1587,7 @@
|
||||
📦 ${platformLabel}: ${platform}
|
||||
</div>
|
||||
<div style="margin-top: 3px; font-size: 0.85em; color: #666;">
|
||||
💾 ${sizeLabel}: ${totalMo} Mo
|
||||
💾 ${sizeLabel}: ${sizeFormatted}
|
||||
</div>
|
||||
<div style="margin-top: 3px; font-size: 0.85em; color: #666;">
|
||||
📅 Date: ${timestamp}
|
||||
@@ -1744,12 +1772,12 @@
|
||||
<h3 style="margin-top: 30px; margin-bottom: 15px;">RGSX Configuration ⚙️</h3>
|
||||
|
||||
<div style="margin-bottom: 20px; background: #f0f8ff; padding: 15px; border-radius: 8px; border: 2px solid #007bff;">
|
||||
<label style="display: block; font-weight: bold; margin-bottom: 10px; font-size: 1.1em;">📁 ${t('web_settings_roms_folder')}</label>
|
||||
<label style="display: block; margin-bottom: 10px; font-size: 1.1em;">📁 ${t('web_settings_roms_folder')}</label>
|
||||
<div style="display: flex; gap: 10px; margin-bottom: 8px; flex-wrap: wrap;">
|
||||
<input type="text" id="setting-roms-folder" value="${settings.roms_folder || ''}"
|
||||
data-translate-placeholder="web_settings_roms_placeholder"
|
||||
placeholder="${t('web_settings_roms_placeholder')}"
|
||||
style="flex: 1; min-width: 200px; padding: 10px; border: 2px solid #ddd; border-radius: 5px; font-size: 16px;">
|
||||
style="flex: 1; min-width: 200px;">
|
||||
<button onclick="browseRomsFolder()"
|
||||
style="background: linear-gradient(135deg, #007bff 0%, #0056b3 100%); color: white; border: none; padding: 10px 20px; border-radius: 5px; font-weight: bold; cursor: pointer; white-space: nowrap; flex-shrink: 0;">
|
||||
📂 ${t('web_settings_browse')}
|
||||
@@ -1764,8 +1792,8 @@
|
||||
|
||||
<div style="background: #f9f9f9; padding: 20px; border-radius: 8px;">
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: block; font-weight: bold; margin-bottom: 5px;">🌍 ${t('web_settings_language')}</label>
|
||||
<select id="setting-language" style="width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 5px; font-size: 16px;">
|
||||
<label>🌍 ${t('web_settings_language')}</label>
|
||||
<select id="setting-language">
|
||||
<option value="en" ${settings.language === 'en' ? 'selected' : ''}>English</option>
|
||||
<option value="fr" ${settings.language === 'fr' ? 'selected' : ''}>Français</option>
|
||||
<option value="es" ${settings.language === 'es' ? 'selected' : ''}>Español</option>
|
||||
@@ -1776,23 +1804,22 @@
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: flex; align-items: center; cursor: pointer;">
|
||||
<input type="checkbox" id="setting-music" ${settings.music_enabled ? 'checked' : ''}
|
||||
style="width: 20px; height: 20px; margin-right: 10px;">
|
||||
<span style="font-weight: bold;">🎵 ${t('web_settings_music')}</span>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="setting-music" ${settings.music_enabled ? 'checked' : ''}>
|
||||
<span>🎵 ${t('web_settings_music')}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: block; font-weight: bold; margin-bottom: 5px;">🔤 ${t('web_settings_font_scale')} (${settings.accessibility?.font_scale || 1.0})</label>
|
||||
<label>🔤 ${t('web_settings_font_scale')} (${settings.accessibility?.font_scale || 1.0})</label>
|
||||
<input type="range" id="setting-font-scale" min="0.5" max="2.0" step="0.1"
|
||||
value="${settings.accessibility?.font_scale || 1.0}"
|
||||
style="width: 100%;">
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: block; font-weight: bold; margin-bottom: 5px;">📐 ${t('web_settings_grid')}</label>
|
||||
<select id="setting-grid" style="width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 5px; font-size: 16px;">
|
||||
<label>📐 ${t('web_settings_grid')}</label>
|
||||
<select id="setting-grid">
|
||||
<option value="3x3" ${settings.display?.grid === '3x3' ? 'selected' : ''}>3x3</option>
|
||||
<option value="3x4" ${settings.display?.grid === '3x4' ? 'selected' : ''}>3x4</option>
|
||||
<option value="4x3" ${settings.display?.grid === '4x3' ? 'selected' : ''}>4x3</option>
|
||||
@@ -1801,54 +1828,50 @@
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: block; font-weight: bold; margin-bottom: 5px;">🖋️ ${t('web_settings_font_family')}</label>
|
||||
<select id="setting-font-family" style="width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 5px; font-size: 16px;">
|
||||
<label>🖋️ ${t('web_settings_font_family')}</label>
|
||||
<select id="setting-font-family">
|
||||
<option value="pixel" ${settings.display?.font_family === 'pixel' ? 'selected' : ''}>Pixel</option>
|
||||
<option value="dejavu" ${settings.display?.font_family === 'dejavu' ? 'selected' : ''}>DejaVu</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: flex; align-items: center; cursor: pointer;">
|
||||
<input type="checkbox" id="setting-symlink" ${settings.symlink?.enabled ? 'checked' : ''}
|
||||
style="width: 20px; height: 20px; margin-right: 10px;">
|
||||
<span style="font-weight: bold;">🔗 ${t('web_settings_symlink')}</span>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="setting-symlink" ${settings.symlink?.enabled ? 'checked' : ''}>
|
||||
<span>🔗 ${t('web_settings_symlink')}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: block; font-weight: bold; margin-bottom: 5px;">📦 ${t('web_settings_source_mode')}</label>
|
||||
<select id="setting-sources-mode" style="width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 5px; font-size: 16px;">
|
||||
<label>📦 ${t('web_settings_source_mode')}</label>
|
||||
<select id="setting-sources-mode">
|
||||
<option value="rgsx" ${settings.sources?.mode === 'rgsx' ? 'selected' : ''}>RGSX (default)</option>
|
||||
<option value="custom" ${settings.sources?.mode === 'custom' ? 'selected' : ''}>Custom</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: block; font-weight: bold; margin-bottom: 5px;">🔗 ${t('web_settings_custom_url')}</label>
|
||||
<label>🔗 ${t('web_settings_custom_url')}</label>
|
||||
<input type="text" id="setting-custom-url" value="${settings.sources?.custom_url || ''}"
|
||||
data-translate-placeholder="web_settings_custom_url_placeholder"
|
||||
placeholder="${t('web_settings_custom_url_placeholder')}"
|
||||
style="width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 5px; font-size: 16px;">
|
||||
placeholder="${t('web_settings_custom_url_placeholder')}">
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: flex; align-items: center; cursor: pointer;">
|
||||
<input type="checkbox" id="setting-show-unsupported" ${settings.show_unsupported_platforms ? 'checked' : ''}
|
||||
style="width: 20px; height: 20px; margin-right: 10px;">
|
||||
<span style="font-weight: bold;">👀 ${showUnsupportedLabel}</span>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="setting-show-unsupported" ${settings.show_unsupported_platforms ? 'checked' : ''}>
|
||||
<span>👀 ${showUnsupportedLabel}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: flex; align-items: center; cursor: pointer;">
|
||||
<input type="checkbox" id="setting-allow-unknown" ${settings.allow_unknown_extensions ? 'checked' : ''}
|
||||
style="width: 20px; height: 20px; margin-right: 10px;">
|
||||
<span style="font-weight: bold;">⚠️ ${allowUnknownLabel}</span>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="setting-allow-unknown" ${settings.allow_unknown_extensions ? 'checked' : ''}>
|
||||
<span>⚠️ ${allowUnknownLabel}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button onclick="saveSettings()" style="width: 100%; background: linear-gradient(135deg, #28a745 0%, #20c997 100%); color: white; border: none; padding: 15px; border-radius: 8px; font-size: 18px; font-weight: bold; cursor: pointer; margin-top: 10px;">
|
||||
<button id="save-settings-btn" style="width: 100%; background: linear-gradient(135deg, #28a745 0%, #20c997 100%); color: white; border: none; padding: 15px; border-radius: 8px; font-size: 18px; font-weight: bold; cursor: pointer; margin-top: 10px;">
|
||||
💾 ${t('web_settings_save')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -1860,13 +1883,24 @@
|
||||
label.textContent = `🔤 ${t('web_settings_font_scale')} (${e.target.value})`;
|
||||
});
|
||||
|
||||
// Attacher l'événement de sauvegarde au bouton
|
||||
document.getElementById('save-settings-btn').addEventListener('click', saveSettings);
|
||||
|
||||
} catch (error) {
|
||||
container.innerHTML = `<p style="color:red;">${t('web_error')}: ${error.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Sauvegarder les settings
|
||||
async function saveSettings() {
|
||||
async function saveSettings(event) {
|
||||
// Désactiver le bouton pendant la sauvegarde
|
||||
const saveButton = event?.target;
|
||||
const originalText = saveButton?.textContent;
|
||||
if (saveButton) {
|
||||
saveButton.disabled = true;
|
||||
saveButton.textContent = '⏳ Saving...';
|
||||
}
|
||||
|
||||
try {
|
||||
const settings = {
|
||||
language: document.getElementById('setting-language').value,
|
||||
@@ -1899,13 +1933,23 @@
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Réactiver le bouton
|
||||
if (saveButton) {
|
||||
saveButton.disabled = false;
|
||||
saveButton.textContent = originalText;
|
||||
}
|
||||
// Afficher le dialogue de confirmation de redémarrage
|
||||
showRestartDialog();
|
||||
} else {
|
||||
throw new Error(data.error || t('web_error_unknown'));
|
||||
}
|
||||
} catch (error) {
|
||||
alert('❌ ' + t('web_error_save_settings', error.message));
|
||||
// Réactiver le bouton en cas d'erreur
|
||||
if (saveButton) {
|
||||
saveButton.disabled = false;
|
||||
saveButton.textContent = originalText;
|
||||
}
|
||||
alert('❌ ' + t('web_error_save_settings') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,14 @@ logger = logging.getLogger(__name__)
|
||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||
|
||||
# Helper pour vérifier si pygame.mixer est disponible
|
||||
def is_mixer_available():
|
||||
"""Vérifie si pygame.mixer est disponible et initialisé."""
|
||||
try:
|
||||
return pygame is not None and hasattr(pygame, 'mixer') and pygame.mixer.get_init() is not None
|
||||
except (AttributeError, NotImplementedError):
|
||||
return False
|
||||
|
||||
# Liste globale pour stocker les systèmes avec une erreur 404
|
||||
unavailable_systems = []
|
||||
|
||||
@@ -65,7 +73,8 @@ def restart_application(delay_ms: int = 2000):
|
||||
if int(delay_ms) <= 0:
|
||||
try:
|
||||
try:
|
||||
pygame.mixer.music.stop()
|
||||
if is_mixer_available():
|
||||
pygame.mixer.music.stop()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
@@ -300,6 +309,176 @@ def toggle_web_service_at_boot(enable: bool):
|
||||
return (False, error_msg)
|
||||
|
||||
|
||||
def toggle_custom_dns_at_boot(enable: bool):
|
||||
"""Active ou désactive le service custom DNS au démarrage de Batocera.
|
||||
|
||||
Args:
|
||||
enable: True pour activer, False pour désactiver
|
||||
|
||||
Returns:
|
||||
tuple: (success: bool, message: str)
|
||||
"""
|
||||
try:
|
||||
# Vérifier si on est sur un système compatible (Linux avec batocera-services)
|
||||
if config.OPERATING_SYSTEM != "Linux":
|
||||
return (False, "Custom DNS service is only available on Batocera/Linux systems")
|
||||
|
||||
services_dir = "/userdata/system/services"
|
||||
service_file = os.path.join(services_dir, "custom_dns")
|
||||
source_file = os.path.join(config.APP_FOLDER, "assets", "progs", "custom_dns")
|
||||
|
||||
if enable:
|
||||
# Mode ENABLE
|
||||
logger.debug("Activation du service custom DNS au démarrage...")
|
||||
|
||||
# 1. Créer le dossier services s'il n'existe pas
|
||||
try:
|
||||
os.makedirs(services_dir, exist_ok=True)
|
||||
logger.debug(f"Dossier services vérifié/créé: {services_dir}")
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to create services directory: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
|
||||
# 2. Copier le fichier custom_dns
|
||||
try:
|
||||
if not os.path.exists(source_file):
|
||||
error_msg = f"Source service file not found: {source_file}"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
|
||||
shutil.copy2(source_file, service_file)
|
||||
os.chmod(service_file, 0o755) # Rendre exécutable
|
||||
logger.debug(f"Fichier service copié et rendu exécutable: {service_file}")
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to copy service file: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
|
||||
# 3. Activer le service avec batocera-services
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['batocera-services', 'enable', 'custom_dns'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if result.returncode != 0:
|
||||
error_msg = f"batocera-services enable failed: {result.stderr}"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
logger.debug(f"Service activé: {result.stdout}")
|
||||
except FileNotFoundError:
|
||||
error_msg = "batocera-services command not found"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to enable service: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
|
||||
# 4. Démarrer le service immédiatement
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['batocera-services', 'start', 'custom_dns'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if result.returncode != 0:
|
||||
# Le service peut ne pas démarrer si déjà en cours, ce n'est pas grave
|
||||
logger.warning(f"batocera-services start warning: {result.stderr}")
|
||||
else:
|
||||
logger.debug(f"Service démarré: {result.stdout}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to start service (non-critical): {str(e)}")
|
||||
|
||||
success_msg = _("settings_custom_dns_success_enabled") if _ else "Custom DNS enabled at boot"
|
||||
logger.info(success_msg)
|
||||
|
||||
# Sauvegarder l'état dans rgsx_settings.json
|
||||
settings = load_rgsx_settings()
|
||||
settings["custom_dns_at_boot"] = True
|
||||
save_rgsx_settings(settings)
|
||||
|
||||
return (True, success_msg)
|
||||
|
||||
else:
|
||||
# Mode DISABLE
|
||||
logger.debug("Désactivation du service custom DNS au démarrage...")
|
||||
|
||||
# 1. Désactiver le service avec batocera-services
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['batocera-services', 'disable', 'custom_dns'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if result.returncode != 0:
|
||||
error_msg = f"batocera-services disable failed: {result.stderr}"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
logger.debug(f"Service désactivé: {result.stdout}")
|
||||
except FileNotFoundError:
|
||||
error_msg = "batocera-services command not found"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to disable service: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
|
||||
# 2. Arrêter le service immédiatement
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['batocera-services', 'stop', 'custom_dns'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.warning(f"batocera-services stop warning: {result.stderr}")
|
||||
else:
|
||||
logger.debug(f"Service arrêté: {result.stdout}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to stop service (non-critical): {str(e)}")
|
||||
|
||||
success_msg = _("settings_custom_dns_success_disabled") if _ else "✓ Custom DNS disabled at boot"
|
||||
logger.info(success_msg)
|
||||
|
||||
# Sauvegarder l'état dans rgsx_settings.json
|
||||
settings = load_rgsx_settings()
|
||||
settings["custom_dns_at_boot"] = False
|
||||
save_rgsx_settings(settings)
|
||||
|
||||
return (True, success_msg)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unexpected error: {str(e)}"
|
||||
logger.exception(error_msg)
|
||||
return (False, error_msg)
|
||||
|
||||
|
||||
def check_custom_dns_status():
|
||||
"""Vérifie si le service custom DNS est activé au démarrage.
|
||||
|
||||
Returns:
|
||||
bool: True si activé, False sinon
|
||||
"""
|
||||
try:
|
||||
if config.OPERATING_SYSTEM != "Linux":
|
||||
return False
|
||||
|
||||
# Lire l'état depuis rgsx_settings.json
|
||||
settings = load_rgsx_settings()
|
||||
return settings.get("custom_dns_at_boot", False)
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to check custom DNS status: {e}")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
_extensions_cache = None # type: ignore
|
||||
_extensions_json_regenerated = False
|
||||
@@ -1983,8 +2162,9 @@ def handle_xbox(dest_dir, iso_files, url=None):
|
||||
|
||||
|
||||
def play_random_music(music_files, music_folder, current_music=None):
|
||||
if not getattr(config, "music_enabled", True):
|
||||
pygame.mixer.music.stop()
|
||||
if not getattr(config, "music_enabled", True) or not is_mixer_available():
|
||||
if is_mixer_available():
|
||||
pygame.mixer.music.stop()
|
||||
return current_music
|
||||
if music_files:
|
||||
# Éviter de rejouer la même musique consécutivement
|
||||
@@ -1997,11 +2177,12 @@ def play_random_music(music_files, music_folder, current_music=None):
|
||||
|
||||
def load_and_play_music():
|
||||
try:
|
||||
pygame.mixer.music.load(music_path)
|
||||
pygame.mixer.music.set_volume(0.5)
|
||||
pygame.mixer.music.play(loops=0) # Jouer une seule fois
|
||||
pygame.mixer.music.set_endevent(pygame.USEREVENT + 1) # Événement de fin
|
||||
set_music_popup(music_file) # Afficher le nom de la musique dans la popup
|
||||
if is_mixer_available():
|
||||
pygame.mixer.music.load(music_path)
|
||||
pygame.mixer.music.set_volume(0.5)
|
||||
pygame.mixer.music.play(loops=0) # Jouer une seule fois
|
||||
pygame.mixer.music.set_endevent(pygame.USEREVENT + 1) # Événement de fin
|
||||
set_music_popup(music_file) # Afficher le nom de la musique dans la popup
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du chargement de la musique {music_path}: {str(e)}")
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2.3.2.0"
|
||||
"version": "2.3.2.6"
|
||||
}
|
||||
Reference in New Issue
Block a user