mirror of
https://github.com/RetroGameSets/RGSX.git
synced 2026-03-19 08:16:49 +01:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff30e6d297 | ||
|
|
5c7fa0484f | ||
|
|
814861e9ee | ||
|
|
56c87ab05f | ||
|
|
b12d645fbf | ||
|
|
04e68adef0 | ||
|
|
52f2b960c2 | ||
|
|
1ea604840e | ||
|
|
802696e78f | ||
|
|
6f17173a8c | ||
|
|
05a8df5933 | ||
|
|
55231bb823 | ||
|
|
d9c1ca6794 | ||
|
|
6613b43264 | ||
|
|
d60dc31291 | ||
|
|
ace6ec876f | ||
|
|
9f759c1928 | ||
|
|
db287e33d7 | ||
|
|
217392dcd1 | ||
|
|
fd9037139c | ||
|
|
c3bbb15c40 | ||
|
|
0c5e307112 | ||
|
|
f9d95b9a2d | ||
|
|
2033eb2f76 | ||
|
|
f1c4955670 | ||
|
|
5569238e55 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -17,7 +17,9 @@ ports/RGSX.bat
|
||||
audit_i18n.py
|
||||
prune_i18n.py
|
||||
Info.txt
|
||||
pygame/
|
||||
|
||||
# Docker test data
|
||||
data/
|
||||
|
||||
docker-compose.test.yml
|
||||
config/
|
||||
|
||||
377
README.md
377
README.md
@@ -1,241 +1,236 @@
|
||||
# 🎮 Retro Game Sets Xtra (RGSX)
|
||||
|
||||
## SUPPORT / HELP: https://discord.gg/Vph9jwg3VV
|
||||
## LISEZ-MOI / INSTRUCTIONS EN FRANCAIS : https://github.com/RetroGameSets/RGSX/blob/main/README_FR.md
|
||||
**[Discord Support](https://discord.gg/Vph9jwg3VV)** • **[Installation](#-installation)** • **[French Documentation](https://github.com/RetroGameSets/RGSX/blob/main/README_FR.md)**
|
||||
|
||||
RGSX is a Python application using Pygame for its graphical interface, created by and for the RetroGameSets community. It is completely free.
|
||||
A free, user-friendly ROM downloader for Batocera, Knulli, and RetroBat with multi-source support.
|
||||
|
||||
The application currently supports multiple download sources such as myrient and 1fichier (with optional unlocking / fallback via AllDebrid and Real-Debrid). Sources can be updated frequently.
|
||||
<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 : https://github.com/RetroGameSets/RGSX#-installation
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **Game downloads**: Supports ZIP files and handles unsupported raw archives automatically based on allowed extensions defined in EmulationStation's `es_systems.cfg` (and custom `es_systems_*.cfg` on Batocera). RGSX reads the per‑system allowed extensions and extracts archives automatically if the target system does not support zipped files.
|
||||
- Most downloads require no account or authentication.
|
||||
- Systems tagged with `(1fichier)` in their name require a valid API key (1Fichier, AllDebrid or Real-Debrid) for premium links.
|
||||
|
||||
---
|
||||
> ## IMPORTANT (1Fichier / AllDebrid / Real-Debrid)
|
||||
> To download from 1Fichier links you may use one of: your 1Fichier API key, an AllDebrid API key (automatic fallback), or a Real-Debrid API key (fallback if others missing / limited).
|
||||
>
|
||||
> Where to paste your API key (file must contain ONLY the key):
|
||||
> - `/saves/ports/rgsx/1FichierAPI.txt` (1Fichier API key)
|
||||
> - `/saves/ports/rgsx/AllDebridAPI.txt` (AllDebrid API key – optional fallback)
|
||||
> - `/saves/ports/rgsx/RealDebridAPI.txt` (Real-Debrid API key – optional fallback)
|
||||
>
|
||||
> Do NOT create these files manually. Launch RGSX once: it will auto‑create the empty files if they are missing. Then open the relevant file and paste your key.
|
||||
---
|
||||
|
||||
**🧰 Command Line (CLI) Usage**
|
||||
|
||||
RGSX also provides a headless command‑line interface to list platforms/games and download ROMs:
|
||||
|
||||
- French CLI guide: https://github.com/RetroGameSets/RGSX/blob/main/README_CLI.md
|
||||
- English CLI guide: https://github.com/RetroGameSets/RGSX/blob/main/README_CLI_EN.md
|
||||
|
||||
- **Download history**: View all current and past downloads.
|
||||
- **Multi‑selection downloads**: Mark several games using the key mapped to Clear History (default X) to prepare a batch, then Confirm to launch sequential downloads.
|
||||
- **Control customization**: Remap keyboard / controller buttons; many popular pads are auto‑configured on first launch.
|
||||
- **Platform grid layouts**: Switch between 3x3, 3x4, 4x3, 4x4.
|
||||
- **Hide unsupported systems**: Automatically hides systems whose ROM folder is missing (toggle in Display menu).
|
||||
- **Change font & size**: Accessibility & readability adjustments directly in the menu.
|
||||
- **Search / filter mode**: Quickly filter games by name; includes on‑screen virtual keyboard for controllers.
|
||||
- **Multi‑language interface**: Switch language any time in the menu.
|
||||
- **Adaptive interface**: Scales cleanly from 800x600 up to 1080p (higher resolutions untested but should work).
|
||||
- **Auto update & restart**: The application restarts itself after applying an update.
|
||||
- **System & extension discovery**: On first run, RGSX parses `es_systems.cfg` (Batocera / RetroBat) and generates `/saves/ports/rgsx/rom_extensions.json` plus the supported systems list.
|
||||
|
||||
---
|
||||
|
||||
## 🖥️ Requirements
|
||||
|
||||
### Operating System
|
||||
- Batocera / Knulli or RetroBat
|
||||
|
||||
### Hardware
|
||||
- PC, Raspberry Pi, handheld console...
|
||||
- Controller (recommended) or keyboard
|
||||
- Active internet connection
|
||||
|
||||
### Disk Space
|
||||
- ~100 MB for the application (additional space for downloaded games)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
### Automatic Method (Batocera / Knulli)
|
||||
### Quick Install (Batocera / Knulli)
|
||||
|
||||
On the target system:
|
||||
- On Batocera PC: open an xTERM (F1 > Applications > xTERM), or
|
||||
- From another machine: connect via SSH (root / linux) using PuTTY, PowerShell, etc.
|
||||
**SSH or Terminal access required:**
|
||||
```bash
|
||||
curl -L bit.ly/rgsx-install | sh
|
||||
```
|
||||
|
||||
Run:
|
||||
`curl -L bit.ly/rgsx-install | sh`
|
||||
After installation:
|
||||
1. Update game lists: `Menu > Game Settings > Update game list`
|
||||
2. Find RGSX under **PORTS** or **Homebrew and ports**
|
||||
|
||||
Wait for the script to finish (log file and on‑screen output). Then update the game list via:
|
||||
`Menu > Game Settings > Update game list`
|
||||
### Manual Install (All Systems)
|
||||
1. **Download**: [RGSX_full_latest.zip](https://github.com/RetroGameSets/RGSX/releases/latest/download/RGSX_full_latest.zip)
|
||||
2. **Extract**:
|
||||
- **Batocera/Knulli**: Extract `ports` folder to `/roms/`
|
||||
- **RetroBat**: Extract both `ports` and `windows` folders to `/roms/`
|
||||
3. **Refresh**: `Menu > Game Settings > Update game list`
|
||||
|
||||
You will find RGSX under the "PORTS" or "Homebrew and ports" system. Physical paths created: `/roms/ports/RGSX` (and `/roms/windows/RGSX` on RetroBat environments as needed).
|
||||
### Manual Update (if automatic update failed)
|
||||
Download latest release : [RGSX_update_latest.zip](https://github.com/RetroGameSets/RGSX/releases/latest/download/RGSX_full_latest.zip)
|
||||
|
||||
### Manual Method (RetroBat / Batocera)
|
||||
|
||||
1. Download ZIP: https://github.com/RetroGameSets/RGSX/archive/refs/heads/main.zip
|
||||
2. Extract into your ROMS folder:
|
||||
- Batocera: only extract the `ports` folder contents
|
||||
- RetroBat: extract both `ports` and `windows`
|
||||
3. Ensure you now have: `/roms/ports/RGSX` and (RetroBat) `/roms/windows/RGSX`
|
||||
4. Update the game list: `Menu > Game Settings > Update game list`
|
||||
**Installed paths:**
|
||||
- `/roms/ports/RGSX` (all systems)
|
||||
- `/roms/windows/RGSX` (RetroBat only)
|
||||
|
||||
---
|
||||
|
||||
## 🏁 First Launch
|
||||
## 🎮 Usage
|
||||
|
||||
- RGSX appears in the "WINDOWS" system on RetroBat, and in "PORTS" / "Homebrew and ports" on Batocera/Knulli.
|
||||
- On first launch, if your controller matches a predefined profile in `/roms/ports/RGSX/assets/controls`, mapping is auto‑imported.
|
||||
- The app then downloads required data (system images, game lists, etc.).
|
||||
- If controls act strangely or are corrupt, delete `/saves/ports/rgsx/controls.json` and restart (it will be regenerated).
|
||||
### First Launch
|
||||
|
||||
INFO (RetroBat only): On the first run, Python (~50 MB) is downloaded into `/system/tools/python`. The screen may appear frozen on the loading splash for several seconds—this is normal. Installation output is logged in `/roms/ports/RGSX-INSTALL.log` (share this if you need support).
|
||||
- Auto-downloads system images and game lists
|
||||
- Auto-configures controls if your controller is recognized
|
||||
- **Controls broken?** Delete `/saves/ports/rgsx/controls.json` and restart
|
||||
|
||||
**Keyboard Mode**: When no controller is detected, controls display as `[Key]` instead of icons.
|
||||
|
||||
### Pause Menu Structure
|
||||
|
||||
**Controls**
|
||||
- View Controls Help
|
||||
- Remap Controls
|
||||
|
||||
**Display**
|
||||
- Layout (3×3, 3×4, 4×3, 4×4)
|
||||
- Font Size (general UI)
|
||||
- Footer Font Size (controls/version text)
|
||||
- Font Family (pixel fonts)
|
||||
- Hide Unknown Extension Warning
|
||||
|
||||
**Games**
|
||||
- Download History
|
||||
- Source Mode (RGSX / Custom)
|
||||
- Update Game Cache
|
||||
- Show Unsupported Platforms
|
||||
- Hide Premium Systems
|
||||
- Filter Platforms
|
||||
|
||||
**Settings**
|
||||
- Background Music Toggle
|
||||
- Symlink Options (Batocera)
|
||||
- Web Service (Batocera)
|
||||
- API Keys Management
|
||||
- Language Selection
|
||||
|
||||
---
|
||||
|
||||
## 🕹️ Usage
|
||||
## ✨ Features
|
||||
|
||||
### Menu Navigation
|
||||
- 🎯 **Smart System Detection** – Auto-discovers supported systems from `es_systems.cfg`
|
||||
- 📦 **Intelligent Archive Handling** – Auto-extracts archives when systems don't support ZIP files
|
||||
- 🔑 **Premium Unlocking** – 1Fichier API + AllDebrid/Real-Debrid fallback for unlimited downloads
|
||||
- 🎨 **Fully Customizable** – Layout (3×3 to 4×4), fonts, font sizes (UI + footer), languages (EN/FR/DE/ES/IT/PT)
|
||||
- 🎮 **Controller-First Design** – Auto-mapping for popular controllers + custom remapping support
|
||||
- 🔍 **Advanced Filtering** – Search by name, hide/show unsupported systems, filter platforms
|
||||
- 📊 **Download Management** – Queue system, history tracking, progress notifications
|
||||
- 🌐 **Custom Sources** – Use your own game repository URLs
|
||||
- ♿ **Accessibility** – Separate font scaling for UI and footer, keyboard-only mode support
|
||||
|
||||
- Use D‑Pad / Arrow keys to move between platforms, games, and options.
|
||||
- Press the Start key (default: `P` or controller Start) for the pause menu with all configuration options.
|
||||
- From the pause menu you can regenerate cached system/game/image lists to pull latest updates.
|
||||
|
||||
### Display Menu
|
||||
|
||||
- Layout: switch platform grid (3x3, 3x4, 4x3, 4x4)
|
||||
- Font size: adjust text scale (accessibility)
|
||||
- Show unsupported systems: toggle systems whose ROM directory is missing
|
||||
- Filter systems: persistently include/exclude systems by name
|
||||
> ### 🔑 API Keys Setup
|
||||
> For unlimited 1Fichier downloads, add your API key(s) to `/saves/ports/rgsx/`:
|
||||
> - `1FichierAPI.txt` – 1Fichier API key (recommended)
|
||||
> - `AllDebridAPI.txt` – AllDebrid fallback (optional)
|
||||
> - `RealDebridAPI.txt` – Real-Debrid fallback (optional)
|
||||
>
|
||||
> **Each file must contain ONLY the key, no extra text.**
|
||||
|
||||
### Downloading Games
|
||||
|
||||
1. Select a platform then a game
|
||||
2. Press the Confirm key (default: Enter / A) to start downloading
|
||||
3. (Optional) Press the Clear History key (default: X) on multiple games to toggle multi‑selection ([X] marker), then Confirm to launch a sequential batch
|
||||
4. Track progress in the HISTORY menu
|
||||
1. Browse platforms → Select game
|
||||
2. **Direct Download**: Press `Confirm`
|
||||
3. **Queue Download**: Press `X` (West button)
|
||||
4. Track progress in **History** menu or via popup notifications
|
||||
|
||||
### Control Customization
|
||||
### Custom Game Sources
|
||||
|
||||
- Open pause menu → Reconfigure controls
|
||||
- Hold each desired key/button for ~3 seconds when prompted
|
||||
- Button labels adapt to your pad (A/B/X/Y, LB/RB/LT/RT, etc.)
|
||||
- Delete `/saves/ports/rgsx/controls.json` if mapping breaks; restart to regenerate
|
||||
Switch to custom sources via **Pause Menu > Games > Source Mode**.
|
||||
|
||||
### History
|
||||
|
||||
- Access from pause menu or press the History key (default: H)
|
||||
- Select an entry to re‑download (e.g. after an error or cancellation)
|
||||
- CLEAR button empties the list only (does not delete installed games)
|
||||
- BACK cancels an active download
|
||||
|
||||
### Logs
|
||||
|
||||
Logs are stored at: `/roms/ports/RGSX/logs/RGSX.log` (provide this for troubleshooting).
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Changelog
|
||||
See Discord or GitHub commits for the latest changes.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Custom Game Sources
|
||||
Switch the game source in the pause menu (Game Source: RGSX / Custom).
|
||||
|
||||
Custom mode expects an HTTP/HTTPS ZIP URL pointing to a sources archive mirroring the default structure. Configure in:
|
||||
`{rgsx_settings path}` → key: `sources.custom_url`
|
||||
|
||||
Behavior:
|
||||
- If custom mode is selected and URL is empty/invalid → empty list + popup (no fallback)
|
||||
- Fix the URL then choose "Update games list" (restart if prompted)
|
||||
|
||||
Example `rgsx_settings.json` snippet:
|
||||
Configure in `/saves/ports/rgsx/rgsx_settings.json`:
|
||||
```json
|
||||
"sources": {
|
||||
"mode": "custom",
|
||||
"custom_url": "https://example.com/my-sources.zip"
|
||||
{
|
||||
"sources": {
|
||||
"mode": "custom",
|
||||
"custom_url": "https://example.com/my-sources.zip"
|
||||
}
|
||||
}
|
||||
```
|
||||
Switch back to RGSX mode any time via the pause menu.
|
||||
**Note**: If custom mode activated but Invalid/empty URL = using /saves/ports/rgsx/games.zip . You need to update games cache on RGSX menu after fixing URL.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Project Structure
|
||||
```
|
||||
/roms/windows/RGSX
|
||||
│
|
||||
├── RGSX Retrobat.bat # Windows/RetroBat launcher (not needed on Batocera/Knulli)
|
||||
## 🌐 Web Interface (Batocera/Knulli Only)
|
||||
|
||||
/roms/ports/
|
||||
├── RGSX-INSTALL.log # Install log (first scripted install)
|
||||
└── RGSX/
|
||||
├── __main__.py # Main entry point
|
||||
├── controls.py # Input handling & menu navigation events
|
||||
├── controls_mapper.py # Interactive control remapping & auto button naming
|
||||
├── display.py # Pygame rendering layer
|
||||
├── config.py # Global paths / parameters
|
||||
├── rgsx_settings.py # Unified settings manager
|
||||
├── network.py # Download logic (multi-provider, fallback)
|
||||
├── history.py # Download history store & UI logic
|
||||
├── language.py # Localization manager
|
||||
├── accessibility.py # Accessibility options (fonts, layout)
|
||||
├── utils.py # Helper utilities (text wrapping, truncation, etc.)
|
||||
├── update_gamelist.py # Game list updater (Batocera/Knulli)
|
||||
├── update_gamelist_windows.py # RetroBat gamelist auto-update on launch
|
||||
├── assets/ # Fonts, binaries, music, predefined control maps
|
||||
├── languages/ # Translation files
|
||||
└── logs/
|
||||
└── RGSX.log # Runtime log
|
||||
RGSX includes a web interface that launched automatically when using RGSX for remote browsing and downloading games from any device on your network.
|
||||
|
||||
### Accessing the Web Interface
|
||||
|
||||
1. **Find your Batocera IP address**:
|
||||
- Check Batocera menu: `Network Settings`
|
||||
- Or from terminal: `ip addr show`
|
||||
|
||||
2. **Open in browser**: `http://[BATOCERA_IP]:5000` or `http://BATOCERA:5000`
|
||||
- Example: `http://192.168.1.100:5000`
|
||||
|
||||
3. **Available from any device**: Phone, tablet, PC on the same network
|
||||
|
||||
### Web Interface Features
|
||||
|
||||
- 📱 **Mobile-Friendly** – Responsive design works on all screen sizes
|
||||
- 🔍 **Browse All Systems** – View all platforms and games
|
||||
- ⬇️ **Remote Downloads** – Queue downloads directly to your Batocera
|
||||
- 📊 **Real-Time Status** – See active downloads and history
|
||||
- 🎮 **Same Game Lists** – Uses identical sources as the main app
|
||||
|
||||
|
||||
### Enable/Disable Web Service at Boot, without the need to launch RGSX
|
||||
|
||||
**From RGSX Menu**
|
||||
1. Open **Pause Menu** (Start/ALTGr)
|
||||
2. Navigate to **Settings > Web Service**
|
||||
3. Toggle **Enable at Boot**
|
||||
4. Restart your device
|
||||
|
||||
|
||||
**Port Configuration**: The web service runs on port `5000` by default. Ensure this port is not blocked by firewall rules.
|
||||
|
||||
---
|
||||
|
||||
## 📁 File Structure
|
||||
|
||||
/saves/ports/RGSX/
|
||||
├── systems_list.json # Discovered systems / folders / images
|
||||
├── games/ # Platform game link repositories
|
||||
├── images/ # Downloaded platform images
|
||||
├── rgsx_settings.json # Unified config (settings, language, music, symlinks, sources)
|
||||
├── controls.json # Generated control mapping
|
||||
├── history.json # Download history database
|
||||
├── rom_extensions.json # Allowed ROM extensions cache from es_systems.cfg
|
||||
├── 1FichierAPI.txt # 1Fichier API key (empty until you paste key)
|
||||
├── AllDebridAPI.txt # AllDebrid API key (optional fallback)
|
||||
└── RealDebridAPI.txt # Real-Debrid API key (optional fallback)
|
||||
```
|
||||
/roms/ports/RGSX/
|
||||
├── __main__.py # Entry point
|
||||
├── controls.py # Input handling
|
||||
├── display.py # Rendering engine
|
||||
├── network.py # Download manager
|
||||
├── rgsx_settings.py # Settings manager
|
||||
├── assets/controls/ # Controller profiles
|
||||
├── languages/ # Translations (EN/FR/DE/ES/IT/PT)
|
||||
└── logs/RGSX.log # Runtime logs
|
||||
|
||||
/roms/windows/RGSX/
|
||||
└── RGSX Retrobat.bat # RetroBat launcher
|
||||
|
||||
/saves/ports/rgsx/
|
||||
├── rgsx_settings.json # User preferences
|
||||
├── controls.json # Control mapping
|
||||
├── history.json # Download history
|
||||
├── rom_extensions.json # Supported extensions cache
|
||||
├── systems_list.json # Detected systems
|
||||
├── games/ # Game databases (per platform)
|
||||
├── images/ # Platform images
|
||||
├── 1FichierAPI.txt # 1Fichier API key
|
||||
├── AllDebridAPI.txt # AllDebrid API key
|
||||
└── RealDebridAPI.txt # Real-Debrid API key
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| Controls not working | Delete `/saves/ports/rgsx/controls.json` + restart |
|
||||
| Games not showing | Pause Menu > Games > Update Game Cache |
|
||||
| Download stuck | Check API keys in `/saves/ports/rgsx/` |
|
||||
| App crashes | Check `/roms/ports/RGSX/logs/RGSX.log` |
|
||||
| Layout change not applied | Restart RGSX after changing layout |
|
||||
|
||||
**Need help?** Share logs from `/roms/ports/RGSX/logs/` on [Discord](https://discord.gg/Vph9jwg3VV).
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
### Report a Bug
|
||||
1. Review `/roms/ports/RGSX/logs/RGSX.log`.
|
||||
2. Open a GitHub issue with a clear description + relevant log excerpt OR share it on Discord.
|
||||
|
||||
### Propose a Feature
|
||||
- Open an issue (or discuss on Discord first) describing the feature and its integration.
|
||||
|
||||
### Contribute Code
|
||||
1. Fork the repository & create a feature branch:
|
||||
```bash
|
||||
git checkout -b feature/your-feature-name
|
||||
```
|
||||
2. Test on Batocera / RetroBat.
|
||||
3. Open a Pull Request with a detailed summary.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Known Issues
|
||||
- (None currently listed)
|
||||
- **Bug Reports**: Open GitHub issue with logs or post on Discord
|
||||
- **Feature Requests**: Discuss on Discord first, then open issue
|
||||
- **Code Contributions**:
|
||||
```bash
|
||||
git checkout -b feature/your-feature
|
||||
# Test on Batocera/RetroBat
|
||||
# Submit Pull Request
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 License
|
||||
This project is free software. You are free to use, modify, and distribute it under the terms of the included license.
|
||||
|
||||
Developed with ❤️ for retro gaming enthusiasts.
|
||||
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.**
|
||||
|
||||
251
README_CLI.md
251
README_CLI.md
@@ -1,251 +0,0 @@
|
||||
# RGSX CLI — Guide d’utilisation
|
||||
|
||||
Ce guide couvre toutes les commandes disponibles du CLI et fournit des exemples prêts à copier (Windows PowerShell).
|
||||
|
||||
## Nouveau: mode interactif
|
||||
Vous pouvez maintenant lancer une session interactive et enchaîner les commandes sans retaper `python rgsx_cli.py` à chaque fois :
|
||||
|
||||
```powershell
|
||||
python rgsx_cli.py
|
||||
```
|
||||
Vous verrez :
|
||||
```
|
||||
RGSX CLI interactive mode. Type 'help' for commands, 'exit' to quit.
|
||||
rgsx>
|
||||
```
|
||||
Dans cette session tapez directement les sous-commandes :
|
||||
```
|
||||
rgsx> platforms
|
||||
rgsx> games --platform snes --search mario
|
||||
rgsx> download --platform snes --game "Super Mario World (USA).zip"
|
||||
rgsx> history --tail 10
|
||||
rgsx> exit
|
||||
```
|
||||
Extras :
|
||||
- `help` ou `?` affiche l’aide globale.
|
||||
- `exit` ou `quit` quitte la session.
|
||||
- `--verbose` une fois active les logs détaillés pour toute la session.
|
||||
|
||||
## Tableau formaté (platforms)
|
||||
La commande `platforms` affiche maintenant un tableau ASCII à largeur fixe (sauf avec `--json`) :
|
||||
```
|
||||
+--------------------------------+-----------------+
|
||||
| Nom de plateforme | Dossier |
|
||||
+--------------------------------+-----------------+
|
||||
| Nintendo Entertainment System | nes |
|
||||
| Super Nintendo Entertainment.. | snes |
|
||||
| Sega Mega Drive | megadrive |
|
||||
+--------------------------------+-----------------+
|
||||
```
|
||||
Colonnes : 30 caractères pour le nom, 15 pour le dossier (troncature par `...`).
|
||||
|
||||
## Aliases & synonymes d’options (mis à jour)
|
||||
Aliases des sous-commandes :
|
||||
- `platforms` → `p`
|
||||
- `games` → `g`
|
||||
- `download` → `dl`
|
||||
- `clear-history` → `clear`
|
||||
|
||||
Options équivalentes (toutes les formes listées sont acceptées) :
|
||||
- Plateforme : `--platform`, `--p`, `-p`
|
||||
- Jeu : `--game`, `--g`, `-g`
|
||||
- Recherche : `--search`, `--s`, `-s`
|
||||
- Forcer (download) : `--force`, `-f`
|
||||
- Mode interactif (download) : `--interactive`, `-i`
|
||||
|
||||
Exemples avec alias :
|
||||
```powershell
|
||||
python rgsx_cli.py dl -p snes -g "Super Mario World (USA).zip"
|
||||
python rgsx_cli.py g --p snes --s mario
|
||||
python rgsx_cli.py p --json
|
||||
python rgsx_cli.py clear
|
||||
```
|
||||
|
||||
## Sélection ambiguë lors d’un download (nouveau tableau)
|
||||
Quand vous tentez un téléchargement avec un titre non exact et que le mode interactif est actif (TTY ou `--interactive`), les correspondances s’affichent en tableau :
|
||||
```
|
||||
No exact result found for this game: mario super yoshi
|
||||
Select a match to download:
|
||||
+------+--------------------------------------------------------------+------------+
|
||||
| # | Title | Size |
|
||||
+------+--------------------------------------------------------------+------------+
|
||||
| 1 | Super Mario - Yoshi Island (Japan).zip | 3.2M |
|
||||
| 2 | Super Mario - Yoshi Island (Japan) (Rev 1).zip | 3.2M |
|
||||
| 3 | Super Mario - Yoshi Island (Japan) (Rev 2).zip | 3.2M |
|
||||
| 4 | Super Mario World 2 - Yoshi's Island (USA).zip | 3.3M |
|
||||
| 5 | Super Mario - Yoshi Island (Japan) (Beta) (1995-07-10).zip | 3.1M |
|
||||
+------+--------------------------------------------------------------+------------+
|
||||
Enter number (or press Enter to cancel):
|
||||
```
|
||||
Si vous annulez ou que le mode interactif n’est pas actif, un tableau similaire est affiché (sans le prompt) suivi d’un conseil.
|
||||
|
||||
## Recherche améliorée (multi‑tokens) pour `games`
|
||||
L’option `--search` / `--s` / `-s` utilise maintenant la même logique de classement que les suggestions du download :
|
||||
1. Correspondance sous-chaîne (position la plus tôt) — priorité 0
|
||||
2. Séquence de tokens dans l’ordre (non contiguë) — priorité 1 (écart le plus faible)
|
||||
3. Tous les tokens présents dans n’importe quel ordre — priorité 2 (ensemble de tokens plus petit privilégié)
|
||||
|
||||
Les doublons sont dédupliqués en gardant le meilleur score. Ainsi une requête :
|
||||
```powershell
|
||||
python rgsx_cli.py games --p snes --s "super mario yoshi"
|
||||
```
|
||||
affiche toutes les variantes pertinentes de "Super Mario World 2 - Yoshi's Island" même si l’ordre des mots diffère.
|
||||
|
||||
Exemple de sortie :
|
||||
```
|
||||
+--------------------------------------------------------------+------------+
|
||||
| Game Title | Size |
|
||||
+--------------------------------------------------------------+------------+
|
||||
| Super Mario World 2 - Yoshi's Island (USA).zip | 3.3M |
|
||||
| Super Mario World 2 - Yoshi's Island (Europe) (En,Fr,De).zip | 3.3M |
|
||||
| Super Mario - Yoshi Island (Japan).zip | 3.2M |
|
||||
| Super Mario - Yoshi Island (Japan) (Rev 1).zip | 3.2M |
|
||||
| Super Mario - Yoshi Island (Japan) (Rev 2).zip | 3.2M |
|
||||
+--------------------------------------------------------------+------------+
|
||||
```
|
||||
Si aucun résultat n’est trouvé, seul l’en-tête est affiché puis un message.
|
||||
|
||||
## Prérequis
|
||||
- Python installé et accessible (le projet utilise un mode headless; aucune fenêtre ne s’ouvrira).
|
||||
- Exécuter depuis le dossier contenant `rgsx_cli.py`.
|
||||
|
||||
## Syntaxe générale (mode classique)
|
||||
Les options globales peuvent être placées avant ou après la sous-commande.
|
||||
|
||||
- Forme 1:
|
||||
```powershell
|
||||
python rgsx_cli.py [--verbose] [--force-update|-force-update] <commande> [options]
|
||||
```
|
||||
- Forme 2:
|
||||
```powershell
|
||||
python rgsx_cli.py <commande> [options] [--verbose] [--force-update|-force-update]
|
||||
```
|
||||
|
||||
- `--verbose` active les logs détaillés (DEBUG) sur stderr.
|
||||
- `--force-update` (ou `-force-update`) purge les données locales et force le re-téléchargement du pack de données (systems_list, games/*.json, images).
|
||||
|
||||
Quand les données sources sont manquantes, le CLI télécharge et extrait automatiquement le pack (avec progression).
|
||||
|
||||
## Commandes
|
||||
|
||||
### 1) platforms (`platforms` / `p`) — lister les plateformes
|
||||
- Options:
|
||||
- `--json`: sortie JSON (objets `{ name, folder }`).
|
||||
|
||||
Exemples:
|
||||
```powershell
|
||||
python rgsx_cli.py platforms
|
||||
python rgsx_cli.py p --json
|
||||
python rgsx_cli.py --verbose p
|
||||
python rgsx_cli.py p --verbose
|
||||
```
|
||||
|
||||
Sortie texte: une ligne par plateforme, au format `Nom<TAB>Dossier`.
|
||||
|
||||
### 2) games (`games` / `g`) — lister les jeux d’une plateforme
|
||||
- Options:
|
||||
- `--platform | --p | -p <nom_ou_dossier>` (ex: `n64` ou "Nintendo 64").
|
||||
- `--search | --s | -s <texte>`: filtre par sous-chaîne.
|
||||
|
||||
Exemples:
|
||||
```powershell
|
||||
python rgsx_cli.py games --platform n64
|
||||
python rgsx_cli.py g --p "Nintendo 64" --s zelda
|
||||
python rgsx_cli.py g -p n64 --verbose
|
||||
```
|
||||
|
||||
Remarques:
|
||||
- La plateforme est résolue par nom affiché (platform_name) ou dossier, insensible à la casse.
|
||||
|
||||
### 3) download (`download` / `dl`) — télécharger un jeu
|
||||
- Options:
|
||||
- `--platform | --p | -p <nom_ou_dossier>`
|
||||
- `--game | --g | -g "<titre exact ou partiel>"`
|
||||
- `--force | -f`: ignorer l’avertissement d’extension non supportée.
|
||||
- `--interactive | -i`: choisir un titre parmi des correspondances quand aucun exact n’est trouvé.
|
||||
|
||||
Exemples:
|
||||
```powershell
|
||||
# Titre exact
|
||||
python rgsx_cli.py dl --p n64 --g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip"
|
||||
|
||||
# Titre partiel (sélection numérotée si aucun exact)
|
||||
python rgsx_cli.py dl -p n64 -g "Ocarina of Time (Beta)"
|
||||
|
||||
# Forcer malgré extension
|
||||
python rgsx_cli.py dl -p snes -g "pack_roms.rar" -f
|
||||
|
||||
# Verbose après sous-commande
|
||||
python rgsx_cli.py dl -p n64 -g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip" --verbose
|
||||
```
|
||||
|
||||
Pendant le téléchargement: progression %, taille (MB), vitesse (MB/s). Résultat final aussi dans l’historique.
|
||||
|
||||
Notes:
|
||||
- Les ROMs sont enregistrées dans le dossier plateforme correspondant (ex: `R:\roms\n64`).
|
||||
- Si le fichier est une archive (zip/rar) et que la plateforme ne supporte pas l’extension, un avertissement apparaît (utiliser `--force`).
|
||||
|
||||
### 4) history — afficher l’historique
|
||||
- Options:
|
||||
- `--tail <N>`: n dernières entrées (défaut: 50)
|
||||
- `--json`: sortie JSON
|
||||
|
||||
Exemples:
|
||||
```powershell
|
||||
python rgsx_cli.py history
|
||||
python rgsx_cli.py history --tail 20
|
||||
python rgsx_cli.py history --json
|
||||
```
|
||||
|
||||
### 5) clear-history (`clear-history` / `clear`) — vider l’historique
|
||||
Exemple:
|
||||
```powershell
|
||||
python rgsx_cli.py clear
|
||||
```
|
||||
|
||||
### Option globale: --force-update — purge + re-téléchargement des données
|
||||
- Supprime `systems_list.json`, `games/`, `images/` puis retélécharge/extrait le pack.
|
||||
|
||||
Exemples:
|
||||
```powershell
|
||||
python rgsx_cli.py --force-update
|
||||
python rgsx_cli.py p --force-update
|
||||
```
|
||||
|
||||
## Comportements et conseils
|
||||
- Résolution plateforme: par nom affiché ou dossier, insensible à la casse.
|
||||
- `--verbose`: utile surtout pour téléchargements/extractions.
|
||||
- Données manquantes: téléchargement + extraction automatiques.
|
||||
- Codes de sortie (indicatif):
|
||||
- `0`: succès
|
||||
- `1`: échec téléchargement/erreur générique
|
||||
- `2`: plateforme introuvable
|
||||
- `3`: jeu introuvable
|
||||
- `4`: extension non supportée (sans `--force`)
|
||||
|
||||
## Exemples rapides (copier-coller)
|
||||
```powershell
|
||||
# Démarrer le shell interactif
|
||||
python rgsx_cli.py
|
||||
|
||||
# Lister plateformes (alias)
|
||||
python rgsx_cli.py p
|
||||
|
||||
# Lister plateformes (JSON)
|
||||
python rgsx_cli.py p --json
|
||||
|
||||
# Lister jeux N64 avec filtre (synonymes)
|
||||
python rgsx_cli.py g --p n64 --s zelda
|
||||
|
||||
# Télécharger un jeu N64 (titre exact) avec alias
|
||||
python rgsx_cli.py dl --p n64 --g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip"
|
||||
|
||||
# Télécharger (titre partiel) + sélection
|
||||
python rgsx_cli.py dl -p n64 -g "Ocarina of Time"
|
||||
|
||||
# Historique (20 dernières entrées)
|
||||
python rgsx_cli.py history --tail 20
|
||||
|
||||
# Purger et recharger le pack
|
||||
python rgsx_cli.py --force-update
|
||||
```
|
||||
254
README_CLI_EN.md
254
README_CLI_EN.md
@@ -1,254 +0,0 @@
|
||||
# RGSX CLI — Usage Guide
|
||||
|
||||
This guide covers all available CLI commands with copy-ready Windows PowerShell examples.
|
||||
|
||||
## Prerequisites
|
||||
- Python installed and on PATH (the app runs in headless mode; no window will open).
|
||||
- Run commands from the folder that contains `rgsx_cli.py`.
|
||||
|
||||
## Quick interactive mode (new)
|
||||
You can now start an interactive shell once and issue multiple commands without retyping `python rgsx_cli.py` each time:
|
||||
|
||||
```powershell
|
||||
python rgsx_cli.py
|
||||
```
|
||||
You will see a prompt like:
|
||||
```
|
||||
RGSX CLI interactive mode. Type 'help' for commands, 'exit' to quit.
|
||||
rgsx>
|
||||
```
|
||||
Inside this shell type subcommands exactly as you would after `python rgsx_cli.py`:
|
||||
```
|
||||
rgsx> platforms
|
||||
rgsx> games --platform snes --search mario
|
||||
rgsx> download --platform snes --game "Super Mario World (USA).zip"
|
||||
rgsx> history --tail 10
|
||||
rgsx> exit
|
||||
```
|
||||
Extras:
|
||||
- `help` or `?` prints the global help.
|
||||
- `exit` or `quit` leaves the shell.
|
||||
- `--verbose` once sets persistent verbose logging for the rest of the session.
|
||||
|
||||
## Formatted table output (platforms)
|
||||
The `platforms` command now renders a fixed-width ASCII table (unless `--json` is used):
|
||||
```
|
||||
+--------------------------------+-----------------+
|
||||
| Platform Name | Folder |
|
||||
+--------------------------------+-----------------+
|
||||
| Nintendo Entertainment System | nes |
|
||||
| Super Nintendo Entertainment.. | snes |
|
||||
| Sega Mega Drive | megadrive |
|
||||
+--------------------------------+-----------------+
|
||||
```
|
||||
Columns: 30 chars for name, 15 for folder (values longer are truncated with `...`).
|
||||
|
||||
## Aliases & option synonyms (updated)
|
||||
Subcommand aliases:
|
||||
- `platforms` → `p`
|
||||
- `games` → `g`
|
||||
- `download` → `dl`
|
||||
- `clear-history` → `clear`
|
||||
|
||||
Option aliases (all shown forms are accepted; they are equivalent):
|
||||
- Platform: `--platform`, `--p`, `-p`
|
||||
- Game: `--game`, `--g`, `-g`
|
||||
- Search: `--search`, `--s`, `-s`
|
||||
- Force (download): `--force`, `-f`
|
||||
- Interactive (download): `--interactive`, `-i`
|
||||
|
||||
Examples with aliases:
|
||||
```powershell
|
||||
python rgsx_cli.py dl -p snes -g "Super Mario World (USA).zip"
|
||||
python rgsx_cli.py g --p snes --s mario
|
||||
python rgsx_cli.py p --json
|
||||
python rgsx_cli.py clear
|
||||
```
|
||||
|
||||
## Ambiguous download selection (new table)
|
||||
When you attempt a download with a non-exact title and interactive mode is active (TTY or `--interactive`), matches are displayed in a table:
|
||||
```
|
||||
No exact result found for this game: mario super yoshi
|
||||
Select a match to download:
|
||||
+------+--------------------------------------------------------------+------------+
|
||||
| # | Title | Size |
|
||||
+------+--------------------------------------------------------------+------------+
|
||||
| 1 | Super Mario - Yoshi Island (Japan).zip | 3.2M |
|
||||
| 2 | Super Mario - Yoshi Island (Japan) (Rev 1).zip | 3.2M |
|
||||
| 3 | Super Mario - Yoshi Island (Japan) (Rev 2).zip | 3.2M |
|
||||
| 4 | Super Mario World 2 - Yoshi's Island (USA).zip | 3.3M |
|
||||
| 5 | Super Mario - Yoshi Island (Japan) (Beta) (1995-07-10).zip | 3.1M |
|
||||
+------+--------------------------------------------------------------+------------+
|
||||
Enter number (or press Enter to cancel):
|
||||
```
|
||||
If you cancel or are not in interactive mode, a similar table is still shown (without the prompt) followed by a tip.
|
||||
|
||||
## Improved fuzzy search for games (multi-token)
|
||||
The `--search` / `--s` / `-s` option now uses the same multi-strategy ranking as the download suggestion logic:
|
||||
1. Substring match (position-based) — highest priority
|
||||
2. Ordered non-contiguous token sequence (smallest gap wins)
|
||||
3. All tokens present in any order (smaller token set size wins)
|
||||
|
||||
Duplicate titles are deduplicated by keeping the best scoring strategy. This means queries like:
|
||||
```powershell
|
||||
python rgsx_cli.py games --p snes --s "super mario yoshi"
|
||||
```
|
||||
will surface all relevant "Super Mario World 2 - Yoshi's Island" variants even if the word order differs.
|
||||
|
||||
Example output:
|
||||
```
|
||||
+--------------------------------------------------------------+------------+
|
||||
| Game Title | Size |
|
||||
+--------------------------------------------------------------+------------+
|
||||
| Super Mario World 2 - Yoshi's Island (USA).zip | 3.3M |
|
||||
| Super Mario World 2 - Yoshi's Island (Europe) (En,Fr,De).zip | 3.3M |
|
||||
| Super Mario - Yoshi Island (Japan).zip | 3.2M |
|
||||
| Super Mario - Yoshi Island (Japan) (Rev 1).zip | 3.2M |
|
||||
| Super Mario - Yoshi Island (Japan) (Rev 2).zip | 3.2M |
|
||||
+--------------------------------------------------------------+------------+
|
||||
```
|
||||
If no results are found the table displays only headers followed by a message.
|
||||
|
||||
## General syntax (non-interactive)
|
||||
Global options can be placed before or after the subcommand.
|
||||
|
||||
- Form 1:
|
||||
```powershell
|
||||
python rgsx_cli.py [--verbose] [--force-update|-force-update] <command> [options]
|
||||
```
|
||||
- Form 2:
|
||||
```powershell
|
||||
python rgsx_cli.py <command> [options] [--verbose] [--force-update|-force-update]
|
||||
```
|
||||
|
||||
- `--verbose` enables detailed logs (DEBUG) on stderr.
|
||||
- `--force-update` (or `-force-update`) purges local data and re-downloads the data pack (systems_list, games/*.json, images).
|
||||
|
||||
When source data is missing, the CLI will automatically download and extract the data pack (with progress).
|
||||
|
||||
## Commands
|
||||
|
||||
### 1) platforms (`platforms` / `p`) — list platforms
|
||||
- Options:
|
||||
- `--json`: JSON output (objects `{ name, folder }`).
|
||||
|
||||
Examples:
|
||||
```powershell
|
||||
python rgsx_cli.py platforms
|
||||
python rgsx_cli.py p --json
|
||||
python rgsx_cli.py --verbose p
|
||||
python rgsx_cli.py p --verbose
|
||||
```
|
||||
|
||||
Text output: one line per platform, formatted as `Name<TAB>Folder`.
|
||||
|
||||
### 2) games (`games` / `g`) — list games for a platform
|
||||
- Options:
|
||||
- `--platform | --p | -p <name_or_folder>` (e.g., `n64` or "Nintendo 64").
|
||||
- `--search | --s | -s <text>`: filter by substring in game title.
|
||||
|
||||
Examples:
|
||||
```powershell
|
||||
python rgsx_cli.py games --platform n64
|
||||
python rgsx_cli.py g --p "Nintendo 64" --s zelda
|
||||
python rgsx_cli.py g -p n64 --verbose
|
||||
```
|
||||
|
||||
Notes:
|
||||
- The platform is resolved by display name (platform_name) or folder, case-insensitively.
|
||||
|
||||
### 3) download (`download` / `dl`) — download a game
|
||||
- Options:
|
||||
- `--platform | --p | -p <name_or_folder>`
|
||||
- `--game | --g | -g "<exact or partial title>"`
|
||||
- `--force | -f`: ignore unsupported-extension warning for the platform.
|
||||
- `--interactive | -i`: prompt to choose from matches when no exact title is found.
|
||||
|
||||
Examples:
|
||||
```powershell
|
||||
# Exact title
|
||||
python rgsx_cli.py dl --p n64 --g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip"
|
||||
|
||||
# Partial match (interactive numbered selection if no exact match)
|
||||
python rgsx_cli.py dl -p n64 -g "Ocarina of Time (Beta)"
|
||||
|
||||
# Forced despite extension
|
||||
python rgsx_cli.py dl -p snes -g "pack_roms.rar" -f
|
||||
|
||||
# Verbose after subcommand
|
||||
python rgsx_cli.py dl -p n64 -g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip" --verbose
|
||||
```
|
||||
|
||||
During download, progress %, size (MB) and speed (MB/s) are shown. The final result is also written to history.
|
||||
|
||||
Notes:
|
||||
- ROMs are saved into the corresponding platform directory (e.g., `R:\roms\n64`).
|
||||
- If the file is an archive (zip/rar) and the platform doesn’t support that extension, a warning is shown (you can use `--force`).
|
||||
|
||||
### 4) history — show history
|
||||
- Options:
|
||||
- `--tail <N>`: last N entries (default: 50)
|
||||
- `--json`: JSON output
|
||||
|
||||
Examples:
|
||||
```powershell
|
||||
python rgsx_cli.py history
|
||||
python rgsx_cli.py history --tail 20
|
||||
python rgsx_cli.py history --json
|
||||
```
|
||||
|
||||
### 5) clear-history (`clear-history` / `clear`) — clear history
|
||||
Example:
|
||||
```powershell
|
||||
python rgsx_cli.py clear
|
||||
```
|
||||
|
||||
### Global option: --force-update — purge + re-download data
|
||||
- Removes `systems_list.json`, the `games/` and `images/` folders, then downloads/extracts the data pack again.
|
||||
|
||||
Examples:
|
||||
```powershell
|
||||
# Without subcommand: purge + re-download then exit
|
||||
python rgsx_cli.py --force-update
|
||||
|
||||
# Placed after a subcommand (also accepted)
|
||||
python rgsx_cli.py p --force-update
|
||||
```
|
||||
|
||||
## Behavior and tips
|
||||
- Platform resolution: by display name or folder, case-insensitive. For `games` and `download`, if no exact match is found a search-like suggestion list is shown.
|
||||
- `--verbose` logs: most useful during downloads/extraction; printed at DEBUG level.
|
||||
- Missing data download: automatic, with consistent progress (download then extraction).
|
||||
- Exit codes (indicative):
|
||||
- `0`: success
|
||||
- `1`: download failure/generic error
|
||||
- `2`: platform not found
|
||||
- `3`: game not found
|
||||
- `4`: unsupported extension (without `--force`)
|
||||
|
||||
## Quick examples (copy/paste)
|
||||
```powershell
|
||||
# Start interactive shell
|
||||
python rgsx_cli.py
|
||||
|
||||
# List platforms (text)
|
||||
python rgsx_cli.py p
|
||||
|
||||
# List platforms (JSON)
|
||||
python rgsx_cli.py p --json
|
||||
|
||||
# List N64 games with filter (using alias synonyms)
|
||||
python rgsx_cli.py g --p n64 --s zelda
|
||||
|
||||
# Download an N64 game (exact title) using aliases
|
||||
python rgsx_cli.py dl --p n64 --g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip"
|
||||
|
||||
# Download with approximate title (suggestions + interactive pick)
|
||||
python rgsx_cli.py dl -p n64 -g "Ocarina of Time"
|
||||
|
||||
# View last 20 history entries
|
||||
python rgsx_cli.py history --tail 20
|
||||
|
||||
# Purge and refresh data pack
|
||||
python rgsx_cli.py --force-update
|
||||
```
|
||||
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.
|
||||
|
||||
@@ -5,28 +5,29 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
p7zip-full \
|
||||
unrar-free \
|
||||
curl \
|
||||
rsync \
|
||||
gosu \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create required directories
|
||||
RUN mkdir -p /userdata/saves/ports/rgsx \
|
||||
&& mkdir -p /userdata/roms/ports \
|
||||
&& mkdir -p /app
|
||||
# Create app directory
|
||||
RUN mkdir -p /app
|
||||
|
||||
# Copy RGSX application files to /app (will be copied to volume at runtime)
|
||||
# Copy RGSX application files to /app
|
||||
COPY ports/RGSX/ /app/RGSX/
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY docker-entrypoint.sh /usr/local/bin/
|
||||
COPY docker/docker-entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
# Install Python dependencies
|
||||
# pygame is imported in some modules even in headless mode, so we include it
|
||||
RUN pip install --no-cache-dir requests pygame
|
||||
|
||||
# Set environment to headless mode
|
||||
ENV RGSX_HEADLESS=1
|
||||
# Set environment variables for Docker mode
|
||||
# These tell RGSX to use /config for settings and /data for ROMs
|
||||
ENV RGSX_HEADLESS=1 \
|
||||
RGSX_APP_DIR=/app \
|
||||
RGSX_CONFIG_DIR=/config \
|
||||
RGSX_DATA_DIR=/data
|
||||
|
||||
# Expose web interface port
|
||||
EXPOSE 5000
|
||||
@@ -35,6 +36,9 @@ EXPOSE 5000
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
CMD curl -f http://localhost:5000/ || exit 1
|
||||
|
||||
# Entrypoint copies app to volume, then runs command
|
||||
# Set working directory
|
||||
WORKDIR /app/RGSX
|
||||
|
||||
# Entrypoint handles user permissions and directory setup
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
CMD ["python", "rgsx_web.py", "--host", "0.0.0.0", "--port", "5000"]
|
||||
|
||||
@@ -5,19 +5,16 @@ Run RGSX as a web-only service without the Pygame UI. Perfect for homelab/server
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Build the image
|
||||
docker build -t rgsx .
|
||||
# Using docker-compose (recommended)
|
||||
docker-compose up -d
|
||||
|
||||
# Run with docker
|
||||
# Or build and run manually
|
||||
docker build -f docker/Dockerfile -t rgsx .
|
||||
docker run -d \
|
||||
--name rgsx \
|
||||
-p 5000:5000 \
|
||||
-e PUID=99 \
|
||||
-e PGID=100 \
|
||||
-e RGSX_HEADLESS=1 \
|
||||
-v ./data/saves:/userdata/saves/ports/rgsx \
|
||||
-v ./data/roms:/userdata/roms/ports \
|
||||
-v ./data/logs:/userdata/roms/ports/RGSX/logs \
|
||||
-v ./config:/config \
|
||||
-v ./data:/data \
|
||||
rgsx
|
||||
|
||||
# Access the web interface
|
||||
@@ -28,65 +25,63 @@ open http://localhost:5000
|
||||
|
||||
- Runs RGSX web server in headless mode (no Pygame UI)
|
||||
- Web interface accessible from any browser
|
||||
- ROMs and settings persist in `./data/` volumes
|
||||
- Container restarts automatically
|
||||
- Config persists in `/config` volume (settings, metadata, history)
|
||||
- ROMs download to `/data/roms/{platform}/` and extract there
|
||||
- Environment variables pre-configured (no manual setup needed)
|
||||
|
||||
## Environment Variables
|
||||
|
||||
**Pre-configured in the container (no need to set these):**
|
||||
- `RGSX_HEADLESS=1` - Runs in headless mode
|
||||
- `RGSX_CONFIG_DIR=/config` - Config location
|
||||
- `RGSX_DATA_DIR=/data` - Data location
|
||||
|
||||
**Optional (only if needed):**
|
||||
- `PUID` - User ID for file ownership (default: root)
|
||||
- `PGID` - Group ID for file ownership (default: root)
|
||||
|
||||
## Configuration
|
||||
|
||||
### Docker Compose
|
||||
|
||||
See `docker-compose.example.yml` for a complete example configuration.
|
||||
|
||||
### User Permissions (Important!)
|
||||
|
||||
**For SMB mounts (Unraid, Windows shares):**
|
||||
|
||||
Don't set PUID/PGID. The container runs as root, and the SMB server maps files to your authenticated user.
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-e RGSX_HEADLESS=1 \
|
||||
...
|
||||
```
|
||||
- Don't set PUID/PGID
|
||||
- The container runs as root, and the SMB server maps files to your authenticated user
|
||||
|
||||
**For NFS/local storage:**
|
||||
- Set PUID and PGID to match your host user (files will be owned by that user)
|
||||
- Find your user ID: `id -u` and `id -g`
|
||||
|
||||
Set PUID and PGID to match your host user. Files will be owned by that user.
|
||||
### Volumes
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-e PUID=1000 \
|
||||
-e PGID=1000 \
|
||||
-e RGSX_HEADLESS=1 \
|
||||
...
|
||||
```
|
||||
Two volumes are used:
|
||||
|
||||
**Find your user ID:**
|
||||
```bash
|
||||
id -u # Your UID
|
||||
id -g # Your GID
|
||||
```
|
||||
**`/config`** - Configuration and metadata
|
||||
- `rgsx_settings.json` - Settings
|
||||
- `games/` - Platform game database files (JSON)
|
||||
- `images/` - Game cover art
|
||||
- `history.json` - Download history
|
||||
- `logs/` - Application logs
|
||||
- `*.txt` - API keys
|
||||
|
||||
### Change Port
|
||||
|
||||
```bash
|
||||
docker run -p 8080:5000 ... # Access on port 8080
|
||||
```
|
||||
|
||||
### Custom ROM Location
|
||||
|
||||
Map to your existing ROM collection:
|
||||
```bash
|
||||
docker run -v /your/existing/roms:/userdata/roms/ports ...
|
||||
```
|
||||
**`/data`** - ROM storage
|
||||
- `roms/` - ROMs by platform (snes/, nes/, psx/, etc.) - downloads extract here
|
||||
|
||||
### API Keys
|
||||
|
||||
Add your download service API keys to `./data/saves/`:
|
||||
Add your download service API keys to `./config/`:
|
||||
|
||||
```bash
|
||||
# Add your API key (just the key, no extra text)
|
||||
echo "YOUR_KEY_HERE" > ./data/saves/1FichierAPI.txt
|
||||
echo "YOUR_KEY_HERE" > ./config/1FichierAPI.txt
|
||||
|
||||
# Optional: AllDebrid/RealDebrid fallbacks
|
||||
echo "YOUR_KEY" > ./data/saves/AllDebridAPI.txt
|
||||
echo "YOUR_KEY" > ./data/saves/RealDebridAPI.txt
|
||||
# Optional: AllDebrid/RealDebrid
|
||||
echo "YOUR_KEY" > ./config/AllDebridAPI.txt
|
||||
echo "YOUR_KEY" > ./config/RealDebridAPI.txt
|
||||
|
||||
# Restart to apply
|
||||
docker restart rgsx
|
||||
@@ -112,14 +107,29 @@ docker stop rgsx && docker rm rgsx
|
||||
|
||||
## Directory Structure
|
||||
|
||||
**On Host:**
|
||||
```
|
||||
RGSX/
|
||||
├── data/ # Created on first run
|
||||
│ ├── saves/ # Settings, history, API keys
|
||||
│ ├── roms/ # Downloaded ROMs
|
||||
│ └── logs/ # Application logs
|
||||
├── Dockerfile
|
||||
└── docker-compose.yml
|
||||
./
|
||||
├── config/ # Config volume (created on first run)
|
||||
│ ├── rgsx_settings.json
|
||||
│ ├── games/ # Platform game database (JSON)
|
||||
│ ├── images/ # Platform images
|
||||
│ ├── logs/ # Application logs
|
||||
│ └── *.txt # API keys (1FichierAPI.txt, etc.)
|
||||
└── data/
|
||||
└── roms/ # ROMs by platform
|
||||
├── snes/
|
||||
├── n64/
|
||||
└── ...
|
||||
```
|
||||
|
||||
**In Container:**
|
||||
```
|
||||
/app/RGSX/ # Application code
|
||||
/config/ # Mapped to ./config on host
|
||||
└── games/, images/, logs/, etc.
|
||||
/data/ # Mapped to ./data on host
|
||||
└── roms/ # ROM downloads go here
|
||||
```
|
||||
|
||||
## How It Works
|
||||
@@ -137,22 +147,15 @@ The container creates files with the UID/GID specified by PUID/PGID environment
|
||||
docker run -e PUID=1000 -e PGID=1000 ...
|
||||
```
|
||||
|
||||
**Changed PUID/PGID and container won't start:**
|
||||
**Changed PUID/PGID and permission errors:**
|
||||
|
||||
When you change PUID/PGID, old files with different ownership will cause rsync to fail. You MUST fix ownership on the storage server:
|
||||
Fix ownership of your volumes:
|
||||
|
||||
```bash
|
||||
# On your NAS/Unraid (via SSH), either:
|
||||
|
||||
# Option 1: Delete old files (easiest)
|
||||
rm -rf /mnt/user/roms/rgsx/roms/ports/RGSX/*
|
||||
|
||||
# Option 2: Change ownership to new PUID/PGID
|
||||
chown -R 1000:1000 /mnt/user/roms/rgsx/roms/ports/RGSX/
|
||||
# Fix ownership to match new PUID/PGID
|
||||
sudo chown -R 1000:1000 ./config ./data
|
||||
```
|
||||
|
||||
Then restart the container.
|
||||
|
||||
**Port already in use:**
|
||||
```bash
|
||||
docker run -p 8080:5000 ... # Use port 8080 instead
|
||||
|
||||
29
docker/docker-compose.example.yml
Normal file
29
docker/docker-compose.example.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
rgsx:
|
||||
# Option 1: Build from source
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/Dockerfile
|
||||
|
||||
# Option 2: Use pre-built image (push to your own registry)
|
||||
# image: your-registry/rgsx:latest
|
||||
|
||||
container_name: rgsx
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5000:5000"
|
||||
volumes:
|
||||
- ./config:/config
|
||||
- ./data:/data
|
||||
# Optional: Set PUID/PGID for NFS/local storage (not needed for SMB mounts)
|
||||
# environment:
|
||||
# - PUID=1000
|
||||
# - PGID=1000
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:5000/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
@@ -1,10 +1,12 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== RGSX Docker Container Startup ==="
|
||||
|
||||
# If PUID/PGID are set, create user and run as that user
|
||||
# If not set, run as root (works for SMB mounts)
|
||||
if [ -n "$PUID" ] && [ -n "$PGID" ]; then
|
||||
echo "=== Creating user with PUID=$PUID, PGID=$PGID ==="
|
||||
echo "Creating user with PUID=$PUID, PGID=$PGID..."
|
||||
|
||||
# Create group if it doesn't exist
|
||||
if ! getent group $PGID >/dev/null 2>&1; then
|
||||
@@ -16,43 +18,28 @@ if [ -n "$PUID" ] && [ -n "$PGID" ]; then
|
||||
useradd -u $PUID -g $PGID -m -s /bin/bash rgsx
|
||||
fi
|
||||
|
||||
# Fix ownership of app files
|
||||
chown -R $PUID:$PGID /app /userdata 2>/dev/null || true
|
||||
|
||||
echo "=== Running as user $(id -un $PUID) (UID=$PUID, GID=$PGID) ==="
|
||||
echo "Running as user $(id -un $PUID) (UID=$PUID, GID=$PGID)"
|
||||
RUN_USER="gosu rgsx"
|
||||
else
|
||||
echo "=== Running as root (no PUID/PGID set) - for SMB mounts ==="
|
||||
echo "Running as root (no PUID/PGID set) - suitable for SMB mounts"
|
||||
RUN_USER=""
|
||||
fi
|
||||
|
||||
# Always sync RGSX app code to the mounted volume (for updates)
|
||||
echo "Syncing RGSX app code to /userdata/roms/ports/RGSX..."
|
||||
$RUN_USER mkdir -p /userdata/roms/ports/RGSX
|
||||
# Create necessary directories
|
||||
# /config needs logs directory, app will create others (like images/, games/) as needed
|
||||
# /data needs roms directory
|
||||
echo "Setting up directories..."
|
||||
$RUN_USER mkdir -p /config/logs
|
||||
$RUN_USER mkdir -p /data/roms
|
||||
|
||||
# Try rsync
|
||||
if ! $RUN_USER rsync -av --delete /app/RGSX/ /userdata/roms/ports/RGSX/ 2>&1; then
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "WARNING: rsync partially failed!"
|
||||
echo "=========================================="
|
||||
echo "Some files may not have synced. Container will continue for debugging."
|
||||
echo ""
|
||||
if [ -n "$PUID" ] && [ -n "$PGID" ]; then
|
||||
echo "If using SMB, try removing PUID/PGID to run as root"
|
||||
fi
|
||||
echo ""
|
||||
# Fix ownership of volumes if PUID/PGID are set
|
||||
if [ -n "$PUID" ] && [ -n "$PGID" ]; then
|
||||
echo "Setting ownership on volumes..."
|
||||
chown -R $PUID:$PGID /config /data 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "RGSX app code sync attempted."
|
||||
|
||||
# Create Batocera folder structure only if folders don't exist
|
||||
$RUN_USER mkdir -p /userdata/saves/ports/rgsx/images
|
||||
$RUN_USER mkdir -p /userdata/saves/ports/rgsx/games
|
||||
$RUN_USER mkdir -p /userdata/roms/ports/RGSX/logs
|
||||
|
||||
# Create default settings with show_unsupported_platforms enabled if config doesn't exist
|
||||
SETTINGS_FILE="/userdata/saves/ports/rgsx/rgsx_settings.json"
|
||||
SETTINGS_FILE="/config/rgsx_settings.json"
|
||||
if [ ! -f "$SETTINGS_FILE" ]; then
|
||||
echo "Creating default settings with all platforms visible..."
|
||||
$RUN_USER bash -c "cat > '$SETTINGS_FILE' << 'EOF'
|
||||
@@ -60,11 +47,15 @@ if [ ! -f "$SETTINGS_FILE" ]; then
|
||||
\"show_unsupported_platforms\": true
|
||||
}
|
||||
EOF"
|
||||
echo "Default settings created!"
|
||||
echo "Default settings created at $SETTINGS_FILE"
|
||||
fi
|
||||
|
||||
# Run the command
|
||||
cd /userdata/roms/ports/RGSX
|
||||
echo "=== Starting RGSX Web Server ==="
|
||||
echo "Config directory: /config"
|
||||
echo "ROMs directory: /data/roms"
|
||||
echo "======================================"
|
||||
|
||||
# Run the command from the working directory (/app/RGSX set in Dockerfile)
|
||||
if [ -z "$RUN_USER" ]; then
|
||||
exec "$@"
|
||||
else
|
||||
|
||||
@@ -22,7 +22,7 @@ from display import (
|
||||
init_display, draw_loading_screen, draw_error_screen, draw_platform_grid,
|
||||
draw_progress_screen, draw_controls, draw_virtual_keyboard,
|
||||
draw_extension_warning, draw_pause_menu, draw_controls_help, draw_game_list,
|
||||
draw_display_menu,
|
||||
draw_display_menu, draw_filter_menu_choice, draw_filter_advanced, draw_filter_priority_config,
|
||||
draw_history_list, draw_clear_history_dialog, draw_cancel_download_dialog,
|
||||
draw_confirm_dialog, draw_reload_games_data_dialog, draw_popup, draw_gradient,
|
||||
draw_toast, show_toast, THEME_COLORS
|
||||
@@ -127,6 +127,12 @@ for i, scale in enumerate(config.font_scale_options):
|
||||
config.current_font_scale_index = i
|
||||
break
|
||||
|
||||
# Charger le footer_font_scale
|
||||
for i, scale in enumerate(config.footer_font_scale_options):
|
||||
if scale == config.accessibility_settings.get("footer_font_scale", 1.0):
|
||||
config.current_footer_font_scale_index = i
|
||||
break
|
||||
|
||||
# Chargement et initialisation de la langue
|
||||
from language import initialize_language
|
||||
initialize_language()
|
||||
@@ -160,6 +166,7 @@ pygame.display.set_caption("RGSX")
|
||||
|
||||
# Initialisation des polices via config
|
||||
config.init_font()
|
||||
config.init_footer_font()
|
||||
|
||||
# Mise à jour de la résolution dans config
|
||||
config.screen_width, config.screen_height = pygame.display.get_surface().get_size()
|
||||
@@ -231,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")
|
||||
@@ -412,6 +420,21 @@ async def main():
|
||||
global current_music, music_files, music_folder, joystick
|
||||
logger.debug("Début main")
|
||||
|
||||
# Charger les filtres de jeux sauvegardés
|
||||
try:
|
||||
from game_filters import GameFilters
|
||||
from rgsx_settings import load_game_filters
|
||||
config.game_filter_obj = GameFilters()
|
||||
filter_dict = load_game_filters()
|
||||
if filter_dict:
|
||||
config.game_filter_obj.load_from_dict(filter_dict)
|
||||
if config.game_filter_obj.is_active():
|
||||
config.filter_active = True
|
||||
logger.info("Filtres de jeux chargés et actifs")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du chargement des filtres: {e}")
|
||||
config.game_filter_obj = None
|
||||
|
||||
# Démarrer le serveur web en arrière-plan
|
||||
start_web_server()
|
||||
|
||||
@@ -664,6 +687,11 @@ async def main():
|
||||
"history_error_details",
|
||||
"history_confirm_delete",
|
||||
"history_extract_archive",
|
||||
"text_file_viewer", # Visualiseur de fichiers texte
|
||||
# Menus filtrage avancé
|
||||
"filter_menu_choice",
|
||||
"filter_advanced",
|
||||
"filter_priority_config",
|
||||
}
|
||||
if config.menu_state in SIMPLE_HANDLE_STATES:
|
||||
action = handle_controls(event, sources, joystick, screen)
|
||||
@@ -1062,6 +1090,12 @@ async def main():
|
||||
elif config.menu_state == "filter_platforms":
|
||||
from display import draw_filter_platforms_menu
|
||||
draw_filter_platforms_menu(screen)
|
||||
elif config.menu_state == "filter_menu_choice":
|
||||
draw_filter_menu_choice(screen)
|
||||
elif config.menu_state == "filter_advanced":
|
||||
draw_filter_advanced(screen)
|
||||
elif config.menu_state == "filter_priority_config":
|
||||
draw_filter_priority_config(screen)
|
||||
elif config.menu_state == "controls_help":
|
||||
draw_controls_help(screen, config.previous_menu_state)
|
||||
elif config.menu_state == "history":
|
||||
@@ -1082,6 +1116,9 @@ async def main():
|
||||
elif config.menu_state == "history_error_details":
|
||||
from display import draw_history_error_details
|
||||
draw_history_error_details(screen)
|
||||
elif config.menu_state == "text_file_viewer":
|
||||
from display import draw_text_file_viewer
|
||||
draw_text_file_viewer(screen)
|
||||
elif config.menu_state == "history_confirm_delete":
|
||||
from display import draw_history_confirm_delete
|
||||
draw_history_confirm_delete(screen)
|
||||
@@ -1361,7 +1398,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()
|
||||
|
||||
@@ -11,10 +11,10 @@ def load_accessibility_settings():
|
||||
|
||||
try:
|
||||
settings = load_rgsx_settings()
|
||||
return settings.get("accessibility", {"font_scale": 1.0})
|
||||
return settings.get("accessibility", {"font_scale": 1.0, "footer_font_scale": 1.0})
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du chargement des paramètres d'accessibilité: {str(e)}")
|
||||
return {"font_scale": 1.0}
|
||||
return {"font_scale": 1.0, "footer_font_scale": 1.0}
|
||||
|
||||
def save_accessibility_settings(accessibility_settings):
|
||||
"""Sauvegarde les paramètres d'accessibilité dans rgsx_settings.json."""
|
||||
@@ -28,7 +28,7 @@ def save_accessibility_settings(accessibility_settings):
|
||||
logger.error(f"Erreur lors de la sauvegarde des paramètres d'accessibilité: {str(e)}")
|
||||
|
||||
def draw_accessibility_menu(screen):
|
||||
"""Affiche le menu d'accessibilité avec curseur pour la taille de police."""
|
||||
"""Affiche le menu d'accessibilité avec curseurs pour la taille de police générale et du footer."""
|
||||
from display import OVERLAY, THEME_COLORS, draw_stylized_button
|
||||
|
||||
screen.blit(OVERLAY, (0, 0))
|
||||
@@ -44,47 +44,87 @@ def draw_accessibility_menu(screen):
|
||||
pygame.draw.rect(screen, THEME_COLORS["border"], title_bg_rect, 2, border_radius=10)
|
||||
screen.blit(title_surface, title_rect)
|
||||
|
||||
# Curseur de taille de police
|
||||
# Déterminer quel curseur est sélectionné (0 = général, 1 = footer)
|
||||
selected_cursor = getattr(config, 'accessibility_selected_cursor', 0)
|
||||
|
||||
# Curseur 1: Taille de police générale
|
||||
current_scale = config.font_scale_options[config.current_font_scale_index]
|
||||
font_text = _("accessibility_font_size").format(f"{current_scale:.1f}")
|
||||
|
||||
# Position du curseur
|
||||
cursor_y = config.screen_height // 2
|
||||
cursor_y1 = config.screen_height // 2 - 50
|
||||
cursor_width = 400
|
||||
cursor_height = 60
|
||||
cursor_x = (config.screen_width - cursor_width) // 2
|
||||
|
||||
# Fond du curseur
|
||||
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (cursor_x, cursor_y, cursor_width, cursor_height), border_radius=10)
|
||||
pygame.draw.rect(screen, THEME_COLORS["border"], (cursor_x, cursor_y, cursor_width, cursor_height), 2, border_radius=10)
|
||||
# Fond du curseur 1
|
||||
cursor1_color = THEME_COLORS["fond_lignes"] if selected_cursor == 0 else THEME_COLORS["button_idle"]
|
||||
pygame.draw.rect(screen, cursor1_color, (cursor_x, cursor_y1, cursor_width, cursor_height), border_radius=10)
|
||||
border_width = 3 if selected_cursor == 0 else 2
|
||||
pygame.draw.rect(screen, THEME_COLORS["border"], (cursor_x, cursor_y1, cursor_width, cursor_height), border_width, border_radius=10)
|
||||
|
||||
# Flèches gauche/droite
|
||||
# Flèches gauche/droite pour curseur 1
|
||||
arrow_size = 30
|
||||
left_arrow_x = cursor_x + 20
|
||||
right_arrow_x = cursor_x + cursor_width - arrow_size - 20
|
||||
arrow_y = cursor_y + (cursor_height - arrow_size) // 2
|
||||
arrow_y1 = cursor_y1 + (cursor_height - arrow_size) // 2
|
||||
|
||||
# Flèche gauche
|
||||
left_color = THEME_COLORS["fond_lignes"] if config.current_font_scale_index > 0 else THEME_COLORS["border"]
|
||||
left_color = THEME_COLORS["text"] if config.current_font_scale_index > 0 else THEME_COLORS["border"]
|
||||
pygame.draw.polygon(screen, left_color, [
|
||||
(left_arrow_x + arrow_size, arrow_y),
|
||||
(left_arrow_x, arrow_y + arrow_size // 2),
|
||||
(left_arrow_x + arrow_size, arrow_y + arrow_size)
|
||||
(left_arrow_x + arrow_size, arrow_y1),
|
||||
(left_arrow_x, arrow_y1 + arrow_size // 2),
|
||||
(left_arrow_x + arrow_size, arrow_y1 + arrow_size)
|
||||
])
|
||||
|
||||
# Flèche droite
|
||||
right_color = THEME_COLORS["fond_lignes"] if config.current_font_scale_index < len(config.font_scale_options) - 1 else THEME_COLORS["border"]
|
||||
right_color = THEME_COLORS["text"] if config.current_font_scale_index < len(config.font_scale_options) - 1 else THEME_COLORS["border"]
|
||||
pygame.draw.polygon(screen, right_color, [
|
||||
(right_arrow_x, arrow_y),
|
||||
(right_arrow_x + arrow_size, arrow_y + arrow_size // 2),
|
||||
(right_arrow_x, arrow_y + arrow_size)
|
||||
(right_arrow_x, arrow_y1),
|
||||
(right_arrow_x + arrow_size, arrow_y1 + arrow_size // 2),
|
||||
(right_arrow_x, arrow_y1 + arrow_size)
|
||||
])
|
||||
|
||||
# Texte au centre
|
||||
text_surface = config.font.render(font_text, True, THEME_COLORS["text"])
|
||||
text_rect = text_surface.get_rect(center=(cursor_x + cursor_width // 2, cursor_y + cursor_height // 2))
|
||||
text_rect = text_surface.get_rect(center=(cursor_x + cursor_width // 2, cursor_y1 + cursor_height // 2))
|
||||
screen.blit(text_surface, text_rect)
|
||||
|
||||
# Curseur 2: Taille de police du footer
|
||||
current_footer_scale = config.footer_font_scale_options[config.current_footer_font_scale_index]
|
||||
footer_font_text = _("accessibility_footer_font_size").format(f"{current_footer_scale:.1f}")
|
||||
|
||||
cursor_y2 = cursor_y1 + cursor_height + 20
|
||||
|
||||
# Fond du curseur 2
|
||||
cursor2_color = THEME_COLORS["fond_lignes"] if selected_cursor == 1 else THEME_COLORS["button_idle"]
|
||||
pygame.draw.rect(screen, cursor2_color, (cursor_x, cursor_y2, cursor_width, cursor_height), border_radius=10)
|
||||
border_width = 3 if selected_cursor == 1 else 2
|
||||
pygame.draw.rect(screen, THEME_COLORS["border"], (cursor_x, cursor_y2, cursor_width, cursor_height), border_width, border_radius=10)
|
||||
|
||||
# Flèches gauche/droite pour curseur 2
|
||||
arrow_y2 = cursor_y2 + (cursor_height - arrow_size) // 2
|
||||
|
||||
# Flèche gauche
|
||||
left_color2 = THEME_COLORS["text"] if config.current_footer_font_scale_index > 0 else THEME_COLORS["border"]
|
||||
pygame.draw.polygon(screen, left_color2, [
|
||||
(left_arrow_x + arrow_size, arrow_y2),
|
||||
(left_arrow_x, arrow_y2 + arrow_size // 2),
|
||||
(left_arrow_x + arrow_size, arrow_y2 + arrow_size)
|
||||
])
|
||||
|
||||
# Flèche droite
|
||||
right_color2 = THEME_COLORS["text"] if config.current_footer_font_scale_index < len(config.footer_font_scale_options) - 1 else THEME_COLORS["border"]
|
||||
pygame.draw.polygon(screen, right_color2, [
|
||||
(right_arrow_x, arrow_y2),
|
||||
(right_arrow_x + arrow_size, arrow_y2 + arrow_size // 2),
|
||||
(right_arrow_x, arrow_y2 + arrow_size)
|
||||
])
|
||||
|
||||
# Texte au centre
|
||||
text_surface2 = config.font.render(footer_font_text, True, THEME_COLORS["text"])
|
||||
text_rect2 = text_surface2.get_rect(center=(cursor_x + cursor_width // 2, cursor_y2 + cursor_height // 2))
|
||||
screen.blit(text_surface2, text_rect2)
|
||||
|
||||
# Instructions
|
||||
instruction_text = _("language_select_instruction")
|
||||
instruction_surface = config.small_font.render(instruction_text, True, THEME_COLORS["text"])
|
||||
@@ -93,16 +133,44 @@ def draw_accessibility_menu(screen):
|
||||
|
||||
def handle_accessibility_events(event):
|
||||
"""Gère les événements du menu d'accessibilité avec support clavier et manette."""
|
||||
# Initialiser le curseur sélectionné si non défini
|
||||
if not hasattr(config, 'accessibility_selected_cursor'):
|
||||
config.accessibility_selected_cursor = 0
|
||||
|
||||
# Gestion des touches du clavier
|
||||
if event.type == pygame.KEYDOWN:
|
||||
if event.key == pygame.K_LEFT and config.current_font_scale_index > 0:
|
||||
config.current_font_scale_index -= 1
|
||||
update_font_scale()
|
||||
# Navigation haut/bas entre les curseurs
|
||||
if event.key == pygame.K_UP:
|
||||
config.accessibility_selected_cursor = max(0, config.accessibility_selected_cursor - 1)
|
||||
config.needs_redraw = True
|
||||
return True
|
||||
elif event.key == pygame.K_RIGHT and config.current_font_scale_index < len(config.font_scale_options) - 1:
|
||||
config.current_font_scale_index += 1
|
||||
update_font_scale()
|
||||
elif event.key == pygame.K_DOWN:
|
||||
config.accessibility_selected_cursor = min(1, config.accessibility_selected_cursor + 1)
|
||||
config.needs_redraw = True
|
||||
return True
|
||||
# Navigation gauche/droite pour modifier les valeurs
|
||||
elif event.key == pygame.K_LEFT:
|
||||
if config.accessibility_selected_cursor == 0:
|
||||
if config.current_font_scale_index > 0:
|
||||
config.current_font_scale_index -= 1
|
||||
update_font_scale()
|
||||
return True
|
||||
else:
|
||||
if config.current_footer_font_scale_index > 0:
|
||||
config.current_footer_font_scale_index -= 1
|
||||
update_footer_font_scale()
|
||||
return True
|
||||
elif event.key == pygame.K_RIGHT:
|
||||
if config.accessibility_selected_cursor == 0:
|
||||
if config.current_font_scale_index < len(config.font_scale_options) - 1:
|
||||
config.current_font_scale_index += 1
|
||||
update_font_scale()
|
||||
return True
|
||||
else:
|
||||
if config.current_footer_font_scale_index < len(config.footer_font_scale_options) - 1:
|
||||
config.current_footer_font_scale_index += 1
|
||||
update_footer_font_scale()
|
||||
return True
|
||||
elif event.key == pygame.K_RETURN or event.key == pygame.K_ESCAPE:
|
||||
config.menu_state = "pause_menu"
|
||||
return True
|
||||
@@ -118,36 +186,89 @@ def handle_accessibility_events(event):
|
||||
|
||||
# Gestion du D-pad
|
||||
elif event.type == pygame.JOYHATMOTION:
|
||||
if event.value == (-1, 0): # Gauche
|
||||
if config.current_font_scale_index > 0:
|
||||
config.current_font_scale_index -= 1
|
||||
update_font_scale()
|
||||
return True
|
||||
if event.value == (0, 1): # Haut
|
||||
config.accessibility_selected_cursor = max(0, config.accessibility_selected_cursor - 1)
|
||||
config.needs_redraw = True
|
||||
return True
|
||||
elif event.value == (0, -1): # Bas
|
||||
config.accessibility_selected_cursor = min(1, config.accessibility_selected_cursor + 1)
|
||||
config.needs_redraw = True
|
||||
return True
|
||||
elif event.value == (-1, 0): # Gauche
|
||||
if config.accessibility_selected_cursor == 0:
|
||||
if config.current_font_scale_index > 0:
|
||||
config.current_font_scale_index -= 1
|
||||
update_font_scale()
|
||||
return True
|
||||
else:
|
||||
if config.current_footer_font_scale_index > 0:
|
||||
config.current_footer_font_scale_index -= 1
|
||||
update_footer_font_scale()
|
||||
return True
|
||||
elif event.value == (1, 0): # Droite
|
||||
if config.current_font_scale_index < len(config.font_scale_options) - 1:
|
||||
config.current_font_scale_index += 1
|
||||
update_font_scale()
|
||||
return True
|
||||
if config.accessibility_selected_cursor == 0:
|
||||
if config.current_font_scale_index < len(config.font_scale_options) - 1:
|
||||
config.current_font_scale_index += 1
|
||||
update_font_scale()
|
||||
return True
|
||||
else:
|
||||
if config.current_footer_font_scale_index < len(config.footer_font_scale_options) - 1:
|
||||
config.current_footer_font_scale_index += 1
|
||||
update_footer_font_scale()
|
||||
return True
|
||||
|
||||
# Gestion du joystick analogique (axe horizontal)
|
||||
# Gestion du joystick analogique
|
||||
elif event.type == pygame.JOYAXISMOTION:
|
||||
if event.axis == 0 and abs(event.value) > 0.5: # Joystick gauche horizontal
|
||||
if event.value < -0.5 and config.current_font_scale_index > 0: # Gauche
|
||||
config.current_font_scale_index -= 1
|
||||
update_font_scale()
|
||||
if event.axis == 1 and abs(event.value) > 0.5: # Joystick vertical
|
||||
if event.value < -0.5: # Haut
|
||||
config.accessibility_selected_cursor = max(0, config.accessibility_selected_cursor - 1)
|
||||
config.needs_redraw = True
|
||||
return True
|
||||
elif event.value > 0.5 and config.current_font_scale_index < len(config.font_scale_options) - 1: # Droite
|
||||
config.current_font_scale_index += 1
|
||||
update_font_scale()
|
||||
elif event.value > 0.5: # Bas
|
||||
config.accessibility_selected_cursor = min(1, config.accessibility_selected_cursor + 1)
|
||||
config.needs_redraw = True
|
||||
return True
|
||||
elif event.axis == 0 and abs(event.value) > 0.5: # Joystick horizontal
|
||||
if event.value < -0.5: # Gauche
|
||||
if config.accessibility_selected_cursor == 0:
|
||||
if config.current_font_scale_index > 0:
|
||||
config.current_font_scale_index -= 1
|
||||
update_font_scale()
|
||||
return True
|
||||
else:
|
||||
if config.current_footer_font_scale_index > 0:
|
||||
config.current_footer_font_scale_index -= 1
|
||||
update_footer_font_scale()
|
||||
return True
|
||||
elif event.value > 0.5: # Droite
|
||||
if config.accessibility_selected_cursor == 0:
|
||||
if config.current_font_scale_index < len(config.font_scale_options) - 1:
|
||||
config.current_font_scale_index += 1
|
||||
update_font_scale()
|
||||
return True
|
||||
else:
|
||||
if config.current_footer_font_scale_index < len(config.footer_font_scale_options) - 1:
|
||||
config.current_footer_font_scale_index += 1
|
||||
update_footer_font_scale()
|
||||
return True
|
||||
|
||||
return False
|
||||
def update_font_scale():
|
||||
"""Met à jour l'échelle de police et sauvegarde."""
|
||||
"""Met à jour l'échelle de police générale et sauvegarde."""
|
||||
new_scale = config.font_scale_options[config.current_font_scale_index]
|
||||
config.accessibility_settings["font_scale"] = new_scale
|
||||
save_accessibility_settings(config.accessibility_settings)
|
||||
|
||||
# Réinitialiser les polices
|
||||
config.init_font()
|
||||
config.needs_redraw = True
|
||||
|
||||
def update_footer_font_scale():
|
||||
"""Met à jour l'échelle de police du footer et sauvegarde."""
|
||||
new_scale = config.footer_font_scale_options[config.current_footer_font_scale_index]
|
||||
config.accessibility_settings["footer_font_scale"] = new_scale
|
||||
save_accessibility_settings(config.accessibility_settings)
|
||||
|
||||
# Réinitialiser les polices du footer
|
||||
config.init_footer_font()
|
||||
config.needs_redraw = True
|
||||
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.1.7"
|
||||
app_version = "2.3.3.0"
|
||||
|
||||
|
||||
def get_application_root():
|
||||
@@ -28,29 +28,100 @@ def get_application_root():
|
||||
# Si __file__ n'est pas défini (par exemple, exécution dans un REPL)
|
||||
return os.path.abspath(os.getcwd())
|
||||
|
||||
|
||||
|
||||
### CONSTANTES DES CHEMINS DE BASE
|
||||
|
||||
# Chemins de base
|
||||
APP_FOLDER = os.path.join(get_application_root(), "RGSX")
|
||||
USERDATA_FOLDER = os.path.dirname(os.path.dirname(os.path.dirname(APP_FOLDER))) # remonte de /userdata/roms/ports/rgsx à /userdata ou \Retrobat
|
||||
SAVE_FOLDER = os.path.join(USERDATA_FOLDER, "saves", "ports", "rgsx")
|
||||
|
||||
# ROMS_FOLDER - Charger depuis rgsx_settings.json si défini, sinon valeur par défaut
|
||||
_default_roms_folder = os.path.join(USERDATA_FOLDER, "roms")
|
||||
# Check for Docker mode environment variables
|
||||
_docker_app_dir = os.environ.get("RGSX_APP_DIR", "").strip()
|
||||
_docker_config_dir = os.environ.get("RGSX_CONFIG_DIR", "").strip()
|
||||
_docker_data_dir = os.environ.get("RGSX_DATA_DIR", "").strip()
|
||||
|
||||
# Determine if we're running in Docker mode
|
||||
_is_docker_mode = bool(_docker_config_dir or _docker_data_dir)
|
||||
|
||||
if _is_docker_mode:
|
||||
# ===== DOCKER MODE =====
|
||||
|
||||
# App code location (can be overridden, defaults to file location)
|
||||
if _docker_app_dir:
|
||||
APP_FOLDER = os.path.join(_docker_app_dir, "RGSX")
|
||||
else:
|
||||
APP_FOLDER = os.path.join(get_application_root(), "RGSX")
|
||||
|
||||
# Config directory: where rgsx_settings.json and metadata live
|
||||
if _docker_config_dir:
|
||||
CONFIG_FOLDER = _docker_config_dir
|
||||
SAVE_FOLDER = _docker_config_dir
|
||||
else:
|
||||
# Fallback: derive from traditional structure
|
||||
USERDATA_FOLDER = os.path.dirname(os.path.dirname(os.path.dirname(APP_FOLDER)))
|
||||
CONFIG_FOLDER = os.path.join(USERDATA_FOLDER, "saves", "ports", "rgsx")
|
||||
SAVE_FOLDER = CONFIG_FOLDER
|
||||
|
||||
# Data directory: where ROMs live
|
||||
# If not set, fallback to CONFIG_FOLDER (single volume mode)
|
||||
if _docker_data_dir:
|
||||
DATA_FOLDER = _docker_data_dir
|
||||
elif _docker_config_dir:
|
||||
DATA_FOLDER = _docker_config_dir
|
||||
else:
|
||||
USERDATA_FOLDER = os.path.dirname(os.path.dirname(os.path.dirname(APP_FOLDER)))
|
||||
DATA_FOLDER = USERDATA_FOLDER
|
||||
|
||||
# For backwards compatibility with code that references USERDATA_FOLDER
|
||||
USERDATA_FOLDER = DATA_FOLDER
|
||||
|
||||
else:
|
||||
# ===== TRADITIONAL MODE =====
|
||||
# Derive all paths from app location using original logic
|
||||
|
||||
APP_FOLDER = os.path.join(get_application_root(), "RGSX")
|
||||
|
||||
# Go up 3 directories from APP_FOLDER to find USERDATA
|
||||
# Example: /userdata/roms/ports/RGSX -> /userdata
|
||||
USERDATA_FOLDER = os.path.dirname(os.path.dirname(os.path.dirname(APP_FOLDER)))
|
||||
|
||||
# Config and data are both under USERDATA in traditional mode
|
||||
CONFIG_FOLDER = os.path.join(USERDATA_FOLDER, "saves", "ports", "rgsx")
|
||||
SAVE_FOLDER = CONFIG_FOLDER
|
||||
DATA_FOLDER = USERDATA_FOLDER
|
||||
|
||||
# ROMS_FOLDER - Can be customized via rgsx_settings.json
|
||||
|
||||
# Default ROM location
|
||||
_default_roms_folder = os.path.join(DATA_FOLDER if _is_docker_mode else USERDATA_FOLDER, "roms")
|
||||
|
||||
try:
|
||||
# Import tardif pour éviter les dépendances circulaires
|
||||
# Try to load custom roms_folder from settings
|
||||
_settings_path = os.path.join(SAVE_FOLDER, "rgsx_settings.json")
|
||||
if os.path.exists(_settings_path):
|
||||
import json
|
||||
with open(_settings_path, 'r', encoding='utf-8') as _f:
|
||||
_settings = json.load(_f)
|
||||
_custom_roms = _settings.get("roms_folder", "").strip()
|
||||
if _custom_roms and os.path.isdir(_custom_roms):
|
||||
ROMS_FOLDER = _custom_roms
|
||||
|
||||
if _custom_roms:
|
||||
# Check if it's an absolute path
|
||||
if os.path.isabs(_custom_roms):
|
||||
# Absolute path: use as-is if directory exists
|
||||
if os.path.isdir(_custom_roms):
|
||||
ROMS_FOLDER = _custom_roms
|
||||
else:
|
||||
ROMS_FOLDER = _default_roms_folder
|
||||
else:
|
||||
# Relative path: resolve relative to DATA_FOLDER (docker) or USERDATA_FOLDER (traditional)
|
||||
_base = DATA_FOLDER if _is_docker_mode else USERDATA_FOLDER
|
||||
_resolved = os.path.join(_base, _custom_roms)
|
||||
if os.path.isdir(_resolved):
|
||||
ROMS_FOLDER = _resolved
|
||||
else:
|
||||
ROMS_FOLDER = _default_roms_folder
|
||||
else:
|
||||
# Empty: use default
|
||||
ROMS_FOLDER = _default_roms_folder
|
||||
else:
|
||||
# Settings file doesn't exist yet: use default
|
||||
ROMS_FOLDER = _default_roms_folder
|
||||
except Exception as _e:
|
||||
ROMS_FOLDER = _default_roms_folder
|
||||
@@ -64,7 +135,15 @@ logger = logging.getLogger(__name__)
|
||||
download_queue = [] # Liste de dicts: {url, platform, game_name, ...}
|
||||
# Indique si un téléchargement est en cours
|
||||
download_active = False
|
||||
log_dir = os.path.join(APP_FOLDER, "logs")
|
||||
|
||||
# Log directory
|
||||
# Docker mode: /config/logs (persisted in config volume)
|
||||
# Traditional mode: /app/RGSX/logs (current behavior)
|
||||
if _is_docker_mode:
|
||||
log_dir = os.path.join(CONFIG_FOLDER, "logs")
|
||||
else:
|
||||
log_dir = os.path.join(APP_FOLDER, "logs")
|
||||
|
||||
log_file = os.path.join(log_dir, "RGSX.log")
|
||||
log_file_web = os.path.join(log_dir, 'rgsx_web.log')
|
||||
|
||||
@@ -75,9 +154,17 @@ MUSIC_FOLDER = os.path.join(APP_FOLDER, "assets", "music")
|
||||
GAMELISTXML = os.path.join(ROMS_FOLDER, "ports","gamelist.xml")
|
||||
GAMELISTXML_WINDOWS = os.path.join(ROMS_FOLDER, "windows","gamelist.xml")
|
||||
|
||||
# Dans le Dossier de sauvegarde : /saves/ports/rgsx
|
||||
# Dans le Dossier de sauvegarde : /saves/ports/rgsx (traditional) or /config (Docker)
|
||||
IMAGES_FOLDER = os.path.join(SAVE_FOLDER, "images")
|
||||
GAMES_FOLDER = os.path.join(SAVE_FOLDER, "games")
|
||||
|
||||
# GAME_LISTS_FOLDER: Platform game database JSON files (extracted from games.zip)
|
||||
# Always in SAVE_FOLDER/games for both Docker and Traditional modes
|
||||
# These are small JSON files containing available games per platform
|
||||
GAME_LISTS_FOLDER = os.path.join(SAVE_FOLDER, "games")
|
||||
|
||||
# Legacy alias for backwards compatibility (some code still uses GAMES_FOLDER)
|
||||
GAMES_FOLDER = GAME_LISTS_FOLDER
|
||||
|
||||
SOURCES_FILE = os.path.join(SAVE_FOLDER, "systems_list.json")
|
||||
JSON_EXTENSIONS = os.path.join(SAVE_FOLDER, "rom_extensions.json")
|
||||
PRECONF_CONTROLS_PATH = os.path.join(APP_FOLDER, "assets", "controls")
|
||||
@@ -225,6 +312,7 @@ progress_font = None # Police pour l'affichage de la progression
|
||||
title_font = None # Police pour les titres
|
||||
search_font = None # Police pour la recherche
|
||||
small_font = None # Police pour les petits textes
|
||||
tiny_font = None # Police pour le footer (contrôles/version)
|
||||
FONT_FAMILIES = [
|
||||
"pixel", # police rétro Pixel-UniCode.ttf
|
||||
"dejavu" # police plus standard lisible petites tailles
|
||||
@@ -271,9 +359,11 @@ visible_games = 15 # Nombre de jeux visibles en même temps par défaut
|
||||
|
||||
# Options d'affichage
|
||||
accessibility_mode = False # Mode accessibilité pour les polices agrandies
|
||||
accessibility_settings = {"font_scale": 1.0} # Paramètres d'accessibilité (échelle de police)
|
||||
accessibility_settings = {"font_scale": 1.0, "footer_font_scale": 1.0} # Paramètres d'accessibilité (échelle de police)
|
||||
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] # Options disponibles pour l'échelle de police
|
||||
current_font_scale_index = 3 # Index pour 1.0
|
||||
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, 2.1, 2.2, 2.3, 2.4, 2.5] # Options pour l'échelle de police du footer
|
||||
current_footer_font_scale_index = 3 # Index pour 1.0
|
||||
popup_start_time = 0 # Timestamp de début d'affichage du popup
|
||||
last_progress_update = 0 # Timestamp de la dernière mise à jour de progression
|
||||
transition_state = "idle" # État de la transition d'écran
|
||||
@@ -290,6 +380,11 @@ search_mode = False # Indicateur si le mode recherche est actif
|
||||
search_query = "" # Chaîne de recherche saisie par l'utilisateur
|
||||
filter_active = False # Indicateur si un filtre est appliqué
|
||||
|
||||
# Variables pour le filtrage avancé
|
||||
selected_filter_choice = 0 # Index dans le menu de choix de filtrage (recherche / avancé)
|
||||
selected_filter_option = 0 # Index dans le menu de filtrage avancé
|
||||
game_filter_obj = None # Objet GameFilters pour le filtrage avancé
|
||||
|
||||
# Gestion des états du menu
|
||||
needs_redraw = False # Indicateur si l'écran doit être redessiné
|
||||
selected_option = 0 # Index de l'option sélectionnée dans le menu
|
||||
@@ -412,6 +507,31 @@ def init_font():
|
||||
font = title_font = search_font = progress_font = small_font = None
|
||||
|
||||
|
||||
def init_footer_font():
|
||||
"""Initialise uniquement la police du footer (tiny_font) en fonction de l'échelle séparée."""
|
||||
global tiny_font
|
||||
footer_font_scale = accessibility_settings.get("footer_font_scale", 1.0)
|
||||
|
||||
# Déterminer la famille sélectionnée
|
||||
family_id = FONT_FAMILIES[current_font_family_index] if 0 <= current_font_family_index < len(FONT_FAMILIES) else "pixel"
|
||||
|
||||
footer_base_size = 20 # Taille de base pour le footer
|
||||
|
||||
try:
|
||||
if family_id == "pixel":
|
||||
path = os.path.join(APP_FOLDER, "assets", "fonts", "Pixel-UniCode.ttf")
|
||||
tiny_font = pygame.font.Font(path, int(footer_base_size * footer_font_scale))
|
||||
elif family_id == "dejavu":
|
||||
try:
|
||||
tiny_font = pygame.font.SysFont("dejavusans", int(footer_base_size * footer_font_scale))
|
||||
except Exception:
|
||||
tiny_font = pygame.font.SysFont("dejavu sans", int(footer_base_size * footer_font_scale))
|
||||
logger.debug(f"Police footer initialisée (famille={family_id}, scale={footer_font_scale})")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur chargement police footer: {e}")
|
||||
tiny_font = None
|
||||
|
||||
|
||||
def validate_resolution():
|
||||
"""Valide la résolution de l'écran par rapport aux capacités de l'écran."""
|
||||
if pygame is None:
|
||||
|
||||
@@ -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,
|
||||
@@ -58,7 +58,13 @@ VALID_STATES = [
|
||||
"scraper", # écran du scraper avec métadonnées
|
||||
"history_error_details", # détails de l'erreur
|
||||
"history_confirm_delete", # confirmation suppression jeu
|
||||
"history_extract_archive" # extraction d'archive
|
||||
"history_extract_archive", # extraction d'archive
|
||||
"text_file_viewer", # visualiseur de fichiers texte
|
||||
# Nouveaux menus filtrage avancé
|
||||
"filter_menu_choice", # menu de choix entre recherche et filtrage avancé
|
||||
"filter_search", # recherche par nom (existant, mais renommé)
|
||||
"filter_advanced", # filtrage avancé par région, etc.
|
||||
"filter_priority_config", # configuration priorité régions pour one-rom-per-game
|
||||
]
|
||||
|
||||
def validate_menu_state(state):
|
||||
@@ -196,7 +202,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 +224,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():
|
||||
@@ -440,8 +482,15 @@ def handle_controls(event, sources, joystick, screen):
|
||||
if config.platforms:
|
||||
config.current_platform = config.selected_platform
|
||||
config.games = load_games(config.platforms[config.current_platform])
|
||||
config.filtered_games = config.games
|
||||
config.filter_active = False
|
||||
|
||||
# Apply saved filters automatically if any
|
||||
if config.game_filter_obj and config.game_filter_obj.is_active():
|
||||
config.filtered_games = config.game_filter_obj.apply_filters(config.games)
|
||||
config.filter_active = True
|
||||
else:
|
||||
config.filtered_games = config.games
|
||||
config.filter_active = False
|
||||
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
draw_validation_transition(screen, config.current_platform)
|
||||
@@ -469,6 +518,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
max_row = len(keyboard_layout) - 1
|
||||
max_col = len(keyboard_layout[row]) - 1
|
||||
if is_input_matched(event, "up"):
|
||||
if row == 0: # if you are in the first row and press UP jump to last row
|
||||
row = max_row + (1 if col <= 5 else 0)
|
||||
if row > 0:
|
||||
config.selected_key = (row - 1, min(col, len(keyboard_layout[row - 1]) - 1))
|
||||
config.repeat_action = "up"
|
||||
@@ -477,6 +528,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "down"):
|
||||
if (col <= 5 and row == max_row) or (col > 5 and row == max_row-1): # if you are in the last row and press DOWN jump to first row
|
||||
row = -1
|
||||
if row < max_row:
|
||||
config.selected_key = (row + 1, min(col, len(keyboard_layout[row + 1]) - 1))
|
||||
config.repeat_action = "down"
|
||||
@@ -485,6 +538,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "left"):
|
||||
if col == 0: # if you are in the first col and press LEFT jump to last col
|
||||
col = max_col + 1
|
||||
if col > 0:
|
||||
config.selected_key = (row, col - 1)
|
||||
config.repeat_action = "left"
|
||||
@@ -493,6 +548,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "right"):
|
||||
if col == max_col: # if you are in the last col and press RIGHT jump to first col
|
||||
col = -1
|
||||
if col < max_col:
|
||||
config.selected_key = (row, col + 1)
|
||||
config.repeat_action = "right"
|
||||
@@ -593,41 +650,39 @@ 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
|
||||
config.search_query = ""
|
||||
config.filtered_games = config.games
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.selected_key = (0, 0)
|
||||
# Afficher le menu de choix entre recherche et filtrage avancé
|
||||
config.menu_state = "filter_menu_choice"
|
||||
config.selected_filter_choice = 0
|
||||
config.previous_menu_state = "game"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Entrée en mode recherche")
|
||||
logger.debug("Ouverture du menu de filtrage")
|
||||
elif is_input_matched(event, "history"):
|
||||
config.menu_state = "history"
|
||||
config.needs_redraw = True
|
||||
@@ -954,6 +1009,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
ext = os.path.splitext(actual_filename)[1].lower()
|
||||
if ext in ['.zip', '.rar']:
|
||||
options.append("extract_archive")
|
||||
elif ext == '.txt':
|
||||
options.append("open_file")
|
||||
elif status in ["Erreur", "Error", "Canceled"]:
|
||||
options.append("error_info")
|
||||
options.append("retry")
|
||||
@@ -996,6 +1053,30 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Affichage du dossier de téléchargement pour {game_name}")
|
||||
|
||||
elif selected_option == "open_file":
|
||||
# Ouvrir le fichier texte
|
||||
if actual_path and os.path.exists(actual_path):
|
||||
try:
|
||||
with open(actual_path, 'r', encoding='utf-8', errors='replace') as f:
|
||||
content = f.read()
|
||||
config.text_file_content = content
|
||||
config.text_file_name = actual_filename
|
||||
config.text_file_scroll_offset = 0
|
||||
config.previous_menu_state = "history_game_options"
|
||||
config.menu_state = "text_file_viewer"
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Ouverture du fichier texte: {actual_filename}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'ouverture du fichier texte: {e}")
|
||||
config.menu_state = "error"
|
||||
config.error_message = f"Erreur lors de l'ouverture du fichier: {str(e)}"
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
logger.error(f"Fichier texte introuvable: {actual_path}")
|
||||
config.menu_state = "error"
|
||||
config.error_message = "Fichier introuvable"
|
||||
config.needs_redraw = True
|
||||
|
||||
elif selected_option == "extract_archive":
|
||||
# L'option n'apparaît que si le fichier existe, pas besoin de re-vérifier
|
||||
config.previous_menu_state = "history_game_options"
|
||||
@@ -1147,6 +1228,128 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
|
||||
# Affichage détails erreur
|
||||
# Visualiseur de fichiers texte
|
||||
elif config.menu_state == "text_file_viewer":
|
||||
content = getattr(config, 'text_file_content', '')
|
||||
if content:
|
||||
lines = content.split('\n')
|
||||
line_height = config.small_font.get_height() + 2
|
||||
|
||||
# Calculer le nombre de lignes visibles (approximation)
|
||||
controls_y = config.screen_height - int(config.screen_height * 0.037)
|
||||
margin = 40
|
||||
header_height = 60
|
||||
content_area_height = controls_y - 2 * margin - 10 - header_height - 20
|
||||
visible_lines = int(content_area_height / line_height)
|
||||
|
||||
scroll_offset = getattr(config, 'text_file_scroll_offset', 0)
|
||||
max_scroll = max(0, len(lines) - visible_lines)
|
||||
|
||||
if is_input_matched(event, "up"):
|
||||
config.text_file_scroll_offset = max(0, scroll_offset - 1)
|
||||
update_key_state("up", True, event.type, event.key if event.type == pygame.KEYDOWN else
|
||||
event.button if event.type == pygame.JOYBUTTONDOWN else
|
||||
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
|
||||
event.value)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "down"):
|
||||
config.text_file_scroll_offset = min(max_scroll, scroll_offset + 1)
|
||||
update_key_state("down", True, event.type, event.key if event.type == pygame.KEYDOWN else
|
||||
event.button if event.type == pygame.JOYBUTTONDOWN else
|
||||
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
|
||||
event.value)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "page_up"):
|
||||
config.text_file_scroll_offset = max(0, scroll_offset - visible_lines)
|
||||
update_key_state("page_up", True, event.type, event.key if event.type == pygame.KEYDOWN else
|
||||
event.button if event.type == pygame.JOYBUTTONDOWN else
|
||||
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
|
||||
event.value)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "page_down"):
|
||||
config.text_file_scroll_offset = min(max_scroll, scroll_offset + visible_lines)
|
||||
update_key_state("page_down", True, event.type, event.key if event.type == pygame.KEYDOWN else
|
||||
event.button if event.type == pygame.JOYBUTTONDOWN else
|
||||
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
|
||||
event.value)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "cancel") or is_input_matched(event, "confirm"):
|
||||
config.menu_state = validate_menu_state(config.previous_menu_state)
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
# Si pas de contenu, retourner au menu précédent
|
||||
if is_input_matched(event, "cancel") or is_input_matched(event, "confirm"):
|
||||
config.menu_state = validate_menu_state(config.previous_menu_state)
|
||||
config.needs_redraw = True
|
||||
|
||||
# Visualiseur de fichiers texte
|
||||
elif config.menu_state == "text_file_viewer":
|
||||
content = getattr(config, 'text_file_content', '')
|
||||
if content:
|
||||
from utils import wrap_text
|
||||
|
||||
# Calculer les dimensions
|
||||
controls_y = config.screen_height - int(config.screen_height * 0.037)
|
||||
margin = 40
|
||||
header_height = 60
|
||||
rect_width = config.screen_width - 2 * margin
|
||||
content_area_height = controls_y - 2 * margin - 10 - header_height - 20
|
||||
max_width = rect_width - 60
|
||||
|
||||
# Diviser le contenu en lignes et appliquer le word wrap
|
||||
original_lines = content.split('\n')
|
||||
wrapped_lines = []
|
||||
|
||||
for original_line in original_lines:
|
||||
if original_line.strip(): # Si la ligne n'est pas vide
|
||||
wrapped = wrap_text(original_line, config.small_font, max_width)
|
||||
wrapped_lines.extend(wrapped)
|
||||
else: # Ligne vide
|
||||
wrapped_lines.append('')
|
||||
|
||||
line_height = config.small_font.get_height() + 2
|
||||
visible_lines = int(content_area_height / line_height)
|
||||
|
||||
scroll_offset = getattr(config, 'text_file_scroll_offset', 0)
|
||||
max_scroll = max(0, len(wrapped_lines) - visible_lines)
|
||||
|
||||
if is_input_matched(event, "up"):
|
||||
config.text_file_scroll_offset = max(0, scroll_offset - 1)
|
||||
update_key_state("up", True, event.type, event.key if event.type == pygame.KEYDOWN else
|
||||
event.button if event.type == pygame.JOYBUTTONDOWN else
|
||||
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
|
||||
event.value)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "down"):
|
||||
config.text_file_scroll_offset = min(max_scroll, scroll_offset + 1)
|
||||
update_key_state("down", True, event.type, event.key if event.type == pygame.KEYDOWN else
|
||||
event.button if event.type == pygame.JOYBUTTONDOWN else
|
||||
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
|
||||
event.value)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "page_up"):
|
||||
config.text_file_scroll_offset = max(0, scroll_offset - visible_lines)
|
||||
update_key_state("page_up", True, event.type, event.key if event.type == pygame.KEYDOWN else
|
||||
event.button if event.type == pygame.JOYBUTTONDOWN else
|
||||
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
|
||||
event.value)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "page_down"):
|
||||
config.text_file_scroll_offset = min(max_scroll, scroll_offset + visible_lines)
|
||||
update_key_state("page_down", True, event.type, event.key if event.type == pygame.KEYDOWN else
|
||||
event.button if event.type == pygame.JOYBUTTONDOWN else
|
||||
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
|
||||
event.value)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "cancel") or is_input_matched(event, "confirm"):
|
||||
config.menu_state = validate_menu_state(config.previous_menu_state)
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
# Si pas de contenu, retourner au menu précédent
|
||||
if is_input_matched(event, "cancel") or is_input_matched(event, "confirm"):
|
||||
config.menu_state = validate_menu_state(config.previous_menu_state)
|
||||
config.needs_redraw = True
|
||||
|
||||
elif config.menu_state == "history_error_details":
|
||||
if is_input_matched(event, "confirm") or is_input_matched(event, "cancel"):
|
||||
config.menu_state = validate_menu_state(config.previous_menu_state)
|
||||
@@ -1415,7 +1618,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 = 8 # layout, font size, font family, unsupported, unknown, hide premium, filter, 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 +1626,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,14 +1641,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
|
||||
# Redémarrage automatique
|
||||
# Afficher un popup indiquant que le changement sera effectif après redémarrage
|
||||
try:
|
||||
config.menu_state = "restart_popup"
|
||||
config.popup_message = _("popup_restarting") if _ else "Restarting..."
|
||||
config.popup_timer = 2000
|
||||
restart_application(2000)
|
||||
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 restart 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,8 +1666,21 @@ def handle_controls(event, sources, joystick, screen):
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur init polices: {e}")
|
||||
config.needs_redraw = True
|
||||
# 2 font family cycle
|
||||
elif sel == 2 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
|
||||
# 2 footer font size
|
||||
elif sel == 2 and (is_input_matched(event, "left") or is_input_matched(event, "right")):
|
||||
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(footer_opts)-1, idx+1)
|
||||
if idx != getattr(config, 'current_footer_font_scale_index', 3):
|
||||
config.current_footer_font_scale_index = idx
|
||||
try:
|
||||
update_footer_font_scale()
|
||||
except Exception as 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")):
|
||||
try:
|
||||
families = getattr(config, 'FONT_FAMILIES', ["pixel"]) or ["pixel"]
|
||||
current = get_font_family()
|
||||
@@ -1500,17 +1713,6 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur changement font family: {e}")
|
||||
# 3 unsupported toggle
|
||||
elif sel == 3 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
|
||||
try:
|
||||
current = get_show_unsupported_platforms()
|
||||
new_val = set_show_unsupported_platforms(not current)
|
||||
load_sources()
|
||||
config.popup_message = _("menu_show_unsupported_enabled") if new_val else _("menu_show_unsupported_disabled")
|
||||
config.popup_timer = 3000
|
||||
config.needs_redraw = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur toggle unsupported: {e}")
|
||||
# 4 allow unknown extensions
|
||||
elif sel == 4 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
|
||||
try:
|
||||
@@ -1521,25 +1723,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur toggle allow_unknown_extensions: {e}")
|
||||
# 5 hide premium systems
|
||||
elif sel == 5 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")):
|
||||
try:
|
||||
cur = get_hide_premium_systems()
|
||||
new_val = set_hide_premium_systems(not cur)
|
||||
config.popup_message = ("Premium hidden" if new_val else "Premium visible") if _ is None else (_("popup_hide_premium_on") if new_val else _("popup_hide_premium_off"))
|
||||
config.popup_timer = 2500
|
||||
config.needs_redraw = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur toggle hide_premium_systems: {e}")
|
||||
# 6 filter platforms
|
||||
elif sel == 6 and (is_input_matched(event, "confirm") or is_input_matched(event, "right")):
|
||||
config.filter_return_to = "pause_display_menu"
|
||||
config.menu_state = "filter_platforms"
|
||||
config.selected_filter_index = 0
|
||||
config.filter_platforms_scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
# 7 back
|
||||
elif sel == 7 and (is_input_matched(event, "confirm")):
|
||||
# 5 back
|
||||
elif sel == 5 and is_input_matched(event, "confirm"):
|
||||
config.menu_state = "pause_menu"
|
||||
config.last_state_change_time = pygame.time.get_ticks()
|
||||
config.needs_redraw = True
|
||||
@@ -1551,7 +1736,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
# Sous-menu Games
|
||||
elif config.menu_state == "pause_games_menu":
|
||||
sel = getattr(config, 'pause_games_selection', 0)
|
||||
total = 4 # history, source, redownload, back
|
||||
total = 7 # history, source, redownload, unsupported, hide premium, filter, back
|
||||
if is_input_matched(event, "up"):
|
||||
config.pause_games_selection = (sel - 1) % total
|
||||
config.needs_redraw = True
|
||||
@@ -1559,7 +1744,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
|
||||
@@ -1587,7 +1771,32 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.menu_state = "reload_games_data"
|
||||
config.redownload_confirm_selection = 0
|
||||
config.needs_redraw = True
|
||||
elif sel == 3 and is_input_matched(event, "confirm"): # back
|
||||
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)
|
||||
load_sources()
|
||||
config.popup_message = _("menu_show_unsupported_enabled") if new_val else _("menu_show_unsupported_disabled")
|
||||
config.popup_timer = 3000
|
||||
config.needs_redraw = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur toggle unsupported: {e}")
|
||||
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)
|
||||
config.popup_message = ("Premium hidden" if new_val else "Premium visible") if _ is None else (_("popup_hide_premium_on") if new_val else _("popup_hide_premium_off"))
|
||||
config.popup_timer = 2500
|
||||
config.needs_redraw = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur toggle hide_premium_systems: {e}")
|
||||
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
|
||||
config.filter_platforms_scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
elif sel == 6 and is_input_matched(event, "confirm"): # back
|
||||
config.menu_state = "pause_menu"
|
||||
config.last_state_change_time = pygame.time.get_ticks()
|
||||
config.needs_redraw = True
|
||||
@@ -1613,7 +1822,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
|
||||
@@ -1691,7 +1899,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)]
|
||||
@@ -1879,48 +2086,312 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
logger.debug("Annulation de la sélection de langue, retour au menu pause")
|
||||
|
||||
# Menu de choix filtrage
|
||||
elif config.menu_state == "filter_menu_choice":
|
||||
if is_input_matched(event, "up"):
|
||||
config.selected_filter_choice = (config.selected_filter_choice - 1) % 2
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "down"):
|
||||
config.selected_filter_choice = (config.selected_filter_choice + 1) % 2
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "confirm"):
|
||||
if config.selected_filter_choice == 0:
|
||||
# Recherche par nom (mode existant)
|
||||
config.search_mode = True
|
||||
config.search_query = ""
|
||||
config.filtered_games = config.games
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.selected_key = (0, 0)
|
||||
config.menu_state = "game"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Entrée en mode recherche par nom")
|
||||
else:
|
||||
# Filtrage avancé
|
||||
from game_filters import GameFilters
|
||||
from rgsx_settings import load_game_filters
|
||||
|
||||
# Initialiser le filtre
|
||||
if not hasattr(config, 'game_filter_obj'):
|
||||
config.game_filter_obj = GameFilters()
|
||||
filter_dict = load_game_filters()
|
||||
if filter_dict:
|
||||
config.game_filter_obj.load_from_dict(filter_dict)
|
||||
|
||||
config.menu_state = "filter_advanced"
|
||||
config.selected_filter_option = 0
|
||||
config.needs_redraw = True
|
||||
logger.debug("Entrée en filtrage avancé")
|
||||
elif is_input_matched(event, "cancel"):
|
||||
config.menu_state = "game"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Retour à la liste des jeux")
|
||||
|
||||
# Filtrage avancé
|
||||
elif config.menu_state == "filter_advanced":
|
||||
from game_filters import GameFilters
|
||||
from rgsx_settings import save_game_filters
|
||||
|
||||
# Initialiser le filtre si nécessaire
|
||||
if not hasattr(config, 'game_filter_obj'):
|
||||
config.game_filter_obj = GameFilters()
|
||||
from rgsx_settings import load_game_filters
|
||||
filter_dict = load_game_filters()
|
||||
if filter_dict:
|
||||
config.game_filter_obj.load_from_dict(filter_dict)
|
||||
|
||||
# Construire la liste linéaire des éléments sélectionnables (pour simplifier l'indexation)
|
||||
# Régions individuelles
|
||||
num_regions = len(GameFilters.REGIONS)
|
||||
# Options toggle/button
|
||||
num_other_options = 3 # hide_non_release, one_rom_per_game, priority_config
|
||||
# Boutons en bas
|
||||
num_buttons = 3 # apply, reset, back
|
||||
|
||||
total_items = num_regions + num_other_options + num_buttons
|
||||
|
||||
if is_input_matched(event, "up"):
|
||||
# Navigation verticale dans la grille ou entre sections
|
||||
if config.selected_filter_option < num_regions:
|
||||
# Dans la grille des régions (3 colonnes)
|
||||
if config.selected_filter_option >= 3:
|
||||
# Monter d'une ligne
|
||||
config.selected_filter_option -= 3
|
||||
else:
|
||||
# Déjà en haut, aller aux boutons
|
||||
config.selected_filter_option = total_items - 2 # Bouton du milieu (reset)
|
||||
else:
|
||||
# Dans les options ou boutons, monter normalement
|
||||
config.selected_filter_option = (config.selected_filter_option - 1) % total_items
|
||||
|
||||
config.needs_redraw = True
|
||||
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"):
|
||||
# Navigation verticale
|
||||
if config.selected_filter_option < num_regions:
|
||||
# Dans la grille des régions
|
||||
if config.selected_filter_option + 3 < num_regions:
|
||||
# Descendre d'une ligne
|
||||
config.selected_filter_option += 3
|
||||
else:
|
||||
# Aller aux autres options
|
||||
config.selected_filter_option = num_regions
|
||||
else:
|
||||
# Dans les options ou boutons, descendre normalement
|
||||
config.selected_filter_option = (config.selected_filter_option + 1) % total_items
|
||||
|
||||
config.needs_redraw = True
|
||||
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"):
|
||||
# Navigation horizontale
|
||||
if config.selected_filter_option < num_regions:
|
||||
# Dans la grille des régions
|
||||
if config.selected_filter_option % 3 > 0:
|
||||
config.selected_filter_option -= 1
|
||||
config.needs_redraw = True
|
||||
elif config.selected_filter_option >= num_regions + num_other_options:
|
||||
# Dans les boutons en bas
|
||||
button_idx = config.selected_filter_option - (num_regions + num_other_options)
|
||||
button_idx = (button_idx - 1) % num_buttons
|
||||
config.selected_filter_option = num_regions + num_other_options + button_idx
|
||||
config.needs_redraw = True
|
||||
|
||||
elif is_input_matched(event, "right"):
|
||||
# Navigation horizontale
|
||||
if config.selected_filter_option < num_regions:
|
||||
# Dans la grille des régions
|
||||
if config.selected_filter_option % 3 < 2 and config.selected_filter_option + 1 < num_regions:
|
||||
config.selected_filter_option += 1
|
||||
config.needs_redraw = True
|
||||
elif config.selected_filter_option >= num_regions + num_other_options:
|
||||
# Dans les boutons en bas
|
||||
button_idx = config.selected_filter_option - (num_regions + num_other_options)
|
||||
button_idx = (button_idx + 1) % num_buttons
|
||||
config.selected_filter_option = num_regions + num_other_options + button_idx
|
||||
config.needs_redraw = True
|
||||
|
||||
elif is_input_matched(event, "confirm"):
|
||||
# Déterminer quel élément a été sélectionné
|
||||
if config.selected_filter_option < num_regions:
|
||||
# C'est une région
|
||||
region = GameFilters.REGIONS[config.selected_filter_option]
|
||||
current_state = config.game_filter_obj.region_filters.get(region, 'include')
|
||||
if current_state == 'include':
|
||||
config.game_filter_obj.region_filters[region] = 'exclude'
|
||||
else:
|
||||
config.game_filter_obj.region_filters[region] = 'include'
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Filtre région {region} modifié: {config.game_filter_obj.region_filters[region]}")
|
||||
|
||||
elif config.selected_filter_option < num_regions + num_other_options:
|
||||
# C'est une autre option
|
||||
option_idx = config.selected_filter_option - num_regions
|
||||
if option_idx == 0:
|
||||
# hide_non_release
|
||||
config.game_filter_obj.hide_non_release = not config.game_filter_obj.hide_non_release
|
||||
config.needs_redraw = True
|
||||
logger.debug("Toggle hide_non_release modifié")
|
||||
elif option_idx == 1:
|
||||
# one_rom_per_game
|
||||
config.game_filter_obj.one_rom_per_game = not config.game_filter_obj.one_rom_per_game
|
||||
config.needs_redraw = True
|
||||
logger.debug("Toggle one_rom_per_game modifié")
|
||||
elif option_idx == 2:
|
||||
# priority_config
|
||||
config.menu_state = "filter_priority_config"
|
||||
config.selected_priority_index = 0
|
||||
config.needs_redraw = True
|
||||
logger.debug("Ouverture configuration priorité régions")
|
||||
|
||||
else:
|
||||
# C'est un bouton
|
||||
button_idx = config.selected_filter_option - (num_regions + num_other_options)
|
||||
if button_idx == 0:
|
||||
# Apply
|
||||
save_game_filters(config.game_filter_obj.to_dict())
|
||||
|
||||
# Appliquer aux jeux actuels
|
||||
if config.game_filter_obj.is_active():
|
||||
config.filtered_games = config.game_filter_obj.apply_filters(config.games)
|
||||
config.filter_active = True
|
||||
else:
|
||||
config.filtered_games = config.games
|
||||
config.filter_active = False
|
||||
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.menu_state = "game"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Filtres appliqués")
|
||||
|
||||
elif button_idx == 1:
|
||||
# Reset
|
||||
config.game_filter_obj.reset()
|
||||
save_game_filters(config.game_filter_obj.to_dict())
|
||||
config.filtered_games = config.games
|
||||
config.filter_active = False
|
||||
config.needs_redraw = True
|
||||
logger.debug("Filtres réinitialisés")
|
||||
|
||||
elif button_idx == 2:
|
||||
# Back
|
||||
config.menu_state = "game"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Retour sans appliquer les filtres")
|
||||
|
||||
elif is_input_matched(event, "cancel"):
|
||||
config.menu_state = "game"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Annulation du filtrage avancé")
|
||||
|
||||
# Configuration priorité régions
|
||||
elif config.menu_state == "filter_priority_config":
|
||||
from game_filters import GameFilters
|
||||
from rgsx_settings import save_game_filters
|
||||
|
||||
if not hasattr(config, 'game_filter_obj'):
|
||||
config.game_filter_obj = GameFilters()
|
||||
|
||||
priority_list = config.game_filter_obj.region_priority
|
||||
total_items = len(priority_list) + 1 # +1 pour le bouton Back
|
||||
|
||||
if not hasattr(config, 'selected_priority_index'):
|
||||
config.selected_priority_index = 0
|
||||
|
||||
if is_input_matched(event, "up"):
|
||||
config.selected_priority_index = (config.selected_priority_index - 1) % total_items
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "down"):
|
||||
config.selected_priority_index = (config.selected_priority_index + 1) % total_items
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "confirm"):
|
||||
if config.selected_priority_index >= len(priority_list):
|
||||
# Bouton Back : retour au menu filtrage avancé
|
||||
save_game_filters(config.game_filter_obj.to_dict())
|
||||
config.menu_state = "filter_advanced"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Retour au filtrage avancé")
|
||||
elif is_input_matched(event, "left") and config.selected_priority_index < len(priority_list):
|
||||
# Monter la région dans la priorité
|
||||
idx = config.selected_priority_index
|
||||
if idx > 0:
|
||||
priority_list[idx], priority_list[idx-1] = priority_list[idx-1], priority_list[idx]
|
||||
config.selected_priority_index = idx - 1
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Priorité modifiée: {priority_list}")
|
||||
elif is_input_matched(event, "right") and config.selected_priority_index < len(priority_list):
|
||||
# Descendre la région dans la priorité
|
||||
idx = config.selected_priority_index
|
||||
if idx < len(priority_list) - 1:
|
||||
priority_list[idx], priority_list[idx+1] = priority_list[idx+1], priority_list[idx]
|
||||
config.selected_priority_index = idx + 1
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Priorité modifiée: {priority_list}")
|
||||
elif is_input_matched(event, "cancel"):
|
||||
# Retour sans sauvegarder
|
||||
config.menu_state = "filter_advanced"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Annulation configuration priorité")
|
||||
|
||||
# Menu filtre plateformes
|
||||
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]
|
||||
@@ -1966,6 +2437,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
|
||||
@@ -1982,13 +2461,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:
|
||||
@@ -2076,12 +2574,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
|
||||
@@ -2168,18 +2668,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
|
||||
|
||||
@@ -2191,11 +2704,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
|
||||
@@ -2262,4 +2773,4 @@ def get_emergency_controls():
|
||||
# manette basique
|
||||
"confirm_joy": {"type": "button", "button": 0},
|
||||
"cancel_joy": {"type": "button", "button": 1},
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
237
ports/RGSX/game_filters.py
Normal file
237
ports/RGSX/game_filters.py
Normal file
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Module de filtrage des jeux pour RGSX
|
||||
Partagé entre l'interface graphique et l'interface web
|
||||
"""
|
||||
|
||||
import re
|
||||
import logging
|
||||
from typing import List, Tuple, Dict, Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GameFilters:
|
||||
"""Classe pour gérer les filtres de jeux"""
|
||||
|
||||
# Régions disponibles
|
||||
REGIONS = ['USA', 'Canada', 'Europe', 'France', 'Germany', 'Japan', 'Korea', 'World', 'Other']
|
||||
|
||||
def __init__(self):
|
||||
# Initialiser toutes les régions en mode 'include' par défaut
|
||||
self.region_filters = {region: 'include' for region in self.REGIONS}
|
||||
self.hide_non_release = False
|
||||
self.one_rom_per_game = False
|
||||
self.regex_mode = False
|
||||
self.region_priority = ['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other']
|
||||
|
||||
def load_from_dict(self, filter_dict: Dict[str, Any]):
|
||||
"""Charge les filtres depuis un dictionnaire (depuis settings)"""
|
||||
loaded_region_filters = filter_dict.get('region_filters', {})
|
||||
# Initialiser toutes les régions en 'include' par défaut, puis appliquer celles chargées
|
||||
self.region_filters = {region: 'include' for region in self.REGIONS}
|
||||
self.region_filters.update(loaded_region_filters)
|
||||
|
||||
self.hide_non_release = filter_dict.get('hide_non_release', False)
|
||||
self.one_rom_per_game = filter_dict.get('one_rom_per_game', False)
|
||||
self.regex_mode = filter_dict.get('regex_mode', False)
|
||||
self.region_priority = filter_dict.get('region_priority',
|
||||
['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other'])
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convertit les filtres en dictionnaire (pour sauvegarder dans settings)"""
|
||||
return {
|
||||
'region_filters': self.region_filters,
|
||||
'hide_non_release': self.hide_non_release,
|
||||
'one_rom_per_game': self.one_rom_per_game,
|
||||
'regex_mode': self.regex_mode,
|
||||
'region_priority': self.region_priority
|
||||
}
|
||||
|
||||
def is_active(self) -> bool:
|
||||
"""Vérifie si des filtres sont actifs (au moins une région en exclude ou options activées)"""
|
||||
has_exclude = any(state == 'exclude' for state in self.region_filters.values())
|
||||
return (has_exclude or
|
||||
self.hide_non_release or
|
||||
self.one_rom_per_game)
|
||||
|
||||
def reset(self):
|
||||
"""Réinitialise tous les filtres (toutes les régions en include)"""
|
||||
self.region_filters = {region: 'include' for region in self.REGIONS}
|
||||
self.hide_non_release = False
|
||||
self.one_rom_per_game = False
|
||||
self.regex_mode = False
|
||||
|
||||
@staticmethod
|
||||
def get_game_regions(game_name: str) -> List[str]:
|
||||
"""Extrait les régions d'un nom de jeu"""
|
||||
name = game_name.upper()
|
||||
regions = []
|
||||
|
||||
# Patterns de région communs
|
||||
if 'USA' in name or 'US)' in name:
|
||||
regions.append('USA')
|
||||
if 'CANADA' in name or 'CA)' in name:
|
||||
regions.append('Canada')
|
||||
if 'EUROPE' in name or 'EU)' in name:
|
||||
regions.append('Europe')
|
||||
if 'FRANCE' in name or 'FR)' in name:
|
||||
regions.append('France')
|
||||
if 'GERMANY' in name or 'DE)' in name or 'GER)' in name:
|
||||
regions.append('Germany')
|
||||
if 'JAPAN' in name or 'JP)' in name or 'JPN)' in name:
|
||||
regions.append('Japan')
|
||||
if 'KOREA' in name or 'KR)' in name or 'KOR)' in name:
|
||||
regions.append('Korea')
|
||||
if 'WORLD' in name:
|
||||
regions.append('World')
|
||||
|
||||
# Autres régions
|
||||
if re.search(r'\b(AUSTRALIA|ASIA|KOREA|BRAZIL|CHINA|RUSSIA|SCANDINAVIA|'
|
||||
r'SPAIN|FRANCE|GERMANY|ITALY|CANADA)\b', name):
|
||||
if 'CANADA' in name:
|
||||
regions.append('Canada')
|
||||
else:
|
||||
regions.append('Other')
|
||||
|
||||
# Si aucune région trouvée
|
||||
if not regions:
|
||||
regions.append('Other')
|
||||
|
||||
return regions
|
||||
|
||||
@staticmethod
|
||||
def is_non_release_game(game_name: str) -> bool:
|
||||
"""Vérifie si un jeu est une version non-release (demo, beta, proto)"""
|
||||
name = game_name.upper()
|
||||
non_release_patterns = [
|
||||
r'\([^\)]*BETA[^\)]*\)',
|
||||
r'\([^\)]*DEMO[^\)]*\)',
|
||||
r'\([^\)]*PROTO[^\)]*\)',
|
||||
r'\([^\)]*SAMPLE[^\)]*\)',
|
||||
r'\([^\)]*KIOSK[^\)]*\)',
|
||||
r'\([^\)]*PREVIEW[^\)]*\)',
|
||||
r'\([^\)]*TEST[^\)]*\)',
|
||||
r'\([^\)]*DEBUG[^\)]*\)',
|
||||
r'\([^\)]*ALPHA[^\)]*\)',
|
||||
r'\([^\)]*PRE-RELEASE[^\)]*\)',
|
||||
r'\([^\)]*PRERELEASE[^\)]*\)',
|
||||
r'\([^\)]*UNFINISHED[^\)]*\)',
|
||||
r'\([^\)]*WIP[^\)]*\)',
|
||||
r'\[[^\]]*BETA[^\]]*\]',
|
||||
r'\[[^\]]*DEMO[^\]]*\]',
|
||||
r'\[[^\]]*TEST[^\]]*\]'
|
||||
]
|
||||
return any(re.search(pattern, name) for pattern in non_release_patterns)
|
||||
|
||||
@staticmethod
|
||||
def get_base_game_name(game_name: str) -> str:
|
||||
"""Obtient le nom de base du jeu (sans régions, versions, etc.)"""
|
||||
base = game_name
|
||||
|
||||
# Supprimer extensions
|
||||
base = re.sub(r'\.(zip|7z|rar|gz|iso)$', '', base, flags=re.IGNORECASE)
|
||||
|
||||
# Extraire info disque si présent
|
||||
disc_info = ''
|
||||
disc_match = (re.search(r'\(Dis[ck]\s*(\d+)\)', base, re.IGNORECASE) or
|
||||
re.search(r'\[Dis[ck]\s*(\d+)\]', base, re.IGNORECASE) or
|
||||
re.search(r'Dis[ck]\s*(\d+)', base, re.IGNORECASE) or
|
||||
re.search(r'\(CD\s*(\d+)\)', base, re.IGNORECASE) or
|
||||
re.search(r'CD\s*(\d+)', base, re.IGNORECASE))
|
||||
if disc_match:
|
||||
disc_info = f' (Disc {disc_match.group(1)})'
|
||||
|
||||
# Supprimer contenu entre parenthèses et crochets
|
||||
base = re.sub(r'\([^)]*\)', '', base)
|
||||
base = re.sub(r'\[[^\]]*\]', '', base)
|
||||
|
||||
# Normaliser espaces
|
||||
base = re.sub(r'\s+', ' ', base).strip()
|
||||
|
||||
# Rajouter info disque
|
||||
base = base + disc_info
|
||||
|
||||
return base
|
||||
|
||||
def get_region_priority(self, game_name: str) -> int:
|
||||
"""Obtient la priorité de région pour un jeu (pour one-rom-per-game)"""
|
||||
name = game_name.upper()
|
||||
|
||||
for i, region in enumerate(self.region_priority):
|
||||
region_upper = region.upper()
|
||||
if region_upper in name:
|
||||
return i
|
||||
|
||||
return len(self.region_priority) # Autres régions (priorité la plus basse)
|
||||
|
||||
def apply_filters(self, games: List[Tuple]) -> List[Tuple]:
|
||||
"""
|
||||
Applique les filtres à une liste de jeux
|
||||
games: Liste de tuples (game_name, game_url, size)
|
||||
Retourne: Liste filtrée de tuples
|
||||
"""
|
||||
if not self.is_active():
|
||||
return games
|
||||
|
||||
filtered_games = []
|
||||
|
||||
# Filtrage par région
|
||||
for game in games:
|
||||
game_name = game[0]
|
||||
|
||||
# Vérifier les filtres de région
|
||||
if self.region_filters:
|
||||
game_regions = self.get_game_regions(game_name)
|
||||
|
||||
# Vérifier si le jeu a au moins une région incluse
|
||||
has_included_region = False
|
||||
|
||||
for region in game_regions:
|
||||
filter_state = self.region_filters.get(region, 'include')
|
||||
if filter_state == 'include':
|
||||
has_included_region = True
|
||||
break # Si on trouve une région incluse, c'est bon
|
||||
|
||||
# Le jeu est affiché seulement s'il a au moins une région incluse
|
||||
if not has_included_region:
|
||||
continue
|
||||
|
||||
# Filtrer les non-release
|
||||
if self.hide_non_release and self.is_non_release_game(game_name):
|
||||
continue
|
||||
|
||||
filtered_games.append(game)
|
||||
|
||||
# Appliquer "one rom per game"
|
||||
if self.one_rom_per_game:
|
||||
filtered_games = self._apply_one_rom_per_game(filtered_games)
|
||||
|
||||
return filtered_games
|
||||
|
||||
def _apply_one_rom_per_game(self, games: List[Tuple]) -> List[Tuple]:
|
||||
"""Garde seulement une ROM par jeu selon la priorité de région"""
|
||||
games_by_base = {}
|
||||
|
||||
for game in games:
|
||||
game_name = game[0]
|
||||
base_name = self.get_base_game_name(game_name)
|
||||
|
||||
if base_name not in games_by_base:
|
||||
games_by_base[base_name] = []
|
||||
|
||||
games_by_base[base_name].append(game)
|
||||
|
||||
# Pour chaque jeu de base, garder celui avec la meilleure priorité
|
||||
result = []
|
||||
for base_name, game_list in games_by_base.items():
|
||||
if len(game_list) == 1:
|
||||
result.append(game_list[0])
|
||||
else:
|
||||
# Trier par priorité de région
|
||||
sorted_games = sorted(game_list,
|
||||
key=lambda g: self.get_region_priority(g[0]))
|
||||
result.append(sorted_games[0])
|
||||
|
||||
return result
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -190,6 +190,7 @@
|
||||
"instruction_generic_back": "Zum vorherigen Menü zurückkehren",
|
||||
"instruction_display_layout": "Rasterabmessungen (Spalten × Zeilen) durchschalten",
|
||||
"instruction_display_font_size": "Schriftgröße für bessere Lesbarkeit anpassen",
|
||||
"instruction_display_footer_font_size": "Fußzeilen-Textgröße anpassen (Version & Steuerelemente)",
|
||||
"instruction_display_font_family": "Zwischen verfügbaren Schriftarten wechseln",
|
||||
"instruction_display_show_unsupported": "Nicht in es_systems.cfg definierte Systeme anzeigen/ausblenden",
|
||||
"instruction_display_unknown_ext": "Warnung für in es_systems.cfg fehlende Dateiendungen an-/abschalten",
|
||||
@@ -202,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",
|
||||
@@ -210,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 ↑",
|
||||
@@ -233,7 +242,8 @@
|
||||
"history_game_options_title": "Spiel Optionen",
|
||||
"history_option_download_folder": "Datei lokalisieren",
|
||||
"history_option_extract_archive": "Archiv extrahieren",
|
||||
"history_option_scraper": "Metadaten scrapen",
|
||||
"history_option_open_file": "Datei öffnen",
|
||||
"history_option_scraper": "Metadaten abrufen",
|
||||
"history_option_delete_game": "Spiel löschen",
|
||||
"history_option_error_info": "Fehlerdetails",
|
||||
"history_option_retry": "Download wiederholen",
|
||||
@@ -330,7 +340,7 @@
|
||||
"web_restart_error": "Fehler beim Neustart: {0}",
|
||||
"web_support": "Support",
|
||||
"web_support_title": "📦 Support-Datei erstellt",
|
||||
"web_support_message": "Support-Datei erfolgreich erstellt!\\n\\n📁 Inhalt:\\n• Steuerungskonfiguration\\n• Download-Verlauf\\n• RGSX-Einstellungen\\n• Anwendungsprotokolle\\n• Webserver-Protokolle\\n\\n💬 Um Hilfe zu erhalten:\\n1. Trete dem RGSX Discord bei\\n2. Beschreibe dein Problem\\n3. Teile diese ZIP-Datei\\n\\nDownload startet...",
|
||||
"web_support_message": "Support-Datei erfolgreich erstellt!\n\n📁 Inhalt:\n• Steuerungskonfiguration\n• Download-Verlauf\n• RGSX-Einstellungen\n• Anwendungsprotokolle\n• Webserver-Protokolle\n\n💬 Um Hilfe zu erhalten:\n1. Trete dem RGSX Discord bei\n2. Beschreibe dein Problem\n3. Teile diese ZIP-Datei\n\nDownload startet...",
|
||||
"web_support_generating": "Support-Datei wird generiert...",
|
||||
"web_support_download": "Support-Datei herunterladen",
|
||||
"web_support_error": "Fehler beim Erstellen der Support-Datei: {0}",
|
||||
@@ -358,8 +368,44 @@
|
||||
"web_filter_regex_mode": "Regex-Suche aktivieren",
|
||||
"web_filter_one_rom_per_game": "Eine ROM pro Spiel",
|
||||
"web_filter_configure_priority": "Regions-Prioritätsreihenfolge konfigurieren",
|
||||
"filter_all": "Alles auswählen",
|
||||
"filter_none": "Alles abwählen",
|
||||
"filter_all": "Alle auswählen",
|
||||
"filter_none": "Alle abwählen",
|
||||
"filter_apply": "Filter anwenden",
|
||||
"filter_back": "Zurück"
|
||||
"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",
|
||||
"filter_menu_title": "Filtermenü",
|
||||
"filter_search_by_name": "Nach Namen suchen",
|
||||
"filter_advanced": "Erweiterte Filterung",
|
||||
"filter_advanced_title": "Erweiterte Spielfilterung",
|
||||
"filter_region_title": "Nach Region filtern",
|
||||
"filter_region_include": "Einschließen",
|
||||
"filter_region_exclude": "Ausschließen",
|
||||
"filter_region_usa": "USA",
|
||||
"filter_region_canada": "Kanada",
|
||||
"filter_region_europe": "Europa",
|
||||
"filter_region_france": "Frankreich",
|
||||
"filter_region_germany": "Deutschland",
|
||||
"filter_region_japan": "Japan",
|
||||
"filter_region_korea": "Korea",
|
||||
"filter_region_world": "Welt",
|
||||
"filter_region_other": "Andere",
|
||||
"filter_other_options": "Weitere Optionen",
|
||||
"filter_hide_non_release": "Demos/Betas/Protos ausblenden",
|
||||
"filter_one_rom_per_game": "Eine ROM pro Spiel",
|
||||
"filter_priority_order": "Prioritätsreihenfolge",
|
||||
"filter_priority_title": "Regionsprioritätskonfiguration",
|
||||
"filter_priority_desc": "Prioritätsreihenfolge für \"Eine ROM pro Spiel\" festlegen",
|
||||
"filter_regex_mode": "Regex-Modus",
|
||||
"filter_apply_filters": "Anwenden",
|
||||
"filter_reset_filters": "Zurücksetzen",
|
||||
"filter_back": "Zurück",
|
||||
"filter_active": "Filter aktiv",
|
||||
"filter_games_shown": "{0} Spiel(e) angezeigt"
|
||||
}
|
||||
@@ -192,6 +192,7 @@
|
||||
"instruction_generic_back": "Return to the previous menu",
|
||||
"instruction_display_layout": "Cycle grid dimensions (columns × rows)",
|
||||
"instruction_display_font_size": "Adjust text scale for readability",
|
||||
"instruction_display_footer_font_size": "Adjust footer text scale (version & controls display)",
|
||||
"instruction_display_font_family": "Switch between available font families",
|
||||
"instruction_display_show_unsupported": "Show/hide systems not defined in es_systems.cfg",
|
||||
"instruction_display_unknown_ext": "Enable/disable warning for file extensions absent from es_systems.cfg",
|
||||
@@ -204,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",
|
||||
@@ -212,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 ↑",
|
||||
@@ -235,6 +244,7 @@
|
||||
"history_game_options_title": "Game Options",
|
||||
"history_option_download_folder": "Locate file",
|
||||
"history_option_extract_archive": "Extract archive",
|
||||
"history_option_open_file": "Open file",
|
||||
"history_option_scraper": "Scrape metadata",
|
||||
"history_option_delete_game": "Delete game",
|
||||
"history_option_error_info": "Error details",
|
||||
@@ -332,7 +342,7 @@
|
||||
"web_restart_error": "Restart error: {0}",
|
||||
"web_support": "Support",
|
||||
"web_support_title": "📦 Support File Generated",
|
||||
"web_support_message": "Support file created successfully!\\n\\n📁 Contents:\\n• Controls configuration\\n• Download history\\n• RGSX settings\\n• Application logs\\n• Web server logs\\n\\n💬 To get help:\\n1. Join RGSX Discord\\n2. Describe your issue\\n3. Share this ZIP file\\n\\nDownload will start...",
|
||||
"web_support_message": "Support file created successfully!\n\n📁 Contents:\n• Controls configuration\n• Download history\n• RGSX settings\n• Application logs\n• Web server logs\n\n💬 To get help:\n1. Join RGSX Discord\n2. Describe your issue\n3. Share this ZIP file\n\nDownload will start...",
|
||||
"web_support_generating": "Generating support file...",
|
||||
"web_support_download": "Download support file",
|
||||
"web_support_error": "Error generating support file: {0}",
|
||||
@@ -358,8 +368,44 @@
|
||||
"web_filter_regex_mode": "Enable Regex Search",
|
||||
"web_filter_one_rom_per_game": "One ROM Per Game",
|
||||
"web_filter_configure_priority": "Configure region priority order",
|
||||
"filter_all": "Check All",
|
||||
"filter_none": "Uncheck All",
|
||||
"filter_apply": "Apply Filter",
|
||||
"filter_back": "Back"
|
||||
"filter_all": "Check all",
|
||||
"filter_none": "Uncheck all",
|
||||
"filter_apply": "Apply filter",
|
||||
"accessibility_footer_font_size": "Footer font size: {0}",
|
||||
"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",
|
||||
"filter_menu_title": "Filter Menu",
|
||||
"filter_search_by_name": "Search by name",
|
||||
"filter_advanced": "Advanced filtering",
|
||||
"filter_advanced_title": "Advanced Game Filtering",
|
||||
"filter_region_title": "Filter by region",
|
||||
"filter_region_include": "Include",
|
||||
"filter_region_exclude": "Exclude",
|
||||
"filter_region_usa": "USA",
|
||||
"filter_region_canada": "Canada",
|
||||
"filter_region_europe": "Europe",
|
||||
"filter_region_france": "France",
|
||||
"filter_region_germany": "Germany",
|
||||
"filter_region_japan": "Japan",
|
||||
"filter_region_korea": "Korea",
|
||||
"filter_region_world": "World",
|
||||
"filter_region_other": "Other",
|
||||
"filter_other_options": "Other options",
|
||||
"filter_hide_non_release": "Hide Demos/Betas/Protos",
|
||||
"filter_one_rom_per_game": "One ROM per game",
|
||||
"filter_priority_order": "Priority order",
|
||||
"filter_priority_title": "Region Priority Configuration",
|
||||
"filter_priority_desc": "Set preference order for \"One ROM per game\"",
|
||||
"filter_regex_mode": "Regex Mode",
|
||||
"filter_apply_filters": "Apply",
|
||||
"filter_reset_filters": "Reset",
|
||||
"filter_back": "Back",
|
||||
"filter_active": "Filter active",
|
||||
"filter_games_shown": "{0} game(s) shown"
|
||||
}
|
||||
@@ -192,6 +192,7 @@
|
||||
"instruction_generic_back": "Volver al menú anterior",
|
||||
"instruction_display_layout": "Alternar dimensiones de la cuadrícula (columnas × filas)",
|
||||
"instruction_display_font_size": "Ajustar tamaño del texto para mejor legibilidad",
|
||||
"instruction_display_footer_font_size": "Ajustar el tamaño del texto del pie de página (versión y controles)",
|
||||
"instruction_display_font_family": "Cambiar entre familias de fuentes disponibles",
|
||||
"instruction_display_show_unsupported": "Mostrar/ocultar sistemas no definidos en es_systems.cfg",
|
||||
"instruction_display_unknown_ext": "Activar/desactivar aviso para extensiones no presentes en es_systems.cfg",
|
||||
@@ -204,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",
|
||||
@@ -212,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 ↑",
|
||||
@@ -235,7 +244,8 @@
|
||||
"history_game_options_title": "Opciones del juego",
|
||||
"history_option_download_folder": "Localizar archivo",
|
||||
"history_option_extract_archive": "Extraer archivo",
|
||||
"history_option_scraper": "Scraper metadatos",
|
||||
"history_option_open_file": "Abrir archivo",
|
||||
"history_option_scraper": "Obtener metadatos",
|
||||
"history_option_delete_game": "Eliminar juego",
|
||||
"history_option_error_info": "Detalles del error",
|
||||
"history_option_retry": "Reintentar descarga",
|
||||
@@ -332,7 +342,7 @@
|
||||
"web_restart_error": "Error al reiniciar: {0}",
|
||||
"web_support": "Soporte",
|
||||
"web_support_title": "📦 Archivo de soporte generado",
|
||||
"web_support_message": "¡Archivo de soporte creado con éxito!\\n\\n📁 Contenido:\\n• Configuración de controles\\n• Historial de descargas\\n• Configuración RGSX\\n• Registros de la aplicación\\n• Registros del servidor web\\n\\n💬 Para obtener ayuda:\\n1. Únete al Discord de RGSX\\n2. Describe tu problema\\n3. Comparte este archivo ZIP\\n\\nLa descarga comenzará...",
|
||||
"web_support_message": "¡Archivo de soporte creado con éxito!\n\n📁 Contenido:\n• Configuración de controles\n• Historial de descargas\n• Configuración RGSX\n• Registros de la aplicación\n• Registros del servidor web\n\n💬 Para obtener ayuda:\n1. Únete al Discord de RGSX\n2. Describe tu problema\n3. Comparte este archivo ZIP\n\nLa descarga comenzará...",
|
||||
"web_support_generating": "Generando archivo de soporte...",
|
||||
"web_support_download": "Descargar archivo de soporte",
|
||||
"web_support_error": "Error al generar el archivo de soporte: {0}",
|
||||
@@ -361,5 +371,41 @@
|
||||
"filter_all": "Marcar todo",
|
||||
"filter_none": "Desmarcar todo",
|
||||
"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.",
|
||||
"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",
|
||||
"filter_menu_title": "Menú de filtros",
|
||||
"filter_search_by_name": "Buscar por nombre",
|
||||
"filter_advanced": "Filtrado avanzado",
|
||||
"filter_advanced_title": "Filtrado avanzado de juegos",
|
||||
"filter_region_title": "Filtrar por región",
|
||||
"filter_region_include": "Incluir",
|
||||
"filter_region_exclude": "Excluir",
|
||||
"filter_region_usa": "EE.UU.",
|
||||
"filter_region_canada": "Canadá",
|
||||
"filter_region_europe": "Europa",
|
||||
"filter_region_france": "Francia",
|
||||
"filter_region_germany": "Alemania",
|
||||
"filter_region_japan": "Japón",
|
||||
"filter_region_korea": "Corea",
|
||||
"filter_region_world": "Mundial",
|
||||
"filter_region_other": "Otros",
|
||||
"filter_other_options": "Otras opciones",
|
||||
"filter_hide_non_release": "Ocultar Demos/Betas/Protos",
|
||||
"filter_one_rom_per_game": "Una ROM por juego",
|
||||
"filter_priority_order": "Orden de prioridad",
|
||||
"filter_priority_title": "Configuración de prioridad de regiones",
|
||||
"filter_priority_desc": "Definir orden de preferencia para \"Una ROM por juego\"",
|
||||
"filter_regex_mode": "Modo Regex",
|
||||
"filter_apply_filters": "Aplicar",
|
||||
"filter_reset_filters": "Restablecer",
|
||||
"filter_back": "Volver",
|
||||
"filter_active": "Filtro activo",
|
||||
"filter_games_shown": "{0} juego(s) mostrado(s)"
|
||||
}
|
||||
@@ -192,6 +192,7 @@
|
||||
"instruction_generic_back": "Revenir au menu précédent",
|
||||
"instruction_display_layout": "Changer les dimensions de la grille",
|
||||
"instruction_display_font_size": "Ajuster la taille du texte pour la lisibilité",
|
||||
"instruction_display_footer_font_size": "Ajuster la taille du texte du pied de page (version et contrôles)",
|
||||
"instruction_display_font_family": "Basculer entre les polices disponibles",
|
||||
"instruction_display_show_unsupported": "Afficher/masquer systèmes absents de es_systems.cfg",
|
||||
"instruction_display_unknown_ext": "Avertir ou non pour extensions absentes de es_systems.cfg",
|
||||
@@ -204,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é",
|
||||
@@ -212,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 ↑",
|
||||
@@ -235,10 +244,11 @@
|
||||
"history_game_options_title": "Options du jeu",
|
||||
"history_option_download_folder": "Localiser le fichier",
|
||||
"history_option_extract_archive": "Extraire l'archive",
|
||||
"history_option_scraper": "Scraper métadonnées",
|
||||
"history_option_open_file": "Ouvrir le fichier",
|
||||
"history_option_scraper": "Récupérer métadonnées",
|
||||
"history_option_delete_game": "Supprimer le jeu",
|
||||
"history_option_error_info": "Détails de l'erreur",
|
||||
"history_option_retry": "Réessayer le téléchargement",
|
||||
"history_option_retry": "Retenter le téléchargement",
|
||||
"history_option_back": "Retour",
|
||||
"history_folder_path_label": "Chemin de destination :",
|
||||
"history_scraper_not_implemented": "Scraper pas encore implémenté",
|
||||
@@ -332,7 +342,7 @@
|
||||
"web_restart_error": "Erreur lors du redémarrage : {0}",
|
||||
"web_support": "Support",
|
||||
"web_support_title": "📦 Fichier de support généré",
|
||||
"web_support_message": "Le fichier de support a été créé avec succès !\\n\\n📁 Contenu :\\n• Configuration des contrôles\\n• Historique des téléchargements\\n• Paramètres RGSX\\n• Logs de l'application\\n• Logs du serveur web\\n\\n💬 Pour obtenir de l'aide :\\n1. Rejoignez le Discord RGSX\\n2. Décrivez votre problème\\n3. Partagez ce fichier ZIP\\n\\nLe téléchargement va démarrer...",
|
||||
"web_support_message": "Le fichier de support a été créé avec succès !\n\n📁 Contenu :\n• Configuration des contrôles\n• Historique des téléchargements\n• Paramètres RGSX\n• Logs de l'application\n• Logs du serveur web\n\n💬 Pour obtenir de l'aide :\n1. Rejoignez le Discord RGSX\n2. Décrivez votre problème\n3. Partagez ce fichier ZIP\n\nLe téléchargement va démarrer...",
|
||||
"web_support_generating": "Génération du fichier de support...",
|
||||
"web_support_download": "Télécharger le fichier de support",
|
||||
"web_support_error": "Erreur lors de la génération du fichier de support : {0}",
|
||||
@@ -361,5 +371,41 @@
|
||||
"filter_all": "Tout cocher",
|
||||
"filter_none": "Tout décocher",
|
||||
"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.",
|
||||
"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",
|
||||
"filter_menu_title": "Menu Filtrage",
|
||||
"filter_search_by_name": "Recherche par nom",
|
||||
"filter_advanced": "Filtrage avancé",
|
||||
"filter_advanced_title": "Filtrage avancé des jeux",
|
||||
"filter_region_title": "Filtrer par région",
|
||||
"filter_region_include": "Inclure",
|
||||
"filter_region_exclude": "Exclure",
|
||||
"filter_region_usa": "USA",
|
||||
"filter_region_canada": "Canada",
|
||||
"filter_region_europe": "Europe",
|
||||
"filter_region_france": "France",
|
||||
"filter_region_germany": "Allemagne",
|
||||
"filter_region_japan": "Japon",
|
||||
"filter_region_korea": "Corée",
|
||||
"filter_region_world": "Monde",
|
||||
"filter_region_other": "Autres",
|
||||
"filter_other_options": "Autres options",
|
||||
"filter_hide_non_release": "Masquer Démos/Betas/Protos",
|
||||
"filter_one_rom_per_game": "Une ROM par jeu",
|
||||
"filter_priority_order": "Ordre de priorité",
|
||||
"filter_priority_title": "Configuration de la priorité des régions",
|
||||
"filter_priority_desc": "Définir l'ordre de préférence pour \"Une ROM par jeu\"",
|
||||
"filter_regex_mode": "Mode Regex",
|
||||
"filter_apply_filters": "Appliquer",
|
||||
"filter_reset_filters": "Réinitialiser",
|
||||
"filter_back": "Retour",
|
||||
"filter_active": "Filtre actif",
|
||||
"filter_games_shown": "{0} jeu(x) affiché(s)"
|
||||
}
|
||||
@@ -189,6 +189,7 @@
|
||||
"instruction_generic_back": "Tornare al menu precedente",
|
||||
"instruction_display_layout": "Scorrere dimensioni griglia (colonne × righe)",
|
||||
"instruction_display_font_size": "Regolare dimensione testo per leggibilità",
|
||||
"instruction_display_footer_font_size": "Regola dimensione testo piè di pagina (versione e controlli)",
|
||||
"instruction_display_font_family": "Cambiare famiglia di font disponibile",
|
||||
"instruction_display_show_unsupported": "Mostrare/nascondere sistemi non definiti in es_systems.cfg",
|
||||
"instruction_display_unknown_ext": "Attivare/disattivare avviso per estensioni assenti in es_systems.cfg",
|
||||
@@ -201,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",
|
||||
@@ -209,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 ↑",
|
||||
@@ -232,6 +241,7 @@
|
||||
"history_game_options_title": "Opzioni gioco",
|
||||
"history_option_download_folder": "Localizza file",
|
||||
"history_option_extract_archive": "Estrai archivio",
|
||||
"history_option_open_file": "Apri file",
|
||||
"history_option_scraper": "Scraper metadati",
|
||||
"history_option_delete_game": "Elimina gioco",
|
||||
"history_option_error_info": "Dettagli errore",
|
||||
@@ -329,7 +339,7 @@
|
||||
"web_restart_error": "Errore durante il riavvio: {0}",
|
||||
"web_support": "Supporto",
|
||||
"web_support_title": "📦 File di supporto generato",
|
||||
"web_support_message": "File di supporto creato con successo!\\n\\n📁 Contenuto:\\n• Configurazione controlli\\n• Cronologia download\\n• Impostazioni RGSX\\n• Log dell'applicazione\\n• Log del server web\\n\\n💬 Per ottenere aiuto:\\n1. Unisciti al Discord RGSX\\n2. Descrivi il tuo problema\\n3. Condividi questo file ZIP\\n\\nIl download inizierà...",
|
||||
"web_support_message": "File di supporto creato con successo!\n\n📁 Contenuto:\n• Configurazione controlli\n• Cronologia download\n• Impostazioni RGSX\n• Log dell'applicazione\n• Log del server web\n\n💬 Per ottenere aiuto:\n1. Unisciti al Discord RGSX\n2. Descrivi il tuo problema\n3. Condividi questo file ZIP\n\nIl download inizierà...",
|
||||
"web_support_generating": "Generazione file di supporto...",
|
||||
"web_support_download": "Scarica file di supporto",
|
||||
"web_support_error": "Errore nella generazione del file di supporto: {0}",
|
||||
@@ -361,5 +371,41 @@
|
||||
"filter_all": "Seleziona tutto",
|
||||
"filter_none": "Deseleziona tutto",
|
||||
"filter_apply": "Applica filtro",
|
||||
"filter_back": "Indietro"
|
||||
"accessibility_footer_font_size": "Dimensione carattere piè di pagina: {0}",
|
||||
"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",
|
||||
"filter_menu_title": "Menu filtri",
|
||||
"filter_search_by_name": "Cerca per nome",
|
||||
"filter_advanced": "Filtro avanzato",
|
||||
"filter_advanced_title": "Filtro avanzato giochi",
|
||||
"filter_region_title": "Filtra per regione",
|
||||
"filter_region_include": "Includi",
|
||||
"filter_region_exclude": "Escludi",
|
||||
"filter_region_usa": "USA",
|
||||
"filter_region_canada": "Canada",
|
||||
"filter_region_europe": "Europa",
|
||||
"filter_region_france": "Francia",
|
||||
"filter_region_germany": "Germania",
|
||||
"filter_region_japan": "Giappone",
|
||||
"filter_region_korea": "Corea",
|
||||
"filter_region_world": "Mondo",
|
||||
"filter_region_other": "Altro",
|
||||
"filter_other_options": "Altre opzioni",
|
||||
"filter_hide_non_release": "Nascondi Demo/Beta/Proto",
|
||||
"filter_one_rom_per_game": "Una ROM per gioco",
|
||||
"filter_priority_order": "Ordine di priorità",
|
||||
"filter_priority_title": "Configurazione priorità regioni",
|
||||
"filter_priority_desc": "Imposta ordine di preferenza per \"Una ROM per gioco\"",
|
||||
"filter_regex_mode": "Modalità Regex",
|
||||
"filter_apply_filters": "Applica",
|
||||
"filter_reset_filters": "Reimposta",
|
||||
"filter_back": "Indietro",
|
||||
"filter_active": "Filtro attivo",
|
||||
"filter_games_shown": "{0} gioco/i mostrato/i"
|
||||
}
|
||||
@@ -191,6 +191,7 @@
|
||||
"instruction_generic_back": "Voltar ao menu anterior",
|
||||
"instruction_display_layout": "Alternar dimensões da grade (colunas × linhas)",
|
||||
"instruction_display_font_size": "Ajustar tamanho do texto para legibilidade",
|
||||
"instruction_display_footer_font_size": "Ajustar tamanho do texto do rodapé (versão e controles)",
|
||||
"instruction_display_font_family": "Alternar entre famílias de fontes disponíveis",
|
||||
"instruction_display_show_unsupported": "Mostrar/ocultar sistemas não definidos em es_systems.cfg",
|
||||
"instruction_display_unknown_ext": "Ativar/desativar aviso para extensões ausentes em es_systems.cfg",
|
||||
@@ -203,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",
|
||||
@@ -211,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 ↑",
|
||||
@@ -234,7 +243,8 @@
|
||||
"history_game_options_title": "Opções do jogo",
|
||||
"history_option_download_folder": "Localizar arquivo",
|
||||
"history_option_extract_archive": "Extrair arquivo",
|
||||
"history_option_scraper": "Scraper metadados",
|
||||
"history_option_open_file": "Abrir arquivo",
|
||||
"history_option_scraper": "Obter metadados",
|
||||
"history_option_delete_game": "Excluir jogo",
|
||||
"history_option_error_info": "Detalhes do erro",
|
||||
"history_option_retry": "Tentar novamente",
|
||||
@@ -331,7 +341,7 @@
|
||||
"web_restart_error": "Erro ao reiniciar: {0}",
|
||||
"web_support": "Suporte",
|
||||
"web_support_title": "📦 Arquivo de suporte gerado",
|
||||
"web_support_message": "Arquivo de suporte criado com sucesso!\\n\\n📁 Conteúdo:\\n• Configuração de controles\\n• Histórico de downloads\\n• Configurações RGSX\\n• Logs da aplicação\\n• Logs do servidor web\\n\\n💬 Para obter ajuda:\\n1. Entre no Discord RGSX\\n2. Descreva seu problema\\n3. Compartilhe este arquivo ZIP\\n\\nO download vai começar...",
|
||||
"web_support_message": "Arquivo de suporte criado com sucesso!\n\n📁 Conteúdo:\n• Configuração de controles\n• Histórico de downloads\n• Configurações RGSX\n• Logs da aplicação\n• Logs do servidor web\n\n💬 Para obter ajuda:\n1. Entre no Discord RGSX\n2. Descreva seu problema\n3. Compartilhe este arquivo ZIP\n\nO download vai começar...",
|
||||
"web_support_generating": "Gerando arquivo de suporte...",
|
||||
"web_support_download": "Baixar arquivo de suporte",
|
||||
"web_support_error": "Erro ao gerar arquivo de suporte: {0}",
|
||||
@@ -361,5 +371,41 @@
|
||||
"filter_all": "Marcar tudo",
|
||||
"filter_none": "Desmarcar tudo",
|
||||
"filter_apply": "Aplicar filtro",
|
||||
"filter_back": "Voltar"
|
||||
"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",
|
||||
"filter_menu_title": "Menu de filtros",
|
||||
"filter_search_by_name": "Pesquisar por nome",
|
||||
"filter_advanced": "Filtragem avançada",
|
||||
"filter_advanced_title": "Filtragem avançada de jogos",
|
||||
"filter_region_title": "Filtrar por região",
|
||||
"filter_region_include": "Incluir",
|
||||
"filter_region_exclude": "Excluir",
|
||||
"filter_region_usa": "EUA",
|
||||
"filter_region_canada": "Canadá",
|
||||
"filter_region_europe": "Europa",
|
||||
"filter_region_france": "França",
|
||||
"filter_region_germany": "Alemanha",
|
||||
"filter_region_japan": "Japão",
|
||||
"filter_region_korea": "Coreia",
|
||||
"filter_region_world": "Mundial",
|
||||
"filter_region_other": "Outros",
|
||||
"filter_other_options": "Outras opções",
|
||||
"filter_hide_non_release": "Ocultar Demos/Betas/Protos",
|
||||
"filter_one_rom_per_game": "Uma ROM por jogo",
|
||||
"filter_priority_order": "Ordem de prioridade",
|
||||
"filter_priority_title": "Configuração de prioridade de regiões",
|
||||
"filter_priority_desc": "Definir ordem de preferência para \"Uma ROM por jogo\"",
|
||||
"filter_regex_mode": "Modo Regex",
|
||||
"filter_apply_filters": "Aplicar",
|
||||
"filter_reset_filters": "Redefinir",
|
||||
"filter_back": "Voltar",
|
||||
"filter_active": "Filtro ativo",
|
||||
"filter_games_shown": "{0} jogo(s) exibido(s)"
|
||||
}
|
||||
@@ -453,8 +453,102 @@ async def check_for_updates():
|
||||
config.loading_progress = 5.0
|
||||
config.needs_redraw = True
|
||||
|
||||
response = requests.get(OTA_VERSION_ENDPOINT, timeout=5)
|
||||
response.raise_for_status()
|
||||
# Liste des endpoints à essayer (GitHub principal, puis fallback)
|
||||
endpoints = [
|
||||
OTA_VERSION_ENDPOINT,
|
||||
"https://retrogamesets.fr/softs/version.json"
|
||||
]
|
||||
|
||||
response = None
|
||||
last_error = None
|
||||
|
||||
for endpoint_index, endpoint in enumerate(endpoints):
|
||||
is_fallback = endpoint_index > 0
|
||||
if is_fallback:
|
||||
logger.info(f"Tentative sur endpoint de secours : {endpoint}")
|
||||
|
||||
# Gestion des erreurs de rate limit GitHub (429) avec retry
|
||||
max_retries = 3 if not is_fallback else 1 # Moins de retries sur fallback
|
||||
retry_count = 0
|
||||
|
||||
while retry_count < max_retries:
|
||||
try:
|
||||
response = requests.get(endpoint, timeout=10)
|
||||
|
||||
# Gestion spécifique des erreurs 429 (Too Many Requests) - surtout pour GitHub
|
||||
if response.status_code == 429:
|
||||
retry_after = response.headers.get('retry-after')
|
||||
x_ratelimit_remaining = response.headers.get('x-ratelimit-remaining', '1')
|
||||
x_ratelimit_reset = response.headers.get('x-ratelimit-reset')
|
||||
|
||||
if retry_after:
|
||||
# En-tête retry-after présent : attendre le nombre de secondes spécifié
|
||||
wait_time = int(retry_after)
|
||||
logger.warning(f"Rate limit atteint (429) sur {endpoint}. Attente de {wait_time}s (retry-after header)")
|
||||
elif x_ratelimit_remaining == '0' and x_ratelimit_reset:
|
||||
# x-ratelimit-remaining est 0 : attendre jusqu'à x-ratelimit-reset
|
||||
import time
|
||||
reset_time = int(x_ratelimit_reset)
|
||||
current_time = int(time.time())
|
||||
wait_time = max(reset_time - current_time, 60) # Minimum 60s
|
||||
logger.warning(f"Rate limit atteint (429) sur {endpoint}. Attente de {wait_time}s (x-ratelimit-reset)")
|
||||
else:
|
||||
# Pas d'en-têtes spécifiques : attendre au moins 60s
|
||||
wait_time = 60
|
||||
logger.warning(f"Rate limit atteint (429) sur {endpoint}. Attente de {wait_time}s par défaut")
|
||||
|
||||
if retry_count < max_retries - 1:
|
||||
logger.info(f"Nouvelle tentative dans {wait_time}s... ({retry_count + 1}/{max_retries})")
|
||||
await asyncio.sleep(wait_time)
|
||||
retry_count += 1
|
||||
continue
|
||||
else:
|
||||
# Si rate limit persistant et qu'on est sur GitHub, essayer le fallback
|
||||
if not is_fallback:
|
||||
logger.warning(f"Rate limit GitHub persistant, passage au serveur de secours")
|
||||
break # Sortir de la boucle retry pour essayer le prochain endpoint
|
||||
raise requests.exceptions.HTTPError(
|
||||
f"Limite de débit atteinte (429). Veuillez réessayer plus tard."
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
# Succès, sortir de toutes les boucles
|
||||
logger.debug(f"Version récupérée avec succès depuis : {endpoint}")
|
||||
break
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
last_error = e
|
||||
if response and response.status_code == 429:
|
||||
# 429 géré au-dessus, continuer la boucle ou passer au fallback
|
||||
retry_count += 1
|
||||
if retry_count >= max_retries:
|
||||
break # Passer au prochain endpoint
|
||||
else:
|
||||
# Erreur HTTP autre que 429
|
||||
logger.warning(f"Erreur HTTP {response.status_code if response else 'inconnue'} sur {endpoint}")
|
||||
break # Passer au prochain endpoint
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
last_error = e
|
||||
if retry_count < max_retries - 1:
|
||||
# Erreur réseau, réessayer avec backoff exponentiel
|
||||
wait_time = 2 ** retry_count # 1s, 2s, 4s
|
||||
logger.warning(f"Erreur réseau sur {endpoint}. Nouvelle tentative dans {wait_time}s...")
|
||||
await asyncio.sleep(wait_time)
|
||||
retry_count += 1
|
||||
else:
|
||||
logger.warning(f"Erreur réseau persistante sur {endpoint} : {e}")
|
||||
break # Passer au prochain endpoint
|
||||
|
||||
# Si on a une réponse valide, sortir de la boucle des endpoints
|
||||
if response and response.status_code == 200:
|
||||
break
|
||||
|
||||
# Si aucun endpoint n'a fonctionné
|
||||
if not response or response.status_code != 200:
|
||||
raise last_error if last_error else requests.exceptions.RequestException(
|
||||
"Impossible de vérifier les mises à jour sur tous les serveurs"
|
||||
)
|
||||
|
||||
# Accepter différents content-types (application/json, text/plain, text/html)
|
||||
content_type = response.headers.get("content-type", "")
|
||||
@@ -625,7 +719,8 @@ def request_cancel(task_id: str) -> bool:
|
||||
return False
|
||||
|
||||
def cancel_all_downloads():
|
||||
"""Cancel all active downloads and attempt to stop threads quickly."""
|
||||
"""Cancel all active downloads and queued downloads, and attempt to stop threads quickly."""
|
||||
# Annuler tous les téléchargements actifs via cancel_events
|
||||
for tid, ev in list(cancel_events.items()):
|
||||
try:
|
||||
ev.set()
|
||||
@@ -638,6 +733,22 @@ def cancel_all_downloads():
|
||||
th.join(timeout=0.2)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Vider la file d'attente des téléchargements
|
||||
config.download_queue.clear()
|
||||
config.download_active = False
|
||||
|
||||
# Mettre à jour l'historique pour annuler les téléchargements en statut "Queued"
|
||||
try:
|
||||
history = load_history()
|
||||
for entry in history:
|
||||
if entry.get("status") == "Queued":
|
||||
entry["status"] = "Canceled"
|
||||
entry["message"] = _("download_canceled")
|
||||
logger.info(f"Téléchargement en attente annulé : {entry.get('game_name', '?')}")
|
||||
save_history(history)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'annulation des téléchargements en attente : {e}")
|
||||
|
||||
|
||||
|
||||
@@ -1055,14 +1166,14 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
last_update_time = time.time()
|
||||
last_downloaded = 0
|
||||
update_interval = 0.1 # Mettre à jour toutes les 0,1 secondes
|
||||
download_cancelled = False
|
||||
download_canceled = False
|
||||
with open(dest_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=chunk_size):
|
||||
if cancel_ev is not None and cancel_ev.is_set():
|
||||
logger.debug(f"Annulation détectée, arrêt du téléchargement pour task_id={task_id}")
|
||||
result[0] = False
|
||||
result[1] = _("download_canceled") if _ else "Download canceled"
|
||||
download_cancelled = True
|
||||
download_canceled = True
|
||||
try:
|
||||
f.close()
|
||||
except Exception:
|
||||
@@ -1097,7 +1208,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
logger.debug(f"Mise à jour finale de progression: {downloaded}/{total_size} octets")
|
||||
|
||||
# Si annulé, ne pas continuer avec extraction
|
||||
if download_cancelled:
|
||||
if download_canceled:
|
||||
return
|
||||
|
||||
os.chmod(dest_path, 0o644)
|
||||
@@ -2077,7 +2188,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
last_update_time = time.time()
|
||||
last_downloaded = 0
|
||||
update_interval = 0.1 # Mettre à jour toutes les 0,1 secondes
|
||||
download_cancelled = False
|
||||
download_canceled = False
|
||||
logger.debug(f"Ouverture fichier: {dest_path}")
|
||||
with open(dest_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=chunk_size):
|
||||
@@ -2085,7 +2196,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
logger.debug(f"Annulation détectée, arrêt du téléchargement 1fichier pour task_id={task_id}")
|
||||
result[0] = False
|
||||
result[1] = _("download_canceled") if _ else "Download canceled"
|
||||
download_cancelled = True
|
||||
download_canceled = True
|
||||
try:
|
||||
f.close()
|
||||
except Exception:
|
||||
@@ -2121,7 +2232,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
progress_queues[task_id].put((task_id, downloaded, total_size, speed))
|
||||
|
||||
# Si annulé, ne pas continuer avec extraction
|
||||
if download_cancelled:
|
||||
if download_canceled:
|
||||
return
|
||||
|
||||
# Déterminer si extraction est nécessaire
|
||||
|
||||
@@ -53,7 +53,8 @@ def load_rgsx_settings():
|
||||
"language": "en",
|
||||
"music_enabled": True,
|
||||
"accessibility": {
|
||||
"font_scale": 1.0
|
||||
"font_scale": 1.0,
|
||||
"footer_font_scale": 1.5
|
||||
},
|
||||
"display": {
|
||||
"grid": "3x4",
|
||||
@@ -338,3 +339,26 @@ def get_language(settings=None):
|
||||
if settings is None:
|
||||
settings = load_rgsx_settings()
|
||||
return settings.get("language", "en")
|
||||
|
||||
|
||||
def load_game_filters():
|
||||
"""Charge les filtres de jeux depuis rgsx_settings.json."""
|
||||
try:
|
||||
settings = load_rgsx_settings()
|
||||
return settings.get("game_filters", {})
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading game filters: {str(e)}")
|
||||
return {}
|
||||
|
||||
|
||||
def save_game_filters(filters_dict):
|
||||
"""Sauvegarde les filtres de jeux dans rgsx_settings.json."""
|
||||
try:
|
||||
settings = load_rgsx_settings()
|
||||
settings["game_filters"] = filters_dict
|
||||
save_rgsx_settings(settings)
|
||||
logger.debug(f"Game filters saved: {filters_dict}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving game filters: {str(e)}")
|
||||
return False
|
||||
|
||||
@@ -20,7 +20,7 @@ import mimetypes
|
||||
from datetime import datetime, timezone
|
||||
from email.utils import formatdate, parsedate_to_datetime
|
||||
import config
|
||||
from history import load_history
|
||||
from history import load_history, save_history
|
||||
from utils import load_sources, load_games, extract_data
|
||||
from network import download_rom, download_from_1fichier
|
||||
from pathlib import Path
|
||||
@@ -246,11 +246,11 @@ def get_translation(key, default=None):
|
||||
return key
|
||||
|
||||
# Fonction pour normaliser les tailles de fichier
|
||||
def normalize_size(size_str):
|
||||
def normalize_size(size_str, lang='en'):
|
||||
"""
|
||||
Normalise une taille de fichier dans différents formats (Ko, KiB, Mo, MiB, Go, GiB)
|
||||
en un format uniforme (Mo ou Go).
|
||||
Exemples: "150 Mo" -> "150 Mo", "1.5 Go" -> "1.5 Go", "500 Ko" -> "0.5 Mo", "2 GiB" -> "2.15 Go"
|
||||
en un format uniforme selon la langue (MB/GB pour anglais, Mo/Go pour français).
|
||||
Exemples: "150 Mo" -> "150 MB" (en), "1.5 Go" -> "1.5 GB" (en), "500 Ko" -> "0.5 MB"
|
||||
"""
|
||||
if not size_str:
|
||||
return None
|
||||
@@ -282,16 +282,24 @@ def normalize_size(size_str):
|
||||
elif unit in ['gio', 'gib']:
|
||||
value = value * 1024 # GiB en Mo
|
||||
|
||||
# Afficher en Go si > 1024 Mo, sinon en Mo
|
||||
if value >= 1024:
|
||||
return f"{value / 1024:.2f} Go".rstrip('0').rstrip('.')
|
||||
# Déterminer les unités selon la langue
|
||||
if lang == 'fr':
|
||||
mb_unit = 'Mo'
|
||||
gb_unit = 'Go'
|
||||
else:
|
||||
# Arrondir à 1 décimale pour Mo
|
||||
mb_unit = 'MB'
|
||||
gb_unit = 'GB'
|
||||
|
||||
# Afficher en GB/Go si > 1024 Mo, sinon en MB/Mo
|
||||
if value >= 1024:
|
||||
return f"{value / 1024:.2f} {gb_unit}".replace('.00 ', ' ').rstrip('0').rstrip('.')
|
||||
else:
|
||||
# Arrondir à 1 décimale pour MB/Mo
|
||||
rounded = round(value, 1)
|
||||
if rounded == int(rounded):
|
||||
return f"{int(rounded)} Mo"
|
||||
return f"{int(rounded)} {mb_unit}"
|
||||
else:
|
||||
return f"{rounded} Mo".rstrip('0').rstrip('.')
|
||||
return f"{rounded} {mb_unit}".rstrip('0').rstrip('.')
|
||||
except (ValueError, TypeError):
|
||||
return size_str # Retourner original si conversion échoue
|
||||
|
||||
@@ -460,13 +468,32 @@ 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."""
|
||||
self._set_headers('text/plain; charset=utf-8', status=404)
|
||||
self.wfile.write(b'Not found')
|
||||
|
||||
def _get_language_from_cookies(self):
|
||||
"""Récupère la langue depuis les cookies ou retourne 'en' par défaut"""
|
||||
cookie_header = self.headers.get('Cookie', '')
|
||||
if cookie_header:
|
||||
# Parser les cookies
|
||||
cookies = {}
|
||||
for cookie in cookie_header.split(';'):
|
||||
cookie = cookie.strip()
|
||||
if '=' in cookie:
|
||||
key, value = cookie.split('=', 1)
|
||||
cookies[key] = value
|
||||
return cookies.get('language', 'en')
|
||||
return 'en'
|
||||
|
||||
def _asset_version(self, relative_path: str) -> str:
|
||||
"""Retourne un identifiant de version basé sur la date de modification du fichier statique."""
|
||||
@@ -676,7 +703,7 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
'game_name': game_name,
|
||||
'platform': platform_name,
|
||||
'url': game[1] if len(game) > 1 and isinstance(game, (list, tuple)) else None,
|
||||
'size': normalize_size(game[2] if len(game) > 2 and isinstance(game, (list, tuple)) else None)
|
||||
'size': normalize_size(game[2] if len(game) > 2 and isinstance(game, (list, tuple)) else None, self._get_language_from_cookies())
|
||||
})
|
||||
except Exception as e:
|
||||
logger.debug(f"Erreur lors de la recherche dans {platform_name}: {e}")
|
||||
@@ -703,10 +730,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
|
||||
@@ -714,12 +744,15 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
platform_name = path.split('/api/games/')[-1]
|
||||
platform_name = urllib.parse.unquote(platform_name)
|
||||
|
||||
# Récupérer la langue depuis les cookies ou utiliser 'en' par défaut
|
||||
lang = self._get_language_from_cookies()
|
||||
|
||||
games, _, games_last_modified = get_cached_games(platform_name)
|
||||
games_formatted = [
|
||||
{
|
||||
'name': g[0],
|
||||
'url': g[1] if len(g) > 1 else None,
|
||||
'size': normalize_size(g[2] if len(g) > 2 else None)
|
||||
'size': normalize_size(g[2] if len(g) > 2 else None, lang)
|
||||
}
|
||||
for g in games
|
||||
]
|
||||
@@ -1363,6 +1396,16 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
try:
|
||||
cleared_count = len(config.download_queue)
|
||||
config.download_queue.clear()
|
||||
|
||||
# Mettre à jour l'historique pour annuler les téléchargements en statut "Queued"
|
||||
history = load_history()
|
||||
for entry in history:
|
||||
if entry.get("status") == "Queued":
|
||||
entry["status"] = "Canceled"
|
||||
entry["message"] = get_translation('download_canceled')
|
||||
logger.info(f"Téléchargement en attente annulé : {entry.get('game_name', '?')}")
|
||||
save_history(history)
|
||||
|
||||
logger.info(f"📋 Queue vidée ({cleared_count} éléments supprimés)")
|
||||
self._send_json({
|
||||
'success': True,
|
||||
@@ -1394,6 +1437,16 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
removed_item = config.download_queue.pop(idx)
|
||||
logger.info(f"📋 {removed_item['game_name']} supprimé de la queue")
|
||||
found = True
|
||||
|
||||
# Mettre à jour l'historique pour cet élément
|
||||
history = load_history()
|
||||
for entry in history:
|
||||
if entry.get('task_id') == task_id and entry.get('status') == 'Queued':
|
||||
entry['status'] = 'Canceled'
|
||||
entry['message'] = get_translation('download_canceled')
|
||||
logger.info(f"Téléchargement en attente annulé dans l'historique : {entry.get('game_name', '?')}")
|
||||
break
|
||||
save_history(history)
|
||||
break
|
||||
|
||||
if found:
|
||||
@@ -1441,6 +1494,47 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
'error': str(e)
|
||||
}, status=500)
|
||||
|
||||
# Route: Sauvegarder seulement les filtres (sauvegarde rapide)
|
||||
elif path == '/api/save_filters':
|
||||
try:
|
||||
from rgsx_settings import load_rgsx_settings, save_rgsx_settings
|
||||
|
||||
# Charger les settings actuels
|
||||
current_settings = load_rgsx_settings()
|
||||
|
||||
# Mettre à jour seulement les filtres
|
||||
if 'game_filters' not in current_settings:
|
||||
current_settings['game_filters'] = {}
|
||||
|
||||
current_settings['game_filters']['region_filters'] = data.get('region_filters', {})
|
||||
current_settings['game_filters']['hide_non_release'] = data.get('hide_non_release', False)
|
||||
current_settings['game_filters']['one_rom_per_game'] = data.get('one_rom_per_game', False)
|
||||
current_settings['game_filters']['regex_mode'] = data.get('regex_mode', False)
|
||||
current_settings['game_filters']['region_priority'] = data.get('region_priority', ['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other'])
|
||||
|
||||
# Sauvegarder
|
||||
save_rgsx_settings(current_settings)
|
||||
|
||||
# Mettre à jour config.game_filter_obj
|
||||
if hasattr(config, 'game_filter_obj'):
|
||||
config.game_filter_obj.region_filters = data.get('region_filters', {})
|
||||
config.game_filter_obj.hide_non_release = data.get('hide_non_release', False)
|
||||
config.game_filter_obj.one_rom_per_game = data.get('one_rom_per_game', False)
|
||||
config.game_filter_obj.regex_mode = data.get('regex_mode', False)
|
||||
config.game_filter_obj.region_priority = data.get('region_priority', ['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other'])
|
||||
|
||||
self._send_json({
|
||||
'success': True,
|
||||
'message': 'Filtres sauvegardés'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la sauvegarde des filtres: {e}")
|
||||
self._send_json({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}, status=500)
|
||||
|
||||
# Route: Vider l'historique
|
||||
elif path == '/api/clear-history':
|
||||
try:
|
||||
|
||||
@@ -212,7 +212,7 @@ def get_game_metadata(game_name, platform_name):
|
||||
# Vérifier si des résultats ont été trouvés
|
||||
if "data" not in data or "games" not in data["data"] or not data["data"]["games"]:
|
||||
logger.warning(f"Aucun résultat trouvé pour '{clean_name}'")
|
||||
return {"error": "Aucun résultat trouvé"}
|
||||
return {"error": f"No result found for '{clean_name}'"}
|
||||
|
||||
# Prendre le premier résultat (meilleure correspondance)
|
||||
games = data["data"]["games"]
|
||||
|
||||
@@ -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;
|
||||
@@ -424,3 +473,70 @@ header p { opacity: 0.9; font-size: 1.1em; }
|
||||
padding: 3px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal Support */
|
||||
.support-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10000;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.support-modal-content {
|
||||
background: #2c2c2c;
|
||||
color: #ffffff;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.support-modal h2 {
|
||||
margin: 0 0 20px 0;
|
||||
color: #4CAF50;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.support-modal-message {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 25px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.support-modal button {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 30px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
width: 100%;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.support-modal button:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
@@ -109,6 +109,53 @@
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// Modal pour afficher les messages support avec formatage
|
||||
function showSupportModal(title, message) {
|
||||
// Remplacer les \n littéraux par de vrais retours à la ligne
|
||||
message = message.replace(/\\n/g, '\n');
|
||||
|
||||
// Créer la modal
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'support-modal';
|
||||
|
||||
const modalContent = document.createElement('div');
|
||||
modalContent.className = 'support-modal-content';
|
||||
|
||||
// Titre
|
||||
const titleElement = document.createElement('h2');
|
||||
titleElement.textContent = title;
|
||||
|
||||
// Message avec retours à la ligne préservés
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.className = 'support-modal-message';
|
||||
messageElement.textContent = message;
|
||||
|
||||
// Bouton OK
|
||||
const okButton = document.createElement('button');
|
||||
okButton.textContent = 'OK';
|
||||
okButton.onclick = () => {
|
||||
modal.style.animation = 'fadeOut 0.2s ease-in';
|
||||
setTimeout(() => modal.remove(), 200);
|
||||
};
|
||||
|
||||
// Assembler la modal
|
||||
modalContent.appendChild(titleElement);
|
||||
modalContent.appendChild(messageElement);
|
||||
modalContent.appendChild(okButton);
|
||||
modal.appendChild(modalContent);
|
||||
|
||||
// Ajouter au DOM
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Fermer en cliquant sur le fond
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) {
|
||||
modal.style.animation = 'fadeOut 0.2s ease-in';
|
||||
setTimeout(() => modal.remove(), 200);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Charger les traductions au démarrage
|
||||
async function loadTranslations() {
|
||||
try {
|
||||
@@ -135,6 +182,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
|
||||
@@ -281,6 +356,9 @@
|
||||
|
||||
// Restaurer l'état depuis l'URL au chargement
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
// Load saved filters first
|
||||
loadSavedFilters();
|
||||
|
||||
const path = window.location.pathname;
|
||||
|
||||
if (path.startsWith('/platform/')) {
|
||||
@@ -450,9 +528,130 @@
|
||||
// Filter state: Map of region -> 'include' or 'exclude'
|
||||
let regionFilters = new Map();
|
||||
|
||||
// Checkbox filter states (stored globally to restore after page changes)
|
||||
let savedHideNonRelease = false;
|
||||
let savedOneRomPerGame = false;
|
||||
let savedRegexMode = false;
|
||||
|
||||
// Region priority order for "One ROM Per Game" (customizable)
|
||||
let regionPriorityOrder = JSON.parse(localStorage.getItem('regionPriorityOrder')) ||
|
||||
['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other'];
|
||||
['USA', 'Canada', 'Europe', 'France', 'Germany', 'Japan', 'Korea', 'World', 'Other'];
|
||||
|
||||
// Save filters to backend
|
||||
async function saveFiltersToBackend() {
|
||||
try {
|
||||
const regionFiltersObj = {};
|
||||
regionFilters.forEach((mode, region) => {
|
||||
regionFiltersObj[region] = mode;
|
||||
});
|
||||
|
||||
// Update saved states from checkboxes if they exist
|
||||
if (document.getElementById('hide-non-release')) {
|
||||
savedHideNonRelease = document.getElementById('hide-non-release').checked;
|
||||
}
|
||||
if (document.getElementById('one-rom-per-game')) {
|
||||
savedOneRomPerGame = document.getElementById('one-rom-per-game').checked;
|
||||
}
|
||||
if (document.getElementById('regex-mode')) {
|
||||
savedRegexMode = document.getElementById('regex-mode').checked;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/save_filters', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
region_filters: regionFiltersObj,
|
||||
hide_non_release: savedHideNonRelease,
|
||||
one_rom_per_game: savedOneRomPerGame,
|
||||
regex_mode: savedRegexMode,
|
||||
region_priority: regionPriorityOrder
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success) {
|
||||
console.warn('Failed to save filters:', data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to save filters:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load saved filters from settings
|
||||
async function loadSavedFilters() {
|
||||
try {
|
||||
const response = await fetch('/api/settings');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.settings.game_filters) {
|
||||
const filters = data.settings.game_filters;
|
||||
|
||||
// Load region filters
|
||||
if (filters.region_filters) {
|
||||
regionFilters.clear();
|
||||
Object.entries(filters.region_filters).forEach(([region, mode]) => {
|
||||
regionFilters.set(region, mode);
|
||||
});
|
||||
}
|
||||
|
||||
// Load region priority
|
||||
if (filters.region_priority) {
|
||||
regionPriorityOrder = filters.region_priority;
|
||||
localStorage.setItem('regionPriorityOrder', JSON.stringify(regionPriorityOrder));
|
||||
}
|
||||
|
||||
// Save checkbox states to global variables
|
||||
savedHideNonRelease = filters.hide_non_release || false;
|
||||
savedOneRomPerGame = filters.one_rom_per_game || false;
|
||||
savedRegexMode = filters.regex_mode || false;
|
||||
|
||||
// Load checkboxes when they exist (in games view)
|
||||
if (document.getElementById('hide-non-release')) {
|
||||
document.getElementById('hide-non-release').checked = savedHideNonRelease;
|
||||
}
|
||||
if (document.getElementById('one-rom-per-game')) {
|
||||
document.getElementById('one-rom-per-game').checked = savedOneRomPerGame;
|
||||
}
|
||||
if (document.getElementById('regex-mode')) {
|
||||
document.getElementById('regex-mode').checked = savedRegexMode;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load saved filters:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore filter button states in the UI
|
||||
function restoreFilterStates() {
|
||||
// Restore region button states
|
||||
regionFilters.forEach((mode, region) => {
|
||||
const btn = document.querySelector(`.region-btn[data-region="${region}"]`);
|
||||
if (btn) {
|
||||
if (mode === 'include') {
|
||||
btn.classList.add('active');
|
||||
btn.classList.remove('excluded');
|
||||
} else if (mode === 'exclude') {
|
||||
btn.classList.remove('active');
|
||||
btn.classList.add('excluded');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Restore checkbox states
|
||||
if (document.getElementById('hide-non-release')) {
|
||||
document.getElementById('hide-non-release').checked = savedHideNonRelease;
|
||||
}
|
||||
if (document.getElementById('one-rom-per-game')) {
|
||||
document.getElementById('one-rom-per-game').checked = savedOneRomPerGame;
|
||||
}
|
||||
if (document.getElementById('regex-mode')) {
|
||||
document.getElementById('regex-mode').checked = savedRegexMode;
|
||||
}
|
||||
|
||||
// Apply filters to display the games correctly
|
||||
applyAllFilters();
|
||||
}
|
||||
|
||||
|
||||
// Helper: Extract region(s) from game name - returns array of regions
|
||||
function getGameRegions(gameName) {
|
||||
@@ -462,12 +661,16 @@
|
||||
// Common region patterns - check all, not just first match
|
||||
// Handle both "(USA)" and "(USA, Europe)" formats
|
||||
if (name.includes('USA') || name.includes('US)')) regions.push('USA');
|
||||
if (name.includes('CANADA')) regions.push('Canada');
|
||||
if (name.includes('EUROPE') || name.includes('EU)')) regions.push('Europe');
|
||||
if (name.includes('FRANCE') || name.includes('FR)')) regions.push('France');
|
||||
if (name.includes('GERMANY') || name.includes('DE)')) regions.push('Germany');
|
||||
if (name.includes('JAPAN') || name.includes('JP)') || name.includes('JPN)')) regions.push('Japan');
|
||||
if (name.includes('KOREA') || name.includes('KR)')) regions.push('Korea');
|
||||
if (name.includes('WORLD')) regions.push('World');
|
||||
|
||||
// Check for other regions
|
||||
if (name.match(/\b(AUSTRALIA|ASIA|KOREA|BRAZIL|CHINA|RUSSIA|SCANDINAVIA|SPAIN|FRANCE|GERMANY|ITALY)\b/)) {
|
||||
// Check for other regions (excluding the ones above)
|
||||
if (name.match(/\b(AUSTRALIA|ASIA|BRAZIL|CHINA|RUSSIA|SCANDINAVIA|SPAIN|ITALY)\b/)) {
|
||||
if (!regions.includes('Other')) regions.push('Other');
|
||||
}
|
||||
|
||||
@@ -550,7 +753,10 @@
|
||||
if (region === 'CANADA' && name.includes('CANADA')) return i;
|
||||
if (region === 'WORLD' && name.includes('WORLD')) return i;
|
||||
if (region === 'EUROPE' && (name.includes('EUROPE') || name.includes('EU)'))) return i;
|
||||
if (region === 'FRANCE' && (name.includes('FRANCE') || name.includes('FR)'))) return i;
|
||||
if (region === 'GERMANY' && (name.includes('GERMANY') || name.includes('DE)'))) return i;
|
||||
if (region === 'JAPAN' && (name.includes('JAPAN') || name.includes('JP)') || name.includes('JPN)'))) return i;
|
||||
if (region === 'KOREA' && (name.includes('KOREA') || name.includes('KR)'))) return i;
|
||||
}
|
||||
|
||||
return regionPriorityOrder.length; // Other regions (lowest priority)
|
||||
@@ -578,6 +784,7 @@
|
||||
[regionPriorityOrder[idx-1], regionPriorityOrder[idx]];
|
||||
saveRegionPriorityOrder();
|
||||
renderRegionPriorityConfig();
|
||||
saveFiltersToBackend();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -589,14 +796,16 @@
|
||||
[regionPriorityOrder[idx+1], regionPriorityOrder[idx]];
|
||||
saveRegionPriorityOrder();
|
||||
renderRegionPriorityConfig();
|
||||
saveFiltersToBackend();
|
||||
}
|
||||
}
|
||||
|
||||
// Reset region priority to default
|
||||
function resetRegionPriority() {
|
||||
regionPriorityOrder = ['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other'];
|
||||
regionPriorityOrder = ['USA', 'Canada', 'Europe', 'France', 'Germany', 'Japan', 'Korea', 'World', 'Other'];
|
||||
saveRegionPriorityOrder();
|
||||
renderRegionPriorityConfig();
|
||||
saveFiltersToBackend();
|
||||
}
|
||||
|
||||
// Render region priority configuration UI
|
||||
@@ -613,11 +822,11 @@
|
||||
<span style="font-weight: bold; color: #666; min-width: 25px;">${idx + 1}.</span>
|
||||
<span style="flex: 1; font-weight: 500;">${region}</span>
|
||||
<button onclick="moveRegionUp('${region}')"
|
||||
style="padding: 4px 8px; border: 1px solid #ccc; background: white; cursor: pointer; border-radius: 3px;"
|
||||
${idx === 0 ? 'disabled' : ''}>▲</button>
|
||||
style="padding: 4px 8px; border: 1px solid #ccc; background: white; cursor: pointer; border-radius: 3px; font-size: 14px;"
|
||||
${idx === 0 ? 'disabled' : ''}>🔼</button>
|
||||
<button onclick="moveRegionDown('${region}')"
|
||||
style="padding: 4px 8px; border: 1px solid #ccc; background: white; cursor: pointer; border-radius: 3px;"
|
||||
${idx === regionPriorityOrder.length - 1 ? 'disabled' : ''}>▼</button>
|
||||
style="padding: 4px 8px; border: 1px solid #ccc; background: white; cursor: pointer; border-radius: 3px; font-size: 14px;"
|
||||
${idx === regionPriorityOrder.length - 1 ? 'disabled' : ''}>🔽</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
@@ -678,14 +887,15 @@
|
||||
}
|
||||
|
||||
applyAllFilters();
|
||||
saveFiltersToBackend();
|
||||
}
|
||||
|
||||
// Apply all filters
|
||||
function applyAllFilters() {
|
||||
const searchInput = document.getElementById('game-search');
|
||||
const searchTerm = searchInput ? searchInput.value : '';
|
||||
const hideNonRelease = document.getElementById('hide-non-release')?.checked || false;
|
||||
const regexMode = document.getElementById('regex-mode')?.checked || false;
|
||||
const hideNonRelease = document.getElementById('hide-non-release')?.checked || savedHideNonRelease;
|
||||
const regexMode = document.getElementById('regex-mode')?.checked || savedRegexMode;
|
||||
|
||||
const items = document.querySelectorAll('.game-item');
|
||||
let visibleCount = 0;
|
||||
@@ -776,7 +986,7 @@
|
||||
});
|
||||
|
||||
// Apply one-rom-per-game filter (after other filters)
|
||||
const oneRomPerGame = document.getElementById('one-rom-per-game')?.checked || false;
|
||||
const oneRomPerGame = document.getElementById('one-rom-per-game')?.checked || savedOneRomPerGame;
|
||||
if (oneRomPerGame) {
|
||||
// Group currently visible games by base name
|
||||
const gameGroups = new Map();
|
||||
@@ -873,13 +1083,24 @@
|
||||
const getSizeInMo = (sizeElem) => {
|
||||
if (!sizeElem) return 0;
|
||||
const text = sizeElem.textContent;
|
||||
// Les tailles sont maintenant normalisées: "100 Mo" ou "2.5 Go"
|
||||
const match = text.match(/([0-9.]+)\\s*(Mo|Go)/i);
|
||||
// Support des formats: "100 Mo", "2.5 Go" (français) et "100 MB", "2.5 GB" (anglais)
|
||||
// Plus Ko/KB, o/B, To/TB
|
||||
const match = text.match(/([0-9.]+)\s*(o|B|Ko|KB|Mo|MB|Go|GB|To|TB)/i);
|
||||
if (!match) return 0;
|
||||
let size = parseFloat(match[1]);
|
||||
// Convertir Go en Mo pour comparaison
|
||||
if (match[2].toUpperCase() === 'GO') {
|
||||
size *= 1024;
|
||||
const unit = match[2].toUpperCase();
|
||||
|
||||
// Convertir tout en Mo
|
||||
if (unit === 'O' || unit === 'B') {
|
||||
size /= (1024 * 1024); // octets/bytes vers Mo
|
||||
} else if (unit === 'KO' || unit === 'KB') {
|
||||
size /= 1024; // Ko vers Mo
|
||||
} else if (unit === 'MO' || unit === 'MB') {
|
||||
// Déjà en Mo
|
||||
} else if (unit === 'GO' || unit === 'GB') {
|
||||
size *= 1024; // Go vers Mo
|
||||
} else if (unit === 'TO' || unit === 'TB') {
|
||||
size *= 1024 * 1024; // To vers Mo
|
||||
}
|
||||
return size;
|
||||
};
|
||||
@@ -1004,22 +1225,26 @@
|
||||
<div class="filter-row">
|
||||
<span class="filter-label">${t('web_filter_region')}:</span>
|
||||
<button class="region-btn" data-region="USA" onclick="toggleRegionFilter('USA')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1fa-1f1f8.svg" style="width:16px;height:16px" /> USA</button>
|
||||
<button class="region-btn" data-region="Europe" onclick="toggleRegionFilter('Europe')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1ea-1f1fa.svg" style="width:16px;height:16px" /> Europe</button>
|
||||
<button class="region-btn" data-region="Canada" onclick="toggleRegionFilter('Canada')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1e8-1f1e6.svg" style="width:16px;height:16px" /> Canada</button>
|
||||
<button class="region-btn" data-region="Europe" onclick="toggleRegionFilter('Europe')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1ea-1f1fa.svg" style="width:16px;height:16px" /> Europe</button>
|
||||
<button class="region-btn" data-region="France" onclick="toggleRegionFilter('France')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1eb-1f1f7.svg" style="width:16px;height:16px" /> France</button>
|
||||
<button class="region-btn" data-region="Germany" onclick="toggleRegionFilter('Germany')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1e9-1f1ea.svg" style="width:16px;height:16px" /> Germany</button>
|
||||
<button class="region-btn" data-region="Japan" onclick="toggleRegionFilter('Japan')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1ef-1f1f5.svg" style="width:16px;height:16px" /> Japan</button>
|
||||
<button class="region-btn" data-region="Korea" onclick="toggleRegionFilter('Korea')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1f0-1f1f7.svg" style="width:16px;height:16px" /> Korea</button>
|
||||
<button class="region-btn" data-region="World" onclick="toggleRegionFilter('World')">🌍 World</button>
|
||||
<button class="region-btn" data-region="Other" onclick="toggleRegionFilter('Other')">🌐 Other</button>
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox" id="hide-non-release" onchange="applyAllFilters()">
|
||||
<input type="checkbox" id="hide-non-release" onchange="applyAllFilters(); saveFiltersToBackend();">
|
||||
<span>${t('web_filter_hide_non_release')}</span>
|
||||
</label>
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox" id="regex-mode" onchange="applyAllFilters()">
|
||||
<input type="checkbox" id="regex-mode" onchange="applyAllFilters(); saveFiltersToBackend();">
|
||||
<span>${t('web_filter_regex_mode')}</span>
|
||||
</label>
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox" id="one-rom-per-game" onchange="applyAllFilters()">
|
||||
<input type="checkbox" id="one-rom-per-game" onchange="applyAllFilters(); saveFiltersToBackend();">
|
||||
<span>${t('web_filter_one_rom_per_game')} (<span id="region-priority-display">USA → Canada → World → Europe → Japan → Other</span>)</span>
|
||||
<button onclick="showRegionPriorityConfig()" style="margin-left: 8px; padding: 2px 8px; font-size: 0.9em; background: #666; color: white; border: none; border-radius: 3px; cursor: pointer;" title="${t('web_filter_configure_priority')}">⚙️</button>
|
||||
</label>
|
||||
@@ -1054,6 +1279,9 @@
|
||||
`;
|
||||
container.innerHTML = html;
|
||||
|
||||
// Restore filter states from loaded settings
|
||||
restoreFilterStates();
|
||||
|
||||
// Appliquer le tri par défaut (A-Z)
|
||||
sortGames(currentGameSort);
|
||||
|
||||
@@ -1102,8 +1330,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 +1426,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 +1452,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 +1492,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 +1520,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 +1674,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 +1738,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 +1769,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 +1787,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 +1972,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 +1992,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 +2004,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 +2028,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,14 +2083,31 @@
|
||||
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 {
|
||||
// Collect region filters
|
||||
const regionFiltersObj = {};
|
||||
regionFilters.forEach((mode, region) => {
|
||||
regionFiltersObj[region] = mode;
|
||||
});
|
||||
|
||||
const settings = {
|
||||
language: document.getElementById('setting-language').value,
|
||||
music_enabled: document.getElementById('setting-music').checked,
|
||||
@@ -1887,7 +2127,14 @@
|
||||
},
|
||||
show_unsupported_platforms: document.getElementById('setting-show-unsupported').checked,
|
||||
allow_unknown_extensions: document.getElementById('setting-allow-unknown').checked,
|
||||
roms_folder: document.getElementById('setting-roms-folder').value.trim()
|
||||
roms_folder: document.getElementById('setting-roms-folder').value.trim(),
|
||||
game_filters: {
|
||||
region_filters: regionFiltersObj,
|
||||
hide_non_release: document.getElementById('hide-non-release')?.checked || savedHideNonRelease,
|
||||
one_rom_per_game: document.getElementById('one-rom-per-game')?.checked || savedOneRomPerGame,
|
||||
regex_mode: document.getElementById('regex-mode')?.checked || savedRegexMode,
|
||||
region_priority: regionPriorityOrder
|
||||
}
|
||||
};
|
||||
|
||||
const response = await fetch('/api/settings', {
|
||||
@@ -1899,13 +2146,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1976,7 +2233,7 @@
|
||||
}
|
||||
|
||||
// Générer un fichier ZIP de support
|
||||
async function generateSupportZip() {
|
||||
async function generateSupportZip(event) {
|
||||
try {
|
||||
// Afficher un message de chargement
|
||||
const loadingMsg = t('web_support_generating');
|
||||
@@ -2019,8 +2276,8 @@
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
// Afficher le message d'instructions
|
||||
alert(t('web_support_title') + '\\n\\n' + t('web_support_message'));
|
||||
// Afficher le message d'instructions dans une modal
|
||||
showSupportModal(t('web_support_title'), t('web_support_message'));
|
||||
|
||||
// Restaurer le bouton
|
||||
if (originalButton) {
|
||||
|
||||
@@ -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
|
||||
@@ -1115,6 +1294,18 @@ def _handle_special_platforms(dest_dir, archive_path, before_dirs, iso_before=No
|
||||
if not success:
|
||||
return False, error_msg
|
||||
|
||||
# PSVita: extraction dans ux0/app + création fichier .psvita
|
||||
psvita_dir_normal = os.path.join(config.ROMS_FOLDER, "psvita")
|
||||
psvita_dir_symlink = os.path.join(config.ROMS_FOLDER, "psvita", "psvita")
|
||||
is_psvita = (dest_dir == psvita_dir_normal or dest_dir == psvita_dir_symlink)
|
||||
|
||||
if is_psvita:
|
||||
expected_base = os.path.splitext(os.path.basename(archive_path))[0]
|
||||
items_before = before_items if before_items is not None else before_dirs
|
||||
success, error_msg = handle_psvita(dest_dir, items_before, extracted_basename=expected_base)
|
||||
if not success:
|
||||
return False, error_msg
|
||||
|
||||
return True, None
|
||||
|
||||
def extract_zip(zip_path, dest_dir, url):
|
||||
@@ -1780,6 +1971,136 @@ def handle_scummvm(dest_dir, before_items, extracted_basename=None):
|
||||
return False, error_msg
|
||||
|
||||
|
||||
def handle_psvita(dest_dir, before_items, extracted_basename=None):
|
||||
"""Gère l'organisation spécifique des jeux PSVita extraits.
|
||||
|
||||
Structure attendue:
|
||||
- Archive RAR extraite → Dossier "Nom du jeu"/
|
||||
- Dans ce dossier → Fichier "IDJeu.zip" (ex: PCSE00890.zip)
|
||||
- Ce ZIP contient → Dossier "IDJeu" (ex: PCSE00890/)
|
||||
|
||||
Actions:
|
||||
1. Créer fichier "Nom du jeu [IDJeu].psvita" dans dest_dir
|
||||
2. Extraire IDJeu.zip dans config.SAVE_FOLDER/psvita/ux0/app/
|
||||
3. Supprimer le dossier temporaire "Nom du jeu"/
|
||||
|
||||
Args:
|
||||
dest_dir: Dossier de destination (psvita ou psvita/psvita)
|
||||
before_items: Set des éléments présents avant extraction
|
||||
extracted_basename: Nom de base de l'archive extraite (sans extension)
|
||||
"""
|
||||
logger.debug(f"Traitement spécifique PSVita dans: {dest_dir}")
|
||||
time.sleep(2) # Petite latence post-extraction
|
||||
|
||||
try:
|
||||
after_items = set(os.listdir(dest_dir))
|
||||
except Exception:
|
||||
after_items = set()
|
||||
|
||||
ignore_names = {"psvita", "images", "videos", "manuals", "media"}
|
||||
# Filtrer les nouveaux éléments (fichiers ou dossiers)
|
||||
new_items = [item for item in (after_items - before_items)
|
||||
if item not in ignore_names and not item.endswith('.psvita')]
|
||||
|
||||
if not new_items:
|
||||
logger.warning("PSVita: Aucun nouveau dossier détecté après extraction")
|
||||
return True, None
|
||||
|
||||
if not extracted_basename:
|
||||
extracted_basename = new_items[0] if new_items else "game"
|
||||
|
||||
# Chercher le dossier du jeu (normalement il n'y en a qu'un)
|
||||
game_folder = None
|
||||
for item in new_items:
|
||||
item_path = os.path.join(dest_dir, item)
|
||||
if os.path.isdir(item_path):
|
||||
game_folder = item
|
||||
game_folder_path = item_path
|
||||
break
|
||||
|
||||
if not game_folder:
|
||||
logger.error("PSVita: Aucun dossier de jeu trouvé après extraction")
|
||||
return False, "PSVita: Aucun dossier de jeu trouvé"
|
||||
|
||||
logger.debug(f"PSVita: Dossier de jeu trouvé: {game_folder}")
|
||||
|
||||
# Chercher le fichier ZIP à l'intérieur (IDJeu.zip)
|
||||
try:
|
||||
contents = os.listdir(game_folder_path)
|
||||
zip_files = [f for f in contents if f.lower().endswith('.zip')]
|
||||
|
||||
if not zip_files:
|
||||
logger.error(f"PSVita: Aucun fichier ZIP trouvé dans {game_folder}")
|
||||
return False, f"PSVita: Aucun ZIP trouvé dans {game_folder}"
|
||||
|
||||
# Prendre le premier ZIP trouvé
|
||||
zip_filename = zip_files[0]
|
||||
zip_path = os.path.join(game_folder_path, zip_filename)
|
||||
|
||||
# Extraire l'ID du jeu (nom du ZIP sans extension)
|
||||
game_id = os.path.splitext(zip_filename)[0]
|
||||
logger.debug(f"PSVita: ZIP trouvé: {zip_filename}, ID du jeu: {game_id}")
|
||||
|
||||
# 1. Créer le fichier .psvita dans dest_dir
|
||||
psvita_filename = f"{game_folder} [{game_id}].psvita"
|
||||
psvita_file_path = os.path.join(dest_dir, psvita_filename)
|
||||
|
||||
try:
|
||||
# Créer un fichier vide .psvita
|
||||
with open(psvita_file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(f"# PSVita Game\n")
|
||||
f.write(f"# Game: {game_folder}\n")
|
||||
f.write(f"# ID: {game_id}\n")
|
||||
logger.info(f"PSVita: Fichier .psvita créé: {psvita_filename}")
|
||||
except Exception as e:
|
||||
logger.error(f"PSVita: Erreur création fichier .psvita: {e}")
|
||||
return False, f"Erreur création {psvita_filename}: {e}"
|
||||
|
||||
# 2. Extraire le ZIP dans le dossier parent de config.SAVE_FOLDER/psvita/ux0/app/
|
||||
save_parent2 = os.path.dirname(config.SAVE_FOLDER)
|
||||
save_parent = os.path.dirname(save_parent2)
|
||||
ux0_app_dir = os.path.join(save_parent, "psvita", "ux0", "app")
|
||||
os.makedirs(ux0_app_dir, exist_ok=True)
|
||||
|
||||
logger.debug(f"PSVita: Extraction de {zip_filename} dans {ux0_app_dir}")
|
||||
|
||||
try:
|
||||
import zipfile
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||
zip_ref.extractall(ux0_app_dir)
|
||||
logger.info(f"PSVita: ZIP extrait avec succès dans {ux0_app_dir}")
|
||||
|
||||
# Vérifier que le dossier game_id existe bien
|
||||
game_id_path = os.path.join(ux0_app_dir, game_id)
|
||||
if not os.path.exists(game_id_path):
|
||||
logger.warning(f"PSVita: Le dossier {game_id} n'a pas été trouvé dans l'extraction")
|
||||
else:
|
||||
logger.info(f"PSVita: Dossier {game_id} confirmé dans ux0/app/")
|
||||
|
||||
except zipfile.BadZipFile as e:
|
||||
logger.error(f"PSVita: Fichier ZIP corrompu: {e}")
|
||||
return False, f"ZIP corrompu: {zip_filename}"
|
||||
except Exception as e:
|
||||
logger.error(f"PSVita: Erreur extraction ZIP: {e}")
|
||||
return False, f"Erreur extraction {zip_filename}: {e}"
|
||||
|
||||
# 3. Supprimer le dossier temporaire du jeu
|
||||
try:
|
||||
import shutil
|
||||
shutil.rmtree(game_folder_path)
|
||||
logger.info(f"PSVita: Dossier temporaire supprimé: {game_folder}")
|
||||
except Exception as e:
|
||||
logger.warning(f"PSVita: Impossible de supprimer {game_folder}: {e}")
|
||||
# Ne pas échouer pour ça, le jeu est quand même installé
|
||||
|
||||
logger.info(f"PSVita: Traitement terminé avec succès - {psvita_filename} créé, {game_id} installé dans ux0/app/")
|
||||
return True, None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"PSVita: Erreur générale: {e}", exc_info=True)
|
||||
return False, f"Erreur PSVita: {str(e)}"
|
||||
|
||||
|
||||
def handle_xbox(dest_dir, iso_files, url=None):
|
||||
"""Gère la conversion des fichiers Xbox extraits et met à jour l'UI (Converting)."""
|
||||
logger.debug(f"Traitement spécifique Xbox dans: {dest_dir}")
|
||||
@@ -1983,8 +2304,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 +2319,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.1.7"
|
||||
"version": "2.3.3.0"
|
||||
}
|
||||
Reference in New Issue
Block a user