Compare commits

...

34 Commits

Author SHA1 Message Date
skymike03
920914b374 v2.4.0.1 (2026.01.07 bis)
- remove windowed mode (useless)
2026-01-07 14:36:00 +01:00
skymike03
a326cb0b67 Merge branch 'main' of https://github.com/RetroGameSets/RGSX 2026-01-07 14:25:18 +01:00
skymike03
c9fdf93221 v2.4.0.0 (2026.01.07)
- add performance mode to disable some 3d effects for a better experience on low-end devices
- multi screen support (choose default screen in Pause>Display menu )
- update windows launcher for retrobat
- ignore some useless warnings
- Merge pull request [#34](https://github.com/RetroGameSets/RGSX/issues/34) from aaronson2012/start-menu-fix
(feat: enable circular navigation for pause menu options)
2026-01-07 14:25:15 +01:00
RGS
184a8c64fe Update troubleshooting steps for app crashes 2026-01-03 16:37:17 +01:00
RGS
9a2e4ce0db Merge pull request #34 from aaronson2012/start-menu-fix
feat: enable circular navigation for pause menu options
2025-12-06 13:06:31 +01:00
Jacob Christie
73eceeb777 feat: enable circular navigation for pause menu options 2025-12-06 05:26:16 -06:00
RGS
2fcc4ca6df Update README to include troubleshooting link
Added a link to the troubleshooting section in the README.
2025-11-25 19:54:03 +01:00
RGS
2ed889540b Enhance troubleshooting section in README
Updated troubleshooting solutions for controls and games visibility issues.
2025-11-25 19:50:20 +01:00
skymike03
e9a610b5dd v2.3.3.3
- Enhance download queue functionality to stop download, continue queue, remove games  and update related UI options
2025-11-25 19:21:12 +01:00
skymike03
bd3b885736 v2.3.3.2
- Fix French print statements for consistency in output messages
- improve filtering  in game list to be permanent, even in search mode, and more efficient for games that have foreign language on other region
2025-11-25 18:40:13 +01:00
skymike03
1592671ddc v2.3.3.1 (2025.11.24)
- correct menu control handling bug
- enhance UI elements for improved user experience
2025-11-24 22:31:53 +01:00
skymike03
4e029aabf1 Remove unnecessary files from RGSX package builds to streamline release process 2025-11-24 13:16:52 +01:00
skymike03
970fcaf197 Update installation instructions for clarity and add manual update notes 2025-11-24 13:14:30 +01:00
skymike03
ff30e6d297 v2.3.3.0 (2025.11.23)
- add a workaround to github update checking
2025-11-23 16:00:38 +01:00
skymike03
5c7fa0484f v2.3.2.9 (2025.11.23)
- Enhance UI with modern effects and improve PSVita game handling (auto extract and create .psvita file for batocera)
- add text file viewer for game txt informations (windows)
2025-11-23 01:25:15 +01:00
skymike03
814861e9ee - add text file viewer for game txt informations 2025-11-21 00:28:46 +01:00
skymike03
56c87ab05f v2.3.2.8 (2025.11.20)
- Improving virtual keyboard navigation when filtering game list (thanks elieserdejesus)
- web interface : Add modal for displaying support messages
- normalize sizes in bytes when not in french
- Refactor control navigation and improve button rendering in UI
2025-11-20 23:19:31 +01:00
skymike03
b12d645fbf Add support modal for displaying formatted support messages 2025-11-20 18:20:12 +01:00
skymike03
04e68adef0 Merge branch 'main' of https://github.com/RetroGameSets/RGSX 2025-11-20 18:02:29 +01:00
skymike03
52f2b960c2 Refactor control navigation and improve button rendering in UI 2025-11-20 18:02:26 +01:00
RGS
1ea604840e Merge pull request #33 from elieserdejesus/main
Improving virtual keyboard navigation when filtering game list to circular navigate
2025-11-20 17:37:59 +01:00
Elieser de Jesus
802696e78f Improving virtual keyboard navigation when filtering game list
The general idea is allow something like "circular buffer" logic when selecting a key in the virtual keyboard.

When the virtual keyboard is displayed:
 - If you are in the first line and press UP jump to last line
 - If you are in the last line and press DOWN jump to first line
 - If you are in the first col and press LEFT jump to last col
 - If you are in the last col and press RIGHT jump to first col
2025-11-20 12:55:30 -03:00
skymike03
6f17173a8c v2.3.2.7 (2025.11.19)
- BETA : add filtering options of games in RGSX main app / synced with options sets on web interface
Filter by Region, hide beta and demos, show only one rom per game and select prefered display order
2025-11-19 23:15:12 +01:00
skymike03
05a8df5933 v2.3.2.6 (2025.19.11)
- add missing translations on web interface
- correct display bug in web interface settings
- correct units showing in french only
- correct save bug spotted in Web settings
2025-11-19 21:26:17 +01:00
skymike03
55231bb823 v2.3.2.5 (2025.11.18)
- bugs in menu solved and display tweak on filter systems
2025-11-18 16:16:41 +01:00
skymike03
d9c1ca6794 Merge branch 'main' of https://github.com/RetroGameSets/RGSX 2025-11-16 17:14:22 +01:00
skymike03
6613b43264 v2.3.2.4
- update README_FR.md with new features and installation instructions.
- Refactor controls.py for improved input handling specially on page up/ down release repeat
2025-11-16 17:14:18 +01:00
RGS
d60dc31291 Add donation link to README
Added a donation link to support the project.
2025-11-16 16:39:56 +01:00
skymike03
ace6ec876f v2.3.2.3
- correct bug when using both keyboard and controller mixed that cause a repeat key holdind
2025-11-16 14:05:21 +01:00
RGS
9f759c1928 Enhance README with platform and interface images
Added images to enhance the README presentation.
2025-11-16 13:39:45 +01:00
skymike03
db287e33d7 v2.3.2.2 (2025.11.16)
- now keyboard works everytime even when a controller is plugged to be able to reconfigure mapping or navigate
2025-11-16 13:11:33 +01:00
skymike03
217392dcd1 v2.3.2.1
- add custom dns service in menu (activate to use custom DNS 1.1.1.1 at boot and avoid download problems)
- add pygame mixer error handling if crash
2025-11-13 22:40:19 +01:00
skymike03
fd9037139c v2.3.2.0 (2025.11.12)
- Enhance download cancellation handling in the display and network modules (when games are on wait list , queue is canceled on application stop
2025-11-12 19:05:25 +01:00
skymike03
c3bbb15c40 v2.3.9.1 (2025.11.10)
- Update readme for news
- Refactor display and control menus to streamline options and improve user experience
2025-11-11 00:59:09 +01:00
27 changed files with 4995 additions and 1649 deletions

View File

@@ -30,16 +30,9 @@ jobs:
zip -r "../../dist/RGSX_update_latest.zip" . \
-x "logs/*" \
"logs/**" \
"images/*" \
"images/**" \
"games/*" \
"games/**" \
"scripts/*" \
"scripts/**" \
"__pycache__/*" \
"__pycache__/**" \
"*.pyc" \
"sources.json" \
"*.log"
cd ../..
@@ -52,16 +45,9 @@ jobs:
zip -r "dist/RGSX_full_latest.zip" ports windows \
-x "ports/RGSX/logs/*" \
"ports/RGSX/logs/**" \
"ports/RGSX/images/*" \
"ports/RGSX/images/**" \
"ports/RGSX/games/*" \
"ports/RGSX/games/**" \
"ports/RGSX/scripts/*" \
"ports/RGSX/scripts/**" \
"ports/RGSX/__pycache__/*" \
"ports/RGSX/__pycache__/**" \
"ports/RGSX/*.pyc" \
"ports/RGSX/sources.json" \
"ports/RGSX/*.log" \
"windows/logs/*" \
"windows/*.xml" \
@@ -88,12 +74,12 @@ jobs:
## 📥 Automatic Installation (Only for batocera Knulli)
### ON PC :
### ON PC , NUC, SteamDeck or any x86_64 based:
1. Open File Manager (F1) then "Applications" and launch xterm
2. Use the command line `curl -L bit.ly/rgsx-install | sh`
3. Launch RGSX from "Ports" menu
### ON RASPBERRY/ARM SBC / HANDHELD :
### ON RASPBERRY/ ARM based SBC / HANDHELD :
1. Connect your device with SSH on a computer/smartphone connected to same network (ssh root@IPADDRESS , pass:linux)
2. Use the command line `curl -L bit.ly/rgsx-install | sh`
3. Launch RGSX from "Ports" menu
@@ -111,17 +97,12 @@ jobs:
3. Launch RGSX from system "Windows"
## 📥 Manual Update (you shouldn't need to do this as RGSX updates automatically on each start)
#### Batocera/Knulli
## 📥 Manual Update (only if automatic doesn't work for some obcure reason)
#### Batocera/Knulli/Retrobat
1. Download latest update : https://github.com/RetroGameSets/RGSX/releases/latest/download/RGSX_update_latest.zip
2. Extract only PORTS folder in `/userdata/roms/`
3. Launch RGSX from the Ports menu
#### Retrobat
1. Download latest update : https://github.com/RetroGameSets/RGSX/releases/latest/download/RGSX_update_latest.zip
2. Extract all folders in your Retrobat\roms folder
3. Launch RGSX from system "Windows"
2. Extract zip content on in `/userdata/roms/ports/RGSX`
3. Launch RGSX
### 📖 Documentation

2
.gitignore vendored
View File

@@ -17,9 +17,9 @@ ports/RGSX.bat
audit_i18n.py
prune_i18n.py
Info.txt
pygame/
# Docker test data
data/
docker-compose.test.yml
config/

378
README.md
View File

@@ -1,241 +1,237 @@
# 🎮 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)** • **[Troubleshoot / Common Errors](https://github.com/RetroGameSets/RGSX#%EF%B8%8F-troubleshooting)** •
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 persystem 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 autocreate 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 commandline 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.
- **Multiselection 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 autoconfigured 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 onscreen virtual keyboard for controllers.
- **Multilanguage 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 onscreen 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 autoimported.
- 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 DPad / 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 multiselection ([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 redownload (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 app, you can try delete /roms/ports/RGSX/assets/controls/xx.json too |
| No games ? | Pause Menu > Games > Update Game Cache |
| Missing systems on the list? | RGSX read es_systems.cfg to show only supported systems, if you want all systems : Pause Menu > Games > Show unsupported systems |
| App crashes | Check `/roms/ports/RGSX/logs/RGSX.log` or `/roms/windows/logs/Retrobat_RGSX_log.txt` |
| Layout change not applied | Restart RGSX after changing layout |
| Downloading BIOS file is ok but you can't download any games? | Activate custom DNS on Pause Menu> Settings and reboot , server can be blocked by your ISP. check any threat/website protection on your router too, especially on ASUS one|
**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**
[![Stargazers over time](https://starchart.cc/RetroGameSets/RGSX.svg?variant=adaptive)](https://starchart.cc/RetroGameSets/RGSX)
**Developed with ❤️ for the retro gaming community.**

View File

@@ -1,251 +0,0 @@
# RGSX CLI — Guide dutilisation
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 laide 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 doptions (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 dun 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 saffichent 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 nest pas actif, un tableau similaire est affiché (sans le prompt) suivi dun conseil.
## Recherche améliorée (multitokens) pour `games`
Loption `--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 lordre (non contiguë) — priorité 1 (écart le plus faible)
3. Tous les tokens présents dans nimporte 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 lordre 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 nest trouvé, seul len-tête est affiché puis un message.
## Prérequis
- Python installé et accessible (le projet utilise un mode headless; aucune fenêtre ne souvrira).
- 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 dune 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 lavertissement dextension non supportée.
- `--interactive | -i`: choisir un titre parmi des correspondances quand aucun exact nest 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 lhistorique.
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 lextension, un avertissement apparaît (utiliser `--force`).
### 4) history — afficher lhistorique
- 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 lhistorique
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
```

View File

@@ -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 doesnt 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
```

View File

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

View File

@@ -1,5 +1,11 @@
import os
import platform
import warnings
# Ignorer le warning de deprecation de pkg_resources dans pygame
warnings.filterwarnings("ignore", category=UserWarning, module="pygame.pkgdata")
warnings.filterwarnings("ignore", message="pkg_resources is deprecated")
# Ne pas forcer SDL_FBDEV ici; si déjà défini par l'environnement, on le garde
try:
if "SDL_FBDEV" in os.environ:
@@ -22,13 +28,13 @@ 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
)
from language import _
from network import test_internet, download_rom, is_1fichier_url, download_from_1fichier, check_for_updates, cancel_all_downloads
from network import test_internet, download_rom, is_1fichier_url, download_from_1fichier, check_for_updates, cancel_all_downloads, download_queue_worker
from controls import handle_controls, validate_menu_state, process_key_repeats, get_emergency_controls
from controls_mapper import map_controls, draw_controls_mapping, get_actions
from controls import load_controls_config
@@ -213,7 +219,7 @@ except Exception:
normalized_names = [n.lower() for n in joystick_names]
if not joystick_names:
joystick_names = ["Clavier"]
print("Aucun joystick détecté, utilisation du clavier par défaut")
print("Aucun joystick detecte, utilisation du clavier par defaut")
logger.debug("Aucun joystick détecté, utilisation du clavier par défaut.")
config.joystick = False
config.keyboard = True
@@ -238,11 +244,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")
@@ -419,9 +426,29 @@ 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()
# Démarrer le worker de la queue de téléchargement
queue_worker_thread = threading.Thread(target=download_queue_worker, daemon=True)
queue_worker_thread.start()
logger.info("Worker de la queue de téléchargement démarré")
running = True
loading_step = "none"
sources = []
@@ -457,7 +484,7 @@ async def main():
config.needs_redraw = True
last_redraw_time = current_time
# Forcer redraw toutes les 100 ms dans history avec téléchargement actif
if config.menu_state == "history" and any(entry["status"] == "Téléchargement" for entry in config.history):
if config.menu_state == "history" and any(entry["status"] in ["Downloading", "Téléchargement"] for entry in config.history):
if current_time - last_redraw_time >= 100:
config.needs_redraw = True
last_redraw_time = current_time
@@ -671,6 +698,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)
@@ -1069,6 +1101,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":
@@ -1089,6 +1127,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)
@@ -1368,7 +1409,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()

View 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

View File

@@ -13,7 +13,7 @@ except Exception:
pygame = None # type: ignore
# Version actuelle de l'application
app_version = "2.3.1.9"
app_version = "2.4.0.1"
def get_application_root():
@@ -133,6 +133,7 @@ logger = logging.getLogger(__name__)
# File d'attente de téléchargements (jobs en attente)
download_queue = [] # Liste de dicts: {url, platform, game_name, ...}
pending_download_is_queue = False # Indique si pending_download doit être ajouté à la queue
# Indique si un téléchargement est en cours
download_active = False
@@ -380,6 +381,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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

282
ports/RGSX/game_filters.py Normal file
View File

@@ -0,0 +1,282 @@
#!/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 - chercher les codes entre parenthèses d'abord
# Codes de région/langue dans les parenthèses (Ex: (Fr,De) ou (En,Nl))
paren_content = re.findall(r'\(([^)]+)\)', name)
for content in paren_content:
# Codes de langue/région séparés par virgules
codes = [c.strip() for c in content.split(',')]
for code in codes:
if code in ['FR', 'FRA']:
if 'France' not in regions:
regions.append('France')
elif code in ['DE', 'GER', 'DEU']:
if 'Germany' not in regions:
regions.append('Germany')
elif code in ['EN', 'ENG'] or code.startswith('EN-'):
# EN peut être USA, Europe ou autre - on vérifie le contexte
if 'EU' in codes or 'EUR' in codes:
if 'Europe' not in regions:
regions.append('Europe')
elif code in ['ES', 'ESP', 'SPA']:
if 'Other' not in regions:
regions.append('Other')
elif code in ['IT', 'ITA']:
if 'Other' not in regions:
regions.append('Other')
elif code in ['NL', 'NLD', 'DU', 'DUT']:
if 'Europe' not in regions:
regions.append('Europe')
elif code in ['PT', 'POR']:
if 'Other' not in regions:
regions.append('Other')
# Patterns de région complets (mots entiers)
if 'USA' in name or 'US)' in name or re.search(r'\bUS\b', name):
if 'USA' not in regions:
regions.append('USA')
if 'CANADA' in name or 'CA)' in name:
if 'Canada' not in regions:
regions.append('Canada')
if 'EUROPE' in name or 'EU)' in name or re.search(r'\bEU\b', name):
if 'Europe' not in regions:
regions.append('Europe')
if 'FRANCE' in name or 'FR)' in name:
if 'France' not in regions:
regions.append('France')
if 'GERMANY' in name or 'DE)' in name or 'GER)' in name:
if 'Germany' not in regions:
regions.append('Germany')
if 'JAPAN' in name or 'JP)' in name or 'JPN)' in name or re.search(r'\bJP\b', name):
if 'Japan' not in regions:
regions.append('Japan')
if 'KOREA' in name or 'KR)' in name or 'KOR)' in name:
if 'Korea' not in regions:
regions.append('Korea')
if 'WORLD' in name:
if 'World' not in regions:
regions.append('World')
# Autres régions
if re.search(r'\b(AUSTRALIA|ASIA|BRAZIL|CHINA|RUSSIA|SCANDINAVIA|'
r'SPAIN|ITALY)\b', name):
if 'Other' not in regions:
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)"""
# Utiliser la fonction de détection de régions pour être cohérent
game_regions = self.get_game_regions(game_name)
# Trouver la meilleure priorité parmi toutes les régions détectées
best_priority = len(self.region_priority) # Par défaut: priorité la plus basse
for region in game_regions:
try:
priority = self.region_priority.index(region)
if priority < best_priority:
best_priority = priority
except ValueError:
# La région n'est pas dans la liste de priorité
continue
return best_priority
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

View File

@@ -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.

View File

@@ -44,6 +44,7 @@
"free_mode_completed": "[Kostenloser Modus] Abgeschlossen: {0}",
"download_status": "{0}: {1}",
"download_canceled": "Download vom Benutzer abgebrochen.",
"download_removed_from_queue": "Aus der Download-Warteschlange entfernt",
"extension_warning_zip": "Die Datei '{0}' ist ein Archiv und Batocera unterstützt keine Archive für dieses System. Die automatische Extraktion der Datei erfolgt nach dem Download, fortfahren?",
"extension_warning_unsupported": "Die Dateierweiterung für '{0}' wird laut der Konfiguration es_systems.cfg von Batocera nicht unterstützt. Möchtest du fortfahren?",
"extension_warning_enable_unknown_hint": "\nUm diese Meldung auszublenden: \"Warnung bei unbekannter Erweiterung ausblenden\" in Pausenmenü > Anzeige aktivieren",
@@ -64,10 +65,21 @@
"menu_accessibility": "Barrierefreiheit",
"menu_display": "Anzeige",
"display_layout": "Anzeigelayout",
"display_monitor": "Monitor",
"display_monitor_single": "Einzelner Monitor",
"display_monitor_single_only": "Nur ein Monitor erkannt",
"display_monitor_restart_required": "Neustart erforderlich um Monitor zu ändern",
"display_mode": "Anzeigemodus",
"display_fullscreen": "Vollbild",
"display_windowed": "Fenster",
"display_mode_restart_required": "Neustart erforderlich für Modusänderung",
"display_light_mode": "Performance-Modus",
"display_light_mode_enabled": "Performance-Modus aktiviert - Effekte deaktiviert",
"display_light_mode_disabled": "Performance-Modus deaktiviert - Effekte aktiviert",
"menu_redownload_cache": "Spieleliste aktualisieren",
"menu_music_enabled": "Musik aktiviert: {0}",
"menu_music_disabled": "Musik deaktiviert",
"menu_restart": "Neustart",
"menu_restart": "RGSX neu starten",
"menu_support": "Unterstützung",
"menu_filter_platforms": "Systeme filtern",
"filter_platforms_title": "Systemsichtbarkeit",
@@ -80,6 +92,7 @@
"menu_allow_unknown_ext_enabled": "Ausblenden der Warnung bei unbekannter Erweiterung aktiviert",
"menu_allow_unknown_ext_disabled": "Ausblenden der Warnung bei unbekannter Erweiterung deaktiviert",
"menu_quit": "Beenden",
"menu_quit_app": "RGSX beenden",
"support_dialog_title": "Support-Datei",
"support_dialog_message": "Eine Support-Datei wurde mit allen Ihren Konfigurations- und Protokolldateien erstellt.\n\nDatei: {0}\n\nUm Hilfe zu erhalten:\n1. Treten Sie dem RGSX Discord-Server bei\n2. Beschreiben Sie Ihr Problem\n3. Teilen Sie diese ZIP-Datei\n\nDrücken Sie {1}, um zum Menü zurückzukehren.",
"support_dialog_error": "Fehler beim Erstellen der Support-Datei:\n{0}\n\nDrücken Sie {1}, um zum Menü zurückzukehren.",
@@ -184,7 +197,9 @@
"instruction_pause_settings": "Musik, Symlink-Option & API-Schlüsselstatus",
"instruction_pause_restart": "RGSX neu starten um Konfiguration neu zu laden",
"instruction_pause_support": "Eine Diagnose-ZIP-Datei für den Support erstellen",
"instruction_pause_quit": "RGSX Anwendung beenden",
"instruction_pause_quit": "Menü für Beenden oder Neustart aufrufen",
"instruction_quit_app": "RGSX Anwendung beenden",
"instruction_quit_restart": "RGSX Anwendung neu starten",
"instruction_controls_help": "Komplette Referenz für Controller & Tastatur anzeigen",
"instruction_controls_remap": "Tasten / Buttons neu zuordnen",
"instruction_generic_back": "Zum vorherigen Menü zurückkehren",
@@ -192,6 +207,9 @@
"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_monitor": "Monitor für RGSX-Anzeige auswählen",
"instruction_display_mode": "Zwischen Vollbild und Fenstermodus wechseln",
"instruction_display_light_mode": "Performance-Modus für bessere FPS aktivieren",
"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",
"instruction_display_hide_premium": "Systeme ausblenden, die Premiumzugang erfordern über API: {providers}",
@@ -203,6 +221,7 @@
"instruction_settings_symlink": "Verwendung von Symlinks für Installationen umschalten",
"instruction_settings_api_keys": "Gefundene Premium-API-Schlüssel ansehen",
"instruction_settings_web_service": "Web-Dienst Autostart beim Booten aktivieren/deaktivieren",
"instruction_settings_custom_dns": "Custom DNS (Cloudflare 1.1.1.1) beim Booten aktivieren/deaktivieren",
"settings_web_service": "Web-Dienst beim Booten",
"settings_web_service_enabled": "Aktiviert",
"settings_web_service_disabled": "Deaktiviert",
@@ -211,6 +230,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 ↑",
@@ -234,7 +260,10 @@
"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_remove_from_queue": "Aus Warteschlange entfernen",
"history_option_cancel_download": "Download abbrechen",
"history_option_delete_game": "Spiel löschen",
"history_option_error_info": "Fehlerdetails",
"history_option_retry": "Download wiederholen",
@@ -331,7 +360,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}",
@@ -359,10 +388,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",
"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",
"accessibility_footer_font_size": "Fußzeilen-Schriftgröße: {0}",
"popup_layout_changed_restart": "Layout geändert auf {0}x{1}. Bitte starten Sie die App neu."
"filter_active": "Filter aktiv",
"filter_games_shown": "{0} Spiel(e) angezeigt"
}

View File

@@ -44,6 +44,7 @@
"free_mode_completed": "[Free mode] Completed: {0}",
"download_status": "{0}: {1}",
"download_canceled": "Download canceled by user.",
"download_removed_from_queue": "Removed from download queue",
"extension_warning_zip": "The file '{0}' is an archive and Batocera does not support archives for this system. Automatic extraction will occur after download, continue?",
"extension_warning_unsupported": "The file extension for '{0}' is not supported by Batocera according to the es_systems.cfg configuration. Do you want to continue?",
"extension_warning_enable_unknown_hint": "\nTo hide this message: enable \"Hide unknown extension warning\" in Pause Menu > Display",
@@ -64,10 +65,21 @@
"menu_accessibility": "Accessibility",
"menu_display": "Display",
"display_layout": "Display layout",
"display_monitor": "Monitor",
"display_monitor_single": "Single monitor",
"display_monitor_single_only": "Only one monitor detected",
"display_monitor_restart_required": "Restart required to apply monitor change",
"display_mode": "Screen mode",
"display_fullscreen": "Fullscreen",
"display_windowed": "Windowed",
"display_mode_restart_required": "Restart required to apply screen mode",
"display_light_mode": "Performance mode",
"display_light_mode_enabled": "Performance mode enabled - effects disabled",
"display_light_mode_disabled": "Performance mode disabled - effects enabled",
"menu_redownload_cache": "Update games list",
"menu_music_enabled": "Music enabled: {0}",
"menu_music_disabled": "Music disabled",
"menu_restart": "Restart",
"menu_restart": "Restart RGSX",
"menu_filter_platforms": "Filter systems",
"filter_platforms_title": "Systems visibility",
"filter_platforms_info": "Visible: {0} | Hidden: {1} / Total: {2}",
@@ -80,6 +92,7 @@
"menu_allow_unknown_ext_disabled": "Hide unknown extension warning disabled",
"menu_support": "Support",
"menu_quit": "Quit",
"menu_quit_app": "Quit RGSX",
"button_yes": "Yes",
"button_no": "No",
"button_OK": "OK",
@@ -186,7 +199,9 @@
"instruction_pause_settings": "Music, symlink option & API keys status",
"instruction_pause_restart": "Restart RGSX to reload configuration",
"instruction_pause_support": "Generate a diagnostic ZIP file for support",
"instruction_pause_quit": "Exit the RGSX application",
"instruction_pause_quit": "Access menu to quit or restart",
"instruction_quit_app": "Exit the RGSX application",
"instruction_quit_restart": "Restart the RGSX application",
"instruction_controls_help": "Show full controller & keyboard reference",
"instruction_controls_remap": "Change button / key bindings",
"instruction_generic_back": "Return to the previous menu",
@@ -194,6 +209,9 @@
"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_monitor": "Select which monitor to display RGSX on",
"instruction_display_mode": "Toggle between fullscreen and windowed mode",
"instruction_display_light_mode": "Enable performance mode for better FPS on low-end devices",
"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",
"instruction_display_hide_premium": "Hide systems requiring premium access via API: {providers}",
@@ -205,6 +223,7 @@
"instruction_settings_symlink": "Toggle using filesystem symlinks for installs",
"instruction_settings_api_keys": "See detected premium provider API keys",
"instruction_settings_web_service": "Enable/disable web service auto-start at boot",
"instruction_settings_custom_dns": "Enable/disable custom DNS (Cloudflare 1.1.1.1) at boot",
"settings_web_service": "Web Service at Boot",
"settings_web_service_enabled": "Enabled",
"settings_web_service_disabled": "Disabled",
@@ -213,6 +232,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 ↑",
@@ -236,7 +262,10 @@
"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_remove_from_queue": "Remove from queue",
"history_option_cancel_download": "Cancel download",
"history_option_delete_game": "Delete game",
"history_option_error_info": "Error details",
"history_option_retry": "Retry download",
@@ -333,7 +362,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}",
@@ -359,10 +388,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."
"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"
}

View File

@@ -44,6 +44,7 @@
"free_mode_completed": "[Modo gratuito] Completado: {0}",
"download_status": "{0}: {1}",
"download_canceled": "Descarga cancelada por el usuario.",
"download_removed_from_queue": "Eliminado de la cola de descarga",
"extension_warning_zip": "El archivo '{0}' es un archivo comprimido y Batocera no soporta archivos comprimidos para este sistema. La extracción automática del archivo se realizará después de la descarga, ¿continuar?",
"extension_warning_unsupported": "La extensión del archivo '{0}' no está soportada por Batocera según la configuración es_systems.cfg. ¿Deseas continuar?",
"extension_warning_enable_unknown_hint": "\nPara no mostrar este mensaje: activa \"Ocultar aviso de extensión desconocida\" en Menú de pausa > Pantalla",
@@ -64,10 +65,21 @@
"menu_accessibility": "Accesibilidad",
"menu_display": "Pantalla",
"display_layout": "Distribución",
"display_monitor": "Monitor",
"display_monitor_single": "Monitor único",
"display_monitor_single_only": "Solo un monitor detectado",
"display_monitor_restart_required": "Reinicio necesario para cambiar de monitor",
"display_mode": "Modo de pantalla",
"display_fullscreen": "Pantalla completa",
"display_windowed": "Ventana",
"display_mode_restart_required": "Reinicio necesario para cambiar el modo",
"display_light_mode": "Modo rendimiento",
"display_light_mode_enabled": "Modo rendimiento activado - efectos desactivados",
"display_light_mode_disabled": "Modo rendimiento desactivado - efectos activados",
"menu_redownload_cache": "Actualizar lista de juegos",
"menu_music_enabled": "Música activada: {0}",
"menu_music_disabled": "Música desactivada",
"menu_restart": "Reiniciar",
"menu_restart": "Reiniciar RGSX",
"menu_support": "Soporte",
"menu_filter_platforms": "Filtrar sistemas",
"filter_platforms_title": "Visibilidad de sistemas",
@@ -80,6 +92,7 @@
"menu_allow_unknown_ext_enabled": "Aviso de extensión desconocida oculto (activado)",
"menu_allow_unknown_ext_disabled": "Aviso de extensión desconocida visible (desactivado)",
"menu_quit": "Salir",
"menu_quit_app": "Salir de RGSX",
"support_dialog_title": "Archivo de soporte",
"support_dialog_message": "Se ha creado un archivo de soporte con todos sus archivos de configuración y registros.\n\nArchivo: {0}\n\nPara obtener ayuda:\n1. Únete al servidor Discord de RGSX\n2. Describe tu problema\n3. Comparte este archivo ZIP\n\nPresiona {1} para volver al menú.",
"support_dialog_error": "Error al generar el archivo de soporte:\n{0}\n\nPresiona {1} para volver al menú.",
@@ -186,7 +199,9 @@
"instruction_pause_settings": "Música, opción symlink y estado de claves API",
"instruction_pause_restart": "Reiniciar RGSX para recargar configuración",
"instruction_pause_support": "Generar un archivo ZIP de diagnóstico para soporte",
"instruction_pause_quit": "Salir de la aplicación RGSX",
"instruction_pause_quit": "Acceder al menú para salir o reiniciar",
"instruction_quit_app": "Salir de la aplicación RGSX",
"instruction_quit_restart": "Reiniciar la aplicación RGSX",
"instruction_controls_help": "Mostrar referencia completa de mando y teclado",
"instruction_controls_remap": "Cambiar asignación de botones / teclas",
"instruction_generic_back": "Volver al menú anterior",
@@ -194,6 +209,9 @@
"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_monitor": "Seleccionar monitor para mostrar RGSX",
"instruction_display_mode": "Alternar entre pantalla completa y ventana",
"instruction_display_light_mode": "Activar modo rendimiento para mejores FPS",
"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",
"instruction_display_hide_premium": "Ocultar sistemas que requieren acceso premium vía API: {providers}",
@@ -205,6 +223,7 @@
"instruction_settings_symlink": "Alternar uso de symlinks en instalaciones",
"instruction_settings_api_keys": "Ver claves API premium detectadas",
"instruction_settings_web_service": "Activar/desactivar inicio automático del servicio web",
"instruction_settings_custom_dns": "Activar/desactivar DNS personalizado (Cloudflare 1.1.1.1) al inicio",
"settings_web_service": "Servicio Web al Inicio",
"settings_web_service_enabled": "Activado",
"settings_web_service_disabled": "Desactivado",
@@ -213,6 +232,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 ↑",
@@ -236,7 +262,10 @@
"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_remove_from_queue": "Quitar de la cola",
"history_option_cancel_download": "Cancelar descarga",
"history_option_delete_game": "Eliminar juego",
"history_option_error_info": "Detalles del error",
"history_option_retry": "Reintentar descarga",
@@ -333,7 +362,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}",
@@ -362,7 +391,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."
"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)"
}

View File

@@ -44,6 +44,7 @@
"free_mode_completed": "[Mode gratuit] Terminé: {0}",
"download_status": "{0} : {1}",
"download_canceled": "Téléchargement annulé par l'utilisateur.",
"download_removed_from_queue": "Retiré de la file de téléchargement",
"extension_warning_zip": "Le fichier '{0}' est une archive et Batocera ne prend pas en charge les archives pour ce système. L'extraction automatique du fichier aura lieu après le téléchargement, continuer ?",
"extension_warning_unsupported": "L'extension du fichier '{0}' n'est pas supportée par Batocera d'après la configuration es_systems.cfg. Voulez-vous continuer ?",
"extension_warning_enable_unknown_hint": "\nPour ne plus afficher ce messager : Activer l'option \"Masquer avertissement\" dans le Menu Pause>Display",
@@ -64,12 +65,24 @@
"menu_accessibility": "Accessibilité",
"menu_display": "Affichage",
"display_layout": "Disposition",
"display_monitor": "Écran",
"display_monitor_single": "Écran unique",
"display_monitor_single_only": "Un seul écran détecté",
"display_monitor_restart_required": "Redémarrage requis pour changer d'écran",
"display_mode": "Mode d'affichage",
"display_fullscreen": "Plein écran",
"display_windowed": "Fenêtré",
"display_mode_restart_required": "Redémarrage requis pour changer le mode",
"display_light_mode": "Mode performance",
"display_light_mode_enabled": "Mode performance activé - effets désactivés",
"display_light_mode_disabled": "Mode performance désactivé - effets activés",
"menu_redownload_cache": "Mettre à jour la liste des jeux",
"menu_support": "Support",
"menu_quit": "Quitter",
"menu_quit_app": "Quitter RGSX",
"menu_music_enabled": "Musique activée : {0}",
"menu_music_disabled": "Musique désactivée",
"menu_restart": "Redémarrer",
"menu_restart": "Redémarrer RGSX",
"menu_filter_platforms": "Filtrer les systèmes",
"filter_platforms_title": "Affichage des systèmes",
"filter_platforms_info": "Visibles: {0} | Masqués: {1} / Total: {2}",
@@ -186,7 +199,9 @@
"instruction_pause_settings": "Musique, option symlink & statut des clés API",
"instruction_pause_restart": "Redémarrer RGSX pour recharger la configuration",
"instruction_pause_support": "Générer un fichier ZIP de diagnostic pour l'assistance",
"instruction_pause_quit": "Quitter l'application RGSX",
"instruction_pause_quit": "Accéder au menu pour quitter ou redémarrer",
"instruction_quit_app": "Quitter l'application RGSX",
"instruction_quit_restart": "Redémarrer l'application RGSX",
"instruction_controls_help": "Afficher la référence complète manette & clavier",
"instruction_controls_remap": "Modifier l'association boutons / touches",
"instruction_generic_back": "Revenir au menu précédent",
@@ -194,6 +209,9 @@
"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_monitor": "Sélectionner l'écran pour afficher RGSX",
"instruction_display_mode": "Basculer entre plein écran et fenêtré",
"instruction_display_light_mode": "Activer le mode performance pour de meilleurs FPS",
"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",
"instruction_display_hide_premium": "Masquer les systèmes nécessitant un accès premium via API: {providers}",
@@ -205,6 +223,7 @@
"instruction_settings_symlink": "Basculer l'utilisation de symlinks pour l'installation",
"instruction_settings_api_keys": "Voir les clés API détectées des services premium",
"instruction_settings_web_service": "Activer/désactiver le démarrage automatique du service web",
"instruction_settings_custom_dns": "Activer/désactiver les DNS personnalisés (Cloudflare 1.1.1.1) au démarrage",
"settings_web_service": "Service Web au démarrage",
"settings_web_service_enabled": "Activé",
"settings_web_service_disabled": "Désactivé",
@@ -213,6 +232,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 ↑",
@@ -236,10 +262,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é",
@@ -333,7 +360,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}",
@@ -362,7 +389,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."
"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)"
}

View File

@@ -44,6 +44,7 @@
"free_mode_completed": "[Modalità gratuita] Completato: {0}",
"download_status": "{0}: {1}",
"download_canceled": "Download annullato dall'utente.",
"download_removed_from_queue": "Rimosso dalla coda di download",
"extension_warning_zip": "Il file '{0}' è un archivio e Batocera non supporta archivi per questo sistema. L'estrazione automatica avverrà dopo il download, continuare?",
"extension_warning_unsupported": "L'estensione del file '{0}' non è supportata da Batocera secondo la configurazione di es_systems.cfg. Vuoi continuare?",
"extension_warning_enable_unknown_hint": "\nPer non visualizzare questo messaggio: abilita \"Nascondi avviso estensione sconosciuta\" in Menu Pausa > Schermo",
@@ -64,10 +65,19 @@
"menu_accessibility": "Accessibilità",
"menu_display": "Schermo",
"display_layout": "Layout schermo",
"menu_redownload_cache": "Aggiorna elenco giochi",
"display_monitor": "Monitor",
"display_monitor_single": "Monitor singolo",
"display_monitor_single_only": "Rilevato un solo monitor",
"display_monitor_restart_required": "Riavvio necessario per cambiare monitor",
"display_mode": "Modalità schermo",
"display_fullscreen": "Schermo intero",
"display_windowed": "Finestra",
"display_mode_restart_required": "Riavvio necessario per cambiare modalità", "display_light_mode": "Modalità performance",
"display_light_mode_enabled": "Modalità performance attivata - effetti disattivati",
"display_light_mode_disabled": "Modalità performance disattivata - effetti attivati", "menu_redownload_cache": "Aggiorna elenco giochi",
"menu_music_enabled": "Musica attivata: {0}",
"menu_music_disabled": "Musica disattivata",
"menu_restart": "Riavvia",
"menu_restart": "Riavvia RGSX",
"menu_support": "Supporto",
"menu_filter_platforms": "Filtra sistemi",
"filter_platforms_title": "Visibilità sistemi",
@@ -80,6 +90,7 @@
"menu_allow_unknown_ext_enabled": "Nascondi avviso estensione sconosciuta abilitato",
"menu_allow_unknown_ext_disabled": "Nascondi avviso estensione sconosciuta disabilitato",
"menu_quit": "Esci",
"menu_quit_app": "Esci da RGSX",
"support_dialog_title": "File di supporto",
"support_dialog_message": "È stato creato un file di supporto con tutti i file di configurazione e di registro.\n\nFile: {0}\n\nPer ottenere aiuto:\n1. Unisciti al server Discord RGSX\n2. Descrivi il tuo problema\n3. Condividi questo file ZIP\n\nPremi {1} per tornare al menu.",
"support_dialog_error": "Errore durante la generazione del file di supporto:\n{0}\n\nPremi {1} per tornare al menu.",
@@ -183,7 +194,9 @@
"instruction_pause_settings": "Musica, opzione symlink e stato chiavi API",
"instruction_pause_restart": "Riavvia RGSX per ricaricare la configurazione",
"instruction_pause_support": "Genera un file ZIP diagnostico per il supporto",
"instruction_pause_quit": "Uscire dall'applicazione RGSX",
"instruction_pause_quit": "Accedere al menu per uscire o riavviare",
"instruction_quit_app": "Uscire dall'applicazione RGSX",
"instruction_quit_restart": "Riavviare l'applicazione RGSX",
"instruction_controls_help": "Mostrare riferimento completo controller & tastiera",
"instruction_controls_remap": "Modificare associazione pulsanti / tasti",
"instruction_generic_back": "Tornare al menu precedente",
@@ -191,6 +204,9 @@
"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_monitor": "Selezionare monitor per visualizzare RGSX",
"instruction_display_mode": "Alternare tra schermo intero e finestra",
"instruction_display_light_mode": "Attivare modalità performance per FPS migliori",
"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",
"instruction_display_hide_premium": "Nascondere sistemi che richiedono accesso premium via API: {providers}",
@@ -202,6 +218,7 @@
"instruction_settings_symlink": "Abilitare/disabilitare uso symlink per installazioni",
"instruction_settings_api_keys": "Mostrare chiavi API premium rilevate",
"instruction_settings_web_service": "Attivare/disattivare avvio automatico servizio web all'avvio",
"instruction_settings_custom_dns": "Attivare/disattivare DNS personalizzato (Cloudflare 1.1.1.1) all'avvio",
"settings_web_service": "Servizio Web all'Avvio",
"settings_web_service_enabled": "Abilitato",
"settings_web_service_disabled": "Disabilitato",
@@ -210,6 +227,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 ↑",
@@ -233,6 +257,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",
@@ -330,7 +355,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}",
@@ -362,7 +387,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 in {0}x{1}. Riavviare l'app per applicare."
"popup_layout_changed_restart": "Layout cambiato a {0}x{1}. Riavvia l'app per applicare.",
"web_started": "Avviato",
"web_downloading": "Download",
"web_in_progress": "In corso",
"web_added_to_queue": "aggiunto alla coda",
"web_download_success": "scaricato con successo!",
"web_download_error_for": "Errore durante il download di",
"web_already_present": "era già presente",
"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"
}

View File

@@ -44,6 +44,7 @@
"free_mode_completed": "[Modo gratuito] Concluído: {0}",
"download_status": "{0}: {1}",
"download_canceled": "Download cancelado pelo usuário.",
"download_removed_from_queue": "Removido da fila de download",
"extension_warning_zip": "O arquivo '{0}' é um arquivo compactado e o Batocera não suporta arquivos compactados para este sistema. A extração automática ocorrerá após o download, continuar?",
"extension_warning_unsupported": "A extensão do arquivo '{0}' não é suportada pelo Batocera segundo a configuração es_systems.cfg. Deseja continuar?",
"extension_warning_enable_unknown_hint": "\nPara não ver esta mensagem: ative \"Ocultar aviso de extensão desconhecida\" em Menu de Pausa > Exibição",
@@ -64,10 +65,21 @@
"menu_accessibility": "Acessibilidade",
"menu_display": "Exibição",
"display_layout": "Layout de exibição",
"display_monitor": "Monitor",
"display_monitor_single": "Monitor único",
"display_monitor_single_only": "Apenas um monitor detectado",
"display_monitor_restart_required": "Reinício necessário para mudar de monitor",
"display_mode": "Modo de tela",
"display_fullscreen": "Tela cheia",
"display_windowed": "Janela",
"display_mode_restart_required": "Reinício necessário para mudar o modo",
"display_light_mode": "Modo performance",
"display_light_mode_enabled": "Modo performance ativado - efeitos desativados",
"display_light_mode_disabled": "Modo performance desativado - efeitos ativados",
"menu_redownload_cache": "Atualizar lista de jogos",
"menu_music_enabled": "Música ativada: {0}",
"menu_music_disabled": "Música desativada",
"menu_restart": "Reiniciar",
"menu_restart": "Reiniciar RGSX",
"menu_support": "Suporte",
"menu_filter_platforms": "Filtrar sistemas",
"filter_platforms_title": "Visibilidade dos sistemas",
@@ -80,6 +92,7 @@
"menu_allow_unknown_ext_enabled": "Aviso de extensão desconhecida oculto (ativado)",
"menu_allow_unknown_ext_disabled": "Aviso de extensão desconhecida visível (desativado)",
"menu_quit": "Sair",
"menu_quit_app": "Sair do RGSX",
"support_dialog_title": "Arquivo de suporte",
"support_dialog_message": "Foi criado um arquivo de suporte com todos os seus arquivos de configuração e logs.\n\nArquivo: {0}\n\nPara obter ajuda:\n1. Junte-se ao servidor Discord RGSX\n2. Descreva seu problema\n3. Compartilhe este arquivo ZIP\n\nPressione {1} para voltar ao menu.",
"support_dialog_error": "Erro ao gerar o arquivo de suporte:\n{0}\n\nPressione {1} para voltar ao menu.",
@@ -185,7 +198,9 @@
"instruction_pause_settings": "Música, opção symlink e status das chaves API",
"instruction_pause_restart": "Reiniciar RGSX para recarregar configuração",
"instruction_pause_support": "Gerar um arquivo ZIP de diagnóstico para suporte",
"instruction_pause_quit": "Sair da aplicação RGSX",
"instruction_pause_quit": "Acessar menu para sair ou reiniciar",
"instruction_quit_app": "Sair da aplicação RGSX",
"instruction_quit_restart": "Reiniciar a aplicação RGSX",
"instruction_controls_help": "Mostrar referência completa de controle e teclado",
"instruction_controls_remap": "Modificar associação de botões / teclas",
"instruction_generic_back": "Voltar ao menu anterior",
@@ -193,6 +208,9 @@
"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_monitor": "Selecionar monitor para exibir RGSX",
"instruction_display_mode": "Alternar entre tela cheia e janela",
"instruction_display_light_mode": "Ativar modo performance para melhor FPS",
"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",
"instruction_display_hide_premium": "Ocultar sistemas que exigem acesso premium via API: {providers}",
@@ -204,6 +222,7 @@
"instruction_settings_symlink": "Ativar/desativar uso de symlinks para instalações",
"instruction_settings_api_keys": "Ver chaves API premium detectadas",
"instruction_settings_web_service": "Ativar/desativar início automático do serviço web na inicialização",
"instruction_settings_custom_dns": "Ativar/desativar DNS personalizado (Cloudflare 1.1.1.1) na inicialização",
"settings_web_service": "Serviço Web na Inicialização",
"settings_web_service_enabled": "Ativado",
"settings_web_service_disabled": "Desativado",
@@ -212,6 +231,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 ↑",
@@ -235,7 +261,10 @@
"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_remove_from_queue": "Remover da fila",
"history_option_cancel_download": "Cancelar download",
"history_option_delete_game": "Excluir jogo",
"history_option_error_info": "Detalhes do erro",
"history_option_retry": "Tentar novamente",
@@ -332,7 +361,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}",
@@ -362,7 +391,41 @@
"filter_all": "Marcar tudo",
"filter_none": "Desmarcar tudo",
"filter_apply": "Aplicar filtro",
"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",
"accessibility_footer_font_size": "Tamanho fonte rodapé: {0}",
"popup_layout_changed_restart": "Layout alterado para {0}x{1}. Reinicie o app para aplicar."
"filter_active": "Filtro ativo",
"filter_games_shown": "{0} jogo(s) exibido(s)"
}

View File

@@ -453,8 +453,102 @@ async def check_for_updates():
config.loading_progress = 5.0
config.needs_redraw = True
response = requests.get(OTA_VERSION_ENDPOINT, timeout=5)
response.raise_for_status()
# Liste des endpoints à essayer (GitHub principal, puis fallback)
endpoints = [
OTA_VERSION_ENDPOINT,
"https://retrogamesets.fr/softs/version.json"
]
response = None
last_error = None
for endpoint_index, endpoint in enumerate(endpoints):
is_fallback = endpoint_index > 0
if is_fallback:
logger.info(f"Tentative sur endpoint de secours : {endpoint}")
# Gestion des erreurs de rate limit GitHub (429) avec retry
max_retries = 3 if not is_fallback else 1 # Moins de retries sur fallback
retry_count = 0
while retry_count < max_retries:
try:
response = requests.get(endpoint, timeout=10)
# Gestion spécifique des erreurs 429 (Too Many Requests) - surtout pour GitHub
if response.status_code == 429:
retry_after = response.headers.get('retry-after')
x_ratelimit_remaining = response.headers.get('x-ratelimit-remaining', '1')
x_ratelimit_reset = response.headers.get('x-ratelimit-reset')
if retry_after:
# En-tête retry-after présent : attendre le nombre de secondes spécifié
wait_time = int(retry_after)
logger.warning(f"Rate limit atteint (429) sur {endpoint}. Attente de {wait_time}s (retry-after header)")
elif x_ratelimit_remaining == '0' and x_ratelimit_reset:
# x-ratelimit-remaining est 0 : attendre jusqu'à x-ratelimit-reset
import time
reset_time = int(x_ratelimit_reset)
current_time = int(time.time())
wait_time = max(reset_time - current_time, 60) # Minimum 60s
logger.warning(f"Rate limit atteint (429) sur {endpoint}. Attente de {wait_time}s (x-ratelimit-reset)")
else:
# Pas d'en-têtes spécifiques : attendre au moins 60s
wait_time = 60
logger.warning(f"Rate limit atteint (429) sur {endpoint}. Attente de {wait_time}s par défaut")
if retry_count < max_retries - 1:
logger.info(f"Nouvelle tentative dans {wait_time}s... ({retry_count + 1}/{max_retries})")
await asyncio.sleep(wait_time)
retry_count += 1
continue
else:
# Si rate limit persistant et qu'on est sur GitHub, essayer le fallback
if not is_fallback:
logger.warning(f"Rate limit GitHub persistant, passage au serveur de secours")
break # Sortir de la boucle retry pour essayer le prochain endpoint
raise requests.exceptions.HTTPError(
f"Limite de débit atteinte (429). Veuillez réessayer plus tard."
)
response.raise_for_status()
# Succès, sortir de toutes les boucles
logger.debug(f"Version récupérée avec succès depuis : {endpoint}")
break
except requests.exceptions.HTTPError as e:
last_error = e
if response and response.status_code == 429:
# 429 géré au-dessus, continuer la boucle ou passer au fallback
retry_count += 1
if retry_count >= max_retries:
break # Passer au prochain endpoint
else:
# Erreur HTTP autre que 429
logger.warning(f"Erreur HTTP {response.status_code if response else 'inconnue'} sur {endpoint}")
break # Passer au prochain endpoint
except requests.exceptions.RequestException as e:
last_error = e
if retry_count < max_retries - 1:
# Erreur réseau, réessayer avec backoff exponentiel
wait_time = 2 ** retry_count # 1s, 2s, 4s
logger.warning(f"Erreur réseau sur {endpoint}. Nouvelle tentative dans {wait_time}s...")
await asyncio.sleep(wait_time)
retry_count += 1
else:
logger.warning(f"Erreur réseau persistante sur {endpoint} : {e}")
break # Passer au prochain endpoint
# Si on a une réponse valide, sortir de la boucle des endpoints
if response and response.status_code == 200:
break
# Si aucun endpoint n'a fonctionné
if not response or response.status_code != 200:
raise last_error if last_error else requests.exceptions.RequestException(
"Impossible de vérifier les mises à jour sur tous les serveurs"
)
# Accepter différents content-types (application/json, text/plain, text/html)
content_type = response.headers.get("content-type", "")
@@ -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}")
@@ -814,6 +925,16 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
logger.info(f"Le fichier {dest_path} existe déjà et la taille est correcte, téléchargement ignoré")
result[0] = True
result[1] = _("network_download_ok").format(game_name) + _("download_already_present")
# Mettre à jour l'historique
for entry in config.history:
if entry.get("url") == url:
entry["status"] = "Download_OK"
entry["progress"] = 100
entry["message"] = result[1]
save_history(config.history)
break
# Afficher un toast au lieu d'ouvrir l'historique
try:
show_toast(result[1])
@@ -822,6 +943,13 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
with urls_lock:
urls_in_progress.discard(url)
logger.debug(f"URL supprimée du set des téléchargements en cours: {url} (URLs restantes: {len(urls_in_progress)})")
# Libérer le slot de la queue
try:
notify_download_finished()
except Exception:
pass
return result[0], result[1]
file_found = True
@@ -864,6 +992,16 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
logger.info(f"Un fichier avec le même nom de base existe déjà: {existing_path}, téléchargement ignoré")
result[0] = True
result[1] = _("network_download_ok").format(game_name) + _("download_already_extracted")
# Mettre à jour l'historique
for entry in config.history:
if entry.get("url") == url:
entry["status"] = "Download_OK"
entry["progress"] = 100
entry["message"] = result[1]
save_history(config.history)
break
# Afficher un toast au lieu d'ouvrir l'historique
try:
show_toast(result[1])
@@ -872,6 +1010,13 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
with urls_lock:
urls_in_progress.discard(url)
logger.debug(f"URL supprimée du set des téléchargements en cours: {url} (URLs restantes: {len(urls_in_progress)})")
# Libérer le slot de la queue
try:
notify_download_finished()
except Exception:
pass
return result[0], result[1]
except Exception as e:
logger.debug(f"Erreur lors de la vérification des fichiers existants: {e}")
@@ -1055,14 +1200,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 +1242,12 @@ 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:
# Libérer le slot de la queue
try:
notify_download_finished()
except Exception:
pass
return
os.chmod(dest_path, 0o644)
@@ -1319,6 +1469,12 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
if url in url_done_events:
url_done_events[url].set()
# Libérer le slot de la queue
try:
notify_download_finished()
except Exception:
pass
return result[0], result[1]
async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=False, task_id=None):
@@ -2077,7 +2233,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 +2241,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 +2277,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

View File

@@ -29,7 +29,7 @@ def delete_old_files():
try:
if os.path.exists(file_path):
os.remove(file_path)
print(f"Ancien fichier supprimé : {file_path}")
print(f"Ancien fichier supprime : {file_path}")
logger.info(f"Ancien fichier supprimé : {file_path}")
except Exception as e:
print(f"Erreur lors de la suppression de {file_path} : {str(e)}")
@@ -39,7 +39,7 @@ def delete_old_files():
try:
if os.path.exists(file_path):
os.remove(file_path)
print(f"Ancien fichier supprimé : {file_path}")
print(f"Ancien fichier supprime : {file_path}")
logger.info(f"Ancien fichier supprimé : {file_path}")
except Exception as e:
print(f"Erreur lors de la suppression de {file_path} : {str(e)}")
@@ -49,6 +49,8 @@ def load_rgsx_settings():
"""Charge tous les paramètres depuis rgsx_settings.json."""
from config import RGSX_SETTINGS_PATH
logger.debug(f"Chargement des settings depuis: {RGSX_SETTINGS_PATH}")
default_settings = {
"language": "en",
"music_enabled": True,
@@ -58,7 +60,10 @@ def load_rgsx_settings():
},
"display": {
"grid": "3x4",
"font_family": "pixel"
"font_family": "pixel",
"monitor": 0,
"fullscreen": True,
"light_mode": False
},
"symlink": {
"enabled": False,
@@ -78,13 +83,17 @@ def load_rgsx_settings():
if os.path.exists(RGSX_SETTINGS_PATH):
with open(RGSX_SETTINGS_PATH, 'r', encoding='utf-8') as f:
settings = json.load(f)
logger.debug(f"Settings JSON chargé: display={settings.get('display', {})}")
# Fusionner avec les valeurs par défaut pour assurer la compatibilité
for key, value in default_settings.items():
if key not in settings:
settings[key] = value
return settings
else:
logger.warning(f"Fichier settings non trouvé: {RGSX_SETTINGS_PATH}")
except Exception as e:
print(f"Erreur lors du chargement de rgsx_settings.json: {str(e)}")
logger.error(f"Erreur chargement settings: {e}")
return default_settings
@@ -307,6 +316,92 @@ def set_display_grid(cols: int, rows: int):
save_rgsx_settings(settings)
return cols, rows
# ----------------------- Monitor/Display settings ----------------------- #
def get_display_monitor(settings=None):
"""Retourne l'index du moniteur configuré (par défaut 0 = principal)."""
if settings is None:
settings = load_rgsx_settings()
return settings.get("display", {}).get("monitor", 0)
def set_display_monitor(monitor_index: int):
"""Définit et sauvegarde l'index du moniteur à utiliser."""
settings = load_rgsx_settings()
disp = settings.setdefault("display", {})
disp["monitor"] = max(0, int(monitor_index))
save_rgsx_settings(settings)
return disp["monitor"]
def get_display_fullscreen(settings=None):
"""Retourne True si le mode plein écran est activé."""
if settings is None:
settings = load_rgsx_settings()
return settings.get("display", {}).get("fullscreen", True)
def set_display_fullscreen(fullscreen: bool):
"""Définit et sauvegarde le mode plein écran."""
settings = load_rgsx_settings()
disp = settings.setdefault("display", {})
disp["fullscreen"] = bool(fullscreen)
save_rgsx_settings(settings)
return disp["fullscreen"]
def get_light_mode(settings=None):
"""Retourne True si le mode léger (performance) est activé."""
if settings is None:
settings = load_rgsx_settings()
return settings.get("display", {}).get("light_mode", False)
def set_light_mode(enabled: bool):
"""Définit et sauvegarde le mode léger (performance)."""
settings = load_rgsx_settings()
disp = settings.setdefault("display", {})
disp["light_mode"] = bool(enabled)
save_rgsx_settings(settings)
return disp["light_mode"]
def get_available_monitors():
"""Retourne la liste des moniteurs disponibles avec leurs informations.
Compatible Windows, Linux (Batocera), et autres plateformes.
Retourne une liste de dicts: [{"index": 0, "name": "Monitor 1", "resolution": "1920x1080"}, ...]
"""
monitors = []
try:
import pygame
if not pygame.display.get_init():
pygame.display.init()
num_displays = pygame.display.get_num_displays()
for i in range(num_displays):
try:
# Essayer d'obtenir le mode desktop pour ce display
mode = pygame.display.get_desktop_sizes()[i] if hasattr(pygame.display, 'get_desktop_sizes') else None
if mode:
width, height = mode
else:
# Fallback: utiliser la résolution actuelle si disponible
info = pygame.display.Info()
width, height = info.current_w, info.current_h
monitors.append({
"index": i,
"name": f"Monitor {i + 1}",
"resolution": f"{width}x{height}"
})
except Exception as e:
# Si on ne peut pas obtenir les infos, ajouter quand même le moniteur
monitors.append({
"index": i,
"name": f"Monitor {i + 1}",
"resolution": "Unknown"
})
except Exception as e:
logger.error(f"Error getting monitors: {e}")
# Fallback: au moins un moniteur
monitors = [{"index": 0, "name": "Monitor 1 (Default)", "resolution": "Auto"}]
return monitors if monitors else [{"index": 0, "name": "Monitor 1 (Default)", "resolution": "Auto"}]
def get_font_family(settings=None):
if settings is None:
settings = load_rgsx_settings()
@@ -339,3 +434,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

View File

@@ -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
@@ -356,7 +364,7 @@ try:
logger.info("Test d'écriture dans le fichier de log réussi")
except Exception as e:
logger.error(f"Erreur lors du test d'écriture : {e}")
print(f"ERREUR: Impossible d'écrire dans {config.log_file_web}: {e}", file=sys.stderr)
print(f"ERREUR: Impossible d'ecrire dans {config.log_file_web}: {e}", file=sys.stderr)
# Initialiser les données au démarrage
logger.info("Chargement initial des données...")
@@ -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
]
@@ -739,7 +772,7 @@ class RGSXHandler(BaseHTTPRequestHandler):
# Lire depuis history.json - filtrer seulement les téléchargements en cours
history = load_history() or []
print(f"\n[DEBUG PROGRESS] history.json chargé avec {len(history)} entrées totales")
print(f"\n[DEBUG PROGRESS] history.json charge avec {len(history)} entrees totales")
# Filtrer les entrées avec status "Downloading", "Téléchargement", "Connecting", "Try X/Y"
in_progress_statuses = ["Downloading", "Téléchargement", "Downloading", "Connecting", "Extracting"]
@@ -764,9 +797,9 @@ class RGSXHandler(BaseHTTPRequestHandler):
else:
# Debug: afficher les premiers status qui ne matchent pas
if len(downloads) < 3:
print(f" [DEBUG] Ignoré - Status: '{status}', Game: {entry.get('game_name', '')[:50]}")
print(f" [DEBUG] Ignore - Status: '{status}', Game: {entry.get('game_name', '')[:50]}")
print(f"[DEBUG PROGRESS] {len(downloads)} téléchargements en cours trouvés")
print(f"[DEBUG PROGRESS] {len(downloads)} telechargements en cours trouves")
if downloads:
for url, data in list(downloads.items())[:2]:
print(f" - URL: {url[:80]}...")
@@ -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:
@@ -1994,7 +2088,7 @@ def run_server(host='0.0.0.0', port=5000):
if __name__ == '__main__':
print("="*60, flush=True)
print("Demarrage du serveur RGSX Web...", flush=True)
print(f"Fichier de log prévu: {config.log_file_web}", flush=True)
print(f"Fichier de log prevu: {config.log_file_web}", flush=True)
print("="*60, flush=True)
parser = argparse.ArgumentParser(description='RGSX Web Server')

View File

@@ -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; }
}

View File

@@ -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) {

View File

@@ -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)}")

View File

@@ -1,3 +1,3 @@
{
"version": "2.3.1.9"
"version": "2.4.0.1"
}

View File

@@ -1,149 +1,385 @@
@echo off
setlocal EnableDelayedExpansion
:: Fichier de log
if not exist "%CD%\logs" MD "%CD%\logs"
set "LOG_FILE=%CD%\logs\Retrobat_RGSX_log.txt"
:: Fichier de log (chemin absolu pour fiabilité)
:: Détecter la racine (ROOT_DIR) d'abord pour construire un chemin stable
set CURRENT_DIR=%CD%
pushd "%CURRENT_DIR%\..\.."
set "ROOT_DIR=%CD%"
popd
if not exist "%ROOT_DIR%\roms\windows\logs" MD "%ROOT_DIR%\roms\windows\logs"
set "LOG_FILE=%ROOT_DIR%\roms\windows\logs\Retrobat_RGSX_log.txt"
:: =============================================================================
:: RGSX Retrobat Launcher v1.3
:: =============================================================================
:: Usage: "RGSX Retrobat.bat" [options]
:: --display=N Launch on display N (0=primary, 1=secondary, etc.)
:: --windowed Launch in windowed mode instead of fullscreen
:: --help Show this help
:: =============================================================================
:: Ajouter un horodatage au début du log
echo [%DATE% %TIME%] Script start >> "%LOG_FILE%"
:: Configuration des couleurs (codes ANSI)
for /F "tokens=1,2 delims=#" %%a in ('"prompt #$H#$E# & echo on & for %%b in (1) do rem"') do (
set "ESC=%%b"
)
:: Afficher un message de démarrage
:: Couleurs
set "GREEN=[92m"
set "YELLOW=[93m"
set "RED=[91m"
set "CYAN=[96m"
set "RESET=[0m"
set "BOLD=[1m"
:: =============================================================================
:: Traitement des arguments
:: =============================================================================
set "DISPLAY_NUM="
set "WINDOWED_MODE="
set "CONFIG_FILE="
:parse_args
if "%~1"=="" goto :args_done
if /i "%~1"=="--help" goto :show_help
if /i "%~1"=="-h" goto :show_help
if /i "%~1"=="--windowed" (
set "WINDOWED_MODE=1"
shift
goto :parse_args
)
:: Check for --display=N format
echo %~1 | findstr /r "^--display=" >nul
if !ERRORLEVEL! EQU 0 (
for /f "tokens=2 delims==" %%a in ("%~1") do set "DISPLAY_NUM=%%a"
shift
goto :parse_args
)
shift
goto :parse_args
:show_help
echo.
echo %ESC%%CYAN%RGSX Retrobat Launcher - Help%ESC%%RESET%
echo.
echo Usage: "RGSX Retrobat.bat" [options]
echo.
echo Options:
echo --display=N Launch on display N (0=primary, 1=secondary, etc.)
echo --windowed Launch in windowed mode instead of fullscreen
echo --help, -h Show this help
echo.
echo Examples:
echo "RGSX Retrobat.bat" Launch on primary display
echo "RGSX Retrobat.bat" --display=1 Launch on secondary display (TV)
echo "RGSX Retrobat.bat" --windowed Launch in windowed mode
echo.
echo You can also create shortcuts with different display settings.
echo.
pause
exit /b 0
:args_done
:: URL de telechargement Python
set "PYTHON_ZIP_URL=https://github.com/RetroGameSets/RGSX/raw/main/windows/python.zip"
:: Obtenir le chemin du script de maniere fiable
set "SCRIPT_DIR=%~dp0"
set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
:: Detecter le repertoire racine
for %%I in ("%SCRIPT_DIR%\..\.." ) do set "ROOT_DIR=%%~fI"
:: Configuration des logs
set "LOG_DIR=%ROOT_DIR%\roms\windows\logs"
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
set "LOG_FILE=%LOG_DIR%\Retrobat_RGSX_log.txt"
set "LOG_BACKUP=%LOG_DIR%\Retrobat_RGSX_log.old.txt"
:: Rotation des logs avec backup
if exist "%LOG_FILE%" (
for %%A in ("%LOG_FILE%") do (
if %%~zA GTR 100000 (
if exist "%LOG_BACKUP%" del /q "%LOG_BACKUP%"
move /y "%LOG_FILE%" "%LOG_BACKUP%" >nul 2>&1
echo [%DATE% %TIME%] Log rotated - previous log saved as .old.txt > "%LOG_FILE%"
)
)
)
:: =============================================================================
:: Ecran d'accueil
:: =============================================================================
cls
echo Running __main__.py for RetroBat...
echo [%DATE% %TIME%] Running __main__.py for RetroBat >> "%LOG_FILE%"
echo.
echo %ESC%%CYAN% ____ ____ ______ __ %ESC%%RESET%
echo %ESC%%CYAN% ^| _ \ / ___^/ ___\ \/ / %ESC%%RESET%
echo %ESC%%CYAN% ^| ^|_) ^| ^| _\___ \\ / %ESC%%RESET%
echo %ESC%%CYAN% ^| _ ^<^| ^|_^| ^|___) / \ %ESC%%RESET%
echo %ESC%%CYAN% ^|_^| \_\\____^|____/_/\_\ %ESC%%RESET%
echo.
echo %ESC%%BOLD% RetroBat Launcher v1.3%ESC%%RESET%
echo --------------------------------
if "!DISPLAY_NUM!" NEQ "0" (
echo %ESC%%CYAN%Display: !DISPLAY_NUM!%ESC%%RESET%
)
if "!WINDOWED_MODE!"=="1" (
echo %ESC%%CYAN%Mode: Windowed%ESC%%RESET%
)
echo.
:: Définir les chemins relatifs et les convertir en absolus
set CURRENT_DIR=%CD%
set PYTHON_EXE=python.exe
:: Debut du log
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
echo [%DATE% %TIME%] RGSX Launcher v1.3 started >> "%LOG_FILE%"
echo [%DATE% %TIME%] Display: !DISPLAY_NUM!, Windowed: !WINDOWED_MODE! >> "%LOG_FILE%"
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
:: Détecter le répertoire racine en remontant de deux niveaux depuis le script
pushd "%CURRENT_DIR%\..\.."
set "ROOT_DIR=%CD%"
popd
:: Définir le chemin du script principal selon les spécifications
:: Configuration des chemins
set "PYTHON_DIR=%ROOT_DIR%\system\tools\Python"
set "PYTHON_EXE=%PYTHON_DIR%\python.exe"
set "MAIN_SCRIPT=%ROOT_DIR%\roms\ports\RGSX\__main__.py"
set "ZIP_FILE=%ROOT_DIR%\roms\windows\python.zip"
:: Definir le chemin du script de mise à jour de la gamelist Windows
set "UPDATE_GAMELIST_SCRIPT=%ROOT_DIR%\roms\ports\RGSX\update_gamelist_windows.py"
:: Exporter RGSX_ROOT pour le script Python
set "RGSX_ROOT=%ROOT_DIR%"
:: Convertir les chemins relatifs en absolus avec pushd/popd
pushd "%ROOT_DIR%\system\tools\Python"
set "PYTHON_EXE_FULL=%ROOT_DIR%\system\tools\Python\!PYTHON_EXE!"
set "PYTHONW_EXE_FULL=%ROOT_DIR%\system\tools\Python\pythonw.exe"
popd
:: Logger les chemins
echo [%DATE% %TIME%] System info: >> "%LOG_FILE%"
echo [%DATE% %TIME%] ROOT_DIR: %ROOT_DIR% >> "%LOG_FILE%"
echo [%DATE% %TIME%] PYTHON_EXE: %PYTHON_EXE% >> "%LOG_FILE%"
echo [%DATE% %TIME%] MAIN_SCRIPT: %MAIN_SCRIPT% >> "%LOG_FILE%"
echo [%DATE% %TIME%] RGSX_ROOT: %RGSX_ROOT% >> "%LOG_FILE%"
:: Afficher et logger les variables
:: =============================================================================
:: Verification Python
:: =============================================================================
echo %ESC%%YELLOW%[1/3]%ESC%%RESET% Checking Python environment...
echo [%DATE% %TIME%] Step 1/3: Checking Python >> "%LOG_FILE%"
echo ROOT_DIR : %ROOT_DIR% >> "%LOG_FILE%"
echo CURRENT_DIR : !CURRENT_DIR! >> "%LOG_FILE%"
echo ROOT_DIR : !ROOT_DIR! >> "%LOG_FILE%"
echo PYTHON_EXE_FULL : !PYTHON_EXE_FULL! >> "%LOG_FILE%"
echo MAIN_SCRIPT : !MAIN_SCRIPT! >> "%LOG_FILE%"
echo UPDATE_GAMELIST_SCRIPT : !UPDATE_GAMELIST_SCRIPT! >> "%LOG_FILE%"
:: Vérifier si l'exécutable Python existe
echo Checking python.exe...
echo [%DATE% %TIME%] Checking python.exe at !PYTHON_EXE_FULL! >> "%LOG_FILE%"
if not exist "!PYTHON_EXE_FULL!" (
echo python.exe not found in system/tools. Preparing to extract..
echo [%DATE% %TIME%] python.exe not found in system/tools. Preparing to extract.. >> "%LOG_FILE%"
if not exist "%PYTHON_EXE%" (
echo %ESC%%YELLOW%^> Python not found, installing...%ESC%%RESET%
echo [%DATE% %TIME%] Python not found, starting installation >> "%LOG_FILE%"
:: Créer le dossier Python s'il n'existe pas
set "TOOLS_FOLDER_FULL=!ROOT_DIR!\system\tools"
if not exist "!TOOLS_FOLDER_FULL!\Python" (
echo Creating folder !TOOLS_FOLDER_FULL!\Python...
echo [%DATE% %TIME%] Creating folder !TOOLS_FOLDER_FULL!\Python... >> "%LOG_FILE%"
mkdir "!TOOLS_FOLDER_FULL!\Python"
:: Creer le dossier Python
if not exist "%PYTHON_DIR%" (
mkdir "%PYTHON_DIR%" 2>nul
echo [%DATE% %TIME%] Created folder: %PYTHON_DIR% >> "%LOG_FILE%"
)
set "ZIP_FILE=%ROOT_DIR%\roms\windows\python.zip"
echo Extracting ZIP_FILE : !ZIP_FILE! in /system/tools/Python
echo [%DATE% %TIME%] ZIP_FILE : !ZIP_FILE! >> "%LOG_FILE%"
if exist "!ZIP_FILE!" (
echo [%DATE% %TIME%] Extracting python.zip to !TOOLS_FOLDER_FULL!... >> "%LOG_FILE%"
tar -xf "!ZIP_FILE!" -C "!TOOLS_FOLDER_FULL!\Python" --strip-components=0
echo Extraction finished.
echo [%DATE% %TIME%] Extraction finished. >> "%LOG_FILE%"
del /s /q "!ZIP_FILE!"
echo python.zip file deleted.
echo [%DATE% %TIME%] python.zip file deleted. >> "%LOG_FILE%"
) else (
echo Error: Error python.zip not found please download it from github and put in /roms/windows folder.
echo [%DATE% %TIME%] Error: Error python.zip not found please download it from github and put in /roms/windows folder >> "%LOG_FILE%"
:: Verifier si le ZIP existe, sinon le telecharger
if not exist "%ZIP_FILE%" (
echo %ESC%%YELLOW%^> python.zip not found, downloading from GitHub...%ESC%%RESET%
echo [%DATE% %TIME%] python.zip not found, attempting download >> "%LOG_FILE%"
echo [%DATE% %TIME%] Download URL: %PYTHON_ZIP_URL% >> "%LOG_FILE%"
:: Verifier si curl est disponible
where curl.exe >nul 2>&1
if !ERRORLEVEL! EQU 0 (
echo %ESC%%CYAN%^> Using curl to download...%ESC%%RESET%
echo [%DATE% %TIME%] Using curl.exe for download >> "%LOG_FILE%"
curl.exe -L -# -o "%ZIP_FILE%" "%PYTHON_ZIP_URL%"
set DOWNLOAD_RESULT=!ERRORLEVEL!
) else (
:: Fallback sur PowerShell
echo %ESC%%CYAN%^> Using PowerShell to download...%ESC%%RESET%
echo [%DATE% %TIME%] curl not found, using PowerShell >> "%LOG_FILE%"
powershell -NoProfile -ExecutionPolicy Bypass -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest -Uri '%PYTHON_ZIP_URL%' -OutFile '%ZIP_FILE%'"
set DOWNLOAD_RESULT=!ERRORLEVEL!
)
:: Verifier le resultat du telechargement
if !DOWNLOAD_RESULT! NEQ 0 (
echo.
echo %ESC%%RED% ERROR: Download failed!%ESC%%RESET%
echo.
echo Please download python.zip manually from:
echo %ESC%%CYAN%%PYTHON_ZIP_URL%%ESC%%RESET%
echo.
echo And place it in:
echo %ESC%%CYAN%%ROOT_DIR%\roms\windows\%ESC%%RESET%
echo.
echo [%DATE% %TIME%] ERROR: Download failed with code !DOWNLOAD_RESULT! >> "%LOG_FILE%"
goto :error
)
:: Verifier que le fichier a bien ete telecharge et n'est pas vide
if not exist "%ZIP_FILE%" (
echo.
echo %ESC%%RED% ERROR: Download failed - file not created!%ESC%%RESET%
echo [%DATE% %TIME%] ERROR: ZIP file not created after download >> "%LOG_FILE%"
goto :error
)
:: Verifier la taille du fichier (doit etre > 1MB pour etre valide)
for %%A in ("%ZIP_FILE%") do set ZIP_SIZE=%%~zA
if !ZIP_SIZE! LSS 1000000 (
echo.
echo %ESC%%RED% ERROR: Downloaded file appears invalid ^(too small^)!%ESC%%RESET%
echo [%DATE% %TIME%] ERROR: Downloaded file too small: !ZIP_SIZE! bytes >> "%LOG_FILE%"
del /q "%ZIP_FILE%" 2>nul
goto :error
)
echo %ESC%%GREEN%^> Download complete ^(!ZIP_SIZE! bytes^)%ESC%%RESET%
echo [%DATE% %TIME%] Download successful: !ZIP_SIZE! bytes >> "%LOG_FILE%"
)
:: Verifier que tar existe (Windows 10 1803+)
where tar >nul 2>&1
if !ERRORLEVEL! NEQ 0 (
echo.
echo %ESC%%RED% ERROR: tar command not available!%ESC%%RESET%
echo.
echo Please update Windows 10 or extract manually to:
echo %ESC%%CYAN%%PYTHON_DIR%%ESC%%RESET%
echo.
echo [%DATE% %TIME%] ERROR: tar command not found >> "%LOG_FILE%"
goto :error
)
:: Vérifier à nouveau si python.exe existe après extraction
if not exist "!PYTHON_EXE_FULL!" (
echo Error: python.exe not found after extraction at !PYTHON_EXE_FULL!.
echo [%DATE% %TIME%] Error: python.exe not found after extraction at !PYTHON_EXE_FULL! >> "%LOG_FILE%"
:: Extraction avec progression simulee
echo %ESC%%YELLOW%^> Extracting Python...%ESC%%RESET%
echo [%DATE% %TIME%] Extracting python.zip >> "%LOG_FILE%"
<nul set /p "= ["
tar -xf "%ZIP_FILE%" -C "%PYTHON_DIR%" --strip-components=0
set TAR_RESULT=!ERRORLEVEL!
echo %ESC%%GREEN%##########%ESC%%RESET%] Done
if !TAR_RESULT! NEQ 0 (
echo.
echo %ESC%%RED% ERROR: Extraction failed!%ESC%%RESET%
echo [%DATE% %TIME%] ERROR: tar extraction failed with code !TAR_RESULT! >> "%LOG_FILE%"
goto :error
)
echo [%DATE% %TIME%] Extraction completed >> "%LOG_FILE%"
:: Supprimer ZIP
del /q "%ZIP_FILE%" 2>nul
echo %ESC%%GREEN%^> python.zip cleaned up%ESC%%RESET%
echo [%DATE% %TIME%] python.zip deleted >> "%LOG_FILE%"
:: Verifier installation
if not exist "%PYTHON_EXE%" (
echo.
echo %ESC%%RED% ERROR: Python not found after extraction!%ESC%%RESET%
echo [%DATE% %TIME%] ERROR: python.exe not found after extraction >> "%LOG_FILE%"
goto :error
)
)
echo python.exe found.
echo [%DATE% %TIME%] python.exe found. >> "%LOG_FILE%"
:: Vérifier si le script Python existe
echo Checking __main__.py...
echo [%DATE% %TIME%] Checking __main__.py at !MAIN_SCRIPT! >> "%LOG_FILE%"
if not exist "!MAIN_SCRIPT!" (
echo Error: __main__.py not found at !MAIN_SCRIPT!.
echo [%DATE% %TIME%] Error: __main__.py not found at !MAIN_SCRIPT! >> "%LOG_FILE%"
:: Afficher et logger la version Python
for /f "tokens=*" %%v in ('"%PYTHON_EXE%" --version 2^>^&1') do set "PYTHON_VERSION=%%v"
echo %ESC%%GREEN%^> %PYTHON_VERSION% found%ESC%%RESET%
echo [%DATE% %TIME%] %PYTHON_VERSION% detected >> "%LOG_FILE%"
:: =============================================================================
:: Verification script principal
:: =============================================================================
echo %ESC%%YELLOW%[2/3]%ESC%%RESET% Checking RGSX application...
echo [%DATE% %TIME%] Step 2/3: Checking RGSX files >> "%LOG_FILE%"
if not exist "%MAIN_SCRIPT%" (
echo.
echo %ESC%%RED% ERROR: __main__.py not found!%ESC%%RESET%
echo.
echo Expected location:
echo %ESC%%CYAN%%MAIN_SCRIPT%%ESC%%RESET%
echo.
echo [%DATE% %TIME%] ERROR: __main__.py not found at %MAIN_SCRIPT% >> "%LOG_FILE%"
goto :error
)
echo __main__.py found.
echo [%DATE% %TIME%] __main__.py found. >> "%LOG_FILE%"
:: L'étape de mise à jour de la gamelist est désormais appelée depuis __main__.py
echo [%DATE% %TIME%] Skipping external gamelist update (handled in app). >> "%LOG_FILE%"
echo %ESC%%GREEN%^> RGSX files OK%ESC%%RESET%
echo [%DATE% %TIME%] RGSX files verified >> "%LOG_FILE%"
echo Launching __main__.py (attached)...
echo [%DATE% %TIME%] Preparing to launch main. >> "%LOG_FILE%"
:: =============================================================================
:: Lancement
:: =============================================================================
echo %ESC%%YELLOW%[3/3]%ESC%%RESET% Launching RGSX...
echo [%DATE% %TIME%] Step 3/3: Launching application >> "%LOG_FILE%"
:: Assurer le bon dossier de travail pour l'application
:: Changer le repertoire de travail
cd /d "%ROOT_DIR%\roms\ports\RGSX"
echo [%DATE% %TIME%] Working directory: %CD% >> "%LOG_FILE%"
:: Forcer les drivers SDL côté Windows et réduire le bruit console
:: Configuration SDL/Pygame
set PYGAME_HIDE_SUPPORT_PROMPT=1
set SDL_VIDEODRIVER=windows
set SDL_AUDIODRIVER=directsound
echo [%DATE% %TIME%] CWD before launch: %CD% >> "%LOG_FILE%"
set PYTHONWARNINGS=ignore::UserWarning:pygame.pkgdata
:: Lancer l'application dans la même console et attendre sa fin
:: Forcer python.exe pour capturer la sortie
set "PY_MAIN_EXE=!PYTHON_EXE_FULL!"
echo [%DATE% %TIME%] Using interpreter: !PY_MAIN_EXE! >> "%LOG_FILE%"
echo [%DATE% %TIME%] Launching "!MAIN_SCRIPT!" now... >> "%LOG_FILE%"
"!PY_MAIN_EXE!" "!MAIN_SCRIPT!" >> "%LOG_FILE%" 2>&1
set EXITCODE=!ERRORLEVEL!
echo [%DATE% %TIME%] __main__.py exit code: !EXITCODE! >> "%LOG_FILE%"
if "!EXITCODE!"=="0" (
echo Execution finished successfully.
echo [%DATE% %TIME%] Execution of __main__.py finished successfully. >> "%LOG_FILE%"
:: =============================================================================
:: Configuration multi-ecran
:: =============================================================================
:: SDL_VIDEO_FULLSCREEN_HEAD: Selectionne l'ecran pour le mode plein ecran
:: 0 = ecran principal, 1 = ecran secondaire, etc.
:: Ces variables ne sont definies que si --display=N ou --windowed est passe
:: Sinon, le script Python utilisera les parametres de rgsx_settings.json
echo [%DATE% %TIME%] Display configuration: >> "%LOG_FILE%"
if defined DISPLAY_NUM (
set SDL_VIDEO_FULLSCREEN_HEAD=!DISPLAY_NUM!
set RGSX_DISPLAY=!DISPLAY_NUM!
echo [%DATE% %TIME%] SDL_VIDEO_FULLSCREEN_HEAD=!DISPLAY_NUM! ^(from --display arg^) >> "%LOG_FILE%"
echo [%DATE% %TIME%] RGSX_DISPLAY=!DISPLAY_NUM! ^(from --display arg^) >> "%LOG_FILE%"
) else (
echo Error: Failed to execute __main__.py (code !EXITCODE!).
echo [%DATE% %TIME%] Error: Failed to execute __main__.py with error code !EXITCODE!. >> "%LOG_FILE%"
echo [%DATE% %TIME%] Display: using rgsx_settings.json config >> "%LOG_FILE%"
)
if defined WINDOWED_MODE (
set RGSX_WINDOWED=!WINDOWED_MODE!
echo [%DATE% %TIME%] RGSX_WINDOWED=!WINDOWED_MODE! ^(from --windowed arg^) >> "%LOG_FILE%"
) else (
echo [%DATE% %TIME%] Windowed: using rgsx_settings.json config >> "%LOG_FILE%"
)
:: Log environnement
echo [%DATE% %TIME%] Environment variables set: >> "%LOG_FILE%"
echo [%DATE% %TIME%] RGSX_ROOT=%RGSX_ROOT% >> "%LOG_FILE%"
echo [%DATE% %TIME%] SDL_VIDEODRIVER=%SDL_VIDEODRIVER% >> "%LOG_FILE%"
echo [%DATE% %TIME%] SDL_AUDIODRIVER=%SDL_AUDIODRIVER% >> "%LOG_FILE%"
echo.
if defined DISPLAY_NUM (
echo %ESC%%CYAN%Launching on display !DISPLAY_NUM!...%ESC%%RESET%
)
if defined WINDOWED_MODE (
echo %ESC%%CYAN%Windowed mode enabled%ESC%%RESET%
)
echo %ESC%%CYAN%Starting RGSX application...%ESC%%RESET%
echo %ESC%%BOLD%Press Ctrl+C to force quit if needed%ESC%%RESET%
echo.
echo [%DATE% %TIME%] Executing: "%PYTHON_EXE%" "%MAIN_SCRIPT%" >> "%LOG_FILE%"
echo [%DATE% %TIME%] --- Application output start --- >> "%LOG_FILE%"
"%PYTHON_EXE%" "%MAIN_SCRIPT%" >> "%LOG_FILE%" 2>&1
set EXITCODE=!ERRORLEVEL!
echo [%DATE% %TIME%] --- Application output end --- >> "%LOG_FILE%"
echo [%DATE% %TIME%] Exit code: !EXITCODE! >> "%LOG_FILE%"
if "!EXITCODE!"=="0" (
echo.
echo %ESC%%GREEN%RGSX closed successfully.%ESC%%RESET%
echo.
echo [%DATE% %TIME%] Application closed successfully >> "%LOG_FILE%"
) else (
echo.
echo %ESC%%RED%RGSX exited with error code !EXITCODE!%ESC%%RESET%
echo.
echo [%DATE% %TIME%] ERROR: Application exited with code !EXITCODE! >> "%LOG_FILE%"
goto :error
)
:end
echo Task completed.
echo [%DATE% %TIME%] Task completed successfully. >> "%LOG_FILE%"
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
echo [%DATE% %TIME%] Session ended normally >> "%LOG_FILE%"
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
timeout /t 2 >nul
exit /b 0
:error
echo An error occurred.
echo [%DATE% %TIME%] An error occurred. >> "%LOG_FILE%"
echo.
echo %ESC%%RED%An error occurred. Check the log file:%ESC%%RESET%
echo %ESC%%CYAN%%LOG_FILE%%ESC%%RESET%
echo.
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
echo [%DATE% %TIME%] Session ended with errors >> "%LOG_FILE%"
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
echo.
echo Press any key to close...
pause >nul
exit /b 1