Compare commits

..

42 Commits

Author SHA1 Message Date
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
skymike03
0c5e307112 v2.3.1.9 (2025.11.10)
- Add footer font scale settings and accessibility options
- Adjusted scraper error messages
2025-11-10 22:43:48 +01:00
skymike03
f9d95b9a2d v2.3.1.8 (2025.11.08)
Merge pull request [#30](https://github.com/RetroGameSets/RGSX/issues/30) from SeeThruHead/main

Refactor Docker setup with proper volume separation and backwards compatibility

Implemented environment variable-based configuration to support both Docker
and traditional Batocera/RetroBat installations with a single codebase.
2025-11-08 23:01:19 +01:00
RGS
2033eb2f76 Merge pull request #30 from SeeThruHead/main
Refactor Docker setup with proper volume separation and backwards com…
2025-11-09 00:04:38 +01:00
skymike03
61b615f4c7 v2.3.1.7
- correct typo error in xiso linux
2025-11-05 23:04:54 +01:00
Shane Keulen
f1c4955670 Merge branch 'RetroGameSets:main' into main 2025-11-05 14:28:48 -05:00
skymike03
03d64d4401 v2.3.1.6 (2025.11.05)
- Replace xdvdfs by extract_xiso because conversion not working in all xbox games
2025-11-05 05:33:18 +01:00
shane keulen
5569238e55 Refactor Docker setup with proper volume separation and backwards compatibility
Implemented environment variable-based configuration to support both Docker
and traditional Batocera/RetroBat installations with a single codebase.

Key Changes:
- Added RGSX_CONFIG_DIR and RGSX_DATA_DIR environment variables
- Separate /config and /data volumes in Docker mode
- App files now copied into container at build time (not runtime sync)
- Simplified directory structure (removed __downloads concept)
- Maintained 100% backwards compatibility with non-Docker installations

File Structure by Mode:

| Location        | Docker Mode     | Traditional Mode                   |
|-----------------|-----------------|----------------------------------- |
| Settings/Config | /config/        | /userdata/saves/ports/rgsx/        |
| Game Lists      | /config/games/  | /userdata/saves/ports/rgsx/games/  |
| Images          | /config/images/ | /userdata/saves/ports/rgsx/images/ |
| Logs            | /config/logs/   | /userdata/roms/ports/RGSX/logs/    |
| ROMs            | /data/roms/     | /userdata/roms/                    |

Detection:
- Docker mode: Activated when RGSX_CONFIG_DIR or RGSX_DATA_DIR is set
- Traditional mode: Default when no Docker env vars present

Tested and verified working in both modes.
2025-11-04 21:36:30 -05:00
skymike03
798ef13dd3 v2.3.1.5 (2025.11.04)
- update integrated roms info scraper to use tgdb api instead of https web requests
- Add Docker support (web server)
- Add region filters (web server)
- Add one-ROM-per-game filter with region priority (web server)
- Refactor code structure in RGSX Web to improved readability and maintainability (js, css, html separated)
- update language files and correct a bug that crash
when changing language, or changing filter
2025-11-04 19:25:43 +01:00
skymike03
40d0826a6b update language files and correct a bug that crash
when changing language
2025-11-04 19:08:43 +01:00
skymike03
82dbf4e49d Refactor code structure in RGSX Web to improved readability and maintainability, move docker files to folder, update language files 2025-11-04 19:00:54 +01:00
RGS
26f8499c83 Merge pull request #28 from SeeThruHead/main
Improve Docker support with configurable permissions and SMB compatib…
2025-11-04 18:35:15 +01:00
shane keulen
0f671ccdf2 Improve Docker support with configurable permissions and SMB compatibility
Added flexible user/group ID handling to support different storage backends:

- Configurable PUID/PGID environment variables for NFS and local storage
- RUN_AS_ROOT mode for SMB mounts that only allow root writes
- Pre-chown app files during build to enable non-root rsync
- Improved error messages with troubleshooting guidance
- Updated documentation with setup examples for different scenarios

This allows the container to work correctly with Unraid SMB shares, NFS mounts,
and local storage by adapting to how different filesystems handle permissions.

Default behavior (PUID=99, PGID=100) remains compatible with Unraid nobody:users.
2025-11-04 09:31:04 -05:00
skymike03
0c55d6d6d6 update jap logo 2025-11-03 23:45:59 +01:00
skymike03
cdc81f795d update region priority to allow user change order, add regions icons , add multiple region search ex if game has 'USA, EUROPE' in name, you can find it with USA or EUROPE filter. 2025-11-03 23:43:40 +01:00
skymike03
961e46a77d Merge branch 'pr-27' 2025-11-03 23:41:01 +01:00
skymike03
7a061ef0bc update scraper to use tgdb api 2025-11-03 22:44:55 +01:00
Shane Keulen
57b75dc199 Merge branch 'RetroGameSets:main' into main 2025-11-02 14:21:07 -05:00
shane keulen
3caa780e5a Add region exclude filters (3-state toggle) and preserve disc numbers in 1G1R filter 2025-11-02 01:08:53 -04:00
shane keulen
716fa23e4e Add one-ROM-per-game filter with region priority (USA→Canada→World→Europe) 2025-11-02 00:54:01 -04:00
shane keulen
d0eaf387b2 Fix: Always sync app code on container start
- Changed entrypoint to always copy/update code
- Enables container updates without manual intervention
- Fixes issue where code changes weren't reflected
2025-11-02 00:35:03 -04:00
shane keulen
28a0013bee Add advanced game filtering to web interface
- Region filtering: USA, Europe, Japan, World, Other
- Hide demos/betas/protos checkbox
- Regex search mode option
- Live filter status display
- All filters work together with AND logic
2025-11-02 00:25:43 -04:00
shane keulen
acb3eb33c3 Add Docker support for RGSX web server
- Minimal Dockerfile with Python 3.11 and required dependencies
- docker-entrypoint.sh initializes folder structure and settings
- README-DOCKER.md with simple build and run instructions
- Updated .gitignore to exclude Docker test data
2025-11-01 23:42:46 -04:00
37 changed files with 9235 additions and 5044 deletions

6
.gitignore vendored
View File

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

377
README.md
View File

@@ -1,241 +1,236 @@
# 🎮 Retro Game Sets Xtra (RGSX)
## SUPPORT / HELP: https://discord.gg/Vph9jwg3VV
## LISEZ-MOI / INSTRUCTIONS EN FRANCAIS : https://github.com/RetroGameSets/RGSX/blob/main/README_FR.md
**[Discord Support](https://discord.gg/Vph9jwg3VV)** • **[Installation](#-installation)** • **[French Documentation](https://github.com/RetroGameSets/RGSX/blob/main/README_FR.md)**
RGSX is a Python application using Pygame for its graphical interface, created by and for the RetroGameSets community. It is completely free.
A free, user-friendly ROM downloader for Batocera, Knulli, and RetroBat with multi-source support.
The application currently supports multiple download sources such as myrient and 1fichier (with optional unlocking / fallback via AllDebrid and Real-Debrid). Sources can be updated frequently.
<p align="center">
<img width="69%" alt="platform menu" src="https://github.com/user-attachments/assets/4464b57b-06a8-45e9-a411-cc12b421545a" />
<img width="30%" alt="controls help" src="https://github.com/user-attachments/assets/38cac7e6-14f2-4e83-91da-0679669822ee" />
</p>
<p align="center">
<img width="49%" alt="web interface" src="https://github.com/user-attachments/assets/71f8bd39-5901-45a9-82b2-91426b3c31a7" />
<img width="49%" alt="api menu" src="https://github.com/user-attachments/assets/5bae018d-b7d9-4a95-9f1b-77db751ff24f" />
</p>
## INSTALLATION : https://github.com/RetroGameSets/RGSX#-installation
## ✨ Features
- **Game downloads**: Supports ZIP files and handles unsupported raw archives automatically based on allowed extensions defined in EmulationStation's `es_systems.cfg` (and custom `es_systems_*.cfg` on Batocera). RGSX reads the 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 |
| Games not showing | Pause Menu > Games > Update Game Cache |
| Download stuck | Check API keys in `/saves/ports/rgsx/` |
| App crashes | Check `/roms/ports/RGSX/logs/RGSX.log` |
| Layout change not applied | Restart RGSX after changing layout |
**Need help?** Share logs from `/roms/ports/RGSX/logs/` on [Discord](https://discord.gg/Vph9jwg3VV).
---
## 🤝 Contributing
### Report a Bug
1. Review `/roms/ports/RGSX/logs/RGSX.log`.
2. Open a GitHub issue with a clear description + relevant log excerpt OR share it on Discord.
### Propose a Feature
- Open an issue (or discuss on Discord first) describing the feature and its integration.
### Contribute Code
1. Fork the repository & create a feature branch:
```bash
git checkout -b feature/your-feature-name
```
2. Test on Batocera / RetroBat.
3. Open a Pull Request with a detailed summary.
---
## ⚠️ Known Issues
- (None currently listed)
- **Bug Reports**: Open GitHub issue with logs or post on Discord
- **Feature Requests**: Discuss on Discord first, then open issue
- **Code Contributions**:
```bash
git checkout -b feature/your-feature
# Test on Batocera/RetroBat
# Submit Pull Request
```
---
## 📝 License
This project is free software. You are free to use, modify, and distribute it under the terms of the included license.
Developed with ❤️ for retro gaming enthusiasts.
Free and open-source software. Use, modify, and distribute freely.
## Thanks to all contributors, and followers of this app
**If you want to support my project you can buy me a beer : https://bit.ly/donate-to-rgsx**
[![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.

44
docker/Dockerfile Normal file
View File

@@ -0,0 +1,44 @@
FROM python:3.11-slim
# Install system dependencies for ROM extraction
RUN apt-get update && apt-get install -y --no-install-recommends \
p7zip-full \
unrar-free \
curl \
gosu \
&& rm -rf /var/lib/apt/lists/*
# Create app directory
RUN mkdir -p /app
# Copy RGSX application files to /app
COPY ports/RGSX/ /app/RGSX/
# Copy entrypoint script
COPY docker/docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Install Python dependencies
# pygame is imported in some modules even in headless mode, so we include it
RUN pip install --no-cache-dir requests pygame
# Set environment variables for Docker mode
# These tell RGSX to use /config for settings and /data for ROMs
ENV RGSX_HEADLESS=1 \
RGSX_APP_DIR=/app \
RGSX_CONFIG_DIR=/config \
RGSX_DATA_DIR=/data
# Expose web interface port
EXPOSE 5000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:5000/ || exit 1
# Set working directory
WORKDIR /app/RGSX
# Entrypoint handles user permissions and directory setup
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["python", "rgsx_web.py", "--host", "0.0.0.0", "--port", "5000"]

182
docker/README-DOCKER.md Normal file
View File

@@ -0,0 +1,182 @@
# 🐳 RGSX Docker - Headless Web Server
Run RGSX as a web-only service without the Pygame UI. Perfect for homelab/server setups.
## Quick Start
```bash
# Using docker-compose (recommended)
docker-compose up -d
# Or build and run manually
docker build -f docker/Dockerfile -t rgsx .
docker run -d \
--name rgsx \
-p 5000:5000 \
-v ./config:/config \
-v ./data:/data \
rgsx
# Access the web interface
open http://localhost:5000
```
## What This Does
- Runs RGSX web server in headless mode (no Pygame UI)
- Web interface accessible from any browser
- Config persists in `/config` volume (settings, metadata, history)
- ROMs download to `/data/roms/{platform}/` and extract there
- Environment variables pre-configured (no manual setup needed)
## Environment Variables
**Pre-configured in the container (no need to set these):**
- `RGSX_HEADLESS=1` - Runs in headless mode
- `RGSX_CONFIG_DIR=/config` - Config location
- `RGSX_DATA_DIR=/data` - Data location
**Optional (only if needed):**
- `PUID` - User ID for file ownership (default: root)
- `PGID` - Group ID for file ownership (default: root)
## Configuration
### Docker Compose
See `docker-compose.example.yml` for a complete example configuration.
### User Permissions (Important!)
**For SMB mounts (Unraid, Windows shares):**
- Don't set PUID/PGID
- The container runs as root, and the SMB server maps files to your authenticated user
**For NFS/local storage:**
- Set PUID and PGID to match your host user (files will be owned by that user)
- Find your user ID: `id -u` and `id -g`
### Volumes
Two volumes are used:
**`/config`** - Configuration and metadata
- `rgsx_settings.json` - Settings
- `games/` - Platform game database files (JSON)
- `images/` - Game cover art
- `history.json` - Download history
- `logs/` - Application logs
- `*.txt` - API keys
**`/data`** - ROM storage
- `roms/` - ROMs by platform (snes/, nes/, psx/, etc.) - downloads extract here
### API Keys
Add your download service API keys to `./config/`:
```bash
# Add your API key (just the key, no extra text)
echo "YOUR_KEY_HERE" > ./config/1FichierAPI.txt
# Optional: AllDebrid/RealDebrid
echo "YOUR_KEY" > ./config/AllDebridAPI.txt
echo "YOUR_KEY" > ./config/RealDebridAPI.txt
# Restart to apply
docker restart rgsx
```
## Commands
```bash
# Start
docker start rgsx
# View logs
docker logs -f rgsx
# Stop
docker stop rgsx
# Update (after git pull)
docker build --no-cache -t rgsx .
docker stop rgsx && docker rm rgsx
# Then re-run the docker run command
```
## Directory Structure
**On Host:**
```
./
├── config/ # Config volume (created on first run)
│ ├── rgsx_settings.json
│ ├── games/ # Platform game database (JSON)
│ ├── images/ # Platform images
│ ├── logs/ # Application logs
│ └── *.txt # API keys (1FichierAPI.txt, etc.)
└── data/
└── roms/ # ROMs by platform
├── snes/
├── n64/
└── ...
```
**In Container:**
```
/app/RGSX/ # Application code
/config/ # Mapped to ./config on host
└── games/, images/, logs/, etc.
/data/ # Mapped to ./data on host
└── roms/ # ROM downloads go here
```
## How It Works
RGSX already has a headless mode (`RGSX_HEADLESS=1`) and the web server (`rgsx_web.py`) works standalone - this was designed for the Batocera web service. The Docker setup just runs it in a container with proper volume mappings.
## Troubleshooting
**Permission denied errors / Can't delete files:**
The container creates files with the UID/GID specified by PUID/PGID environment variables:
```bash
# Set correct PUID/PGID for your environment
docker run -e PUID=1000 -e PGID=1000 ...
```
**Changed PUID/PGID and permission errors:**
Fix ownership of your volumes:
```bash
# Fix ownership to match new PUID/PGID
sudo chown -R 1000:1000 ./config ./data
```
**Port already in use:**
```bash
docker run -p 8080:5000 ... # Use port 8080 instead
```
**Container won't start:**
```bash
docker logs rgsx
```
## vs Traditional Install
| Feature | Docker | Batocera/RetroBat |
|---------|--------|-------------------|
| Interface | Web only | Pygame UI + Web |
| Install | `docker run` | Manual setup |
| Updates | `docker build` | git pull |
| Access | Any device on network | Device only |
| Use Case | Server/homelab | Gaming device |
## Support
- RGSX Issues: https://github.com/RetroGameSets/RGSX/issues
- Discord: https://discord.gg/Vph9jwg3VV

View File

@@ -0,0 +1,29 @@
version: '3.8'
services:
rgsx:
# Option 1: Build from source
build:
context: ..
dockerfile: docker/Dockerfile
# Option 2: Use pre-built image (push to your own registry)
# image: your-registry/rgsx:latest
container_name: rgsx
restart: unless-stopped
ports:
- "5000:5000"
volumes:
- ./config:/config
- ./data:/data
# Optional: Set PUID/PGID for NFS/local storage (not needed for SMB mounts)
# environment:
# - PUID=1000
# - PGID=1000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s

View File

@@ -0,0 +1,63 @@
#!/bin/bash
set -e
echo "=== RGSX Docker Container Startup ==="
# If PUID/PGID are set, create user and run as that user
# If not set, run as root (works for SMB mounts)
if [ -n "$PUID" ] && [ -n "$PGID" ]; then
echo "Creating user with PUID=$PUID, PGID=$PGID..."
# Create group if it doesn't exist
if ! getent group $PGID >/dev/null 2>&1; then
groupadd -g $PGID rgsx
fi
# Create user if it doesn't exist
if ! getent passwd $PUID >/dev/null 2>&1; then
useradd -u $PUID -g $PGID -m -s /bin/bash rgsx
fi
echo "Running as user $(id -un $PUID) (UID=$PUID, GID=$PGID)"
RUN_USER="gosu rgsx"
else
echo "Running as root (no PUID/PGID set) - suitable for SMB mounts"
RUN_USER=""
fi
# Create necessary directories
# /config needs logs directory, app will create others (like images/, games/) as needed
# /data needs roms directory
echo "Setting up directories..."
$RUN_USER mkdir -p /config/logs
$RUN_USER mkdir -p /data/roms
# Fix ownership of volumes if PUID/PGID are set
if [ -n "$PUID" ] && [ -n "$PGID" ]; then
echo "Setting ownership on volumes..."
chown -R $PUID:$PGID /config /data 2>/dev/null || true
fi
# Create default settings with show_unsupported_platforms enabled if config doesn't exist
SETTINGS_FILE="/config/rgsx_settings.json"
if [ ! -f "$SETTINGS_FILE" ]; then
echo "Creating default settings with all platforms visible..."
$RUN_USER bash -c "cat > '$SETTINGS_FILE' << 'EOF'
{
\"show_unsupported_platforms\": true
}
EOF"
echo "Default settings created at $SETTINGS_FILE"
fi
echo "=== Starting RGSX Web Server ==="
echo "Config directory: /config"
echo "ROMs directory: /data/roms"
echo "======================================"
# Run the command from the working directory (/app/RGSX set in Dockerfile)
if [ -z "$RUN_USER" ]; then
exec "$@"
else
exec $RUN_USER "$@"
fi

View File

@@ -22,7 +22,7 @@ from display import (
init_display, draw_loading_screen, draw_error_screen, draw_platform_grid,
draw_progress_screen, draw_controls, draw_virtual_keyboard,
draw_extension_warning, draw_pause_menu, draw_controls_help, draw_game_list,
draw_display_menu,
draw_display_menu, draw_filter_menu_choice, draw_filter_advanced, draw_filter_priority_config,
draw_history_list, draw_clear_history_dialog, draw_cancel_download_dialog,
draw_confirm_dialog, draw_reload_games_data_dialog, draw_popup, draw_gradient,
draw_toast, show_toast, THEME_COLORS
@@ -127,6 +127,12 @@ for i, scale in enumerate(config.font_scale_options):
config.current_font_scale_index = i
break
# Charger le footer_font_scale
for i, scale in enumerate(config.footer_font_scale_options):
if scale == config.accessibility_settings.get("footer_font_scale", 1.0):
config.current_footer_font_scale_index = i
break
# Chargement et initialisation de la langue
from language import initialize_language
initialize_language()
@@ -160,6 +166,7 @@ pygame.display.set_caption("RGSX")
# Initialisation des polices via config
config.init_font()
config.init_footer_font()
# Mise à jour de la résolution dans config
config.screen_width, config.screen_height = pygame.display.get_surface().get_size()
@@ -231,11 +238,12 @@ except Exception as e:
# Initialisation du mixer Pygame (déférée/évitable si musique désactivée)
if getattr(config, 'music_enabled', True):
pygame.mixer.pre_init(44100, -16, 2, 4096)
try:
pygame.mixer.pre_init(44100, -16, 2, 4096)
pygame.mixer.init()
except Exception as e:
logger.warning(f"Échec init mixer: {e}")
except (NotImplementedError, AttributeError, Exception) as e:
logger.warning(f"Mixer non disponible ou échec init: {e}")
config.music_enabled = False # Désactiver la musique si mixer non disponible
# Dossier musique Batocera
music_folder = os.path.join(config.APP_FOLDER, "assets", "music")
@@ -412,6 +420,21 @@ async def main():
global current_music, music_files, music_folder, joystick
logger.debug("Début main")
# Charger les filtres de jeux sauvegardés
try:
from game_filters import GameFilters
from rgsx_settings import load_game_filters
config.game_filter_obj = GameFilters()
filter_dict = load_game_filters()
if filter_dict:
config.game_filter_obj.load_from_dict(filter_dict)
if config.game_filter_obj.is_active():
config.filter_active = True
logger.info("Filtres de jeux chargés et actifs")
except Exception as e:
logger.error(f"Erreur lors du chargement des filtres: {e}")
config.game_filter_obj = None
# Démarrer le serveur web en arrière-plan
start_web_server()
@@ -664,6 +687,11 @@ async def main():
"history_error_details",
"history_confirm_delete",
"history_extract_archive",
"text_file_viewer", # Visualiseur de fichiers texte
# Menus filtrage avancé
"filter_menu_choice",
"filter_advanced",
"filter_priority_config",
}
if config.menu_state in SIMPLE_HANDLE_STATES:
action = handle_controls(event, sources, joystick, screen)
@@ -1062,6 +1090,12 @@ async def main():
elif config.menu_state == "filter_platforms":
from display import draw_filter_platforms_menu
draw_filter_platforms_menu(screen)
elif config.menu_state == "filter_menu_choice":
draw_filter_menu_choice(screen)
elif config.menu_state == "filter_advanced":
draw_filter_advanced(screen)
elif config.menu_state == "filter_priority_config":
draw_filter_priority_config(screen)
elif config.menu_state == "controls_help":
draw_controls_help(screen, config.previous_menu_state)
elif config.menu_state == "history":
@@ -1082,6 +1116,9 @@ async def main():
elif config.menu_state == "history_error_details":
from display import draw_history_error_details
draw_history_error_details(screen)
elif config.menu_state == "text_file_viewer":
from display import draw_text_file_viewer
draw_text_file_viewer(screen)
elif config.menu_state == "history_confirm_delete":
from display import draw_history_confirm_delete
draw_history_confirm_delete(screen)
@@ -1361,7 +1398,11 @@ async def main():
clock.tick(60)
await asyncio.sleep(0.01)
pygame.mixer.music.stop()
try:
if pygame.mixer.get_init() is not None:
pygame.mixer.music.stop()
except (AttributeError, NotImplementedError):
pass
# Cancel any ongoing downloads to prevent lingering background threads
try:
cancel_all_downloads()

View File

@@ -11,10 +11,10 @@ def load_accessibility_settings():
try:
settings = load_rgsx_settings()
return settings.get("accessibility", {"font_scale": 1.0})
return settings.get("accessibility", {"font_scale": 1.0, "footer_font_scale": 1.0})
except Exception as e:
logger.error(f"Erreur lors du chargement des paramètres d'accessibilité: {str(e)}")
return {"font_scale": 1.0}
return {"font_scale": 1.0, "footer_font_scale": 1.0}
def save_accessibility_settings(accessibility_settings):
"""Sauvegarde les paramètres d'accessibilité dans rgsx_settings.json."""
@@ -28,7 +28,7 @@ def save_accessibility_settings(accessibility_settings):
logger.error(f"Erreur lors de la sauvegarde des paramètres d'accessibilité: {str(e)}")
def draw_accessibility_menu(screen):
"""Affiche le menu d'accessibilité avec curseur pour la taille de police."""
"""Affiche le menu d'accessibilité avec curseurs pour la taille de police générale et du footer."""
from display import OVERLAY, THEME_COLORS, draw_stylized_button
screen.blit(OVERLAY, (0, 0))
@@ -44,47 +44,87 @@ def draw_accessibility_menu(screen):
pygame.draw.rect(screen, THEME_COLORS["border"], title_bg_rect, 2, border_radius=10)
screen.blit(title_surface, title_rect)
# Curseur de taille de police
# Déterminer quel curseur est sélectionné (0 = général, 1 = footer)
selected_cursor = getattr(config, 'accessibility_selected_cursor', 0)
# Curseur 1: Taille de police générale
current_scale = config.font_scale_options[config.current_font_scale_index]
font_text = _("accessibility_font_size").format(f"{current_scale:.1f}")
# Position du curseur
cursor_y = config.screen_height // 2
cursor_y1 = config.screen_height // 2 - 50
cursor_width = 400
cursor_height = 60
cursor_x = (config.screen_width - cursor_width) // 2
# Fond du curseur
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (cursor_x, cursor_y, cursor_width, cursor_height), border_radius=10)
pygame.draw.rect(screen, THEME_COLORS["border"], (cursor_x, cursor_y, cursor_width, cursor_height), 2, border_radius=10)
# Fond du curseur 1
cursor1_color = THEME_COLORS["fond_lignes"] if selected_cursor == 0 else THEME_COLORS["button_idle"]
pygame.draw.rect(screen, cursor1_color, (cursor_x, cursor_y1, cursor_width, cursor_height), border_radius=10)
border_width = 3 if selected_cursor == 0 else 2
pygame.draw.rect(screen, THEME_COLORS["border"], (cursor_x, cursor_y1, cursor_width, cursor_height), border_width, border_radius=10)
# Flèches gauche/droite
# Flèches gauche/droite pour curseur 1
arrow_size = 30
left_arrow_x = cursor_x + 20
right_arrow_x = cursor_x + cursor_width - arrow_size - 20
arrow_y = cursor_y + (cursor_height - arrow_size) // 2
arrow_y1 = cursor_y1 + (cursor_height - arrow_size) // 2
# Flèche gauche
left_color = THEME_COLORS["fond_lignes"] if config.current_font_scale_index > 0 else THEME_COLORS["border"]
left_color = THEME_COLORS["text"] if config.current_font_scale_index > 0 else THEME_COLORS["border"]
pygame.draw.polygon(screen, left_color, [
(left_arrow_x + arrow_size, arrow_y),
(left_arrow_x, arrow_y + arrow_size // 2),
(left_arrow_x + arrow_size, arrow_y + arrow_size)
(left_arrow_x + arrow_size, arrow_y1),
(left_arrow_x, arrow_y1 + arrow_size // 2),
(left_arrow_x + arrow_size, arrow_y1 + arrow_size)
])
# Flèche droite
right_color = THEME_COLORS["fond_lignes"] if config.current_font_scale_index < len(config.font_scale_options) - 1 else THEME_COLORS["border"]
right_color = THEME_COLORS["text"] if config.current_font_scale_index < len(config.font_scale_options) - 1 else THEME_COLORS["border"]
pygame.draw.polygon(screen, right_color, [
(right_arrow_x, arrow_y),
(right_arrow_x + arrow_size, arrow_y + arrow_size // 2),
(right_arrow_x, arrow_y + arrow_size)
(right_arrow_x, arrow_y1),
(right_arrow_x + arrow_size, arrow_y1 + arrow_size // 2),
(right_arrow_x, arrow_y1 + arrow_size)
])
# Texte au centre
text_surface = config.font.render(font_text, True, THEME_COLORS["text"])
text_rect = text_surface.get_rect(center=(cursor_x + cursor_width // 2, cursor_y + cursor_height // 2))
text_rect = text_surface.get_rect(center=(cursor_x + cursor_width // 2, cursor_y1 + cursor_height // 2))
screen.blit(text_surface, text_rect)
# Curseur 2: Taille de police du footer
current_footer_scale = config.footer_font_scale_options[config.current_footer_font_scale_index]
footer_font_text = _("accessibility_footer_font_size").format(f"{current_footer_scale:.1f}")
cursor_y2 = cursor_y1 + cursor_height + 20
# Fond du curseur 2
cursor2_color = THEME_COLORS["fond_lignes"] if selected_cursor == 1 else THEME_COLORS["button_idle"]
pygame.draw.rect(screen, cursor2_color, (cursor_x, cursor_y2, cursor_width, cursor_height), border_radius=10)
border_width = 3 if selected_cursor == 1 else 2
pygame.draw.rect(screen, THEME_COLORS["border"], (cursor_x, cursor_y2, cursor_width, cursor_height), border_width, border_radius=10)
# Flèches gauche/droite pour curseur 2
arrow_y2 = cursor_y2 + (cursor_height - arrow_size) // 2
# Flèche gauche
left_color2 = THEME_COLORS["text"] if config.current_footer_font_scale_index > 0 else THEME_COLORS["border"]
pygame.draw.polygon(screen, left_color2, [
(left_arrow_x + arrow_size, arrow_y2),
(left_arrow_x, arrow_y2 + arrow_size // 2),
(left_arrow_x + arrow_size, arrow_y2 + arrow_size)
])
# Flèche droite
right_color2 = THEME_COLORS["text"] if config.current_footer_font_scale_index < len(config.footer_font_scale_options) - 1 else THEME_COLORS["border"]
pygame.draw.polygon(screen, right_color2, [
(right_arrow_x, arrow_y2),
(right_arrow_x + arrow_size, arrow_y2 + arrow_size // 2),
(right_arrow_x, arrow_y2 + arrow_size)
])
# Texte au centre
text_surface2 = config.font.render(footer_font_text, True, THEME_COLORS["text"])
text_rect2 = text_surface2.get_rect(center=(cursor_x + cursor_width // 2, cursor_y2 + cursor_height // 2))
screen.blit(text_surface2, text_rect2)
# Instructions
instruction_text = _("language_select_instruction")
instruction_surface = config.small_font.render(instruction_text, True, THEME_COLORS["text"])
@@ -93,16 +133,44 @@ def draw_accessibility_menu(screen):
def handle_accessibility_events(event):
"""Gère les événements du menu d'accessibilité avec support clavier et manette."""
# Initialiser le curseur sélectionné si non défini
if not hasattr(config, 'accessibility_selected_cursor'):
config.accessibility_selected_cursor = 0
# Gestion des touches du clavier
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT and config.current_font_scale_index > 0:
config.current_font_scale_index -= 1
update_font_scale()
# Navigation haut/bas entre les curseurs
if event.key == pygame.K_UP:
config.accessibility_selected_cursor = max(0, config.accessibility_selected_cursor - 1)
config.needs_redraw = True
return True
elif event.key == pygame.K_RIGHT and config.current_font_scale_index < len(config.font_scale_options) - 1:
config.current_font_scale_index += 1
update_font_scale()
elif event.key == pygame.K_DOWN:
config.accessibility_selected_cursor = min(1, config.accessibility_selected_cursor + 1)
config.needs_redraw = True
return True
# Navigation gauche/droite pour modifier les valeurs
elif event.key == pygame.K_LEFT:
if config.accessibility_selected_cursor == 0:
if config.current_font_scale_index > 0:
config.current_font_scale_index -= 1
update_font_scale()
return True
else:
if config.current_footer_font_scale_index > 0:
config.current_footer_font_scale_index -= 1
update_footer_font_scale()
return True
elif event.key == pygame.K_RIGHT:
if config.accessibility_selected_cursor == 0:
if config.current_font_scale_index < len(config.font_scale_options) - 1:
config.current_font_scale_index += 1
update_font_scale()
return True
else:
if config.current_footer_font_scale_index < len(config.footer_font_scale_options) - 1:
config.current_footer_font_scale_index += 1
update_footer_font_scale()
return True
elif event.key == pygame.K_RETURN or event.key == pygame.K_ESCAPE:
config.menu_state = "pause_menu"
return True
@@ -118,36 +186,89 @@ def handle_accessibility_events(event):
# Gestion du D-pad
elif event.type == pygame.JOYHATMOTION:
if event.value == (-1, 0): # Gauche
if config.current_font_scale_index > 0:
config.current_font_scale_index -= 1
update_font_scale()
return True
if event.value == (0, 1): # Haut
config.accessibility_selected_cursor = max(0, config.accessibility_selected_cursor - 1)
config.needs_redraw = True
return True
elif event.value == (0, -1): # Bas
config.accessibility_selected_cursor = min(1, config.accessibility_selected_cursor + 1)
config.needs_redraw = True
return True
elif event.value == (-1, 0): # Gauche
if config.accessibility_selected_cursor == 0:
if config.current_font_scale_index > 0:
config.current_font_scale_index -= 1
update_font_scale()
return True
else:
if config.current_footer_font_scale_index > 0:
config.current_footer_font_scale_index -= 1
update_footer_font_scale()
return True
elif event.value == (1, 0): # Droite
if config.current_font_scale_index < len(config.font_scale_options) - 1:
config.current_font_scale_index += 1
update_font_scale()
return True
if config.accessibility_selected_cursor == 0:
if config.current_font_scale_index < len(config.font_scale_options) - 1:
config.current_font_scale_index += 1
update_font_scale()
return True
else:
if config.current_footer_font_scale_index < len(config.footer_font_scale_options) - 1:
config.current_footer_font_scale_index += 1
update_footer_font_scale()
return True
# Gestion du joystick analogique (axe horizontal)
# Gestion du joystick analogique
elif event.type == pygame.JOYAXISMOTION:
if event.axis == 0 and abs(event.value) > 0.5: # Joystick gauche horizontal
if event.value < -0.5 and config.current_font_scale_index > 0: # Gauche
config.current_font_scale_index -= 1
update_font_scale()
if event.axis == 1 and abs(event.value) > 0.5: # Joystick vertical
if event.value < -0.5: # Haut
config.accessibility_selected_cursor = max(0, config.accessibility_selected_cursor - 1)
config.needs_redraw = True
return True
elif event.value > 0.5 and config.current_font_scale_index < len(config.font_scale_options) - 1: # Droite
config.current_font_scale_index += 1
update_font_scale()
elif event.value > 0.5: # Bas
config.accessibility_selected_cursor = min(1, config.accessibility_selected_cursor + 1)
config.needs_redraw = True
return True
elif event.axis == 0 and abs(event.value) > 0.5: # Joystick horizontal
if event.value < -0.5: # Gauche
if config.accessibility_selected_cursor == 0:
if config.current_font_scale_index > 0:
config.current_font_scale_index -= 1
update_font_scale()
return True
else:
if config.current_footer_font_scale_index > 0:
config.current_footer_font_scale_index -= 1
update_footer_font_scale()
return True
elif event.value > 0.5: # Droite
if config.accessibility_selected_cursor == 0:
if config.current_font_scale_index < len(config.font_scale_options) - 1:
config.current_font_scale_index += 1
update_font_scale()
return True
else:
if config.current_footer_font_scale_index < len(config.footer_font_scale_options) - 1:
config.current_footer_font_scale_index += 1
update_footer_font_scale()
return True
return False
def update_font_scale():
"""Met à jour l'échelle de police et sauvegarde."""
"""Met à jour l'échelle de police générale et sauvegarde."""
new_scale = config.font_scale_options[config.current_font_scale_index]
config.accessibility_settings["font_scale"] = new_scale
save_accessibility_settings(config.accessibility_settings)
# Réinitialiser les polices
config.init_font()
config.needs_redraw = True
def update_footer_font_scale():
"""Met à jour l'échelle de police du footer et sauvegarde."""
new_scale = config.footer_font_scale_options[config.current_footer_font_scale_index]
config.accessibility_settings["footer_font_scale"] = new_scale
save_accessibility_settings(config.accessibility_settings)
# Réinitialiser les polices du footer
config.init_footer_font()
config.needs_redraw = True

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

Binary file not shown.

Binary file not shown.

View File

@@ -13,7 +13,7 @@ except Exception:
pygame = None # type: ignore
# Version actuelle de l'application
app_version = "2.3.1.4"
app_version = "2.3.2.9"
def get_application_root():
@@ -28,29 +28,100 @@ def get_application_root():
# Si __file__ n'est pas défini (par exemple, exécution dans un REPL)
return os.path.abspath(os.getcwd())
### CONSTANTES DES CHEMINS DE BASE
# Chemins de base
APP_FOLDER = os.path.join(get_application_root(), "RGSX")
USERDATA_FOLDER = os.path.dirname(os.path.dirname(os.path.dirname(APP_FOLDER))) # remonte de /userdata/roms/ports/rgsx à /userdata ou \Retrobat
SAVE_FOLDER = os.path.join(USERDATA_FOLDER, "saves", "ports", "rgsx")
# ROMS_FOLDER - Charger depuis rgsx_settings.json si défini, sinon valeur par défaut
_default_roms_folder = os.path.join(USERDATA_FOLDER, "roms")
# Check for Docker mode environment variables
_docker_app_dir = os.environ.get("RGSX_APP_DIR", "").strip()
_docker_config_dir = os.environ.get("RGSX_CONFIG_DIR", "").strip()
_docker_data_dir = os.environ.get("RGSX_DATA_DIR", "").strip()
# Determine if we're running in Docker mode
_is_docker_mode = bool(_docker_config_dir or _docker_data_dir)
if _is_docker_mode:
# ===== DOCKER MODE =====
# App code location (can be overridden, defaults to file location)
if _docker_app_dir:
APP_FOLDER = os.path.join(_docker_app_dir, "RGSX")
else:
APP_FOLDER = os.path.join(get_application_root(), "RGSX")
# Config directory: where rgsx_settings.json and metadata live
if _docker_config_dir:
CONFIG_FOLDER = _docker_config_dir
SAVE_FOLDER = _docker_config_dir
else:
# Fallback: derive from traditional structure
USERDATA_FOLDER = os.path.dirname(os.path.dirname(os.path.dirname(APP_FOLDER)))
CONFIG_FOLDER = os.path.join(USERDATA_FOLDER, "saves", "ports", "rgsx")
SAVE_FOLDER = CONFIG_FOLDER
# Data directory: where ROMs live
# If not set, fallback to CONFIG_FOLDER (single volume mode)
if _docker_data_dir:
DATA_FOLDER = _docker_data_dir
elif _docker_config_dir:
DATA_FOLDER = _docker_config_dir
else:
USERDATA_FOLDER = os.path.dirname(os.path.dirname(os.path.dirname(APP_FOLDER)))
DATA_FOLDER = USERDATA_FOLDER
# For backwards compatibility with code that references USERDATA_FOLDER
USERDATA_FOLDER = DATA_FOLDER
else:
# ===== TRADITIONAL MODE =====
# Derive all paths from app location using original logic
APP_FOLDER = os.path.join(get_application_root(), "RGSX")
# Go up 3 directories from APP_FOLDER to find USERDATA
# Example: /userdata/roms/ports/RGSX -> /userdata
USERDATA_FOLDER = os.path.dirname(os.path.dirname(os.path.dirname(APP_FOLDER)))
# Config and data are both under USERDATA in traditional mode
CONFIG_FOLDER = os.path.join(USERDATA_FOLDER, "saves", "ports", "rgsx")
SAVE_FOLDER = CONFIG_FOLDER
DATA_FOLDER = USERDATA_FOLDER
# ROMS_FOLDER - Can be customized via rgsx_settings.json
# Default ROM location
_default_roms_folder = os.path.join(DATA_FOLDER if _is_docker_mode else USERDATA_FOLDER, "roms")
try:
# Import tardif pour éviter les dépendances circulaires
# Try to load custom roms_folder from settings
_settings_path = os.path.join(SAVE_FOLDER, "rgsx_settings.json")
if os.path.exists(_settings_path):
import json
with open(_settings_path, 'r', encoding='utf-8') as _f:
_settings = json.load(_f)
_custom_roms = _settings.get("roms_folder", "").strip()
if _custom_roms and os.path.isdir(_custom_roms):
ROMS_FOLDER = _custom_roms
if _custom_roms:
# Check if it's an absolute path
if os.path.isabs(_custom_roms):
# Absolute path: use as-is if directory exists
if os.path.isdir(_custom_roms):
ROMS_FOLDER = _custom_roms
else:
ROMS_FOLDER = _default_roms_folder
else:
# Relative path: resolve relative to DATA_FOLDER (docker) or USERDATA_FOLDER (traditional)
_base = DATA_FOLDER if _is_docker_mode else USERDATA_FOLDER
_resolved = os.path.join(_base, _custom_roms)
if os.path.isdir(_resolved):
ROMS_FOLDER = _resolved
else:
ROMS_FOLDER = _default_roms_folder
else:
# Empty: use default
ROMS_FOLDER = _default_roms_folder
else:
# Settings file doesn't exist yet: use default
ROMS_FOLDER = _default_roms_folder
except Exception as _e:
ROMS_FOLDER = _default_roms_folder
@@ -64,7 +135,15 @@ logger = logging.getLogger(__name__)
download_queue = [] # Liste de dicts: {url, platform, game_name, ...}
# Indique si un téléchargement est en cours
download_active = False
log_dir = os.path.join(APP_FOLDER, "logs")
# Log directory
# Docker mode: /config/logs (persisted in config volume)
# Traditional mode: /app/RGSX/logs (current behavior)
if _is_docker_mode:
log_dir = os.path.join(CONFIG_FOLDER, "logs")
else:
log_dir = os.path.join(APP_FOLDER, "logs")
log_file = os.path.join(log_dir, "RGSX.log")
log_file_web = os.path.join(log_dir, 'rgsx_web.log')
@@ -75,9 +154,17 @@ MUSIC_FOLDER = os.path.join(APP_FOLDER, "assets", "music")
GAMELISTXML = os.path.join(ROMS_FOLDER, "ports","gamelist.xml")
GAMELISTXML_WINDOWS = os.path.join(ROMS_FOLDER, "windows","gamelist.xml")
# Dans le Dossier de sauvegarde : /saves/ports/rgsx
# Dans le Dossier de sauvegarde : /saves/ports/rgsx (traditional) or /config (Docker)
IMAGES_FOLDER = os.path.join(SAVE_FOLDER, "images")
GAMES_FOLDER = os.path.join(SAVE_FOLDER, "games")
# GAME_LISTS_FOLDER: Platform game database JSON files (extracted from games.zip)
# Always in SAVE_FOLDER/games for both Docker and Traditional modes
# These are small JSON files containing available games per platform
GAME_LISTS_FOLDER = os.path.join(SAVE_FOLDER, "games")
# Legacy alias for backwards compatibility (some code still uses GAMES_FOLDER)
GAMES_FOLDER = GAME_LISTS_FOLDER
SOURCES_FILE = os.path.join(SAVE_FOLDER, "systems_list.json")
JSON_EXTENSIONS = os.path.join(SAVE_FOLDER, "rom_extensions.json")
PRECONF_CONTROLS_PATH = os.path.join(APP_FOLDER, "assets", "controls")
@@ -106,8 +193,8 @@ OTA_data_ZIP = os.path.join(OTA_SERVER_URL, "games.zip")
#CHEMINS DES EXECUTABLES
UNRAR_EXE = os.path.join(APP_FOLDER,"assets","progs","unrar.exe")
XDVDFS_EXE = os.path.join(APP_FOLDER,"assets", "progs", "xdvdfs.exe")
XDVDFS_LINUX = os.path.join(APP_FOLDER,"assets", "progs", "xdvdfs")
XISO_EXE = os.path.join(APP_FOLDER,"assets", "progs", "extract-xiso_win.exe")
XISO_LINUX = os.path.join(APP_FOLDER,"assets", "progs", "extract-xiso_linux")
PS3DEC_EXE = os.path.join(APP_FOLDER,"assets", "progs", "ps3dec_win.exe")
PS3DEC_LINUX = os.path.join(APP_FOLDER,"assets", "progs", "ps3dec_linux")
SEVEN_Z_LINUX = os.path.join(APP_FOLDER,"assets", "progs", "7zz")
@@ -225,6 +312,7 @@ progress_font = None # Police pour l'affichage de la progression
title_font = None # Police pour les titres
search_font = None # Police pour la recherche
small_font = None # Police pour les petits textes
tiny_font = None # Police pour le footer (contrôles/version)
FONT_FAMILIES = [
"pixel", # police rétro Pixel-UniCode.ttf
"dejavu" # police plus standard lisible petites tailles
@@ -271,9 +359,11 @@ visible_games = 15 # Nombre de jeux visibles en même temps par défaut
# Options d'affichage
accessibility_mode = False # Mode accessibilité pour les polices agrandies
accessibility_settings = {"font_scale": 1.0} # Paramètres d'accessibilité (échelle de police)
accessibility_settings = {"font_scale": 1.0, "footer_font_scale": 1.0} # Paramètres d'accessibilité (échelle de police)
font_scale_options = [0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0] # Options disponibles pour l'échelle de police
current_font_scale_index = 3 # Index pour 1.0
footer_font_scale_options = [0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5] # Options pour l'échelle de police du footer
current_footer_font_scale_index = 3 # Index pour 1.0
popup_start_time = 0 # Timestamp de début d'affichage du popup
last_progress_update = 0 # Timestamp de la dernière mise à jour de progression
transition_state = "idle" # État de la transition d'écran
@@ -290,6 +380,11 @@ search_mode = False # Indicateur si le mode recherche est actif
search_query = "" # Chaîne de recherche saisie par l'utilisateur
filter_active = False # Indicateur si un filtre est appliqué
# Variables pour le filtrage avancé
selected_filter_choice = 0 # Index dans le menu de choix de filtrage (recherche / avancé)
selected_filter_option = 0 # Index dans le menu de filtrage avancé
game_filter_obj = None # Objet GameFilters pour le filtrage avancé
# Gestion des états du menu
needs_redraw = False # Indicateur si l'écran doit être redessiné
selected_option = 0 # Index de l'option sélectionnée dans le menu
@@ -412,6 +507,31 @@ def init_font():
font = title_font = search_font = progress_font = small_font = None
def init_footer_font():
"""Initialise uniquement la police du footer (tiny_font) en fonction de l'échelle séparée."""
global tiny_font
footer_font_scale = accessibility_settings.get("footer_font_scale", 1.0)
# Déterminer la famille sélectionnée
family_id = FONT_FAMILIES[current_font_family_index] if 0 <= current_font_family_index < len(FONT_FAMILIES) else "pixel"
footer_base_size = 20 # Taille de base pour le footer
try:
if family_id == "pixel":
path = os.path.join(APP_FOLDER, "assets", "fonts", "Pixel-UniCode.ttf")
tiny_font = pygame.font.Font(path, int(footer_base_size * footer_font_scale))
elif family_id == "dejavu":
try:
tiny_font = pygame.font.SysFont("dejavusans", int(footer_base_size * footer_font_scale))
except Exception:
tiny_font = pygame.font.SysFont("dejavu sans", int(footer_base_size * footer_font_scale))
logger.debug(f"Police footer initialisée (famille={family_id}, scale={footer_font_scale})")
except Exception as e:
logger.error(f"Erreur chargement police footer: {e}")
tiny_font = None
def validate_resolution():
"""Valide la résolution de l'écran par rapport aux capacités de l'écran."""
if pygame is None:

View File

@@ -20,12 +20,12 @@ from utils import (
ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string
)
from history import load_history, clear_history, add_to_history, save_history
from language import _ # Import de la fonction de traduction
from language import _, get_available_languages, set_language
from rgsx_settings import (
get_allow_unknown_extensions, set_display_grid, get_font_family, set_font_family,
get_show_unsupported_platforms, set_show_unsupported_platforms,
set_allow_unknown_extensions, get_hide_premium_systems, set_hide_premium_systems,
get_sources_mode, set_sources_mode, set_symlink_option, get_symlink_option
get_sources_mode, set_sources_mode, set_symlink_option, get_symlink_option, load_rgsx_settings, save_rgsx_settings
)
from accessibility import save_accessibility_settings
from scraper import get_game_metadata, download_image_to_surface
@@ -58,7 +58,13 @@ VALID_STATES = [
"scraper", # écran du scraper avec métadonnées
"history_error_details", # détails de l'erreur
"history_confirm_delete", # confirmation suppression jeu
"history_extract_archive" # extraction d'archive
"history_extract_archive", # extraction d'archive
"text_file_viewer", # visualiseur de fichiers texte
# Nouveaux menus filtrage avancé
"filter_menu_choice", # menu de choix entre recherche et filtrage avancé
"filter_search", # recherche par nom (existant, mais renommé)
"filter_advanced", # filtrage avancé par région, etc.
"filter_priority_config", # configuration priorité régions pour one-rom-per-game
]
def validate_menu_state(state):
@@ -196,7 +202,21 @@ def is_input_matched(event, action_name):
elif input_type == "axis" and event.type == pygame.JOYAXISMOTION:
axis = mapping.get("axis")
direction = mapping.get("direction")
return event.axis == axis and abs(event.value) > 0.5 and (1 if event.value > 0 else -1) == direction
threshold = 0.5
# Pour les triggers Xbox (axes 4 et 5), la position de repos est -1.0
# Il faut inverser la détection : direction -1 = trigger appuyé (vers +1.0)
if axis in [4, 5]:
# Triggers Xbox: repos à -1.0, appuyé vers +1.0
# On inverse la direction configurée
if direction == -1:
# Direction -1 configurée = détecter quand trigger appuyé (valeur positive)
return event.axis == axis and event.value > threshold
else:
# Direction +1 configurée = détecter aussi quand trigger appuyé
return event.axis == axis and event.value > threshold
else:
# Autres axes: logique normale
return event.axis == axis and abs(event.value) > threshold and (1 if event.value > 0 else -1) == direction
elif input_type == "hat" and event.type == pygame.JOYHATMOTION:
hat_value = mapping.get("value")
if isinstance(hat_value, list):
@@ -204,6 +224,28 @@ def is_input_matched(event, action_name):
return event.value == hat_value
elif input_type == "mouse" and event.type == pygame.MOUSEBUTTONDOWN:
return event.button == mapping.get("button")
# Fallback clavier pour dépannage (fonctionne toujours même avec manette configurée)
if event.type == pygame.KEYDOWN:
keyboard_fallback = {
"up": pygame.K_UP,
"down": pygame.K_DOWN,
"left": pygame.K_LEFT,
"right": pygame.K_RIGHT,
"confirm": pygame.K_RETURN,
"cancel": pygame.K_ESCAPE,
"start": pygame.K_RALT,
"filter": pygame.K_f,
"history": pygame.K_h,
"clear_history": pygame.K_DELETE,
"delete": pygame.K_d,
"space": pygame.K_SPACE,
"page_up": pygame.K_PAGEUP,
"page_down": pygame.K_PAGEDOWN,
}
if action_name in keyboard_fallback:
return event.key == keyboard_fallback[action_name]
return False
def _launch_next_queued_download():
@@ -440,8 +482,15 @@ def handle_controls(event, sources, joystick, screen):
if config.platforms:
config.current_platform = config.selected_platform
config.games = load_games(config.platforms[config.current_platform])
config.filtered_games = config.games
config.filter_active = False
# Apply saved filters automatically if any
if config.game_filter_obj and config.game_filter_obj.is_active():
config.filtered_games = config.game_filter_obj.apply_filters(config.games)
config.filter_active = True
else:
config.filtered_games = config.games
config.filter_active = False
config.current_game = 0
config.scroll_offset = 0
draw_validation_transition(screen, config.current_platform)
@@ -469,6 +518,8 @@ def handle_controls(event, sources, joystick, screen):
max_row = len(keyboard_layout) - 1
max_col = len(keyboard_layout[row]) - 1
if is_input_matched(event, "up"):
if row == 0: # if you are in the first row and press UP jump to last row
row = max_row + (1 if col <= 5 else 0)
if row > 0:
config.selected_key = (row - 1, min(col, len(keyboard_layout[row - 1]) - 1))
config.repeat_action = "up"
@@ -477,6 +528,8 @@ def handle_controls(event, sources, joystick, screen):
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
config.needs_redraw = True
elif is_input_matched(event, "down"):
if (col <= 5 and row == max_row) or (col > 5 and row == max_row-1): # if you are in the last row and press DOWN jump to first row
row = -1
if row < max_row:
config.selected_key = (row + 1, min(col, len(keyboard_layout[row + 1]) - 1))
config.repeat_action = "down"
@@ -485,6 +538,8 @@ def handle_controls(event, sources, joystick, screen):
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
config.needs_redraw = True
elif is_input_matched(event, "left"):
if col == 0: # if you are in the first col and press LEFT jump to last col
col = max_col + 1
if col > 0:
config.selected_key = (row, col - 1)
config.repeat_action = "left"
@@ -493,6 +548,8 @@ def handle_controls(event, sources, joystick, screen):
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
config.needs_redraw = True
elif is_input_matched(event, "right"):
if col == max_col: # if you are in the last col and press RIGHT jump to first col
col = -1
if col < max_col:
config.selected_key = (row, col + 1)
config.repeat_action = "right"
@@ -593,41 +650,39 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
elif is_input_matched(event, "page_up"):
config.current_game = max(0, config.current_game - config.visible_games)
config.repeat_action = None
config.repeat_key = None
config.repeat_start_time = 0
config.repeat_last_action = current_time
update_key_state("page_up", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "left"):
config.current_game = max(0, config.current_game - config.visible_games)
config.repeat_action = None
config.repeat_key = None
config.repeat_start_time = 0
config.repeat_last_action = current_time
update_key_state("left", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "page_down"):
config.current_game = min(len(games) - 1, config.current_game + config.visible_games)
config.repeat_action = None
config.repeat_key = None
config.repeat_start_time = 0
config.repeat_last_action = current_time
update_key_state("page_down", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "right"):
config.current_game = min(len(games) - 1, config.current_game + config.visible_games)
config.repeat_action = None
config.repeat_key = None
config.repeat_start_time = 0
config.repeat_last_action = current_time
update_key_state("right", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "filter"):
config.search_mode = True
config.search_query = ""
config.filtered_games = config.games
config.current_game = 0
config.scroll_offset = 0
config.selected_key = (0, 0)
# Afficher le menu de choix entre recherche et filtrage avancé
config.menu_state = "filter_menu_choice"
config.selected_filter_choice = 0
config.previous_menu_state = "game"
config.needs_redraw = True
logger.debug("Entrée en mode recherche")
logger.debug("Ouverture du menu de filtrage")
elif is_input_matched(event, "history"):
config.menu_state = "history"
config.needs_redraw = True
@@ -954,6 +1009,8 @@ def handle_controls(event, sources, joystick, screen):
ext = os.path.splitext(actual_filename)[1].lower()
if ext in ['.zip', '.rar']:
options.append("extract_archive")
elif ext == '.txt':
options.append("open_file")
elif status in ["Erreur", "Error", "Canceled"]:
options.append("error_info")
options.append("retry")
@@ -996,6 +1053,30 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
logger.debug(f"Affichage du dossier de téléchargement pour {game_name}")
elif selected_option == "open_file":
# Ouvrir le fichier texte
if actual_path and os.path.exists(actual_path):
try:
with open(actual_path, 'r', encoding='utf-8', errors='replace') as f:
content = f.read()
config.text_file_content = content
config.text_file_name = actual_filename
config.text_file_scroll_offset = 0
config.previous_menu_state = "history_game_options"
config.menu_state = "text_file_viewer"
config.needs_redraw = True
logger.debug(f"Ouverture du fichier texte: {actual_filename}")
except Exception as e:
logger.error(f"Erreur lors de l'ouverture du fichier texte: {e}")
config.menu_state = "error"
config.error_message = f"Erreur lors de l'ouverture du fichier: {str(e)}"
config.needs_redraw = True
else:
logger.error(f"Fichier texte introuvable: {actual_path}")
config.menu_state = "error"
config.error_message = "Fichier introuvable"
config.needs_redraw = True
elif selected_option == "extract_archive":
# L'option n'apparaît que si le fichier existe, pas besoin de re-vérifier
config.previous_menu_state = "history_game_options"
@@ -1147,6 +1228,128 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
# Affichage détails erreur
# Visualiseur de fichiers texte
elif config.menu_state == "text_file_viewer":
content = getattr(config, 'text_file_content', '')
if content:
lines = content.split('\n')
line_height = config.small_font.get_height() + 2
# Calculer le nombre de lignes visibles (approximation)
controls_y = config.screen_height - int(config.screen_height * 0.037)
margin = 40
header_height = 60
content_area_height = controls_y - 2 * margin - 10 - header_height - 20
visible_lines = int(content_area_height / line_height)
scroll_offset = getattr(config, 'text_file_scroll_offset', 0)
max_scroll = max(0, len(lines) - visible_lines)
if is_input_matched(event, "up"):
config.text_file_scroll_offset = max(0, scroll_offset - 1)
update_key_state("up", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "down"):
config.text_file_scroll_offset = min(max_scroll, scroll_offset + 1)
update_key_state("down", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "page_up"):
config.text_file_scroll_offset = max(0, scroll_offset - visible_lines)
update_key_state("page_up", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "page_down"):
config.text_file_scroll_offset = min(max_scroll, scroll_offset + visible_lines)
update_key_state("page_down", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "cancel") or is_input_matched(event, "confirm"):
config.menu_state = validate_menu_state(config.previous_menu_state)
config.needs_redraw = True
else:
# Si pas de contenu, retourner au menu précédent
if is_input_matched(event, "cancel") or is_input_matched(event, "confirm"):
config.menu_state = validate_menu_state(config.previous_menu_state)
config.needs_redraw = True
# Visualiseur de fichiers texte
elif config.menu_state == "text_file_viewer":
content = getattr(config, 'text_file_content', '')
if content:
from utils import wrap_text
# Calculer les dimensions
controls_y = config.screen_height - int(config.screen_height * 0.037)
margin = 40
header_height = 60
rect_width = config.screen_width - 2 * margin
content_area_height = controls_y - 2 * margin - 10 - header_height - 20
max_width = rect_width - 60
# Diviser le contenu en lignes et appliquer le word wrap
original_lines = content.split('\n')
wrapped_lines = []
for original_line in original_lines:
if original_line.strip(): # Si la ligne n'est pas vide
wrapped = wrap_text(original_line, config.small_font, max_width)
wrapped_lines.extend(wrapped)
else: # Ligne vide
wrapped_lines.append('')
line_height = config.small_font.get_height() + 2
visible_lines = int(content_area_height / line_height)
scroll_offset = getattr(config, 'text_file_scroll_offset', 0)
max_scroll = max(0, len(wrapped_lines) - visible_lines)
if is_input_matched(event, "up"):
config.text_file_scroll_offset = max(0, scroll_offset - 1)
update_key_state("up", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "down"):
config.text_file_scroll_offset = min(max_scroll, scroll_offset + 1)
update_key_state("down", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "page_up"):
config.text_file_scroll_offset = max(0, scroll_offset - visible_lines)
update_key_state("page_up", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "page_down"):
config.text_file_scroll_offset = min(max_scroll, scroll_offset + visible_lines)
update_key_state("page_down", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "cancel") or is_input_matched(event, "confirm"):
config.menu_state = validate_menu_state(config.previous_menu_state)
config.needs_redraw = True
else:
# Si pas de contenu, retourner au menu précédent
if is_input_matched(event, "cancel") or is_input_matched(event, "confirm"):
config.menu_state = validate_menu_state(config.previous_menu_state)
config.needs_redraw = True
elif config.menu_state == "history_error_details":
if is_input_matched(event, "confirm") or is_input_matched(event, "cancel"):
config.menu_state = validate_menu_state(config.previous_menu_state)
@@ -1415,7 +1618,7 @@ def handle_controls(event, sources, joystick, screen):
# Sous-menu Display
elif config.menu_state == "pause_display_menu":
sel = getattr(config, 'pause_display_selection', 0)
total = 8 # layout, font size, font family, unsupported, unknown, hide premium, filter, back
total = 6 # layout, font size, footer font size, font family, allow unknown extensions, back
if is_input_matched(event, "up"):
config.pause_display_selection = (sel - 1) % total
config.needs_redraw = True
@@ -1423,7 +1626,6 @@ def handle_controls(event, sources, joystick, screen):
config.pause_display_selection = (sel + 1) % total
config.needs_redraw = True
elif is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm"):
sel = getattr(config, 'pause_display_selection', 0)
# 0 layout cycle
if sel == 0 and (is_input_matched(event, "left") or is_input_matched(event, "right")):
layouts = [(3,3),(3,4),(4,3),(4,4)]
@@ -1439,14 +1641,12 @@ def handle_controls(event, sources, joystick, screen):
logger.error(f"Erreur set_display_grid: {e}")
config.GRID_COLS = new_cols
config.GRID_ROWS = new_rows
# Redémarrage automatique
# Afficher un popup indiquant que le changement sera effectif après redémarrage
try:
config.menu_state = "restart_popup"
config.popup_message = _("popup_restarting") if _ else "Restarting..."
config.popup_timer = 2000
restart_application(2000)
config.popup_message = _("popup_layout_changed_restart_required") if _ else "Layout changed. Restart required to apply."
config.popup_timer = 3000
except Exception as e:
logger.error(f"Erreur restart après layout: {e}")
logger.error(f"Erreur popup layout: {e}")
config.needs_redraw = True
# 1 font size
elif sel == 1 and (is_input_matched(event, "left") or is_input_matched(event, "right")):
@@ -1466,8 +1666,21 @@ def handle_controls(event, sources, joystick, screen):
except Exception as e:
logger.error(f"Erreur init polices: {e}")
config.needs_redraw = True
# 2 font family cycle
elif sel == 2 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
# 2 footer font size
elif sel == 2 and (is_input_matched(event, "left") or is_input_matched(event, "right")):
from accessibility import update_footer_font_scale
footer_opts = getattr(config, 'footer_font_scale_options', [0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0])
idx = getattr(config, 'current_footer_font_scale_index', 3)
idx = max(0, idx-1) if is_input_matched(event, "left") else min(len(footer_opts)-1, idx+1)
if idx != getattr(config, 'current_footer_font_scale_index', 3):
config.current_footer_font_scale_index = idx
try:
update_footer_font_scale()
except Exception as e:
logger.error(f"Erreur update footer font scale: {e}")
config.needs_redraw = True
# 3 font family cycle
elif sel == 3 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
try:
families = getattr(config, 'FONT_FAMILIES', ["pixel"]) or ["pixel"]
current = get_font_family()
@@ -1500,17 +1713,6 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur changement font family: {e}")
# 3 unsupported toggle
elif sel == 3 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
try:
current = get_show_unsupported_platforms()
new_val = set_show_unsupported_platforms(not current)
load_sources()
config.popup_message = _("menu_show_unsupported_enabled") if new_val else _("menu_show_unsupported_disabled")
config.popup_timer = 3000
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur toggle unsupported: {e}")
# 4 allow unknown extensions
elif sel == 4 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
try:
@@ -1521,25 +1723,8 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur toggle allow_unknown_extensions: {e}")
# 5 hide premium systems
elif sel == 5 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")):
try:
cur = get_hide_premium_systems()
new_val = set_hide_premium_systems(not cur)
config.popup_message = ("Premium hidden" if new_val else "Premium visible") if _ is None else (_("popup_hide_premium_on") if new_val else _("popup_hide_premium_off"))
config.popup_timer = 2500
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur toggle hide_premium_systems: {e}")
# 6 filter platforms
elif sel == 6 and (is_input_matched(event, "confirm") or is_input_matched(event, "right")):
config.filter_return_to = "pause_display_menu"
config.menu_state = "filter_platforms"
config.selected_filter_index = 0
config.filter_platforms_scroll_offset = 0
config.needs_redraw = True
# 7 back
elif sel == 7 and (is_input_matched(event, "confirm")):
# 5 back
elif sel == 5 and is_input_matched(event, "confirm"):
config.menu_state = "pause_menu"
config.last_state_change_time = pygame.time.get_ticks()
config.needs_redraw = True
@@ -1551,7 +1736,7 @@ def handle_controls(event, sources, joystick, screen):
# Sous-menu Games
elif config.menu_state == "pause_games_menu":
sel = getattr(config, 'pause_games_selection', 0)
total = 4 # history, source, redownload, back
total = 7 # history, source, redownload, unsupported, hide premium, filter, back
if is_input_matched(event, "up"):
config.pause_games_selection = (sel - 1) % total
config.needs_redraw = True
@@ -1559,7 +1744,6 @@ def handle_controls(event, sources, joystick, screen):
config.pause_games_selection = (sel + 1) % total
config.needs_redraw = True
elif is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right"):
sel = getattr(config, 'pause_games_selection', 0)
if sel == 0 and is_input_matched(event, "confirm"): # history
config.history = load_history()
config.current_history_item = 0
@@ -1587,7 +1771,32 @@ def handle_controls(event, sources, joystick, screen):
config.menu_state = "reload_games_data"
config.redownload_confirm_selection = 0
config.needs_redraw = True
elif sel == 3 and is_input_matched(event, "confirm"): # back
elif sel == 3 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # unsupported toggle
try:
current = get_show_unsupported_platforms()
new_val = set_show_unsupported_platforms(not current)
load_sources()
config.popup_message = _("menu_show_unsupported_enabled") if new_val else _("menu_show_unsupported_disabled")
config.popup_timer = 3000
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur toggle unsupported: {e}")
elif sel == 4 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # hide premium
try:
cur = get_hide_premium_systems()
new_val = set_hide_premium_systems(not cur)
config.popup_message = ("Premium hidden" if new_val else "Premium visible") if _ is None else (_("popup_hide_premium_on") if new_val else _("popup_hide_premium_off"))
config.popup_timer = 2500
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur toggle hide_premium_systems: {e}")
elif sel == 5 and is_input_matched(event, "confirm"): # filter platforms
config.filter_return_to = "pause_games_menu"
config.menu_state = "filter_platforms"
config.selected_filter_index = 0
config.filter_platforms_scroll_offset = 0
config.needs_redraw = True
elif sel == 6 and is_input_matched(event, "confirm"): # back
config.menu_state = "pause_menu"
config.last_state_change_time = pygame.time.get_ticks()
config.needs_redraw = True
@@ -1613,7 +1822,6 @@ def handle_controls(event, sources, joystick, screen):
config.pause_settings_selection = (sel + 1) % total
config.needs_redraw = True
elif is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right"):
sel = getattr(config, 'pause_settings_selection', 0)
# Option 0: Music toggle
if sel == 0 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")):
config.music_enabled = not config.music_enabled
@@ -1691,7 +1899,6 @@ def handle_controls(event, sources, joystick, screen):
config.display_menu_selection = (sel + 1) % 5
config.needs_redraw = True
elif is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm"):
sel = getattr(config, 'display_menu_selection', 0)
# 0: layout change
if sel == 0 and (is_input_matched(event, "left") or is_input_matched(event, "right")):
layouts = [(3,3),(3,4),(4,3),(4,4)]
@@ -1879,48 +2086,312 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
logger.debug("Annulation de la sélection de langue, retour au menu pause")
# Menu de choix filtrage
elif config.menu_state == "filter_menu_choice":
if is_input_matched(event, "up"):
config.selected_filter_choice = (config.selected_filter_choice - 1) % 2
config.needs_redraw = True
elif is_input_matched(event, "down"):
config.selected_filter_choice = (config.selected_filter_choice + 1) % 2
config.needs_redraw = True
elif is_input_matched(event, "confirm"):
if config.selected_filter_choice == 0:
# Recherche par nom (mode existant)
config.search_mode = True
config.search_query = ""
config.filtered_games = config.games
config.current_game = 0
config.scroll_offset = 0
config.selected_key = (0, 0)
config.menu_state = "game"
config.needs_redraw = True
logger.debug("Entrée en mode recherche par nom")
else:
# Filtrage avancé
from game_filters import GameFilters
from rgsx_settings import load_game_filters
# Initialiser le filtre
if not hasattr(config, 'game_filter_obj'):
config.game_filter_obj = GameFilters()
filter_dict = load_game_filters()
if filter_dict:
config.game_filter_obj.load_from_dict(filter_dict)
config.menu_state = "filter_advanced"
config.selected_filter_option = 0
config.needs_redraw = True
logger.debug("Entrée en filtrage avancé")
elif is_input_matched(event, "cancel"):
config.menu_state = "game"
config.needs_redraw = True
logger.debug("Retour à la liste des jeux")
# Filtrage avancé
elif config.menu_state == "filter_advanced":
from game_filters import GameFilters
from rgsx_settings import save_game_filters
# Initialiser le filtre si nécessaire
if not hasattr(config, 'game_filter_obj'):
config.game_filter_obj = GameFilters()
from rgsx_settings import load_game_filters
filter_dict = load_game_filters()
if filter_dict:
config.game_filter_obj.load_from_dict(filter_dict)
# Construire la liste linéaire des éléments sélectionnables (pour simplifier l'indexation)
# Régions individuelles
num_regions = len(GameFilters.REGIONS)
# Options toggle/button
num_other_options = 3 # hide_non_release, one_rom_per_game, priority_config
# Boutons en bas
num_buttons = 3 # apply, reset, back
total_items = num_regions + num_other_options + num_buttons
if is_input_matched(event, "up"):
# Navigation verticale dans la grille ou entre sections
if config.selected_filter_option < num_regions:
# Dans la grille des régions (3 colonnes)
if config.selected_filter_option >= 3:
# Monter d'une ligne
config.selected_filter_option -= 3
else:
# Déjà en haut, aller aux boutons
config.selected_filter_option = total_items - 2 # Bouton du milieu (reset)
else:
# Dans les options ou boutons, monter normalement
config.selected_filter_option = (config.selected_filter_option - 1) % total_items
config.needs_redraw = True
update_key_state("up", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
elif is_input_matched(event, "down"):
# Navigation verticale
if config.selected_filter_option < num_regions:
# Dans la grille des régions
if config.selected_filter_option + 3 < num_regions:
# Descendre d'une ligne
config.selected_filter_option += 3
else:
# Aller aux autres options
config.selected_filter_option = num_regions
else:
# Dans les options ou boutons, descendre normalement
config.selected_filter_option = (config.selected_filter_option + 1) % total_items
config.needs_redraw = True
update_key_state("down", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
elif is_input_matched(event, "left"):
# Navigation horizontale
if config.selected_filter_option < num_regions:
# Dans la grille des régions
if config.selected_filter_option % 3 > 0:
config.selected_filter_option -= 1
config.needs_redraw = True
elif config.selected_filter_option >= num_regions + num_other_options:
# Dans les boutons en bas
button_idx = config.selected_filter_option - (num_regions + num_other_options)
button_idx = (button_idx - 1) % num_buttons
config.selected_filter_option = num_regions + num_other_options + button_idx
config.needs_redraw = True
elif is_input_matched(event, "right"):
# Navigation horizontale
if config.selected_filter_option < num_regions:
# Dans la grille des régions
if config.selected_filter_option % 3 < 2 and config.selected_filter_option + 1 < num_regions:
config.selected_filter_option += 1
config.needs_redraw = True
elif config.selected_filter_option >= num_regions + num_other_options:
# Dans les boutons en bas
button_idx = config.selected_filter_option - (num_regions + num_other_options)
button_idx = (button_idx + 1) % num_buttons
config.selected_filter_option = num_regions + num_other_options + button_idx
config.needs_redraw = True
elif is_input_matched(event, "confirm"):
# Déterminer quel élément a été sélectionné
if config.selected_filter_option < num_regions:
# C'est une région
region = GameFilters.REGIONS[config.selected_filter_option]
current_state = config.game_filter_obj.region_filters.get(region, 'include')
if current_state == 'include':
config.game_filter_obj.region_filters[region] = 'exclude'
else:
config.game_filter_obj.region_filters[region] = 'include'
config.needs_redraw = True
logger.debug(f"Filtre région {region} modifié: {config.game_filter_obj.region_filters[region]}")
elif config.selected_filter_option < num_regions + num_other_options:
# C'est une autre option
option_idx = config.selected_filter_option - num_regions
if option_idx == 0:
# hide_non_release
config.game_filter_obj.hide_non_release = not config.game_filter_obj.hide_non_release
config.needs_redraw = True
logger.debug("Toggle hide_non_release modifié")
elif option_idx == 1:
# one_rom_per_game
config.game_filter_obj.one_rom_per_game = not config.game_filter_obj.one_rom_per_game
config.needs_redraw = True
logger.debug("Toggle one_rom_per_game modifié")
elif option_idx == 2:
# priority_config
config.menu_state = "filter_priority_config"
config.selected_priority_index = 0
config.needs_redraw = True
logger.debug("Ouverture configuration priorité régions")
else:
# C'est un bouton
button_idx = config.selected_filter_option - (num_regions + num_other_options)
if button_idx == 0:
# Apply
save_game_filters(config.game_filter_obj.to_dict())
# Appliquer aux jeux actuels
if config.game_filter_obj.is_active():
config.filtered_games = config.game_filter_obj.apply_filters(config.games)
config.filter_active = True
else:
config.filtered_games = config.games
config.filter_active = False
config.current_game = 0
config.scroll_offset = 0
config.menu_state = "game"
config.needs_redraw = True
logger.debug("Filtres appliqués")
elif button_idx == 1:
# Reset
config.game_filter_obj.reset()
save_game_filters(config.game_filter_obj.to_dict())
config.filtered_games = config.games
config.filter_active = False
config.needs_redraw = True
logger.debug("Filtres réinitialisés")
elif button_idx == 2:
# Back
config.menu_state = "game"
config.needs_redraw = True
logger.debug("Retour sans appliquer les filtres")
elif is_input_matched(event, "cancel"):
config.menu_state = "game"
config.needs_redraw = True
logger.debug("Annulation du filtrage avancé")
# Configuration priorité régions
elif config.menu_state == "filter_priority_config":
from game_filters import GameFilters
from rgsx_settings import save_game_filters
if not hasattr(config, 'game_filter_obj'):
config.game_filter_obj = GameFilters()
priority_list = config.game_filter_obj.region_priority
total_items = len(priority_list) + 1 # +1 pour le bouton Back
if not hasattr(config, 'selected_priority_index'):
config.selected_priority_index = 0
if is_input_matched(event, "up"):
config.selected_priority_index = (config.selected_priority_index - 1) % total_items
config.needs_redraw = True
elif is_input_matched(event, "down"):
config.selected_priority_index = (config.selected_priority_index + 1) % total_items
config.needs_redraw = True
elif is_input_matched(event, "confirm"):
if config.selected_priority_index >= len(priority_list):
# Bouton Back : retour au menu filtrage avancé
save_game_filters(config.game_filter_obj.to_dict())
config.menu_state = "filter_advanced"
config.needs_redraw = True
logger.debug("Retour au filtrage avancé")
elif is_input_matched(event, "left") and config.selected_priority_index < len(priority_list):
# Monter la région dans la priorité
idx = config.selected_priority_index
if idx > 0:
priority_list[idx], priority_list[idx-1] = priority_list[idx-1], priority_list[idx]
config.selected_priority_index = idx - 1
config.needs_redraw = True
logger.debug(f"Priorité modifiée: {priority_list}")
elif is_input_matched(event, "right") and config.selected_priority_index < len(priority_list):
# Descendre la région dans la priorité
idx = config.selected_priority_index
if idx < len(priority_list) - 1:
priority_list[idx], priority_list[idx+1] = priority_list[idx+1], priority_list[idx]
config.selected_priority_index = idx + 1
config.needs_redraw = True
logger.debug(f"Priorité modifiée: {priority_list}")
elif is_input_matched(event, "cancel"):
# Retour sans sauvegarder
config.menu_state = "filter_advanced"
config.needs_redraw = True
logger.debug("Annulation configuration priorité")
# Menu filtre plateformes
elif config.menu_state == "filter_platforms":
total_items = len(config.filter_platforms_selection)
action_buttons = 4
extended_max = total_items + action_buttons - 1
# Indices: 0-3 = boutons, 4+ = liste des systèmes
extended_max = action_buttons + total_items - 1
if is_input_matched(event, "up"):
if config.selected_filter_index > 0:
config.selected_filter_index -= 1
config.needs_redraw = True
else:
# Wrap vers les boutons (premier bouton) depuis le haut
if total_items > 0:
config.selected_filter_index = total_items
config.needs_redraw = True
# Wrap vers le bas (dernière ligne de la liste)
config.selected_filter_index = extended_max
config.needs_redraw = True
# Activer la répétition automatique
update_key_state("up", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
elif is_input_matched(event, "down"):
if config.selected_filter_index < extended_max:
config.selected_filter_index += 1
config.needs_redraw = True
else:
# Wrap retour en haut de la liste
# Wrap retour en haut (premier bouton)
config.selected_filter_index = 0
config.needs_redraw = True
# Activer la répétition automatique
update_key_state("down", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
elif is_input_matched(event, "left"):
if config.selected_filter_index >= total_items:
if config.selected_filter_index > total_items:
# Navigation gauche/droite uniquement pour les boutons (indices 0-3)
if config.selected_filter_index < action_buttons:
if config.selected_filter_index > 0:
config.selected_filter_index -= 1
config.needs_redraw = True
# sinon ignorer
# sinon ignorer (dans la liste)
elif is_input_matched(event, "right"):
if config.selected_filter_index >= total_items:
if config.selected_filter_index < extended_max:
# Navigation gauche/droite uniquement pour les boutons (indices 0-3)
if config.selected_filter_index < action_buttons:
if config.selected_filter_index < action_buttons - 1:
config.selected_filter_index += 1
config.needs_redraw = True
# sinon ignorer
# sinon ignorer (dans la liste)
elif is_input_matched(event, "confirm"):
if config.selected_filter_index < total_items:
name, hidden = config.filter_platforms_selection[config.selected_filter_index]
config.filter_platforms_selection[config.selected_filter_index] = (name, not hidden)
config.filter_platforms_dirty = True
config.needs_redraw = True
else:
btn_idx = config.selected_filter_index - total_items
# Indices 0-3 = boutons, 4+ = liste
if config.selected_filter_index < action_buttons:
# Action sur un bouton
btn_idx = config.selected_filter_index
settings = load_rgsx_settings()
if btn_idx == 0: # all visible
config.filter_platforms_selection = [(n, False) for n, _ in config.filter_platforms_selection]
@@ -1966,6 +2437,14 @@ def handle_controls(event, sources, joystick, screen):
config.selected_option = 5
config.filter_return_to = None
config.needs_redraw = True
else:
# Action sur un élément de la liste (indices >= action_buttons)
list_index = config.selected_filter_index - action_buttons
if list_index < total_items:
name, hidden = config.filter_platforms_selection[list_index]
config.filter_platforms_selection[list_index] = (name, not hidden)
config.filter_platforms_dirty = True
config.needs_redraw = True
elif is_input_matched(event, "cancel"):
target = getattr(config, 'filter_return_to', 'pause_menu')
config.menu_state = target
@@ -1982,13 +2461,32 @@ def handle_controls(event, sources, joystick, screen):
# Gestion des relâchements de touches
if event.type == pygame.KEYUP:
# Vérifier quelle touche a été relâchée
for action_name in ["up", "down", "left", "right", "confirm", "cancel"]:
if config.controls_config.get(action_name, {}).get("type") == "key" and \
# Définir le mapping clavier (même que dans is_input_matched)
keyboard_fallback = {
"up": pygame.K_UP,
"down": pygame.K_DOWN,
"left": pygame.K_LEFT,
"right": pygame.K_RIGHT,
"confirm": pygame.K_RETURN,
"cancel": pygame.K_ESCAPE,
"page_up": pygame.K_PAGEUP,
"page_down": pygame.K_PAGEDOWN,
}
for action_name in ["up", "down", "left", "right", "page_up", "page_down", "confirm", "cancel"]:
# Vérifier d'abord le keyboard_fallback
if action_name in keyboard_fallback and keyboard_fallback[action_name] == event.key:
update_key_state(action_name, False)
# Sinon vérifier la config normale
elif config.controls_config.get(action_name, {}).get("type") == "key" and \
config.controls_config.get(action_name, {}).get("key") == event.key:
update_key_state(action_name, False)
# Gestion spéciale pour confirm dans le menu game
if action_name == "confirm" and config.menu_state == "game":
# Gestion spéciale pour confirm dans le menu game (ne dépend pas du key_state)
if action_name == "confirm" and config.menu_state == "game" and \
((action_name in keyboard_fallback and keyboard_fallback[action_name] == event.key) or \
(config.controls_config.get(action_name, {}).get("type") == "key" and \
config.controls_config.get(action_name, {}).get("key") == event.key)):
press_duration = current_time - config.confirm_press_start_time
# Si appui court (< 2 secondes) et pas déjà traité par l'appui long
if press_duration < config.confirm_long_press_threshold and not config.confirm_long_press_triggered:
@@ -2076,12 +2574,14 @@ def handle_controls(event, sources, joystick, screen):
elif event.type == pygame.JOYBUTTONUP:
# Vérifier quel bouton a été relâché
for action_name in ["up", "down", "left", "right", "confirm", "cancel"]:
for action_name in ["up", "down", "left", "right", "page_up", "page_down", "confirm", "cancel"]:
if config.controls_config.get(action_name, {}).get("type") == "button" and \
config.controls_config.get(action_name, {}).get("button") == event.button:
update_key_state(action_name, False)
# Vérifier que cette action était bien activée par un bouton gamepad
if action_name in key_states and key_states[action_name].get("event_type") == pygame.JOYBUTTONDOWN:
update_key_state(action_name, False)
# Gestion spéciale pour confirm dans le menu game
# Gestion spéciale pour confirm dans le menu game (ne dépend pas du key_state)
if action_name == "confirm" and config.menu_state == "game":
press_duration = current_time - config.confirm_press_start_time
# Si appui court (< 2 secondes) et pas déjà traité par l'appui long
@@ -2168,18 +2668,31 @@ def handle_controls(event, sources, joystick, screen):
config.confirm_press_start_time = 0
config.confirm_long_press_triggered = False
elif event.type == pygame.JOYAXISMOTION and abs(event.value) < 0.5:
# Vérifier quel axe a été relâché
for action_name in ["up", "down", "left", "right"]:
if config.controls_config.get(action_name, {}).get("type") == "axis" and \
config.controls_config.get(action_name, {}).get("axis") == event.axis:
update_key_state(action_name, False)
elif event.type == pygame.JOYAXISMOTION:
# Détection de relâchement d'axe
# Pour les triggers Xbox (axes 4 et 5), relâché = retour à -1.0
# Pour les autres axes, relâché = proche de 0
is_released = False
if event.axis in [4, 5]: # Triggers Xbox
is_released = event.value < 0.5 # Relâché si < 0.5 (pas appuyé)
else: # Autres axes
is_released = abs(event.value) < 0.5
if is_released:
for action_name in ["up", "down", "left", "right", "page_up", "page_down"]:
if config.controls_config.get(action_name, {}).get("type") == "axis" and \
config.controls_config.get(action_name, {}).get("axis") == event.axis:
# Vérifier que cette action était bien activée par cet axe
if action_name in key_states and key_states[action_name].get("event_type") == pygame.JOYAXISMOTION:
update_key_state(action_name, False)
elif event.type == pygame.JOYHATMOTION and event.value == (0, 0):
# Vérifier quel hat a été relâché
for action_name in ["up", "down", "left", "right"]:
for action_name in ["up", "down", "left", "right", "page_up", "page_down"]:
if config.controls_config.get(action_name, {}).get("type") == "hat":
update_key_state(action_name, False)
# Vérifier que cette action était bien activée par un hat
if action_name in key_states and key_states[action_name].get("event_type") == pygame.JOYHATMOTION:
update_key_state(action_name, False)
return action
@@ -2191,11 +2704,9 @@ def update_key_state(action, pressed, event_type=None, event_value=None):
if pressed:
# La touche vient d'être pressée
if action not in key_states:
# Ajouter un délai initial pour éviter les doubles actions sur appui court
initial_debounce = REPEAT_ACTION_DEBOUNCE
key_states[action] = {
"pressed": True,
"first_press_time": current_time + initial_debounce, # Ajouter un délai initial
"first_press_time": current_time,
"last_repeat_time": current_time,
"event_type": event_type,
"event_value": event_value
@@ -2262,4 +2773,4 @@ def get_emergency_controls():
# manette basique
"confirm_joy": {"type": "button", "button": 0},
"cancel_joy": {"type": "button", "button": 1},
}
}

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,237 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Module de filtrage des jeux pour RGSX
Partagé entre l'interface graphique et l'interface web
"""
import re
import logging
from typing import List, Tuple, Dict, Any
logger = logging.getLogger(__name__)
class GameFilters:
"""Classe pour gérer les filtres de jeux"""
# Régions disponibles
REGIONS = ['USA', 'Canada', 'Europe', 'France', 'Germany', 'Japan', 'Korea', 'World', 'Other']
def __init__(self):
# Initialiser toutes les régions en mode 'include' par défaut
self.region_filters = {region: 'include' for region in self.REGIONS}
self.hide_non_release = False
self.one_rom_per_game = False
self.regex_mode = False
self.region_priority = ['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other']
def load_from_dict(self, filter_dict: Dict[str, Any]):
"""Charge les filtres depuis un dictionnaire (depuis settings)"""
loaded_region_filters = filter_dict.get('region_filters', {})
# Initialiser toutes les régions en 'include' par défaut, puis appliquer celles chargées
self.region_filters = {region: 'include' for region in self.REGIONS}
self.region_filters.update(loaded_region_filters)
self.hide_non_release = filter_dict.get('hide_non_release', False)
self.one_rom_per_game = filter_dict.get('one_rom_per_game', False)
self.regex_mode = filter_dict.get('regex_mode', False)
self.region_priority = filter_dict.get('region_priority',
['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other'])
def to_dict(self) -> Dict[str, Any]:
"""Convertit les filtres en dictionnaire (pour sauvegarder dans settings)"""
return {
'region_filters': self.region_filters,
'hide_non_release': self.hide_non_release,
'one_rom_per_game': self.one_rom_per_game,
'regex_mode': self.regex_mode,
'region_priority': self.region_priority
}
def is_active(self) -> bool:
"""Vérifie si des filtres sont actifs (au moins une région en exclude ou options activées)"""
has_exclude = any(state == 'exclude' for state in self.region_filters.values())
return (has_exclude or
self.hide_non_release or
self.one_rom_per_game)
def reset(self):
"""Réinitialise tous les filtres (toutes les régions en include)"""
self.region_filters = {region: 'include' for region in self.REGIONS}
self.hide_non_release = False
self.one_rom_per_game = False
self.regex_mode = False
@staticmethod
def get_game_regions(game_name: str) -> List[str]:
"""Extrait les régions d'un nom de jeu"""
name = game_name.upper()
regions = []
# Patterns de région communs
if 'USA' in name or 'US)' in name:
regions.append('USA')
if 'CANADA' in name or 'CA)' in name:
regions.append('Canada')
if 'EUROPE' in name or 'EU)' in name:
regions.append('Europe')
if 'FRANCE' in name or 'FR)' in name:
regions.append('France')
if 'GERMANY' in name or 'DE)' in name or 'GER)' in name:
regions.append('Germany')
if 'JAPAN' in name or 'JP)' in name or 'JPN)' in name:
regions.append('Japan')
if 'KOREA' in name or 'KR)' in name or 'KOR)' in name:
regions.append('Korea')
if 'WORLD' in name:
regions.append('World')
# Autres régions
if re.search(r'\b(AUSTRALIA|ASIA|KOREA|BRAZIL|CHINA|RUSSIA|SCANDINAVIA|'
r'SPAIN|FRANCE|GERMANY|ITALY|CANADA)\b', name):
if 'CANADA' in name:
regions.append('Canada')
else:
regions.append('Other')
# Si aucune région trouvée
if not regions:
regions.append('Other')
return regions
@staticmethod
def is_non_release_game(game_name: str) -> bool:
"""Vérifie si un jeu est une version non-release (demo, beta, proto)"""
name = game_name.upper()
non_release_patterns = [
r'\([^\)]*BETA[^\)]*\)',
r'\([^\)]*DEMO[^\)]*\)',
r'\([^\)]*PROTO[^\)]*\)',
r'\([^\)]*SAMPLE[^\)]*\)',
r'\([^\)]*KIOSK[^\)]*\)',
r'\([^\)]*PREVIEW[^\)]*\)',
r'\([^\)]*TEST[^\)]*\)',
r'\([^\)]*DEBUG[^\)]*\)',
r'\([^\)]*ALPHA[^\)]*\)',
r'\([^\)]*PRE-RELEASE[^\)]*\)',
r'\([^\)]*PRERELEASE[^\)]*\)',
r'\([^\)]*UNFINISHED[^\)]*\)',
r'\([^\)]*WIP[^\)]*\)',
r'\[[^\]]*BETA[^\]]*\]',
r'\[[^\]]*DEMO[^\]]*\]',
r'\[[^\]]*TEST[^\]]*\]'
]
return any(re.search(pattern, name) for pattern in non_release_patterns)
@staticmethod
def get_base_game_name(game_name: str) -> str:
"""Obtient le nom de base du jeu (sans régions, versions, etc.)"""
base = game_name
# Supprimer extensions
base = re.sub(r'\.(zip|7z|rar|gz|iso)$', '', base, flags=re.IGNORECASE)
# Extraire info disque si présent
disc_info = ''
disc_match = (re.search(r'\(Dis[ck]\s*(\d+)\)', base, re.IGNORECASE) or
re.search(r'\[Dis[ck]\s*(\d+)\]', base, re.IGNORECASE) or
re.search(r'Dis[ck]\s*(\d+)', base, re.IGNORECASE) or
re.search(r'\(CD\s*(\d+)\)', base, re.IGNORECASE) or
re.search(r'CD\s*(\d+)', base, re.IGNORECASE))
if disc_match:
disc_info = f' (Disc {disc_match.group(1)})'
# Supprimer contenu entre parenthèses et crochets
base = re.sub(r'\([^)]*\)', '', base)
base = re.sub(r'\[[^\]]*\]', '', base)
# Normaliser espaces
base = re.sub(r'\s+', ' ', base).strip()
# Rajouter info disque
base = base + disc_info
return base
def get_region_priority(self, game_name: str) -> int:
"""Obtient la priorité de région pour un jeu (pour one-rom-per-game)"""
name = game_name.upper()
for i, region in enumerate(self.region_priority):
region_upper = region.upper()
if region_upper in name:
return i
return len(self.region_priority) # Autres régions (priorité la plus basse)
def apply_filters(self, games: List[Tuple]) -> List[Tuple]:
"""
Applique les filtres à une liste de jeux
games: Liste de tuples (game_name, game_url, size)
Retourne: Liste filtrée de tuples
"""
if not self.is_active():
return games
filtered_games = []
# Filtrage par région
for game in games:
game_name = game[0]
# Vérifier les filtres de région
if self.region_filters:
game_regions = self.get_game_regions(game_name)
# Vérifier si le jeu a au moins une région incluse
has_included_region = False
for region in game_regions:
filter_state = self.region_filters.get(region, 'include')
if filter_state == 'include':
has_included_region = True
break # Si on trouve une région incluse, c'est bon
# Le jeu est affiché seulement s'il a au moins une région incluse
if not has_included_region:
continue
# Filtrer les non-release
if self.hide_non_release and self.is_non_release_game(game_name):
continue
filtered_games.append(game)
# Appliquer "one rom per game"
if self.one_rom_per_game:
filtered_games = self._apply_one_rom_per_game(filtered_games)
return filtered_games
def _apply_one_rom_per_game(self, games: List[Tuple]) -> List[Tuple]:
"""Garde seulement une ROM par jeu selon la priorité de région"""
games_by_base = {}
for game in games:
game_name = game[0]
base_name = self.get_base_game_name(game_name)
if base_name not in games_by_base:
games_by_base[base_name] = []
games_by_base[base_name].append(game)
# Pour chaque jeu de base, garder celui avec la meilleure priorité
result = []
for base_name, game_list in games_by_base.items():
if len(game_list) == 1:
result.append(game_list[0])
else:
# Trier par priorité de région
sorted_games = sorted(game_list,
key=lambda g: self.get_region_priority(g[0]))
result.append(sorted_games[0])
return result

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

@@ -134,13 +134,12 @@
"network_auth_required": "Authentifizierung erforderlich (HTTP {0})",
"network_access_denied": "Zugriff verweigert (HTTP {0})",
"network_server_error": "Serverfehler (HTTP {0})",
"network_download_ok": "Download erfolgreich: {0}",
"download_already_present": " (bereits vorhanden)",
"network_download_already_queued": "Dieser Download läuft bereits",
"utils_extracted": "Extrahiert: {0}",
"download_already_extracted": " (bereits extrahiert)",
"download_in_progress": "Download läuft...",
"download_queued": "In Download-Warteschlange",
"download_started": "Download gestartet",
"utils_extracted": "Extrahiert: {0}",
"utils_corrupt_zip": "Beschädigtes ZIP-Archiv: {0}",
"utils_permission_denied": "Berechtigung während der Extraktion verweigert: {0}",
"utils_extraction_failed": "Extraktion fehlgeschlagen: {0}",
@@ -175,181 +174,238 @@
"api_key_empty_suffix": "leer",
"menu_hide_premium_systems": "Premium-Systeme ausblenden",
"popup_hide_premium_on": "Premium-Systeme ausgeblendet",
"popup_hide_premium_off": "Premium-Systeme sichtbar"
,"submenu_display_font_family": "Schrift"
,"popup_font_family_changed": "Schrift geändert: {0}"
,"instruction_pause_language": "Sprache der Oberfläche ändern"
,"instruction_pause_controls": "Steuerungsübersicht ansehen oder neu zuordnen"
,"instruction_pause_display": "Layout, Schriften und Systemsichtbarkeit konfigurieren"
,"instruction_pause_games": "Verlauf öffnen, Quelle wechseln oder Liste aktualisieren"
,"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_controls_help": "Komplette Referenz für Controller & Tastatur anzeigen"
,"instruction_controls_remap": "Tasten / Buttons neu zuordnen"
,"instruction_generic_back": "Zum vorherigen Menü zurückkehren"
,"instruction_display_layout": "Rasterabmessungen (Spalten × Zeilen) durchschalten"
,"instruction_display_font_size": "Schriftgröße für bessere Lesbarkeit anpassen"
,"instruction_display_font_family": "Zwischen verfügbaren Schriftarten wechseln"
,"instruction_display_show_unsupported": "Nicht in es_systems.cfg definierte Systeme anzeigen/ausblenden"
,"instruction_display_unknown_ext": "Warnung für in es_systems.cfg fehlende Dateiendungen an-/abschalten"
,"instruction_display_hide_premium": "Systeme ausblenden, die Premiumzugang erfordern über API: {providers}"
,"instruction_display_filter_platforms": "Manuell wählen welche Systeme sichtbar sind"
,"instruction_games_history": "Vergangene Downloads und Status anzeigen"
,"instruction_games_source_mode": "Zwischen RGSX oder eigener Quellliste wechseln"
,"instruction_games_update_cache": "Aktuelle Spieleliste erneut herunterladen & aktualisieren"
,"instruction_settings_music": "Hintergrundmusik aktivieren oder deaktivieren"
,"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"
,"settings_web_service": "Web-Dienst beim Booten"
,"settings_web_service_enabled": "Aktiviert"
,"settings_web_service_disabled": "Deaktiviert"
,"settings_web_service_enabling": "Web-Dienst wird aktiviert..."
,"settings_web_service_disabling": "Web-Dienst wird deaktiviert..."
,"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}"
,"controls_desc_confirm": "Bestätigen (z.B. A/Kreuz)"
,"controls_desc_cancel": "Abbrechen/Zurück (z.B. B/Kreis)"
,"controls_desc_up": "UP ↑"
,"controls_desc_down": "DOWN ↓"
,"controls_desc_left": "LEFT ←"
,"controls_desc_right": "RIGHT →"
,"controls_desc_page_up": "Schnell nach oben (z.B. LT/L2)"
,"controls_desc_page_down": "Schnell nach unten (z.B. RT/R2)"
,"controls_desc_history": "Verlauf öffnen (z.B. Y/Dreieck)"
,"controls_desc_clear_history": "Downloads: Mehrfachauswahl / Verlauf: Leeren (z.B. X/Quadrat)"
,"controls_desc_filter": "Filtermodus: Öffnen/Bestätigen (z.B. Select)"
,"controls_desc_delete": "Filtermodus: Zeichen löschen (z.B. LB/L1)"
,"controls_desc_space": "Filtermodus: Leerzeichen hinzufügen (z.B. RB/R1)"
,"controls_desc_start": "Pausenmenü öffnen (z.B. Start)"
,"controls_mapping_title": "Steuerungszuordnung"
,"controls_mapping_instruction": "Zum Bestätigen gedrückt halten:"
,"controls_mapping_waiting": "Warte auf eine Taste oder einen Button..."
,"controls_mapping_press": "Drücke eine Taste oder einen Button"
,"status_already_present": "Bereits Vorhanden"
,"footer_joystick": "Joystick: {0}"
,"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_delete_game": "Spiel löschen"
,"history_option_error_info": "Fehlerdetails"
,"history_option_retry": "Download wiederholen"
,"history_option_back": "Zurück"
,"history_folder_path_label": "Zielpfad:"
,"history_scraper_not_implemented": "Scraper noch nicht implementiert"
,"history_confirm_delete": "Dieses Spiel von der Festplatte löschen?"
,"history_file_not_found": "Datei nicht gefunden"
,"history_extracting": "Extrahieren..."
,"history_extracted": "Extrahiert"
,"history_delete_success": "Spiel erfolgreich gelöscht"
,"history_delete_error": "Fehler beim Löschen des Spiels: {0}"
,"history_error_details_title": "Fehlerdetails"
,"history_no_error_message": "Keine Fehlermeldung verfügbar"
,"web_title": "RGSX Web-Oberfläche"
,"web_tab_platforms": "Systemliste"
,"web_tab_downloads": "Downloads"
,"web_tab_history": "Verlauf"
,"web_tab_settings": "Einstellungen"
,"web_tab_update": "Liste aktualisieren"
,"web_tooltip_platforms": "Systemliste"
,"web_tooltip_downloads": "Downloads"
,"web_tooltip_history": "Verlauf"
,"web_tooltip_settings": "Einstellungen"
,"web_tooltip_update": "Spieleliste aktualisieren"
,"web_search_platform": "Systeme oder Spiele suchen..."
,"web_search_game": "Spiel suchen..."
,"web_search_results": "Ergebnisse für"
,"web_no_results": "Keine Ergebnisse gefunden"
,"web_platforms": "Systeme"
,"web_games": "Spiele"
,"web_error_search": "Suchfehler"
,"web_back_platforms": "Zurück zu Plattformen"
,"web_back": "Zurück"
,"web_game_count": "{0} ({1} Spiele)"
,"web_download": "Herunterladen"
,"web_cancel": "Abbrechen"
,"web_download_canceled": "Download abgebrochen"
,"web_confirm_cancel": "Möchten Sie diesen Download wirklich abbrechen?"
,"web_update_title": "Spieleliste wird aktualisiert..."
,"web_update_message": "Cache wird gelöscht und Daten neu geladen..."
,"web_update_wait": "Dies kann 10-30 Sekunden dauern"
,"web_error": "Fehler"
,"web_error_unknown": "Unbekannter Fehler"
,"web_error_update": "Fehler beim Aktualisieren der Liste: {0}"
,"web_error_download": "Fehler: {0}"
,"web_history_clear": "Verlauf löschen"
,"web_history_cleared": "Verlauf erfolgreich gelöscht!"
,"web_error_clear_history": "Fehler beim Löschen des Verlaufs: {0}"
,"web_settings_title": "Info & Einstellungen"
,"web_settings_roms_folder": "Benutzerdefinierter ROMs-Ordner"
,"web_settings_roms_placeholder": "Leer lassen für Standard"
,"web_settings_browse": "Durchsuchen"
,"web_settings_language": "Sprache"
,"web_settings_font_scale": "Schriftgröße"
,"web_settings_grid": "Rasterlayout"
,"web_settings_font_family": "Schriftart"
,"web_settings_music": "Musik"
,"web_settings_symlink": "Symlink-Modus"
,"web_settings_source_mode": "Spielequelle"
,"web_settings_custom_url": "Benutzerdefinierte URL"
,"web_settings_custom_url_placeholder": "https://beispiel.com/spiele.zip"
,"web_settings_save": "Einstellungen speichern"
,"web_settings_saved": "Einstellungen erfolgreich gespeichert!"
,"web_settings_saved_restart": "Einstellungen erfolgreich gespeichert!\\n\\n⚠ Einige Einstellungen erfordern einen Serverneustart:\\n- Benutzerdefinierter ROMs-Ordner\\n- Sprache\\n\\nBitte starten Sie den Webserver neu, um diese Änderungen anzuwenden."
,"web_error_save_settings": "Fehler beim Speichern der Einstellungen: {0}"
,"web_browse_title": "Verzeichnisse durchsuchen"
,"web_browse_select_drive": "Laufwerk auswählen..."
,"web_browse_drives": "Laufwerke"
,"web_browse_parent": "Übergeordnet"
,"web_browse_select": "Diesen Ordner auswählen"
,"web_browse_cancel": "Abbrechen"
,"web_browse_empty": "Keine Unterverzeichnisse gefunden"
,"web_browse_alert_restart": "Wichtig: Sie müssen die Einstellungen SPEICHERN und dann den Webserver NEUSTARTEN, damit der benutzerdefinierte ROMs-Ordner wirksam wird.\\n\\n📝 Schritte:\\n1. Klicken Sie unten auf 'Einstellungen speichern'\\n2. Stoppen Sie den Webserver (Strg+C im Terminal)\\n3. Starten Sie den Webserver neu\\n\\nAusgewählter Pfad: {0}"
,"web_error_browse": "Fehler beim Durchsuchen der Verzeichnisse: {0}"
,"web_loading_platforms": "Lade Plattformen..."
,"web_loading_games": "Lade Spiele..."
,"web_no_platforms": "Keine Plattformen gefunden"
,"web_no_downloads": "Keine Downloads im Gange"
,"web_history_empty": "Keine abgeschlossenen Downloads"
,"web_history_platform": "Plattform"
,"web_history_size": "Größe"
,"web_history_status_completed": "Abgeschlossen"
,"web_history_status_error": "Fehler"
,"web_settings_os": "Betriebssystem"
,"web_settings_platforms_count": "Anzahl der Plattformen"
,"web_settings_show_unsupported": "Nicht unterstützte Plattformen anzeigen (System fehlt in es_systems.cfg)"
,"web_settings_allow_unknown": "Unbekannte Erweiterungen erlauben (keine Warnungen anzeigen)"
,"web_restart_confirm_title": "Anwendung neu starten?"
,"web_restart_confirm_message": "Die Einstellungen wurden gespeichert. Möchten Sie die Anwendung jetzt neu starten, um die Änderungen anzuwenden?"
,"web_restart_yes": "Ja, neu starten"
,"web_restart_no": "Nein, später"
,"web_restart_success": "Neustart läuft..."
,"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_generating": "Support-Datei wird generiert..."
,"web_support_download": "Support-Datei herunterladen"
,"web_support_error": "Fehler beim Erstellen der Support-Datei: {0}"
,"web_tab_queue": "Warteschlange"
,"web_tooltip_queue": "Download-Warteschlange"
,"web_queue_active_download": "⏳ Ein Download ist aktiv"
,"web_queue_no_active": "✓ Kein aktiver Download"
,"web_queue_title": "Download-Warteschlange"
,"web_queue_empty": "Keine Elemente in der Warteschlange"
,"web_queue_clear": "Warteschlange löschen"
,"web_queue_cleared": "Warteschlange erfolgreich gelöscht!"
,"web_confirm_remove_queue": "Dieses Element aus der Warteschlange entfernen?"
,"web_confirm_clear_queue": "Gesamte Warteschlange löschen?"
,"web_remove": "Entfernen"
,"web_loading": "Lädt..."
,"web_sort": "Sortieren nach"
,"web_sort_name_asc": "A-Z (Name)"
,"web_sort_name_desc": "Z-A (Name)"
,"web_sort_size_asc": "Größe +- (Klein zuerst)"
,"web_sort_size_desc": "Größe -+ (Groß zuerst)"
"popup_hide_premium_off": "Premium-Systeme sichtbar",
"submenu_display_font_family": "Schrift",
"popup_font_family_changed": "Schrift geändert: {0}",
"instruction_pause_language": "Sprache der Oberfläche ändern",
"instruction_pause_controls": "Steuerungsübersicht ansehen oder neu zuordnen",
"instruction_pause_display": "Layout, Schriften und Systemsichtbarkeit konfigurieren",
"instruction_pause_games": "Verlauf öffnen, Quelle wechseln oder Liste aktualisieren",
"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_controls_help": "Komplette Referenz für Controller & Tastatur anzeigen",
"instruction_controls_remap": "Tasten / Buttons neu zuordnen",
"instruction_generic_back": "Zum vorherigen Menü zurückkehren",
"instruction_display_layout": "Rasterabmessungen (Spalten × Zeilen) durchschalten",
"instruction_display_font_size": "Schriftgröße für bessere Lesbarkeit anpassen",
"instruction_display_footer_font_size": "Fußzeilen-Textgröße anpassen (Version & Steuerelemente)",
"instruction_display_font_family": "Zwischen verfügbaren Schriftarten wechseln",
"instruction_display_show_unsupported": "Nicht in es_systems.cfg definierte Systeme anzeigen/ausblenden",
"instruction_display_unknown_ext": "Warnung für in es_systems.cfg fehlende Dateiendungen an-/abschalten",
"instruction_display_hide_premium": "Systeme ausblenden, die Premiumzugang erfordern über API: {providers}",
"instruction_display_filter_platforms": "Manuell wählen welche Systeme sichtbar sind",
"instruction_games_history": "Vergangene Downloads und Status anzeigen",
"instruction_games_source_mode": "Zwischen RGSX oder eigener Quellliste wechseln",
"instruction_games_update_cache": "Aktuelle Spieleliste erneut herunterladen & aktualisieren",
"instruction_settings_music": "Hintergrundmusik aktivieren oder deaktivieren",
"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",
"settings_web_service_enabling": "Web-Dienst wird aktiviert...",
"settings_web_service_disabling": "Web-Dienst wird deaktiviert...",
"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 ↑",
"controls_desc_down": "DOWN ↓",
"controls_desc_left": "LEFT ←",
"controls_desc_right": "RIGHT →",
"controls_desc_page_up": "Schnell nach oben (z.B. LT/L2)",
"controls_desc_page_down": "Schnell nach unten (z.B. RT/R2)",
"controls_desc_history": "Verlauf öffnen (z.B. Y/Dreieck)",
"controls_desc_clear_history": "Downloads: Mehrfachauswahl / Verlauf: Leeren (z.B. X/Quadrat)",
"controls_desc_filter": "Filtermodus: Öffnen/Bestätigen (z.B. Select)",
"controls_desc_delete": "Filtermodus: Zeichen löschen (z.B. LB/L1)",
"controls_desc_space": "Filtermodus: Leerzeichen hinzufügen (z.B. RB/R1)",
"controls_desc_start": "Pausenmenü öffnen (z.B. Start)",
"controls_mapping_title": "Steuerungszuordnung",
"controls_mapping_instruction": "Zum Bestätigen gedrückt halten:",
"controls_mapping_waiting": "Warte auf eine Taste oder einen Button...",
"controls_mapping_press": "Drücke eine Taste oder einen Button",
"status_already_present": "Bereits Vorhanden",
"footer_joystick": "Joystick: {0}",
"history_game_options_title": "Spiel Optionen",
"history_option_download_folder": "Datei lokalisieren",
"history_option_extract_archive": "Archiv extrahieren",
"history_option_open_file": "Datei öffnen",
"history_option_scraper": "Metadaten abrufen",
"history_option_delete_game": "Spiel löschen",
"history_option_error_info": "Fehlerdetails",
"history_option_retry": "Download wiederholen",
"history_option_back": "Zurück",
"history_folder_path_label": "Zielpfad:",
"history_scraper_not_implemented": "Scraper noch nicht implementiert",
"history_confirm_delete": "Dieses Spiel von der Festplatte löschen?",
"history_file_not_found": "Datei nicht gefunden",
"history_extracting": "Extrahieren...",
"history_extracted": "Extrahiert",
"history_delete_success": "Spiel erfolgreich gelöscht",
"history_delete_error": "Fehler beim Löschen des Spiels: {0}",
"history_error_details_title": "Fehlerdetails",
"history_no_error_message": "Keine Fehlermeldung verfügbar",
"web_title": "RGSX Web-Oberfläche",
"web_tab_platforms": "Systemliste",
"web_tab_downloads": "Downloads",
"web_tab_history": "Verlauf",
"web_tab_settings": "Einstellungen",
"web_tab_update": "Liste aktualisieren",
"web_tooltip_platforms": "Systemliste",
"web_tooltip_downloads": "Downloads",
"web_tooltip_history": "Verlauf",
"web_tooltip_settings": "Einstellungen",
"web_tooltip_update": "Spieleliste aktualisieren",
"web_search_platform": "Systeme oder Spiele suchen...",
"web_search_game": "Spiel suchen...",
"web_search_results": "Ergebnisse für",
"web_no_results": "Keine Ergebnisse gefunden",
"web_platforms": "Systeme",
"web_games": "Spiele",
"web_error_search": "Suchfehler",
"web_back_platforms": "Zurück zu Plattformen",
"web_back": "Zurück",
"web_game_count": "{0} ({1} Spiele)",
"web_download": "Herunterladen",
"web_cancel": "Abbrechen",
"web_download_canceled": "Download abgebrochen",
"web_confirm_cancel": "Möchten Sie diesen Download wirklich abbrechen?",
"web_update_title": "Spieleliste wird aktualisiert...",
"web_update_message": "Cache wird gelöscht und Daten neu geladen...",
"web_update_wait": "Dies kann 10-30 Sekunden dauern",
"web_error": "Fehler",
"web_error_unknown": "Unbekannter Fehler",
"web_error_update": "Fehler beim Aktualisieren der Liste: {0}",
"web_error_download": "Fehler: {0}",
"web_history_clear": "Verlauf löschen",
"web_history_cleared": "Verlauf erfolgreich gelöscht!",
"web_error_clear_history": "Fehler beim Löschen des Verlaufs: {0}",
"web_settings_title": "Info & Einstellungen",
"web_settings_roms_folder": "Benutzerdefinierter ROMs-Ordner",
"web_settings_roms_placeholder": "Leer lassen für Standard",
"web_settings_browse": "Durchsuchen",
"web_settings_language": "Sprache",
"web_settings_font_scale": "Schriftgröße",
"web_settings_grid": "Rasterlayout",
"web_settings_font_family": "Schriftart",
"web_settings_music": "Musik",
"web_settings_symlink": "Symlink-Modus",
"web_settings_source_mode": "Spielequelle",
"web_settings_custom_url": "Benutzerdefinierte URL",
"web_settings_custom_url_placeholder": "https://beispiel.com/spiele.zip",
"web_settings_save": "Einstellungen speichern",
"web_settings_saved": "Einstellungen erfolgreich gespeichert!",
"web_settings_saved_restart": "Einstellungen erfolgreich gespeichert!\\n\\n⚠ Einige Einstellungen erfordern einen Serverneustart:\\n- Benutzerdefinierter ROMs-Ordner\\n- Sprache\\n\\nBitte starten Sie den Webserver neu, um diese Änderungen anzuwenden.",
"web_error_save_settings": "Fehler beim Speichern der Einstellungen: {0}",
"web_browse_title": "Verzeichnisse durchsuchen",
"web_browse_select_drive": "Laufwerk auswählen...",
"web_browse_drives": "Laufwerke",
"web_browse_parent": "Übergeordnet",
"web_browse_select": "Diesen Ordner auswählen",
"web_browse_cancel": "Abbrechen",
"web_browse_empty": "Keine Unterverzeichnisse gefunden",
"web_browse_alert_restart": "Wichtig: Sie müssen die Einstellungen SPEICHERN und dann den Webserver NEUSTARTEN, damit der benutzerdefinierte ROMs-Ordner wirksam wird.\\n\\n📝 Schritte:\\n1. Klicken Sie unten auf 'Einstellungen speichern'\\n2. Stoppen Sie den Webserver (Strg+C im Terminal)\\n3. Starten Sie den Webserver neu\\n\\nAusgewählter Pfad: {0}",
"web_error_browse": "Fehler beim Durchsuchen der Verzeichnisse: {0}",
"web_loading_platforms": "Lade Plattformen...",
"web_loading_games": "Lade Spiele...",
"web_no_platforms": "Keine Plattformen gefunden",
"web_no_downloads": "Keine Downloads im Gange",
"web_history_empty": "Keine abgeschlossenen Downloads",
"web_history_platform": "Plattform",
"web_history_size": "Größe",
"web_history_status_completed": "Abgeschlossen",
"web_history_status_error": "Fehler",
"web_settings_os": "Betriebssystem",
"web_settings_platforms_count": "Anzahl der Plattformen",
"web_settings_show_unsupported": "Nicht unterstützte Plattformen anzeigen (System fehlt in es_systems.cfg)",
"web_settings_allow_unknown": "Unbekannte Erweiterungen erlauben (keine Warnungen anzeigen)",
"web_restart_confirm_title": "Anwendung neu starten?",
"web_restart_confirm_message": "Die Einstellungen wurden gespeichert. Möchten Sie die Anwendung jetzt neu starten, um die Änderungen anzuwenden?",
"web_restart_yes": "Ja, neu starten",
"web_restart_no": "Nein, später",
"web_restart_success": "Neustart läuft...",
"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_generating": "Support-Datei wird generiert...",
"web_support_download": "Support-Datei herunterladen",
"web_support_error": "Fehler beim Erstellen der Support-Datei: {0}",
"web_tab_queue": "Warteschlange",
"web_tooltip_queue": "Download-Warteschlange",
"web_queue_active_download": "⏳ Ein Download ist aktiv",
"web_queue_no_active": "✓ Kein aktiver Download",
"web_queue_title": "Download-Warteschlange",
"web_queue_empty": "Keine Elemente in der Warteschlange",
"web_queue_clear": "Warteschlange löschen",
"web_queue_cleared": "Warteschlange erfolgreich gelöscht!",
"web_confirm_remove_queue": "Dieses Element aus der Warteschlange entfernen?",
"web_confirm_clear_queue": "Gesamte Warteschlange löschen?",
"web_remove": "Entfernen",
"web_loading": "Lädt...",
"web_sort": "Sortieren nach",
"web_sort_name_asc": "A-Z (Name)",
"web_sort_name_desc": "Z-A (Name)",
"web_sort_size_asc": "Größe +- (Klein zuerst)",
"web_sort_size_desc": "Größe -+ (Groß zuerst)",
"download_already_present": " (bereits vorhanden)",
"network_download_ok": "Download erfolgreich: {0}",
"web_filter_region": "Region",
"web_filter_hide_non_release": "Demos/Betas/Protos ausblenden",
"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": "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",
"filter_active": "Filter aktiv",
"filter_games_shown": "{0} Spiel(e) angezeigt"
}

View File

@@ -8,7 +8,7 @@
"loading_test_connection": "Testing connection...",
"loading_download_data": "Downloading initial Data folder...",
"loading_progress": "Progress: {0}%",
"loading_check_updates": "Checking for updates... Please wait...",
"loading_check_updates": "Update in progress... Please wait...",
"error_check_updates_failed": "Failed to check updates.",
"loading_downloading_games_images": "Downloading games and images...",
"loading_extracting_data": "Extracting initial Data folder...",
@@ -93,8 +93,8 @@
"support_dialog_message": "A support file has been created with all your configuration and log files.\n\nFile: {0}\n\nTo get help:\n1. Join the RGSX Discord server\n2. Describe your issue\n3. Share this ZIP file\n\nPress {1} to return to the menu.",
"support_dialog_error": "Error generating support file:\n{0}\n\nPress {1} to return to the menu.",
"controls_action_history": "History / Downloads",
"controls_action_close_history": "Close History",
"network_checking_updates": "Checking for updates...",
"controls_action_close_history": "Close History",
"network_checking_updates": "Update in progress please wait...",
"network_update_available": "Update available: {0}",
"network_extracting_update": "Extracting update...",
"network_update_completed": "Update completed",
@@ -174,183 +174,238 @@
"menu_games": "Games",
"api_keys_hint_manage": "Put your keys in {path}",
"api_key_empty_suffix": "empty",
"menu_hide_premium_systems": "Hide Premium systems"
,"popup_hide_premium_on": "Premium systems hidden"
,"popup_hide_premium_off": "Premium systems visible"
,"submenu_display_font_family": "Font"
,"popup_font_family_changed": "Font changed: {0}",
"menu_hide_premium_systems": "Hide Premium systems",
"popup_hide_premium_on": "Premium systems hidden",
"popup_hide_premium_off": "Premium systems visible",
"submenu_display_font_family": "Font",
"popup_font_family_changed": "Font changed: {0}",
"instruction_pause_language": "Change the interface language",
"instruction_pause_controls": "View control layout or start remapping",
"instruction_pause_display": "Configure layout, fonts and system visibility",
"instruction_pause_games": "Open history, switch source or refresh list",
"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_controls_help": "Show full controller & keyboard reference"
,"instruction_controls_remap": "Change button / key bindings"
,"instruction_generic_back": "Return to the previous menu"
,"instruction_display_layout": "Cycle grid dimensions (columns × rows)"
,"instruction_display_font_size": "Adjust text scale for readability"
,"instruction_display_font_family": "Switch between available font families"
,"instruction_display_show_unsupported": "Show/hide systems not defined in es_systems.cfg"
,"instruction_display_unknown_ext": "Enable/disable warning for file extensions absent from es_systems.cfg"
,"instruction_display_hide_premium": "Hide systems requiring premium access via API: {providers}"
,"instruction_display_filter_platforms": "Manually choose which systems are visible"
,"instruction_games_history": "List past downloads and statuses"
,"instruction_games_source_mode": "Switch between RGSX or your own custom list source"
,"instruction_games_update_cache": "Redownload & refresh current games list"
,"instruction_settings_music": "Enable or disable background music playback"
,"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"
,"settings_web_service": "Web Service at Boot"
,"settings_web_service_enabled": "Enabled"
,"settings_web_service_disabled": "Disabled"
,"settings_web_service_enabling": "Enabling web service..."
,"settings_web_service_disabling": "Disabling web service..."
,"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}"
,"controls_desc_confirm": "Confirm (e.g. A/Cross)"
,"controls_desc_cancel": "Cancel/Back (e.g. B/Circle)"
,"controls_desc_up": "UP ↑"
,"controls_desc_down": "DOWN ↓"
,"controls_desc_left": "LEFT ←"
,"controls_desc_right": "RIGHT →"
,"controls_desc_page_up": "Fast scroll up (e.g. LT/L2)"
,"controls_desc_page_down": "Fast scroll down (e.g. RT/R2)"
,"controls_desc_history": "Open history (e.g. Y/Triangle)"
,"controls_desc_clear_history": "Downloads: Multi-select / History: Clear (e.g. X/Square)"
,"controls_desc_filter": "Filter mode: Open/Confirm (e.g. Select)"
,"controls_desc_delete": "Filter mode: Delete character (e.g. LB/L1)"
,"controls_desc_space": "Filter mode: Add space (e.g. RB/R1)"
,"controls_desc_start": "Open pause menu (e.g. Start)"
,"controls_mapping_title": "Controls mapping"
,"controls_mapping_instruction": "Hold to confirm the mapping:"
,"controls_mapping_waiting": "Waiting for a key or button..."
,"controls_mapping_press": "Press a key or a button"
,"status_already_present": "Already Present"
,"footer_joystick": "Joystick: {0}"
,"history_game_options_title": "Game Options"
,"history_option_download_folder": "Locate file"
,"history_option_extract_archive": "Extract archive"
,"history_option_scraper": "Scrape metadata"
,"history_option_delete_game": "Delete game"
,"history_option_error_info": "Error details"
,"history_option_retry": "Retry download"
,"history_option_back": "Back"
,"history_folder_path_label": "Destination path:"
,"history_scraper_not_implemented": "Scraper not yet implemented"
,"history_confirm_delete": "Delete this game from disk?"
,"history_file_not_found": "File not found"
,"history_extracting": "Extracting..."
,"history_extracted": "Extracted"
,"history_delete_success": "Game deleted successfully"
,"history_delete_error": "Error deleting game: {0}"
,"history_error_details_title": "Error Details"
,"history_no_error_message": "No error message available"
,"web_title": "RGSX Web Interface"
,"web_tab_platforms": "Platforms List"
,"web_tab_downloads": "Downloads"
,"web_tab_history": "History"
,"web_tab_settings": "Settings"
,"web_tab_update": "Update games list"
,"web_tooltip_platforms": "Platforms list"
,"web_tooltip_downloads": "Downloads"
,"web_tooltip_history": "History"
,"web_tooltip_settings": "Settings"
,"web_tooltip_update": "Update games list"
,"web_search_platform": "Search platforms or games..."
,"web_search_game": "Search a game..."
,"web_search_results": "results for"
,"web_no_results": "No results found"
,"web_platforms": "Platforms"
,"web_games": "Games"
,"web_error_search": "Search error"
,"web_back_platforms": "Back to platforms"
,"web_back": "Back"
,"web_game_count": "{0} ({1} games)"
,"web_download": "Download"
,"web_cancel": "Cancel"
,"web_download_canceled": "Download canceled"
,"web_confirm_cancel": "Do you really want to cancel this download?"
,"web_update_title": "Updating games list..."
,"web_update_message": "Clearing cache and reloading data..."
,"web_update_wait": "This may take 10-30 seconds"
,"web_error": "Error"
,"web_error_unknown": "Unknown error"
,"web_error_update": "Error updating games list: {0}"
,"web_error_download": "Error: {0}"
,"web_history_clear": "Clear History"
,"web_history_cleared": "History cleared successfully!"
,"web_error_clear_history": "Error clearing history: {0}"
,"web_settings_title": "Info & Settings"
,"web_settings_roms_folder": "Custom ROMs folder"
,"web_settings_roms_placeholder": "Leave empty for default"
,"web_settings_browse": "Browse"
,"web_settings_language": "Language"
,"web_settings_font_scale": "Font scale"
,"web_settings_grid": "Grid layout"
,"web_settings_font_family": "Font family"
,"web_settings_music": "Music"
,"web_settings_symlink": "Symlink mode"
,"web_settings_source_mode": "Games source"
,"web_settings_custom_url": "Custom URL"
,"web_settings_custom_url_placeholder": "Let empty for local /saves/ports/rgsx/games.zip or use a direct URL like https://example.com/games.zip"
,"web_settings_save": "Save Settings"
,"web_settings_saved": "Settings saved successfully!"
,"web_settings_saved_restart": "Settings saved successfully!\\n\\n⚠ Some settings require a server restart:\\n- Custom ROMs folder\\n- Language\\n\\nPlease restart the web server to apply these changes."
,"web_error_save_settings": "Error saving settings: {0}"
,"web_browse_title": "Browse Directories"
,"web_browse_select_drive": "Select a drive..."
,"web_browse_drives": "Drives"
,"web_browse_parent": "Parent"
,"web_browse_select": "Select this folder"
,"web_browse_cancel": "Cancel"
,"web_browse_empty": "No subdirectories found"
,"web_browse_alert_restart": "Important: You need to SAVE the settings and then RESTART the web server/application for the custom ROMs folder to take effect.\\n\\n📝 Steps:\\n1. Click 'Save Settings' button below\\n2. Stop the web server (Ctrl+C in terminal)\\n3. Restart the web server\\n\\nSelected path: {0}"
,"web_error_browse": "Error browsing directories: {0}"
,"web_loading_platforms": "Loading platforms..."
,"web_loading_games": "Loading games..."
,"web_no_platforms": "No platforms found"
,"web_no_downloads": "No downloads in progress"
,"web_history_empty": "No completed downloads"
,"web_history_platform": "Platform"
,"web_history_size": "Size"
,"web_history_status_completed": "Completed"
,"web_history_status_error": "Error"
,"web_settings_os": "Operating System"
,"web_settings_platforms_count": "Number of platforms"
,"web_settings_show_unsupported": "Show unsupported platforms (system not found in es_systems.cfg)"
,"web_settings_allow_unknown": "Allow unknown extensions (don't show warnings)"
,"web_restart_confirm_title": "Restart application?"
,"web_restart_confirm_message": "Settings have been saved. Do you want to restart the application now to apply the changes?"
,"web_restart_yes": "Yes, restart"
,"web_restart_no": "No, later"
,"web_restart_success": "Restarting..."
,"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_generating": "Generating support file..."
,"web_support_download": "Download support file"
,"web_support_error": "Error generating support file: {0}"
,"web_tab_queue": "Queue"
,"web_tooltip_queue": "Download queue"
,"web_queue_active_download": "⏳ A download is currently active"
,"web_queue_no_active": "✓ No active download"
,"web_queue_title": "Download Queue"
,"web_queue_empty": "No items in queue"
,"web_queue_clear": "Clear Queue"
,"web_queue_cleared": "Queue cleared successfully!"
,"web_confirm_remove_queue": "Remove this item from the queue?"
,"web_confirm_clear_queue": "Clear the entire queue?"
,"web_remove": "Remove"
,"web_loading": "Loading..."
,"web_sort": "Sort by"
,"web_sort_name_asc": "A-Z (Name)"
,"web_sort_name_desc": "Z-A (Name)"
,"web_sort_size_asc": "Size +- (Small first)"
,"web_sort_size_desc": "Size -+ (Large first)"
"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_controls_help": "Show full controller & keyboard reference",
"instruction_controls_remap": "Change button / key bindings",
"instruction_generic_back": "Return to the previous menu",
"instruction_display_layout": "Cycle grid dimensions (columns × rows)",
"instruction_display_font_size": "Adjust text scale for readability",
"instruction_display_footer_font_size": "Adjust footer text scale (version & controls display)",
"instruction_display_font_family": "Switch between available font families",
"instruction_display_show_unsupported": "Show/hide systems not defined in es_systems.cfg",
"instruction_display_unknown_ext": "Enable/disable warning for file extensions absent from es_systems.cfg",
"instruction_display_hide_premium": "Hide systems requiring premium access via API: {providers}",
"instruction_display_filter_platforms": "Manually choose which systems are visible",
"instruction_games_history": "List past downloads and statuses",
"instruction_games_source_mode": "Switch between RGSX or your own custom list source",
"instruction_games_update_cache": "Redownload & refresh current games list",
"instruction_settings_music": "Enable or disable background music playback",
"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",
"settings_web_service_enabling": "Enabling web service...",
"settings_web_service_disabling": "Disabling web service...",
"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 ↑",
"controls_desc_down": "DOWN ↓",
"controls_desc_left": "LEFT ←",
"controls_desc_right": "RIGHT →",
"controls_desc_page_up": "Fast scroll up (e.g. LT/L2)",
"controls_desc_page_down": "Fast scroll down (e.g. RT/R2)",
"controls_desc_history": "Open history (e.g. Y/Triangle)",
"controls_desc_clear_history": "Downloads: Multi-select / History: Clear (e.g. X/Square)",
"controls_desc_filter": "Filter mode: Open/Confirm (e.g. Select)",
"controls_desc_delete": "Filter mode: Delete character (e.g. LB/L1)",
"controls_desc_space": "Filter mode: Add space (e.g. RB/R1)",
"controls_desc_start": "Open pause menu (e.g. Start)",
"controls_mapping_title": "Controls mapping",
"controls_mapping_instruction": "Hold to confirm the mapping:",
"controls_mapping_waiting": "Waiting for a key or button...",
"controls_mapping_press": "Press a key or a button",
"status_already_present": "Already Present",
"footer_joystick": "Joystick: {0}",
"history_game_options_title": "Game Options",
"history_option_download_folder": "Locate file",
"history_option_extract_archive": "Extract archive",
"history_option_open_file": "Open file",
"history_option_scraper": "Scrape metadata",
"history_option_delete_game": "Delete game",
"history_option_error_info": "Error details",
"history_option_retry": "Retry download",
"history_option_back": "Back",
"history_folder_path_label": "Destination path:",
"history_scraper_not_implemented": "Scraper not yet implemented",
"history_confirm_delete": "Delete this game from disk?",
"history_file_not_found": "File not found",
"history_extracting": "Extracting...",
"history_extracted": "Extracted",
"history_delete_success": "Game deleted successfully",
"history_delete_error": "Error deleting game: {0}",
"history_error_details_title": "Error Details",
"history_no_error_message": "No error message available",
"web_title": "RGSX Web Interface",
"web_tab_platforms": "Platforms List",
"web_tab_downloads": "Downloads",
"web_tab_history": "History",
"web_tab_settings": "Settings",
"web_tab_update": "Update games list",
"web_tooltip_platforms": "Platforms list",
"web_tooltip_downloads": "Downloads",
"web_tooltip_history": "History",
"web_tooltip_settings": "Settings",
"web_tooltip_update": "Update games list",
"web_search_platform": "Search platforms or games...",
"web_search_game": "Search a game...",
"web_search_results": "results for",
"web_no_results": "No results found",
"web_platforms": "Platforms",
"web_games": "Games",
"web_error_search": "Search error",
"web_back_platforms": "Back to platforms",
"web_back": "Back",
"web_game_count": "{0} ({1} games)",
"web_download": "Download",
"web_cancel": "Cancel",
"web_download_canceled": "Download canceled",
"web_confirm_cancel": "Do you really want to cancel this download?",
"web_update_title": "Updating games list...",
"web_update_message": "Clearing cache and reloading data...",
"web_update_wait": "This may take 10-30 seconds",
"web_error": "Error",
"web_error_unknown": "Unknown error",
"web_error_update": "Error updating games list: {0}",
"web_error_download": "Error: {0}",
"web_history_clear": "Clear History",
"web_history_cleared": "History cleared successfully!",
"web_error_clear_history": "Error clearing history: {0}",
"web_settings_title": "Info & Settings",
"web_settings_roms_folder": "Custom ROMs folder",
"web_settings_roms_placeholder": "Leave empty for default",
"web_settings_browse": "Browse",
"web_settings_language": "Language",
"web_settings_font_scale": "Font scale",
"web_settings_grid": "Grid layout",
"web_settings_font_family": "Font family",
"web_settings_music": "Music",
"web_settings_symlink": "Symlink mode",
"web_settings_source_mode": "Games source",
"web_settings_custom_url": "Custom URL",
"web_settings_custom_url_placeholder": "Let empty for local /saves/ports/rgsx/games.zip or use a direct URL like https://example.com/games.zip",
"web_settings_save": "Save Settings",
"web_settings_saved": "Settings saved successfully!",
"web_settings_saved_restart": "Settings saved successfully!\\n\\n⚠ Some settings require a server restart:\\n- Custom ROMs folder\\n- Language\\n\\nPlease restart the web server to apply these changes.",
"web_error_save_settings": "Error saving settings: {0}",
"web_browse_title": "Browse Directories",
"web_browse_select_drive": "Select a drive...",
"web_browse_drives": "Drives",
"web_browse_parent": "Parent",
"web_browse_select": "Select this folder",
"web_browse_cancel": "Cancel",
"web_browse_empty": "No subdirectories found",
"web_browse_alert_restart": "Important: You need to SAVE the settings and then RESTART the web server/application for the custom ROMs folder to take effect.\\n\\n📝 Steps:\\n1. Click 'Save Settings' button below\\n2. Stop the web server (Ctrl+C in terminal)\\n3. Restart the web server\\n\\nSelected path: {0}",
"web_error_browse": "Error browsing directories: {0}",
"web_loading_platforms": "Loading platforms...",
"web_loading_games": "Loading games...",
"web_no_platforms": "No platforms found",
"web_no_downloads": "No downloads in progress",
"web_history_empty": "No completed downloads",
"web_history_platform": "Platform",
"web_history_size": "Size",
"web_history_status_completed": "Completed",
"web_history_status_error": "Error",
"web_settings_os": "Operating System",
"web_settings_platforms_count": "Number of platforms",
"web_settings_show_unsupported": "Show unsupported platforms (system not found in es_systems.cfg)",
"web_settings_allow_unknown": "Allow unknown extensions (don't show warnings)",
"web_restart_confirm_title": "Restart application?",
"web_restart_confirm_message": "Settings have been saved. Do you want to restart the application now to apply the changes?",
"web_restart_yes": "Yes, restart",
"web_restart_no": "No, later",
"web_restart_success": "Restarting...",
"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_generating": "Generating support file...",
"web_support_download": "Download support file",
"web_support_error": "Error generating support file: {0}",
"web_tab_queue": "Queue",
"web_tooltip_queue": "Download queue",
"web_queue_active_download": "⏳ A download is currently active",
"web_queue_no_active": "✓ No active download",
"web_queue_title": "Download Queue",
"web_queue_empty": "No items in queue",
"web_queue_clear": "Clear Queue",
"web_queue_cleared": "Queue cleared successfully!",
"web_confirm_remove_queue": "Remove this item from the queue?",
"web_confirm_clear_queue": "Clear the entire queue?",
"web_remove": "Remove",
"web_loading": "Loading...",
"web_sort": "Sort by",
"web_sort_name_asc": "A-Z (Name)",
"web_sort_name_desc": "Z-A (Name)",
"web_sort_size_asc": "Size +- (Small first)",
"web_sort_size_desc": "Size -+ (Large first)",
"web_filter_region": "Region",
"web_filter_hide_non_release": "Hide Demos/Betas/Protos",
"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",
"accessibility_footer_font_size": "Footer font size: {0}",
"popup_layout_changed_restart": "Layout changed to {0}x{1}. Please restart the app to apply.",
"web_started": "Started",
"web_downloading": "Download",
"web_in_progress": "In progress",
"web_added_to_queue": "added to queue",
"web_download_success": "downloaded successfully!",
"web_download_error_for": "Error downloading",
"web_already_present": "was already present",
"filter_menu_title": "Filter Menu",
"filter_search_by_name": "Search by name",
"filter_advanced": "Advanced filtering",
"filter_advanced_title": "Advanced Game Filtering",
"filter_region_title": "Filter by region",
"filter_region_include": "Include",
"filter_region_exclude": "Exclude",
"filter_region_usa": "USA",
"filter_region_canada": "Canada",
"filter_region_europe": "Europe",
"filter_region_france": "France",
"filter_region_germany": "Germany",
"filter_region_japan": "Japan",
"filter_region_korea": "Korea",
"filter_region_world": "World",
"filter_region_other": "Other",
"filter_other_options": "Other options",
"filter_hide_non_release": "Hide Demos/Betas/Protos",
"filter_one_rom_per_game": "One ROM per game",
"filter_priority_order": "Priority order",
"filter_priority_title": "Region Priority Configuration",
"filter_priority_desc": "Set preference order for \"One ROM per game\"",
"filter_regex_mode": "Regex Mode",
"filter_apply_filters": "Apply",
"filter_reset_filters": "Reset",
"filter_back": "Back",
"filter_active": "Filter active",
"filter_games_shown": "{0} game(s) shown"
}

View File

@@ -89,7 +89,7 @@
"popup_restarting": "Reiniciando...",
"controls_action_clear_history": "Vaciar historial",
"controls_action_history": "Historial / Descargas",
"controls_action_close_history": "Cerrar Historial",
"controls_action_close_history": "Cerrar Historial",
"controls_action_delete": "Eliminar",
"controls_action_space": "Espacio",
"controls_action_start": "Ayuda / Configuración",
@@ -140,6 +140,7 @@
"download_in_progress": "Descarga en curso...",
"download_queued": "En cola de descarga",
"download_started": "Descarga iniciada",
"network_download_already_queued": "Esta descarga ya está en curso",
"utils_extracted": "Extraído: {0}",
"utils_corrupt_zip": "Archivo ZIP corrupto: {0}",
"utils_permission_denied": "Permiso denegado durante la extracción: {0}",
@@ -175,181 +176,236 @@
"api_key_empty_suffix": "vacío",
"menu_hide_premium_systems": "Ocultar sistemas Premium",
"popup_hide_premium_on": "Sistemas Premium ocultos",
"popup_hide_premium_off": "Sistemas Premium visibles"
,"submenu_display_font_family": "Fuente"
,"popup_font_family_changed": "Fuente cambiada: {0}"
,"instruction_pause_language": "Cambiar el idioma de la interfaz"
,"instruction_pause_controls": "Ver esquema de controles o remapear"
,"instruction_pause_display": "Configurar distribución, fuentes y visibilidad de sistemas"
,"instruction_pause_games": "Abrir historial, cambiar fuente o refrescar lista"
,"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_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"
,"instruction_display_layout": "Alternar dimensiones de la cuadrícula (columnas × filas)"
,"instruction_display_font_size": "Ajustar tamaño del texto para mejor legibilidad"
,"instruction_display_font_family": "Cambiar entre familias de fuentes disponibles"
,"instruction_display_show_unsupported": "Mostrar/ocultar sistemas no definidos en es_systems.cfg"
,"instruction_display_unknown_ext": "Activar/desactivar aviso para extensiones no presentes en es_systems.cfg"
,"instruction_display_hide_premium": "Ocultar sistemas que requieren acceso premium vía API: {providers}"
,"instruction_display_filter_platforms": "Elegir manualmente qué sistemas son visibles"
,"instruction_games_history": "Ver descargas pasadas y su estado"
,"instruction_games_source_mode": "Cambiar entre lista RGSX o fuente personalizada"
,"instruction_games_update_cache": "Volver a descargar y refrescar la lista de juegos"
,"instruction_settings_music": "Activar o desactivar música de fondo"
,"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"
,"settings_web_service": "Servicio Web al Inicio"
,"settings_web_service_enabled": "Activado"
,"settings_web_service_disabled": "Desactivado"
,"settings_web_service_enabling": "Activando servicio web..."
,"settings_web_service_disabling": "Desactivando servicio web..."
,"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}"
,"controls_desc_confirm": "Confirmar (ej. A/Cruz)"
,"controls_desc_cancel": "Cancelar/Volver (ej. B/Círculo)"
,"controls_desc_up": "UP ↑"
,"controls_desc_down": "DOWN ↓"
,"controls_desc_left": "LEFT ←"
,"controls_desc_right": "RIGHT →"
,"controls_desc_page_up": "Desplazamiento rápido - (ej. LT/L2)"
,"controls_desc_page_down": "Desplazamiento rápido + (ej. RT/R2)"
,"controls_desc_history": "Abrir historial (ej. Y/Triángulo)"
,"controls_desc_clear_history": "Descargas: Selección múltiple / Historial: Limpiar (ej. X/Cuadrado)"
,"controls_desc_filter": "Modo filtro: Abrir/Confirmar (ej. Select)"
,"controls_desc_delete": "Modo filtro: Eliminar carácter (ej. LB/L1)"
,"controls_desc_space": "Modo filtro: Añadir espacio (ej. RB/R1)"
,"controls_desc_start": "Abrir menú pausa (ej. Start)"
,"controls_mapping_title": "Asignación de controles"
,"controls_mapping_instruction": "Mantén para confirmar la asignación:"
,"controls_mapping_waiting": "Esperando una tecla o botón..."
,"controls_mapping_press": "Pulsa una tecla o un botón"
,"status_already_present": "Ya Presente"
,"footer_joystick": "Joystick: {0}"
,"history_game_options_title": "Opciones del juego"
,"history_option_download_folder": "Localizar archivo"
,"history_option_extract_archive": "Extraer archivo"
,"history_option_scraper": "Scraper metadatos"
,"history_option_delete_game": "Eliminar juego"
,"history_option_error_info": "Detalles del error"
,"history_option_retry": "Reintentar descarga"
,"history_option_back": "Volver"
,"history_folder_path_label": "Ruta de destino:"
,"history_scraper_not_implemented": "Scraper aún no implementado"
,"history_confirm_delete": "¿Eliminar este juego del disco?"
,"history_file_not_found": "Archivo no encontrado"
,"history_extracting": "Extrayendo..."
,"history_extracted": "Extraído"
,"history_delete_success": "Juego eliminado con éxito"
,"history_delete_error": "Error al eliminar juego: {0}"
,"history_error_details_title": "Detalles del error"
,"history_no_error_message": "No hay mensaje de error disponible"
,"web_title": "Interfaz Web RGSX"
,"web_tab_platforms": "Lista de sistemas"
,"web_tab_downloads": "Descargas"
,"web_tab_history": "Historial"
,"web_tab_settings": "Configuración"
,"web_tab_update": "Actualizar lista"
,"web_tooltip_platforms": "Lista de sistemas"
,"web_tooltip_downloads": "Descargas"
,"web_tooltip_history": "Historial"
,"web_tooltip_settings": "Configuración"
,"web_tooltip_update": "Actualizar lista de juegos"
,"web_search_platform": "Buscar sistemas o juegos..."
,"web_search_game": "Buscar un juego..."
,"web_search_results": "resultados para"
,"web_no_results": "No se encontraron resultados"
,"web_platforms": "Sistemas"
,"web_games": "Juegos"
,"web_error_search": "Error de búsqueda"
,"web_back_platforms": "Volver a plataformas"
,"web_back": "Volver"
,"web_game_count": "{0} ({1} juegos)"
,"web_download": "Descargar"
,"web_cancel": "Cancelar"
,"web_download_canceled": "Descarga cancelada"
,"web_confirm_cancel": "¿Realmente desea cancelar esta descarga?"
,"web_update_title": "Actualizando lista de juegos..."
,"web_update_message": "Limpiando caché y recargando datos..."
,"web_update_wait": "Esto puede tardar 10-30 segundos"
,"web_error": "Error"
,"web_error_unknown": "Error desconocido"
,"web_error_update": "Error al actualizar la lista: {0}"
,"web_error_download": "Error: {0}"
,"web_history_clear": "Limpiar historial"
,"web_history_cleared": "¡Historial limpiado con éxito!"
,"web_error_clear_history": "Error al limpiar historial: {0}"
,"web_settings_title": "Información y Configuración"
,"web_settings_roms_folder": "Carpeta ROMs personalizada"
,"web_settings_roms_placeholder": "Dejar vacío para predeterminado"
,"web_settings_browse": "Explorar"
,"web_settings_language": "Idioma"
,"web_settings_font_scale": "Escala de fuente"
,"web_settings_grid": "Diseño de cuadrícula"
,"web_settings_font_family": "Familia de fuente"
,"web_settings_music": "Música"
,"web_settings_symlink": "Modo symlink"
,"web_settings_source_mode": "Fuente de juegos"
,"web_settings_custom_url": "URL personalizada"
,"web_settings_custom_url_placeholder": "Dejar vacío para /saves/ports/rgsx/games.zip o usar una URL directa como https://ejemplo.com/juegos.zip"
,"web_settings_save": "Guardar configuración"
,"web_settings_saved": "¡Configuración guardada con éxito!"
,"web_settings_saved_restart": "¡Configuración guardada con éxito!\\n\\n⚠ Algunos ajustes requieren reiniciar el servidor:\\n- Carpeta ROMs personalizada\\n- Idioma\\n\\nPor favor, reinicie el servidor web para aplicar estos cambios."
,"web_error_save_settings": "Error al guardar configuración: {0}"
,"web_browse_title": "Explorar directorios"
,"web_browse_select_drive": "Seleccione una unidad..."
,"web_browse_drives": "Unidades"
,"web_browse_parent": "Arriba"
,"web_browse_select": "Seleccionar esta carpeta"
,"web_browse_cancel": "Cancelar"
,"web_browse_empty": "No se encontraron subdirectorios"
,"web_browse_alert_restart": "Importante: Debe GUARDAR la configuración y luego REINICIAR el servidor web para que la carpeta ROMs personalizada tenga efecto.\\n\\n📝 Pasos:\\n1. Haga clic en 'Guardar configuración' abajo\\n2. Detenga el servidor web (Ctrl+C en terminal)\\n3. Reinicie el servidor web\\n\\nRuta seleccionada: {0}"
,"web_error_browse": "Error al explorar directorios: {0}"
,"web_loading_platforms": "Cargando plataformas..."
,"web_loading_games": "Cargando juegos..."
,"web_no_platforms": "No se encontraron plataformas"
,"web_no_downloads": "No hay descargas en curso"
,"web_history_empty": "No hay descargas completadas"
,"web_history_platform": "Plataforma"
,"web_history_size": "Tamaño"
,"web_history_status_completed": "Completado"
,"web_history_status_error": "Error"
,"web_settings_os": "Sistema operativo"
,"web_settings_platforms_count": "Número de plataformas"
,"web_settings_show_unsupported": "Mostrar plataformas no compatibles (sistema ausente en es_systems.cfg)"
,"web_settings_allow_unknown": "Permitir extensiones desconocidas (no mostrar advertencias)"
,"web_restart_confirm_title": "¿Reiniciar aplicación?"
,"web_restart_confirm_message": "Los parámetros se han guardado. ¿Desea reiniciar la aplicación ahora para aplicar los cambios?"
,"web_restart_yes": "Sí, reiniciar"
,"web_restart_no": "No, más tarde"
,"web_restart_success": "Reiniciando..."
,"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_generating": "Generando archivo de soporte..."
,"web_support_download": "Descargar archivo de soporte"
,"web_support_error": "Error al generar el archivo de soporte: {0}"
,"web_tab_queue": "Cola"
,"web_tooltip_queue": "Cola de descargas"
,"web_queue_active_download": "⏳ Una descarga está activa"
,"web_queue_no_active": "✓ Sin descargas activas"
,"web_queue_title": "Cola de Descargas"
,"web_queue_empty": "No hay elementos en la cola"
,"web_queue_clear": "Limpiar cola"
,"web_queue_cleared": "¡Cola limpiada con éxito!"
,"web_confirm_remove_queue": "¿Eliminar este elemento de la cola?"
,"web_confirm_clear_queue": "¿Limpiar toda la cola?"
,"web_remove": "Eliminar"
,"web_loading": "Cargando..."
,"web_sort": "Ordenar por"
,"web_sort_name_asc": "A-Z (Nombre)"
,"web_sort_name_desc": "Z-A (Nombre)"
,"web_sort_size_asc": "Tamaño +- (Menor primero)"
,"web_sort_size_desc": "Tamaño -+ (Mayor primero)"
"popup_hide_premium_off": "Sistemas Premium visibles",
"submenu_display_font_family": "Fuente",
"popup_font_family_changed": "Fuente cambiada: {0}",
"instruction_pause_language": "Cambiar el idioma de la interfaz",
"instruction_pause_controls": "Ver esquema de controles o remapear",
"instruction_pause_display": "Configurar distribución, fuentes y visibilidad de sistemas",
"instruction_pause_games": "Abrir historial, cambiar fuente o refrescar lista",
"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_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",
"instruction_display_layout": "Alternar dimensiones de la cuadrícula (columnas × filas)",
"instruction_display_font_size": "Ajustar tamaño del texto para mejor legibilidad",
"instruction_display_footer_font_size": "Ajustar el tamaño del texto del pie de página (versión y controles)",
"instruction_display_font_family": "Cambiar entre familias de fuentes disponibles",
"instruction_display_show_unsupported": "Mostrar/ocultar sistemas no definidos en es_systems.cfg",
"instruction_display_unknown_ext": "Activar/desactivar aviso para extensiones no presentes en es_systems.cfg",
"instruction_display_hide_premium": "Ocultar sistemas que requieren acceso premium vía API: {providers}",
"instruction_display_filter_platforms": "Elegir manualmente qué sistemas son visibles",
"instruction_games_history": "Ver descargas pasadas y su estado",
"instruction_games_source_mode": "Cambiar entre lista RGSX o fuente personalizada",
"instruction_games_update_cache": "Volver a descargar y refrescar la lista de juegos",
"instruction_settings_music": "Activar o desactivar música de fondo",
"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",
"settings_web_service_enabling": "Activando servicio web...",
"settings_web_service_disabling": "Desactivando servicio web...",
"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 ↑",
"controls_desc_down": "DOWN ↓",
"controls_desc_left": "LEFT ←",
"controls_desc_right": "RIGHT →",
"controls_desc_page_up": "Desplazamiento rápido - (ej. LT/L2)",
"controls_desc_page_down": "Desplazamiento rápido + (ej. RT/R2)",
"controls_desc_history": "Abrir historial (ej. Y/Triángulo)",
"controls_desc_clear_history": "Descargas: Selección múltiple / Historial: Limpiar (ej. X/Cuadrado)",
"controls_desc_filter": "Modo filtro: Abrir/Confirmar (ej. Select)",
"controls_desc_delete": "Modo filtro: Eliminar carácter (ej. LB/L1)",
"controls_desc_space": "Modo filtro: Añadir espacio (ej. RB/R1)",
"controls_desc_start": "Abrir menú pausa (ej. Start)",
"controls_mapping_title": "Asignación de controles",
"controls_mapping_instruction": "Mantén para confirmar la asignación:",
"controls_mapping_waiting": "Esperando una tecla o botón...",
"controls_mapping_press": "Pulsa una tecla o un botón",
"status_already_present": "Ya Presente",
"footer_joystick": "Joystick: {0}",
"history_game_options_title": "Opciones del juego",
"history_option_download_folder": "Localizar archivo",
"history_option_extract_archive": "Extraer archivo",
"history_option_open_file": "Abrir archivo",
"history_option_scraper": "Obtener metadatos",
"history_option_delete_game": "Eliminar juego",
"history_option_error_info": "Detalles del error",
"history_option_retry": "Reintentar descarga",
"history_option_back": "Volver",
"history_folder_path_label": "Ruta de destino:",
"history_scraper_not_implemented": "Scraper aún no implementado",
"history_confirm_delete": "¿Eliminar este juego del disco?",
"history_file_not_found": "Archivo no encontrado",
"history_extracting": "Extrayendo...",
"history_extracted": "Extraído",
"history_delete_success": "Juego eliminado con éxito",
"history_delete_error": "Error al eliminar juego: {0}",
"history_error_details_title": "Detalles del error",
"history_no_error_message": "No hay mensaje de error disponible",
"web_title": "Interfaz Web RGSX",
"web_tab_platforms": "Lista de sistemas",
"web_tab_downloads": "Descargas",
"web_tab_history": "Historial",
"web_tab_settings": "Configuración",
"web_tab_update": "Actualizar lista",
"web_tooltip_platforms": "Lista de sistemas",
"web_tooltip_downloads": "Descargas",
"web_tooltip_history": "Historial",
"web_tooltip_settings": "Configuración",
"web_tooltip_update": "Actualizar lista de juegos",
"web_search_platform": "Buscar sistemas o juegos...",
"web_search_game": "Buscar un juego...",
"web_search_results": "resultados para",
"web_no_results": "No se encontraron resultados",
"web_platforms": "Sistemas",
"web_games": "Juegos",
"web_error_search": "Error de búsqueda",
"web_back_platforms": "Volver a plataformas",
"web_back": "Volver",
"web_game_count": "{0} ({1} juegos)",
"web_download": "Descargar",
"web_cancel": "Cancelar",
"web_download_canceled": "Descarga cancelada",
"web_confirm_cancel": "¿Realmente desea cancelar esta descarga?",
"web_update_title": "Actualizando lista de juegos...",
"web_update_message": "Limpiando caché y recargando datos...",
"web_update_wait": "Esto puede tardar 10-30 segundos",
"web_error": "Error",
"web_error_unknown": "Error desconocido",
"web_error_update": "Error al actualizar la lista: {0}",
"web_error_download": "Error: {0}",
"web_history_clear": "Limpiar historial",
"web_history_cleared": "¡Historial limpiado con éxito!",
"web_error_clear_history": "Error al limpiar historial: {0}",
"web_settings_title": "Información y Configuración",
"web_settings_roms_folder": "Carpeta ROMs personalizada",
"web_settings_roms_placeholder": "Dejar vacío para predeterminado",
"web_settings_browse": "Explorar",
"web_settings_language": "Idioma",
"web_settings_font_scale": "Escala de fuente",
"web_settings_grid": "Diseño de cuadrícula",
"web_settings_font_family": "Familia de fuente",
"web_settings_music": "Música",
"web_settings_symlink": "Modo symlink",
"web_settings_source_mode": "Fuente de juegos",
"web_settings_custom_url": "URL personalizada",
"web_settings_custom_url_placeholder": "Dejar vacío para /saves/ports/rgsx/games.zip o usar una URL directa como https://ejemplo.com/juegos.zip",
"web_settings_save": "Guardar configuración",
"web_settings_saved": "¡Configuración guardada con éxito!",
"web_settings_saved_restart": "¡Configuración guardada con éxito!\\n\\n⚠ Algunos ajustes requieren reiniciar el servidor:\\n- Carpeta ROMs personalizada\\n- Idioma\\n\\nPor favor, reinicie el servidor web para aplicar estos cambios.",
"web_error_save_settings": "Error al guardar configuración: {0}",
"web_browse_title": "Explorar directorios",
"web_browse_select_drive": "Seleccione una unidad...",
"web_browse_drives": "Unidades",
"web_browse_parent": "Arriba",
"web_browse_select": "Seleccionar esta carpeta",
"web_browse_cancel": "Cancelar",
"web_browse_empty": "No se encontraron subdirectorios",
"web_browse_alert_restart": "Importante: Debe GUARDAR la configuración y luego REINICIAR el servidor web para que la carpeta ROMs personalizada tenga efecto.\\n\\n📝 Pasos:\\n1. Haga clic en 'Guardar configuración' abajo\\n2. Detenga el servidor web (Ctrl+C en terminal)\\n3. Reinicie el servidor web\\n\\nRuta seleccionada: {0}",
"web_error_browse": "Error al explorar directorios: {0}",
"web_loading_platforms": "Cargando plataformas...",
"web_loading_games": "Cargando juegos...",
"web_no_platforms": "No se encontraron plataformas",
"web_no_downloads": "No hay descargas en curso",
"web_history_empty": "No hay descargas completadas",
"web_history_platform": "Plataforma",
"web_history_size": "Tamaño",
"web_history_status_completed": "Completado",
"web_history_status_error": "Error",
"web_settings_os": "Sistema operativo",
"web_settings_platforms_count": "Número de plataformas",
"web_settings_show_unsupported": "Mostrar plataformas no compatibles (sistema ausente en es_systems.cfg)",
"web_settings_allow_unknown": "Permitir extensiones desconocidas (no mostrar advertencias)",
"web_restart_confirm_title": "¿Reiniciar aplicación?",
"web_restart_confirm_message": "Los parámetros se han guardado. ¿Desea reiniciar la aplicación ahora para aplicar los cambios?",
"web_restart_yes": "Sí, reiniciar",
"web_restart_no": "No, más tarde",
"web_restart_success": "Reiniciando...",
"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_generating": "Generando archivo de soporte...",
"web_support_download": "Descargar archivo de soporte",
"web_support_error": "Error al generar el archivo de soporte: {0}",
"web_tab_queue": "Cola",
"web_tooltip_queue": "Cola de descargas",
"web_queue_active_download": "⏳ Una descarga está activa",
"web_queue_no_active": "✓ Sin descargas activas",
"web_queue_title": "Cola de Descargas",
"web_queue_empty": "No hay elementos en la cola",
"web_queue_clear": "Limpiar cola",
"web_queue_cleared": "¡Cola limpiada con éxito!",
"web_confirm_remove_queue": "¿Eliminar este elemento de la cola?",
"web_confirm_clear_queue": "¿Limpiar toda la cola?",
"web_remove": "Eliminar",
"web_loading": "Cargando...",
"web_sort": "Ordenar por",
"web_sort_name_asc": "A-Z (Nombre)",
"web_sort_name_desc": "Z-A (Nombre)",
"web_sort_size_asc": "Tamaño +- (Menor primero)",
"web_sort_size_desc": "Tamaño -+ (Mayor primero)",
"web_filter_region": "Región",
"web_filter_hide_non_release": "Ocultar Demos/Betas/Protos",
"web_filter_regex_mode": "Activar búsqueda Regex",
"web_filter_one_rom_per_game": "Una ROM por juego",
"web_filter_configure_priority": "Configurar orden de prioridad de regiones",
"filter_all": "Marcar todo",
"filter_none": "Desmarcar todo",
"filter_apply": "Aplicar filtro",
"accessibility_footer_font_size": "Tamaño fuente pie de página: {0}",
"popup_layout_changed_restart": "Diseño cambiado a {0}x{1}. Reinicie la app para aplicar.",
"web_started": "Iniciado",
"web_downloading": "Descarga",
"web_in_progress": "En curso",
"web_added_to_queue": "añadido a la cola",
"web_download_success": "¡descargado con éxito!",
"web_download_error_for": "Error al descargar",
"web_already_present": "ya estaba presente",
"filter_menu_title": "Menú de filtros",
"filter_search_by_name": "Buscar por nombre",
"filter_advanced": "Filtrado avanzado",
"filter_advanced_title": "Filtrado avanzado de juegos",
"filter_region_title": "Filtrar por región",
"filter_region_include": "Incluir",
"filter_region_exclude": "Excluir",
"filter_region_usa": "EE.UU.",
"filter_region_canada": "Canadá",
"filter_region_europe": "Europa",
"filter_region_france": "Francia",
"filter_region_germany": "Alemania",
"filter_region_japan": "Japón",
"filter_region_korea": "Corea",
"filter_region_world": "Mundial",
"filter_region_other": "Otros",
"filter_other_options": "Otras opciones",
"filter_hide_non_release": "Ocultar Demos/Betas/Protos",
"filter_one_rom_per_game": "Una ROM por juego",
"filter_priority_order": "Orden de prioridad",
"filter_priority_title": "Configuración de prioridad de regiones",
"filter_priority_desc": "Definir orden de preferencia para \"Una ROM por juego\"",
"filter_regex_mode": "Modo Regex",
"filter_apply_filters": "Aplicar",
"filter_reset_filters": "Restablecer",
"filter_back": "Volver",
"filter_active": "Filtro activo",
"filter_games_shown": "{0} juego(s) mostrado(s)"
}

View File

@@ -141,7 +141,7 @@
"download_queued": "En file d'attente",
"download_started": "Téléchargement démarré",
"network_download_already_queued": "Ce téléchargement est déjà en cours",
"utils_extracted": "Extracted: {0}",
"utils_extracted": "Extrait: {0}",
"utils_corrupt_zip": "Archive ZIP corrompue: {0}",
"utils_permission_denied": "Permission refusée lors de l'extraction: {0}",
"utils_extraction_failed": "Échec de l'extraction: {0}",
@@ -176,181 +176,236 @@
"api_key_empty_suffix": "vide",
"menu_hide_premium_systems": "Masquer systèmes Premium",
"popup_hide_premium_on": "Systèmes Premium masqués",
"popup_hide_premium_off": "Systèmes Premium visibles"
,"submenu_display_font_family": "Police"
,"popup_font_family_changed": "Police changée : {0}"
,"instruction_pause_language": "Changer la langue de l'interface"
,"instruction_pause_controls": "Afficher la configuration ou remapper"
,"instruction_pause_display": "Agencer l'affichage, polices et systèmes visibles"
,"instruction_pause_games": "Historique, source de liste ou rafraîchissement"
,"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_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"
,"instruction_display_layout": "Changer les dimensions de la grille"
,"instruction_display_font_size": "Ajuster la taille du texte pour la lisibilité"
,"instruction_display_font_family": "Basculer entre les polices disponibles"
,"instruction_display_show_unsupported": "Afficher/masquer systèmes absents de es_systems.cfg"
,"instruction_display_unknown_ext": "Avertir ou non pour extensions absentes de es_systems.cfg"
,"instruction_display_hide_premium": "Masquer les systèmes nécessitant un accès premium via API: {providers}"
,"instruction_display_filter_platforms": "Choisir manuellement les systèmes visibles"
,"instruction_games_history": "Lister les téléchargements passés et leur statut"
,"instruction_games_source_mode": "Basculer entre liste RGSX ou source personnalisée"
,"instruction_games_update_cache": "Retélécharger & rafraîchir la liste des jeux"
,"instruction_settings_music": "Activer ou désactiver la lecture musicale"
,"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"
,"settings_web_service": "Service Web au démarrage"
,"settings_web_service_enabled": "Activé"
,"settings_web_service_disabled": "Désactivé"
,"settings_web_service_enabling": "Activation du service web..."
,"settings_web_service_disabling": "Désactivation du service web..."
,"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}"
,"controls_desc_confirm": "Valider (ex: A/Croix)"
,"controls_desc_cancel": "Annuler/Retour (ex: B/Rond)"
,"controls_desc_up": "UP ↑"
,"controls_desc_down": "DOWN ↓"
,"controls_desc_left": "LEFT ←"
,"controls_desc_right": "RIGHT →"
,"controls_desc_page_up": "Défilement Rapide - (ex: LT/L2)"
,"controls_desc_page_down": "Défilement Rapide + (ex: RT/R2)"
,"controls_desc_history": "Ouvrir l'historique (ex: Y/Triangle)"
,"controls_desc_clear_history": "Téléchargements : Sélection multiple / Historique : Vider (ex: X/Carré)"
,"controls_desc_filter": "Mode Filtre : Ouvrir/Valider (ex: Select)"
,"controls_desc_delete": "Mode Filtre : Supprimer caractère (ex: LB/L1)"
,"controls_desc_space": "Mode Filtre : Ajouter espace (ex: RB/R1)"
,"controls_desc_start": "Ouvrir le menu pause (ex: Start)"
,"controls_mapping_title": "Configuration des contrôles"
,"controls_mapping_instruction": "Maintenez pour confirmer l'association :"
,"controls_mapping_waiting": "En attente d'une touche ou d'un bouton..."
,"controls_mapping_press": "Appuyez sur une touche ou un bouton"
,"status_already_present": "Déjà Présent"
,"footer_joystick": "Joystick : {0}"
,"history_game_options_title": "Options du jeu"
,"history_option_download_folder": "Localiser le fichier"
,"history_option_extract_archive": "Extraire l'archive"
,"history_option_scraper": "Scraper 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_back": "Retour"
,"history_folder_path_label": "Chemin de destination :"
,"history_scraper_not_implemented": "Scraper pas encore implémenté"
,"history_confirm_delete": "Supprimer ce jeu du disque ?"
,"history_file_not_found": "Fichier introuvable"
,"history_extracting": "Extraction en cours..."
,"history_extracted": "Extrait"
,"history_delete_success": "Jeu supprimé avec succès"
,"history_delete_error": "Erreur lors de la suppression du jeu : {0}"
,"history_error_details_title": "Détails de l'erreur"
,"history_no_error_message": "Aucun message d'erreur disponible"
,"web_title": "Interface Web RGSX"
,"web_tab_platforms": "Liste des systèmes"
,"web_tab_downloads": "Téléchargements"
,"web_tab_history": "Historique"
,"web_tab_settings": "Paramètres"
,"web_tab_update": "Mettre à jour la liste"
,"web_tooltip_platforms": "Liste des systèmes"
,"web_tooltip_downloads": "Téléchargements"
,"web_tooltip_history": "Historique"
,"web_tooltip_settings": "Paramètres"
,"web_tooltip_update": "Mettre à jour la liste des jeux"
,"web_search_platform": "Rechercher des systèmes ou jeux..."
,"web_search_game": "Rechercher un jeu..."
,"web_search_results": "résultats pour"
,"web_no_results": "Aucun résultat trouvé"
,"web_platforms": "Systèmes"
,"web_games": "Jeux"
,"web_error_search": "Erreur de recherche"
,"web_back_platforms": "Retour aux plateformes"
,"web_back": "Retour"
,"web_game_count": "{0} ({1} jeux)"
,"web_download": "Télécharger"
,"web_cancel": "Annuler"
,"web_download_canceled": "Téléchargement annulé"
,"web_confirm_cancel": "Voulez-vous vraiment annuler ce téléchargement ?"
,"web_update_title": "Mise à jour de la liste des jeux..."
,"web_update_message": "Nettoyage du cache et rechargement des données..."
,"web_update_wait": "Cela peut prendre 10-30 secondes"
,"web_error": "Erreur"
,"web_error_unknown": "Erreur inconnue"
,"web_error_update": "Erreur lors de la mise à jour de la liste : {0}"
,"web_error_download": "Erreur : {0}"
,"web_history_clear": "Vider l'historique"
,"web_history_cleared": "Historique vidé avec succès !"
,"web_error_clear_history": "Erreur lors du vidage de l'historique : {0}"
,"web_settings_title": "Informations & Paramètres"
,"web_settings_roms_folder": "Dossier ROMs personnalisé"
,"web_settings_roms_placeholder": "Laisser vide pour le dossier par défaut"
,"web_settings_browse": "Parcourir"
,"web_settings_language": "Langue"
,"web_settings_font_scale": "Échelle de police"
,"web_settings_grid": "Grille d'affichage"
,"web_settings_font_family": "Police de caractères"
,"web_settings_music": "Musique"
,"web_settings_symlink": "Mode symlink"
,"web_settings_source_mode": "Source des jeux"
,"web_settings_custom_url": "URL personnalisée"
,"web_settings_custom_url_placeholder": "Laisser vide pour /saves/ports/rgsx/games.zip ou utiliser une URL directe comme https://exemple.com/jeux.zip"
,"web_settings_save": "Enregistrer les paramètres"
,"web_settings_saved": "Paramètres enregistrés avec succès !"
,"web_settings_saved_restart": "Paramètres enregistrés avec succès !\\n\\n⚠ Certains paramètres nécessitent un redémarrage du serveur :\\n- Dossier ROMs personnalisé\\n- Langue\\n\\nVeuillez redémarrer le serveur web pour appliquer ces changements."
,"web_error_save_settings": "Erreur lors de l'enregistrement : {0}"
,"web_browse_title": "Parcourir les dossiers"
,"web_browse_select_drive": "Sélectionnez un lecteur..."
,"web_browse_drives": "Lecteurs"
,"web_browse_parent": "Parent"
,"web_browse_select": "Sélectionner ce dossier"
,"web_browse_cancel": "Annuler"
,"web_browse_empty": "Aucun sous-dossier trouvé"
,"web_browse_alert_restart": "Important : Vous devez ENREGISTRER les paramètres puis REDÉMARRER le serveur web pour que le dossier ROMs personnalisé soit pris en compte.\\n\\n📝 Étapes :\\n1. Cliquez sur 'Enregistrer les paramètres' ci-dessous\\n2. Arrêtez le serveur web (Ctrl+C dans le terminal)\\n3. Redémarrez le serveur web\\n\\nChemin sélectionné : {0}"
,"web_error_browse": "Erreur lors de la navigation : {0}"
,"web_loading_platforms": "Chargement des plateformes..."
,"web_loading_games": "Chargement des jeux..."
,"web_no_platforms": "Aucune plateforme trouvée"
,"web_no_downloads": "Aucun téléchargement en cours"
,"web_history_empty": "Aucun téléchargement terminé"
,"web_history_platform": "Plateforme"
,"web_history_size": "Taille"
,"web_history_status_completed": "Terminé"
,"web_history_status_error": "Erreur"
,"web_settings_os": "Système d'exploitation"
,"web_settings_platforms_count": "Nombre de plateformes"
,"web_settings_show_unsupported": "Afficher les systèmes non supportés (absents de es_systems.cfg)"
,"web_settings_allow_unknown": "Autoriser les extensions inconnues (ne pas afficher d'avertissement)"
,"web_restart_confirm_title": "Redémarrer l'application ?"
,"web_restart_confirm_message": "Les paramètres ont été enregistrés. Voulez-vous redémarrer l'application maintenant pour appliquer les changements ?"
,"web_restart_yes": "Oui, redémarrer"
,"web_restart_no": "Non, plus tard"
,"web_restart_success": "Redémarrage en cours..."
,"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_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}"
,"web_tab_queue": "File d'attente"
,"web_tooltip_queue": "File d'attente des téléchargements"
,"web_queue_active_download": "⏳ Un téléchargement est actuellement en cours"
,"web_queue_no_active": "✓ Aucun téléchargement actif"
,"web_queue_title": "File d'attente des téléchargements"
,"web_queue_empty": "Aucun élément en attente"
,"web_queue_clear": "Vider la file d'attente"
,"web_queue_cleared": "File d'attente vidée avec succès !"
,"web_confirm_remove_queue": "Supprimer cet élément de la file d'attente ?"
,"web_confirm_clear_queue": "Vider toute la file d'attente ?"
,"web_remove": "Supprimer"
,"web_loading": "Chargement..."
,"web_sort": "Trier par"
,"web_sort_name_asc": "A-Z (Nom)"
,"web_sort_name_desc": "Z-A (Nom)"
,"web_sort_size_asc": "Taille +- (Petit d'abord)"
,"web_sort_size_desc": "Taille -+ (Grand d'abord)"
"popup_hide_premium_off": "Systèmes Premium visibles",
"submenu_display_font_family": "Police",
"popup_font_family_changed": "Police changée : {0}",
"instruction_pause_language": "Changer la langue de l'interface",
"instruction_pause_controls": "Afficher la configuration ou remapper",
"instruction_pause_display": "Agencer l'affichage, polices et systèmes visibles",
"instruction_pause_games": "Historique, source de liste ou rafraîchissement",
"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_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",
"instruction_display_layout": "Changer les dimensions de la grille",
"instruction_display_font_size": "Ajuster la taille du texte pour la lisibilité",
"instruction_display_footer_font_size": "Ajuster la taille du texte du pied de page (version et contrôles)",
"instruction_display_font_family": "Basculer entre les polices disponibles",
"instruction_display_show_unsupported": "Afficher/masquer systèmes absents de es_systems.cfg",
"instruction_display_unknown_ext": "Avertir ou non pour extensions absentes de es_systems.cfg",
"instruction_display_hide_premium": "Masquer les systèmes nécessitant un accès premium via API: {providers}",
"instruction_display_filter_platforms": "Choisir manuellement les systèmes visibles",
"instruction_games_history": "Lister les téléchargements passés et leur statut",
"instruction_games_source_mode": "Basculer entre liste RGSX ou source personnalisée",
"instruction_games_update_cache": "Retélécharger & rafraîchir la liste des jeux",
"instruction_settings_music": "Activer ou désactiver la lecture musicale",
"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é",
"settings_web_service_enabling": "Activation du service web...",
"settings_web_service_disabling": "Désactivation du service web...",
"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 ↑",
"controls_desc_down": "DOWN ↓",
"controls_desc_left": "LEFT ←",
"controls_desc_right": "RIGHT →",
"controls_desc_page_up": "Défilement Rapide - (ex: LT/L2)",
"controls_desc_page_down": "Défilement Rapide + (ex: RT/R2)",
"controls_desc_history": "Ouvrir l'historique (ex: Y/Triangle)",
"controls_desc_clear_history": "Téléchargements : Sélection multiple / Historique : Vider (ex: X/Carré)",
"controls_desc_filter": "Mode Filtre : Ouvrir/Valider (ex: Select)",
"controls_desc_delete": "Mode Filtre : Supprimer caractère (ex: LB/L1)",
"controls_desc_space": "Mode Filtre : Ajouter espace (ex: RB/R1)",
"controls_desc_start": "Ouvrir le menu pause (ex: Start)",
"controls_mapping_title": "Configuration des contrôles",
"controls_mapping_instruction": "Maintenez pour confirmer l'association :",
"controls_mapping_waiting": "En attente d'une touche ou d'un bouton...",
"controls_mapping_press": "Appuyez sur une touche ou un bouton",
"status_already_present": "Déjà Présent",
"footer_joystick": "Joystick : {0}",
"history_game_options_title": "Options du jeu",
"history_option_download_folder": "Localiser le fichier",
"history_option_extract_archive": "Extraire l'archive",
"history_option_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": "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é",
"history_confirm_delete": "Supprimer ce jeu du disque ?",
"history_file_not_found": "Fichier introuvable",
"history_extracting": "Extraction en cours...",
"history_extracted": "Extrait",
"history_delete_success": "Jeu supprimé avec succès",
"history_delete_error": "Erreur lors de la suppression du jeu : {0}",
"history_error_details_title": "Détails de l'erreur",
"history_no_error_message": "Aucun message d'erreur disponible",
"web_title": "Interface Web RGSX",
"web_tab_platforms": "Liste des systèmes",
"web_tab_downloads": "Téléchargements",
"web_tab_history": "Historique",
"web_tab_settings": "Paramètres",
"web_tab_update": "Mettre à jour la liste",
"web_tooltip_platforms": "Liste des systèmes",
"web_tooltip_downloads": "Téléchargements",
"web_tooltip_history": "Historique",
"web_tooltip_settings": "Paramètres",
"web_tooltip_update": "Mettre à jour la liste des jeux",
"web_search_platform": "Rechercher des systèmes ou jeux...",
"web_search_game": "Rechercher un jeu...",
"web_search_results": "résultats pour",
"web_no_results": "Aucun résultat trouvé",
"web_platforms": "Systèmes",
"web_games": "Jeux",
"web_error_search": "Erreur de recherche",
"web_back_platforms": "Retour aux plateformes",
"web_back": "Retour",
"web_game_count": "{0} ({1} jeux)",
"web_download": "Télécharger",
"web_cancel": "Annuler",
"web_download_canceled": "Téléchargement annulé",
"web_confirm_cancel": "Voulez-vous vraiment annuler ce téléchargement ?",
"web_update_title": "Mise à jour de la liste des jeux...",
"web_update_message": "Nettoyage du cache et rechargement des données...",
"web_update_wait": "Cela peut prendre 10-30 secondes",
"web_error": "Erreur",
"web_error_unknown": "Erreur inconnue",
"web_error_update": "Erreur lors de la mise à jour de la liste : {0}",
"web_error_download": "Erreur : {0}",
"web_history_clear": "Vider l'historique",
"web_history_cleared": "Historique vidé avec succès !",
"web_error_clear_history": "Erreur lors du vidage de l'historique : {0}",
"web_settings_title": "Informations & Paramètres",
"web_settings_roms_folder": "Dossier ROMs personnalisé",
"web_settings_roms_placeholder": "Laisser vide pour le dossier par défaut",
"web_settings_browse": "Parcourir",
"web_settings_language": "Langue",
"web_settings_font_scale": "Échelle de police",
"web_settings_grid": "Grille d'affichage",
"web_settings_font_family": "Police de caractères",
"web_settings_music": "Musique",
"web_settings_symlink": "Mode symlink",
"web_settings_source_mode": "Source des jeux",
"web_settings_custom_url": "URL personnalisée",
"web_settings_custom_url_placeholder": "Laisser vide pour /saves/ports/rgsx/games.zip ou utiliser une URL directe comme https://exemple.com/jeux.zip",
"web_settings_save": "Enregistrer les paramètres",
"web_settings_saved": "Paramètres enregistrés avec succès !",
"web_settings_saved_restart": "Paramètres enregistrés avec succès !\\n\\n⚠ Certains paramètres nécessitent un redémarrage du serveur :\\n- Dossier ROMs personnalisé\\n- Langue\\n\\nVeuillez redémarrer le serveur web pour appliquer ces changements.",
"web_error_save_settings": "Erreur lors de l'enregistrement : {0}",
"web_browse_title": "Parcourir les dossiers",
"web_browse_select_drive": "Sélectionnez un lecteur...",
"web_browse_drives": "Lecteurs",
"web_browse_parent": "Parent",
"web_browse_select": "Sélectionner ce dossier",
"web_browse_cancel": "Annuler",
"web_browse_empty": "Aucun sous-dossier trouvé",
"web_browse_alert_restart": "Important : Vous devez ENREGISTRER les paramètres puis REDÉMARRER le serveur web pour que le dossier ROMs personnalisé soit pris en compte.\\n\\n📝 Étapes :\\n1. Cliquez sur 'Enregistrer les paramètres' ci-dessous\\n2. Arrêtez le serveur web (Ctrl+C dans le terminal)\\n3. Redémarrez le serveur web\\n\\nChemin sélectionné : {0}",
"web_error_browse": "Erreur lors de la navigation : {0}",
"web_loading_platforms": "Chargement des plateformes...",
"web_loading_games": "Chargement des jeux...",
"web_no_platforms": "Aucune plateforme trouvée",
"web_no_downloads": "Aucun téléchargement en cours",
"web_history_empty": "Aucun téléchargement terminé",
"web_history_platform": "Plateforme",
"web_history_size": "Taille",
"web_history_status_completed": "Terminé",
"web_history_status_error": "Erreur",
"web_settings_os": "Système d'exploitation",
"web_settings_platforms_count": "Nombre de plateformes",
"web_settings_show_unsupported": "Afficher les systèmes non supportés (absents de es_systems.cfg)",
"web_settings_allow_unknown": "Autoriser les extensions inconnues (ne pas afficher d'avertissement)",
"web_restart_confirm_title": "Redémarrer l'application ?",
"web_restart_confirm_message": "Les paramètres ont été enregistrés. Voulez-vous redémarrer l'application maintenant pour appliquer les changements ?",
"web_restart_yes": "Oui, redémarrer",
"web_restart_no": "Non, plus tard",
"web_restart_success": "Redémarrage en cours...",
"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_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}",
"web_tab_queue": "File d'attente",
"web_tooltip_queue": "File d'attente des téléchargements",
"web_queue_active_download": "⏳ Un téléchargement est actuellement en cours",
"web_queue_no_active": "✓ Aucun téléchargement actif",
"web_queue_title": "File d'attente des téléchargements",
"web_queue_empty": "Aucun élément en attente",
"web_queue_clear": "Vider la file d'attente",
"web_queue_cleared": "File d'attente vidée avec succès !",
"web_confirm_remove_queue": "Supprimer cet élément de la file d'attente ?",
"web_confirm_clear_queue": "Vider toute la file d'attente ?",
"web_remove": "Supprimer",
"web_loading": "Chargement...",
"web_sort": "Trier par",
"web_sort_name_asc": "A-Z (Nom)",
"web_sort_name_desc": "Z-A (Nom)",
"web_sort_size_asc": "Taille +- (Petit d'abord)",
"web_sort_size_desc": "Taille -+ (Grand d'abord)",
"web_filter_region": "Région",
"web_filter_hide_non_release": "Masquer Démos/Betas/Protos",
"web_filter_regex_mode": "Activer recherche Regex",
"web_filter_one_rom_per_game": "Une ROM par jeu",
"web_filter_configure_priority": "Configurer l'ordre de priorité des régions",
"filter_all": "Tout cocher",
"filter_none": "Tout décocher",
"filter_apply": "Appliquer filtre",
"accessibility_footer_font_size": "Taille police pied de page : {0}",
"popup_layout_changed_restart": "Disposition changée en {0}x{1}. Veuillez redémarrer l'app pour appliquer.",
"web_started": "Démarré",
"web_downloading": "Téléchargement",
"web_in_progress": "En cours",
"web_added_to_queue": "ajouté à la queue",
"web_download_success": "téléchargé avec succès!",
"web_download_error_for": "Erreur lors du téléchargement de",
"web_already_present": "était déjà présent",
"filter_menu_title": "Menu Filtrage",
"filter_search_by_name": "Recherche par nom",
"filter_advanced": "Filtrage avancé",
"filter_advanced_title": "Filtrage avancé des jeux",
"filter_region_title": "Filtrer par région",
"filter_region_include": "Inclure",
"filter_region_exclude": "Exclure",
"filter_region_usa": "USA",
"filter_region_canada": "Canada",
"filter_region_europe": "Europe",
"filter_region_france": "France",
"filter_region_germany": "Allemagne",
"filter_region_japan": "Japon",
"filter_region_korea": "Corée",
"filter_region_world": "Monde",
"filter_region_other": "Autres",
"filter_other_options": "Autres options",
"filter_hide_non_release": "Masquer Démos/Betas/Protos",
"filter_one_rom_per_game": "Une ROM par jeu",
"filter_priority_order": "Ordre de priorité",
"filter_priority_title": "Configuration de la priorité des régions",
"filter_priority_desc": "Définir l'ordre de préférence pour \"Une ROM par jeu\"",
"filter_regex_mode": "Mode Regex",
"filter_apply_filters": "Appliquer",
"filter_reset_filters": "Réinitialiser",
"filter_back": "Retour",
"filter_active": "Filtre actif",
"filter_games_shown": "{0} jeu(x) affiché(s)"
}

View File

@@ -89,7 +89,7 @@
"popup_restarting": "Riavvio...",
"controls_action_clear_history": "Cancella cronologia",
"controls_action_history": "Cronologia / Downloads",
"controls_action_close_history": "Chiudi Cronologia",
"controls_action_close_history": "Chiudi Cronologia",
"controls_action_delete": "Elimina",
"controls_action_space": "Spazio",
"controls_action_start": "Aiuto / Impostazioni",
@@ -128,9 +128,16 @@
"download_in_progress": "Download in corso...",
"download_queued": "In coda di download",
"download_started": "Download iniziato",
"accessibility_font_size": "Dimensione carattere: {0}",
"confirm_cancel_download": "Annullare il download corrente?",
"controls_help_title": "Guida ai controlli",
"network_download_already_queued": "Questo download è già in corso",
"utils_extracted": "Estratto: {0}",
"utils_corrupt_zip": "Archivio ZIP corrotto: {0}",
"utils_permission_denied": "Permesso negato durante l'estrazione: {0}",
"utils_extraction_failed": "Estrazione fallita: {0}",
"utils_unrar_unavailable": "Comando unrar non disponibile",
"utils_rar_list_failed": "Impossibile elencare i file RAR: {0}",
"symlink_option_enabled": "Opzione symlink abilitata",
"symlink_option_disabled": "Opzione symlink disabilitata",
"menu_games_source_prefix": "Sorgente giochi",
"controls_category_navigation": "Navigazione",
"controls_category_main_actions": "Azioni principali",
"controls_category_downloads": "Download",
@@ -140,9 +147,6 @@
"controls_confirm_select": "Conferma/Seleziona",
"controls_cancel_back": "Annulla/Indietro",
"controls_filter_search": "Filtro/Ricerca",
"symlink_option_enabled": "Opzione symlink abilitata",
"symlink_option_disabled": "Opzione symlink disabilitata",
"menu_games_source_prefix": "Sorgente giochi",
"games_source_rgsx": "RGSX",
"sources_mode_rgsx_select_info": "RGSX: aggiorna l'elenco dei giochi",
"games_source_custom": "Personalizzato",
@@ -169,181 +173,239 @@
"api_key_empty_suffix": "vuoto",
"menu_hide_premium_systems": "Nascondi sistemi Premium",
"popup_hide_premium_on": "Sistemi Premium nascosti",
"popup_hide_premium_off": "Sistemi Premium visibili"
,"submenu_display_font_family": "Font"
,"popup_font_family_changed": "Font cambiato: {0}"
,"instruction_pause_language": "Cambiare la lingua dell'interfaccia"
,"instruction_pause_controls": "Vedere schema controlli o avviare rimappatura"
,"instruction_pause_display": "Configurare layout, font e visibilità sistemi"
,"instruction_pause_games": "Aprire cronologia, cambiare sorgente o aggiornare elenco"
,"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_controls_help": "Mostrare riferimento completo controller & tastiera"
,"instruction_controls_remap": "Modificare associazione pulsanti / tasti"
,"instruction_generic_back": "Tornare al menu precedente"
,"instruction_display_layout": "Scorrere dimensioni griglia (colonne × righe)"
,"instruction_display_font_size": "Regolare dimensione testo per leggibilità"
,"instruction_display_font_family": "Cambiare famiglia di font disponibile"
,"instruction_display_show_unsupported": "Mostrare/nascondere sistemi non definiti in es_systems.cfg"
,"instruction_display_unknown_ext": "Attivare/disattivare avviso per estensioni assenti in es_systems.cfg"
,"instruction_display_hide_premium": "Nascondere sistemi che richiedono accesso premium via API: {providers}"
,"instruction_display_filter_platforms": "Scegliere manualmente quali sistemi sono visibili"
,"instruction_games_history": "Elencare download passati e stato"
,"instruction_games_source_mode": "Passare tra elenco RGSX o sorgente personalizzata"
,"instruction_games_update_cache": "Riscaria e aggiorna l'elenco dei giochi"
,"instruction_settings_music": "Abilitare o disabilitare musica di sottofondo"
,"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"
,"settings_web_service": "Servizio Web all'Avvio"
,"settings_web_service_enabled": "Abilitato"
,"settings_web_service_disabled": "Disabilitato"
,"settings_web_service_enabling": "Abilitazione servizio web..."
,"settings_web_service_disabling": "Disabilitazione servizio web..."
,"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}"
,"controls_desc_confirm": "Confermare (es. A/Croce)"
,"controls_desc_cancel": "Annullare/Indietro (es. B/Cerchio)"
,"controls_desc_up": "UP ↑"
,"controls_desc_down": "DOWN ↓"
,"controls_desc_left": "LEFT ←"
,"controls_desc_right": "RIGHT →"
,"controls_desc_page_up": "Scorrimento rapido su (es. LT/L2)"
,"controls_desc_page_down": "Scorrimento rapido giù (es. RT/R2)"
,"controls_desc_history": "Aprire cronologia (es. Y/Triangolo)"
,"controls_desc_clear_history": "Download: Selezione multipla / Cronologia: Svuotare (es. X/Quadrato)"
,"controls_desc_filter": "Modalità filtro: Aprire/Confermare (es. Select)"
,"controls_desc_delete": "Modalità filtro: Eliminare carattere (es. LB/L1)"
,"controls_desc_space": "Modalità filtro: Aggiungere spazio (es. RB/R1)"
,"controls_desc_start": "Aprire menu pausa (es. Start)"
,"controls_mapping_title": "Mappatura controlli"
,"controls_mapping_instruction": "Tieni premuto per confermare l'associazione:"
,"controls_mapping_waiting": "In attesa di un tasto o pulsante..."
,"controls_mapping_press": "Premi un tasto o un pulsante"
,"status_already_present": "Già Presente"
,"footer_joystick": "Joystick: {0}"
,"history_game_options_title": "Opzioni gioco"
,"history_option_download_folder": "Localizza file"
,"history_option_extract_archive": "Estrai archivio"
,"history_option_scraper": "Scraper metadati"
,"history_option_delete_game": "Elimina gioco"
,"history_option_error_info": "Dettagli errore"
,"history_option_retry": "Riprova download"
,"history_option_back": "Indietro"
,"history_folder_path_label": "Percorso destinazione:"
,"history_scraper_not_implemented": "Scraper non ancora implementato"
,"history_confirm_delete": "Eliminare questo gioco dal disco?"
,"history_file_not_found": "File non trovato"
,"history_extracting": "Estrazione in corso..."
,"history_extracted": "Estratto"
,"history_delete_success": "Gioco eliminato con successo"
,"history_delete_error": "Errore durante l'eliminazione del gioco: {0}"
,"history_error_details_title": "Dettagli errore"
,"history_no_error_message": "Nessun messaggio di errore disponibile"
,"web_title": "Interfaccia Web RGSX"
,"web_tab_platforms": "Elenco sistemi"
,"web_tab_downloads": "Download"
,"web_tab_history": "Cronologia"
,"web_tab_settings": "Impostazioni"
,"web_tab_update": "Aggiorna elenco"
,"web_tooltip_platforms": "Elenco sistemi"
,"web_tooltip_downloads": "Download"
,"web_tooltip_history": "Cronologia"
,"web_tooltip_settings": "Impostazioni"
,"web_tooltip_update": "Aggiorna elenco giochi"
,"web_search_platform": "Cerca sistemi o giochi..."
,"web_search_game": "Cerca un gioco..."
,"web_search_results": "risultati per"
,"web_no_results": "Nessun risultato trovato"
,"web_platforms": "Sistemi"
,"web_games": "Giochi"
,"web_error_search": "Errore di ricerca"
,"web_back_platforms": "Torna alle piattaforme"
,"web_back": "Indietro"
,"web_game_count": "{0} ({1} giochi)"
,"web_download": "Scarica"
,"web_cancel": "Annulla"
,"web_download_canceled": "Download annullato"
,"web_confirm_cancel": "Vuoi davvero annullare questo download?"
,"web_update_title": "Aggiornamento elenco giochi..."
,"web_update_message": "Pulizia cache e ricaricamento dati..."
,"web_update_wait": "Potrebbe richiedere 10-30 secondi"
,"web_error": "Errore"
,"web_error_unknown": "Errore sconosciuto"
,"web_error_update": "Errore durante l'aggiornamento dell'elenco: {0}"
,"web_error_download": "Errore: {0}"
,"web_history_clear": "Cancella cronologia"
,"web_history_cleared": "Cronologia cancellata con successo!"
,"web_error_clear_history": "Errore durante la cancellazione della cronologia: {0}"
,"web_settings_title": "Info e Impostazioni"
,"web_settings_roms_folder": "Cartella ROMs personalizzata"
,"web_settings_roms_placeholder": "Lasciare vuoto per predefinito"
,"web_settings_browse": "Sfoglia"
,"web_settings_language": "Lingua"
,"web_settings_font_scale": "Scala carattere"
,"web_settings_grid": "Layout griglia"
,"web_settings_font_family": "Famiglia carattere"
,"web_settings_music": "Musica"
,"web_settings_symlink": "Modalità symlink"
,"web_settings_source_mode": "Fonte giochi"
,"web_settings_custom_url": "URL personalizzato"
,"web_settings_custom_url_placeholder": " Lasciare vuoto per /saves/ports/rgsx/games.zip o usare una URL diretta come https://esempio.com/giochi.zip"
,"web_settings_save": "Salva impostazioni"
,"web_settings_saved": "Impostazioni salvate con successo!"
,"web_settings_saved_restart": "Impostazioni salvate con successo!\\n\\n⚠ Alcune impostazioni richiedono il riavvio del server:\\n- Cartella ROMs personalizzata\\n- Lingua\\n\\nRiavviare il server web per applicare queste modifiche."
,"web_error_save_settings": "Errore durante il salvataggio delle impostazioni: {0}"
,"web_browse_title": "Sfoglia directory"
,"web_browse_select_drive": "Seleziona un'unità..."
,"web_browse_drives": "Unità"
,"web_browse_parent": "Superiore"
,"web_browse_select": "Seleziona questa cartella"
,"web_browse_cancel": "Annulla"
,"web_browse_empty": "Nessuna sottodirectory trovata"
,"web_browse_alert_restart": "Importante: È necessario SALVARE le impostazioni e poi RIAVVIARE il server web affinché la cartella ROMs personalizzata abbia effetto.\\n\\n📝 Passaggi:\\n1. Fare clic su 'Salva impostazioni' qui sotto\\n2. Arrestare il server web (Ctrl+C nel terminale)\\n3. Riavviare il server web\\n\\nPercorso selezionato: {0}"
,"web_error_browse": "Errore durante la navigazione delle directory: {0}"
,"web_loading_platforms": "Caricamento piattaforme..."
,"web_loading_games": "Caricamento giochi..."
,"web_no_platforms": "Nessuna piattaforma trovata"
,"web_no_downloads": "Nessun download in corso"
,"web_history_empty": "Nessun download completato"
,"web_history_platform": "Piattaforma"
,"web_history_size": "Dimensione"
,"web_history_status_completed": "Completato"
,"web_history_status_error": "Errore"
,"web_settings_os": "Sistema operativo"
,"web_settings_platforms_count": "Numero di piattaforme"
,"web_settings_show_unsupported": "Mostra piattaforme non supportate (sistema assente in es_systems.cfg)"
,"web_settings_allow_unknown": "Consenti estensioni sconosciute (non mostrare avvisi)"
,"web_restart_confirm_title": "Riavviare l'applicazione?"
,"web_restart_confirm_message": "Le impostazioni sono state salvate. Vuoi riavviare l'applicazione ora per applicare le modifiche?"
,"web_restart_yes": "Sì, riavvia"
,"web_restart_no": "No, più tardi"
,"web_restart_success": "Riavvio in corso..."
,"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_generating": "Generazione file di supporto..."
,"web_support_download": "Scarica file di supporto"
,"web_support_error": "Errore nella generazione del file di supporto: {0}"
,"web_tab_queue": "Coda"
,"web_tooltip_queue": "Coda di download"
,"web_queue_active_download": "⏳ Un download è attivo"
,"web_queue_no_active": "✓ Nessun download attivo"
,"web_queue_title": "Coda di Download"
,"web_queue_empty": "Nessun elemento in coda"
,"web_queue_clear": "Svuota coda"
,"web_queue_cleared": "Coda svuotata con successo!"
,"web_confirm_remove_queue": "Rimuovere questo elemento dalla coda?"
,"web_confirm_clear_queue": "Svuotare l'intera coda?"
,"web_remove": "Rimuovi"
,"web_loading": "Caricamento..."
,"web_sort": "Ordina per"
,"web_sort_name_asc": "A-Z (Nome)"
,"web_sort_name_desc": "Z-A (Nome)"
,"web_sort_size_asc": "Dimensione +- (Piccolo primo)"
,"web_sort_size_desc": "Dimensione -+ (Grande primo)"
"popup_hide_premium_off": "Sistemi Premium visibili",
"submenu_display_font_family": "Font",
"popup_font_family_changed": "Font cambiato: {0}",
"instruction_pause_language": "Cambiare la lingua dell'interfaccia",
"instruction_pause_controls": "Vedere schema controlli o avviare rimappatura",
"instruction_pause_display": "Configurare layout, font e visibilità sistemi",
"instruction_pause_games": "Aprire cronologia, cambiare sorgente o aggiornare elenco",
"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_controls_help": "Mostrare riferimento completo controller & tastiera",
"instruction_controls_remap": "Modificare associazione pulsanti / tasti",
"instruction_generic_back": "Tornare al menu precedente",
"instruction_display_layout": "Scorrere dimensioni griglia (colonne × righe)",
"instruction_display_font_size": "Regolare dimensione testo per leggibilità",
"instruction_display_footer_font_size": "Regola dimensione testo piè di pagina (versione e controlli)",
"instruction_display_font_family": "Cambiare famiglia di font disponibile",
"instruction_display_show_unsupported": "Mostrare/nascondere sistemi non definiti in es_systems.cfg",
"instruction_display_unknown_ext": "Attivare/disattivare avviso per estensioni assenti in es_systems.cfg",
"instruction_display_hide_premium": "Nascondere sistemi che richiedono accesso premium via API: {providers}",
"instruction_display_filter_platforms": "Scegliere manualmente quali sistemi sono visibili",
"instruction_games_history": "Elencare download passati e stato",
"instruction_games_source_mode": "Passare tra elenco RGSX o sorgente personalizzata",
"instruction_games_update_cache": "Riscaria e aggiorna l'elenco dei giochi",
"instruction_settings_music": "Abilitare o disabilitare musica di sottofondo",
"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",
"settings_web_service_enabling": "Abilitazione servizio web...",
"settings_web_service_disabling": "Disabilitazione servizio web...",
"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 ↑",
"controls_desc_down": "DOWN ↓",
"controls_desc_left": "LEFT ←",
"controls_desc_right": "RIGHT →",
"controls_desc_page_up": "Scorrimento rapido su (es. LT/L2)",
"controls_desc_page_down": "Scorrimento rapido giù (es. RT/R2)",
"controls_desc_history": "Aprire cronologia (es. Y/Triangolo)",
"controls_desc_clear_history": "Download: Selezione multipla / Cronologia: Svuotare (es. X/Quadrato)",
"controls_desc_filter": "Modalità filtro: Aprire/Confermare (es. Select)",
"controls_desc_delete": "Modalità filtro: Eliminare carattere (es. LB/L1)",
"controls_desc_space": "Modalità filtro: Aggiungere spazio (es. RB/R1)",
"controls_desc_start": "Aprire menu pausa (es. Start)",
"controls_mapping_title": "Mappatura controlli",
"controls_mapping_instruction": "Tieni premuto per confermare l'associazione:",
"controls_mapping_waiting": "In attesa di un tasto o pulsante...",
"controls_mapping_press": "Premi un tasto o un pulsante",
"status_already_present": "Già Presente",
"footer_joystick": "Joystick: {0}",
"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",
"history_option_retry": "Riprova download",
"history_option_back": "Indietro",
"history_folder_path_label": "Percorso destinazione:",
"history_scraper_not_implemented": "Scraper non ancora implementato",
"history_confirm_delete": "Eliminare questo gioco dal disco?",
"history_file_not_found": "File non trovato",
"history_extracting": "Estrazione in corso...",
"history_extracted": "Estratto",
"history_delete_success": "Gioco eliminato con successo",
"history_delete_error": "Errore durante l'eliminazione del gioco: {0}",
"history_error_details_title": "Dettagli errore",
"history_no_error_message": "Nessun messaggio di errore disponibile",
"web_title": "Interfaccia Web RGSX",
"web_tab_platforms": "Elenco sistemi",
"web_tab_downloads": "Download",
"web_tab_history": "Cronologia",
"web_tab_settings": "Impostazioni",
"web_tab_update": "Aggiorna elenco",
"web_tooltip_platforms": "Elenco sistemi",
"web_tooltip_downloads": "Download",
"web_tooltip_history": "Cronologia",
"web_tooltip_settings": "Impostazioni",
"web_tooltip_update": "Aggiorna elenco giochi",
"web_search_platform": "Cerca sistemi o giochi...",
"web_search_game": "Cerca un gioco...",
"web_search_results": "risultati per",
"web_no_results": "Nessun risultato trovato",
"web_platforms": "Sistemi",
"web_games": "Giochi",
"web_error_search": "Errore di ricerca",
"web_back_platforms": "Torna alle piattaforme",
"web_back": "Indietro",
"web_game_count": "{0} ({1} giochi)",
"web_download": "Scarica",
"web_cancel": "Annulla",
"web_download_canceled": "Download annullato",
"web_confirm_cancel": "Vuoi davvero annullare questo download?",
"web_update_title": "Aggiornamento elenco giochi...",
"web_update_message": "Pulizia cache e ricaricamento dati...",
"web_update_wait": "Potrebbe richiedere 10-30 secondi",
"web_error": "Errore",
"web_error_unknown": "Errore sconosciuto",
"web_error_update": "Errore durante l'aggiornamento dell'elenco: {0}",
"web_error_download": "Errore: {0}",
"web_history_clear": "Cancella cronologia",
"web_history_cleared": "Cronologia cancellata con successo!",
"web_error_clear_history": "Errore durante la cancellazione della cronologia: {0}",
"web_settings_title": "Info e Impostazioni",
"web_settings_roms_folder": "Cartella ROMs personalizzata",
"web_settings_roms_placeholder": "Lasciare vuoto per predefinito",
"web_settings_browse": "Sfoglia",
"web_settings_language": "Lingua",
"web_settings_font_scale": "Scala carattere",
"web_settings_grid": "Layout griglia",
"web_settings_font_family": "Famiglia carattere",
"web_settings_music": "Musica",
"web_settings_symlink": "Modalità symlink",
"web_settings_source_mode": "Fonte giochi",
"web_settings_custom_url": "URL personalizzato",
"web_settings_custom_url_placeholder": " Lasciare vuoto per /saves/ports/rgsx/games.zip o usare una URL diretta come https://esempio.com/giochi.zip",
"web_settings_save": "Salva impostazioni",
"web_settings_saved": "Impostazioni salvate con successo!",
"web_settings_saved_restart": "Impostazioni salvate con successo!\\n\\n⚠ Alcune impostazioni richiedono il riavvio del server:\\n- Cartella ROMs personalizzata\\n- Lingua\\n\\nRiavviare il server web per applicare queste modifiche.",
"web_error_save_settings": "Errore durante il salvataggio delle impostazioni: {0}",
"web_browse_title": "Sfoglia directory",
"web_browse_select_drive": "Seleziona un'unità...",
"web_browse_drives": "Unità",
"web_browse_parent": "Superiore",
"web_browse_select": "Seleziona questa cartella",
"web_browse_cancel": "Annulla",
"web_browse_empty": "Nessuna sottodirectory trovata",
"web_browse_alert_restart": "Importante: È necessario SALVARE le impostazioni e poi RIAVVIARE il server web affinché la cartella ROMs personalizzata abbia effetto.\\n\\n📝 Passaggi:\\n1. Fare clic su 'Salva impostazioni' qui sotto\\n2. Arrestare il server web (Ctrl+C nel terminale)\\n3. Riavviare il server web\\n\\nPercorso selezionato: {0}",
"web_error_browse": "Errore durante la navigazione delle directory: {0}",
"web_loading_platforms": "Caricamento piattaforme...",
"web_loading_games": "Caricamento giochi...",
"web_no_platforms": "Nessuna piattaforma trovata",
"web_no_downloads": "Nessun download in corso",
"web_history_empty": "Nessun download completato",
"web_history_platform": "Piattaforma",
"web_history_size": "Dimensione",
"web_history_status_completed": "Completato",
"web_history_status_error": "Errore",
"web_settings_os": "Sistema operativo",
"web_settings_platforms_count": "Numero di piattaforme",
"web_settings_show_unsupported": "Mostra piattaforme non supportate (sistema assente in es_systems.cfg)",
"web_settings_allow_unknown": "Consenti estensioni sconosciute (non mostrare avvisi)",
"web_restart_confirm_title": "Riavviare l'applicazione?",
"web_restart_confirm_message": "Le impostazioni sono state salvate. Vuoi riavviare l'applicazione ora per applicare le modifiche?",
"web_restart_yes": "Sì, riavvia",
"web_restart_no": "No, più tardi",
"web_restart_success": "Riavvio in corso...",
"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_generating": "Generazione file di supporto...",
"web_support_download": "Scarica file di supporto",
"web_support_error": "Errore nella generazione del file di supporto: {0}",
"web_tab_queue": "Coda",
"web_tooltip_queue": "Coda di download",
"web_queue_active_download": "⏳ Un download è attivo",
"web_queue_no_active": "✓ Nessun download attivo",
"web_queue_title": "Coda di Download",
"web_queue_empty": "Nessun elemento in coda",
"web_queue_clear": "Svuota coda",
"web_queue_cleared": "Coda svuotata con successo!",
"web_confirm_remove_queue": "Rimuovere questo elemento dalla coda?",
"web_confirm_clear_queue": "Svuotare l'intera coda?",
"web_remove": "Rimuovi",
"web_loading": "Caricamento...",
"web_sort": "Ordina per",
"web_sort_name_asc": "A-Z (Nome)",
"web_sort_name_desc": "Z-A (Nome)",
"web_sort_size_asc": "Dimensione +- (Piccolo primo)",
"web_sort_size_desc": "Dimensione -+ (Grande primo)",
"accessibility_font_size": "Dimensione carattere: {0}",
"confirm_cancel_download": "Annullare il download corrente?",
"controls_help_title": "Guida ai controlli",
"web_filter_region": "Regione",
"web_filter_hide_non_release": "Nascondi Demo/Beta/Proto",
"web_filter_regex_mode": "Attiva ricerca Regex",
"web_filter_one_rom_per_game": "Una ROM per gioco",
"web_filter_configure_priority": "Configura ordine di priorità delle regioni",
"filter_all": "Seleziona tutto",
"filter_none": "Deseleziona tutto",
"filter_apply": "Applica filtro",
"accessibility_footer_font_size": "Dimensione carattere piè di pagina: {0}",
"popup_layout_changed_restart": "Layout cambiato a {0}x{1}. Riavvia l'app per applicare.",
"web_started": "Avviato",
"web_downloading": "Download",
"web_in_progress": "In corso",
"web_added_to_queue": "aggiunto alla coda",
"web_download_success": "scaricato con successo!",
"web_download_error_for": "Errore durante il download di",
"web_already_present": "era già presente",
"filter_menu_title": "Menu filtri",
"filter_search_by_name": "Cerca per nome",
"filter_advanced": "Filtro avanzato",
"filter_advanced_title": "Filtro avanzato giochi",
"filter_region_title": "Filtra per regione",
"filter_region_include": "Includi",
"filter_region_exclude": "Escludi",
"filter_region_usa": "USA",
"filter_region_canada": "Canada",
"filter_region_europe": "Europa",
"filter_region_france": "Francia",
"filter_region_germany": "Germania",
"filter_region_japan": "Giappone",
"filter_region_korea": "Corea",
"filter_region_world": "Mondo",
"filter_region_other": "Altro",
"filter_other_options": "Altre opzioni",
"filter_hide_non_release": "Nascondi Demo/Beta/Proto",
"filter_one_rom_per_game": "Una ROM per gioco",
"filter_priority_order": "Ordine di priorità",
"filter_priority_title": "Configurazione priorità regioni",
"filter_priority_desc": "Imposta ordine di preferenza per \"Una ROM per gioco\"",
"filter_regex_mode": "Modalità Regex",
"filter_apply_filters": "Applica",
"filter_reset_filters": "Reimposta",
"filter_back": "Indietro",
"filter_active": "Filtro attivo",
"filter_games_shown": "{0} gioco/i mostrato/i"
}

View File

@@ -89,7 +89,7 @@
"popup_restarting": "Reiniciando...",
"controls_action_clear_history": "Limpar histórico",
"controls_action_history": "Histórico / Downloads",
"controls_action_close_history": "Fechar Histórico",
"controls_action_close_history": "Fechar Histórico",
"controls_action_delete": "Deletar",
"controls_action_space": "Espaço",
"controls_action_start": "Ajuda / Configurações",
@@ -128,7 +128,13 @@
"download_in_progress": "Download em andamento...",
"download_queued": "Na fila de download",
"download_started": "Download iniciado",
"accessibility_font_size": "Tamanho da fonte: {0}",
"network_download_already_queued": "Este download já está em andamento",
"utils_extracted": "Extraído: {0}",
"utils_corrupt_zip": "Arquivo ZIP corrupto: {0}",
"utils_permission_denied": "Permissão negada durante a extração: {0}",
"utils_extraction_failed": "Falha na extração: {0}",
"utils_unrar_unavailable": "Comando unrar não disponível",
"utils_rar_list_failed": "Falha ao listar arquivos RAR: {0}",
"confirm_cancel_download": "Cancelar download atual?",
"controls_help_title": "Ajuda de Controles",
"controls_category_navigation": "Navegação",
@@ -169,181 +175,237 @@
"api_key_empty_suffix": "vazio",
"menu_hide_premium_systems": "Ocultar sistemas Premium",
"popup_hide_premium_on": "Sistemas Premium ocultos",
"popup_hide_premium_off": "Sistemas Premium visíveis"
,"submenu_display_font_family": "Fonte"
,"popup_font_family_changed": "Fonte alterada: {0}"
,"instruction_pause_language": "Alterar o idioma da interface"
,"instruction_pause_controls": "Ver esquema de controles ou iniciar remapeamento"
,"instruction_pause_display": "Configurar layout, fontes e visibilidade de sistemas"
,"instruction_pause_games": "Abrir histórico, mudar fonte ou atualizar lista"
,"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_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"
,"instruction_display_layout": "Alternar dimensões da grade (colunas × linhas)"
,"instruction_display_font_size": "Ajustar tamanho do texto para legibilidade"
,"instruction_display_font_family": "Alternar entre famílias de fontes disponíveis"
,"instruction_display_show_unsupported": "Mostrar/ocultar sistemas não definidos em es_systems.cfg"
,"instruction_display_unknown_ext": "Ativar/desativar aviso para extensões ausentes em es_systems.cfg"
,"instruction_display_hide_premium": "Ocultar sistemas que exigem acesso premium via API: {providers}"
,"instruction_display_filter_platforms": "Escolher manualmente quais sistemas são visíveis"
,"instruction_games_history": "Listar downloads anteriores e status"
,"instruction_games_source_mode": "Alternar entre lista RGSX ou fonte personalizada"
,"instruction_games_update_cache": "Baixar novamente e atualizar a lista de jogos"
,"instruction_settings_music": "Ativar ou desativar música de fundo"
,"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"
,"settings_web_service": "Serviço Web na Inicialização"
,"settings_web_service_enabled": "Ativado"
,"settings_web_service_disabled": "Desativado"
,"settings_web_service_enabling": "Ativando serviço web..."
,"settings_web_service_disabling": "Desativando serviço web..."
,"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}"
,"controls_desc_confirm": "Confirmar (ex. A/Cruz)"
,"controls_desc_cancel": "Cancelar/Voltar (ex. B/Círculo)"
,"controls_desc_up": "UP ↑"
,"controls_desc_down": "DOWN ↓"
,"controls_desc_left": "LEFT ←"
,"controls_desc_right": "RIGHT →"
,"controls_desc_page_up": "Rolagem rápida para cima (ex. LT/L2)"
,"controls_desc_page_down": "Rolagem rápida para baixo (ex. RT/R2)"
,"controls_desc_history": "Abrir histórico (ex. Y/Triângulo)"
,"controls_desc_clear_history": "Downloads: Seleção múltipla / Histórico: Limpar (ex. X/Quadrado)"
,"controls_desc_filter": "Modo filtro: Abrir/Confirmar (ex. Select)"
,"controls_desc_delete": "Modo filtro: Deletar caractere (ex. LB/L1)"
,"controls_desc_space": "Modo filtro: Adicionar espaço (ex. RB/R1)"
,"controls_desc_start": "Abrir menu pausa (ex. Start)"
,"controls_mapping_title": "Mapeamento de controles"
,"controls_mapping_instruction": "Mantenha para confirmar o mapeamento:"
,"controls_mapping_waiting": "Aguardando uma tecla ou botão..."
,"controls_mapping_press": "Pressione uma tecla ou um botão"
,"status_already_present": "Já Presente"
,"footer_joystick": "Joystick: {0}"
,"history_game_options_title": "Opções do jogo"
,"history_option_download_folder": "Localizar arquivo"
,"history_option_extract_archive": "Extrair arquivo"
,"history_option_scraper": "Scraper metadados"
,"history_option_delete_game": "Excluir jogo"
,"history_option_error_info": "Detalhes do erro"
,"history_option_retry": "Tentar novamente"
,"history_option_back": "Voltar"
,"history_folder_path_label": "Caminho de destino:"
,"history_scraper_not_implemented": "Scraper ainda não implementado"
,"history_confirm_delete": "Excluir este jogo do disco?"
,"history_file_not_found": "Arquivo não encontrado"
,"history_extracting": "Extraindo..."
,"history_extracted": "Extraído"
,"history_delete_success": "Jogo excluído com sucesso"
,"history_delete_error": "Erro ao excluir jogo: {0}"
,"history_error_details_title": "Detalhes do erro"
,"history_no_error_message": "Nenhuma mensagem de erro disponível"
,"web_title": "Interface Web RGSX"
,"web_tab_platforms": "Lista de sistemas"
,"web_tab_downloads": "Downloads"
,"web_tab_history": "Histórico"
,"web_tab_settings": "Configurações"
,"web_tab_update": "Atualizar lista"
,"web_tooltip_platforms": "Lista de sistemas"
,"web_tooltip_downloads": "Downloads"
,"web_tooltip_history": "Histórico"
,"web_tooltip_settings": "Configurações"
,"web_tooltip_update": "Atualizar lista de jogos"
,"web_search_platform": "Pesquisar sistemas ou jogos..."
,"web_search_game": "Pesquisar um jogo..."
,"web_search_results": "resultados para"
,"web_no_results": "Nenhum resultado encontrado"
,"web_platforms": "Sistemas"
,"web_games": "Jogos"
,"web_error_search": "Erro de pesquisa"
,"web_back_platforms": "Voltar às plataformas"
,"web_back": "Voltar"
,"web_game_count": "{0} ({1} jogos)"
,"web_download": "Baixar"
,"web_cancel": "Cancelar"
,"web_download_canceled": "Download cancelado"
,"web_confirm_cancel": "Você realmente deseja cancelar este download?"
,"web_update_title": "Atualizando lista de jogos..."
,"web_update_message": "Limpando cache e recarregando dados..."
,"web_update_wait": "Isso pode levar 10-30 segundos"
,"web_error": "Erro"
,"web_error_unknown": "Erro desconhecido"
,"web_error_update": "Erro ao atualizar a lista: {0}"
,"web_error_download": "Erro: {0}"
,"web_history_clear": "Limpar histórico"
,"web_history_cleared": "Histórico limpo com sucesso!"
,"web_error_clear_history": "Erro ao limpar histórico: {0}"
,"web_settings_title": "Informações e Configurações"
,"web_settings_roms_folder": "Pasta ROMs personalizada"
,"web_settings_roms_placeholder": "Deixar vazio para padrão"
,"web_settings_browse": "Procurar"
,"web_settings_language": "Idioma"
,"web_settings_font_scale": "Escala de fonte"
,"web_settings_grid": "Layout de grade"
,"web_settings_font_family": "Família de fonte"
,"web_settings_music": "Música"
,"web_settings_symlink": "Modo symlink"
,"web_settings_source_mode": "Fonte de jogos"
,"web_settings_custom_url": "URL personalizada"
,"web_settings_custom_url_placeholder": "Deixar vazio para /saves/ports/rgsx/games.zip ou usar uma URL direta como https://example.com/games.zip"
,"web_settings_save": "Salvar configurações"
,"web_settings_saved": "Configurações salvas com sucesso!"
,"web_settings_saved_restart": "Configurações salvas com sucesso!\\n\\n⚠ Algumas configurações exigem reiniciar o servidor:\\n- Pasta ROMs personalizada\\n- Idioma\\n\\nPor favor, reinicie o servidor web para aplicar essas alterações."
,"web_error_save_settings": "Erro ao salvar configurações: {0}"
,"web_browse_title": "Procurar diretórios"
,"web_browse_select_drive": "Selecione uma unidade..."
,"web_browse_drives": "Unidades"
,"web_browse_parent": "Acima"
,"web_browse_select": "Selecionar esta pasta"
,"web_browse_cancel": "Cancelar"
,"web_browse_empty": "Nenhum subdiretório encontrado"
,"web_browse_alert_restart": "Importante: Você precisa SALVAR as configurações e então REINICIAR o servidor web para que a pasta ROMs personalizada tenha efeito.\\n\\n📝 Passos:\\n1. Clique em 'Salvar configurações' abaixo\\n2. Pare o servidor web (Ctrl+C no terminal)\\n3. Reinicie o servidor web\\n\\nCaminho selecionado: {0}"
,"web_error_browse": "Erro ao procurar diretórios: {0}"
,"web_loading_platforms": "Carregando plataformas..."
,"web_loading_games": "Carregando jogos..."
,"web_no_platforms": "Nenhuma plataforma encontrada"
,"web_no_downloads": "Nenhum download em andamento"
,"web_history_empty": "Nenhum download concluído"
,"web_history_platform": "Plataforma"
,"web_history_size": "Tamanho"
,"web_history_status_completed": "Concluído"
,"web_history_status_error": "Erro"
,"web_settings_os": "Sistema operacional"
,"web_settings_platforms_count": "Número de plataformas"
,"web_settings_show_unsupported": "Mostrar plataformas não suportadas (sistema ausente em es_systems.cfg)"
,"web_settings_allow_unknown": "Permitir extensões desconhecidas (não mostrar avisos)"
,"web_restart_confirm_title": "Reiniciar aplicação?"
,"web_restart_confirm_message": "As configurações foram salvas. Deseja reiniciar a aplicação agora para aplicar as alterações?"
,"web_restart_yes": "Sim, reiniciar"
,"web_restart_no": "Não, mais tarde"
,"web_restart_success": "Reiniciando..."
,"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_generating": "Gerando arquivo de suporte..."
,"web_support_download": "Baixar arquivo de suporte"
,"web_support_error": "Erro ao gerar arquivo de suporte: {0}"
,"web_tab_queue": "Fila"
,"web_tooltip_queue": "Fila de downloads"
,"web_queue_active_download": "⏳ Um download está ativo"
,"web_queue_no_active": "✓ Sem downloads ativos"
,"web_queue_title": "Fila de Downloads"
,"web_queue_empty": "Nenhum item na fila"
,"web_queue_clear": "Limpar fila"
,"web_queue_cleared": "Fila limpa com sucesso!"
,"web_confirm_remove_queue": "Remover este item da fila?"
,"web_confirm_clear_queue": "Limpar toda a fila?"
,"web_remove": "Remover"
,"web_loading": "Carregando..."
,"web_sort": "Ordenar por"
,"web_sort_name_asc": "A-Z (Nome)"
,"web_sort_name_desc": "Z-A (Nome)"
,"web_sort_size_asc": "Tamanho +- (Menor primeiro)"
,"web_sort_size_desc": "Tamanho -+ (Maior primeiro)"
"popup_hide_premium_off": "Sistemas Premium visíveis",
"submenu_display_font_family": "Fonte",
"popup_font_family_changed": "Fonte alterada: {0}",
"instruction_pause_language": "Alterar o idioma da interface",
"instruction_pause_controls": "Ver esquema de controles ou iniciar remapeamento",
"instruction_pause_display": "Configurar layout, fontes e visibilidade de sistemas",
"instruction_pause_games": "Abrir histórico, mudar fonte ou atualizar lista",
"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_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",
"instruction_display_layout": "Alternar dimensões da grade (colunas × linhas)",
"instruction_display_font_size": "Ajustar tamanho do texto para legibilidade",
"instruction_display_footer_font_size": "Ajustar tamanho do texto do rodapé (versão e controles)",
"instruction_display_font_family": "Alternar entre famílias de fontes disponíveis",
"instruction_display_show_unsupported": "Mostrar/ocultar sistemas não definidos em es_systems.cfg",
"instruction_display_unknown_ext": "Ativar/desativar aviso para extensões ausentes em es_systems.cfg",
"instruction_display_hide_premium": "Ocultar sistemas que exigem acesso premium via API: {providers}",
"instruction_display_filter_platforms": "Escolher manualmente quais sistemas são visíveis",
"instruction_games_history": "Listar downloads anteriores e status",
"instruction_games_source_mode": "Alternar entre lista RGSX ou fonte personalizada",
"instruction_games_update_cache": "Baixar novamente e atualizar a lista de jogos",
"instruction_settings_music": "Ativar ou desativar música de fundo",
"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",
"settings_web_service_enabling": "Ativando serviço web...",
"settings_web_service_disabling": "Desativando serviço web...",
"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 ↑",
"controls_desc_down": "DOWN ↓",
"controls_desc_left": "LEFT ←",
"controls_desc_right": "RIGHT →",
"controls_desc_page_up": "Rolagem rápida para cima (ex. LT/L2)",
"controls_desc_page_down": "Rolagem rápida para baixo (ex. RT/R2)",
"controls_desc_history": "Abrir histórico (ex. Y/Triângulo)",
"controls_desc_clear_history": "Downloads: Seleção múltipla / Histórico: Limpar (ex. X/Quadrado)",
"controls_desc_filter": "Modo filtro: Abrir/Confirmar (ex. Select)",
"controls_desc_delete": "Modo filtro: Deletar caractere (ex. LB/L1)",
"controls_desc_space": "Modo filtro: Adicionar espaço (ex. RB/R1)",
"controls_desc_start": "Abrir menu pausa (ex. Start)",
"controls_mapping_title": "Mapeamento de controles",
"controls_mapping_instruction": "Mantenha para confirmar o mapeamento:",
"controls_mapping_waiting": "Aguardando uma tecla ou botão...",
"controls_mapping_press": "Pressione uma tecla ou um botão",
"status_already_present": "Já Presente",
"footer_joystick": "Joystick: {0}",
"history_game_options_title": "Opções do jogo",
"history_option_download_folder": "Localizar arquivo",
"history_option_extract_archive": "Extrair arquivo",
"history_option_open_file": "Abrir arquivo",
"history_option_scraper": "Obter metadados",
"history_option_delete_game": "Excluir jogo",
"history_option_error_info": "Detalhes do erro",
"history_option_retry": "Tentar novamente",
"history_option_back": "Voltar",
"history_folder_path_label": "Caminho de destino:",
"history_scraper_not_implemented": "Scraper ainda não implementado",
"history_confirm_delete": "Excluir este jogo do disco?",
"history_file_not_found": "Arquivo não encontrado",
"history_extracting": "Extraindo...",
"history_extracted": "Extraído",
"history_delete_success": "Jogo excluído com sucesso",
"history_delete_error": "Erro ao excluir jogo: {0}",
"history_error_details_title": "Detalhes do erro",
"history_no_error_message": "Nenhuma mensagem de erro disponível",
"web_title": "Interface Web RGSX",
"web_tab_platforms": "Lista de sistemas",
"web_tab_downloads": "Downloads",
"web_tab_history": "Histórico",
"web_tab_settings": "Configurações",
"web_tab_update": "Atualizar lista",
"web_tooltip_platforms": "Lista de sistemas",
"web_tooltip_downloads": "Downloads",
"web_tooltip_history": "Histórico",
"web_tooltip_settings": "Configurações",
"web_tooltip_update": "Atualizar lista de jogos",
"web_search_platform": "Pesquisar sistemas ou jogos...",
"web_search_game": "Pesquisar um jogo...",
"web_search_results": "resultados para",
"web_no_results": "Nenhum resultado encontrado",
"web_platforms": "Sistemas",
"web_games": "Jogos",
"web_error_search": "Erro de pesquisa",
"web_back_platforms": "Voltar às plataformas",
"web_back": "Voltar",
"web_game_count": "{0} ({1} jogos)",
"web_download": "Baixar",
"web_cancel": "Cancelar",
"web_download_canceled": "Download cancelado",
"web_confirm_cancel": "Você realmente deseja cancelar este download?",
"web_update_title": "Atualizando lista de jogos...",
"web_update_message": "Limpando cache e recarregando dados...",
"web_update_wait": "Isso pode levar 10-30 segundos",
"web_error": "Erro",
"web_error_unknown": "Erro desconhecido",
"web_error_update": "Erro ao atualizar a lista: {0}",
"web_error_download": "Erro: {0}",
"web_history_clear": "Limpar histórico",
"web_history_cleared": "Histórico limpo com sucesso!",
"web_error_clear_history": "Erro ao limpar histórico: {0}",
"web_settings_title": "Informações e Configurações",
"web_settings_roms_folder": "Pasta ROMs personalizada",
"web_settings_roms_placeholder": "Deixar vazio para padrão",
"web_settings_browse": "Procurar",
"web_settings_language": "Idioma",
"web_settings_font_scale": "Escala de fonte",
"web_settings_grid": "Layout de grade",
"web_settings_font_family": "Família de fonte",
"web_settings_music": "Música",
"web_settings_symlink": "Modo symlink",
"web_settings_source_mode": "Fonte de jogos",
"web_settings_custom_url": "URL personalizada",
"web_settings_custom_url_placeholder": "Deixar vazio para /saves/ports/rgsx/games.zip ou usar uma URL direta como https://example.com/games.zip",
"web_settings_save": "Salvar configurações",
"web_settings_saved": "Configurações salvas com sucesso!",
"web_settings_saved_restart": "Configurações salvas com sucesso!\\n\\n⚠ Algumas configurações exigem reiniciar o servidor:\\n- Pasta ROMs personalizada\\n- Idioma\\n\\nPor favor, reinicie o servidor web para aplicar essas alterações.",
"web_error_save_settings": "Erro ao salvar configurações: {0}",
"web_browse_title": "Procurar diretórios",
"web_browse_select_drive": "Selecione uma unidade...",
"web_browse_drives": "Unidades",
"web_browse_parent": "Acima",
"web_browse_select": "Selecionar esta pasta",
"web_browse_cancel": "Cancelar",
"web_browse_empty": "Nenhum subdiretório encontrado",
"web_browse_alert_restart": "Importante: Você precisa SALVAR as configurações e então REINICIAR o servidor web para que a pasta ROMs personalizada tenha efeito.\\n\\n📝 Passos:\\n1. Clique em 'Salvar configurações' abaixo\\n2. Pare o servidor web (Ctrl+C no terminal)\\n3. Reinicie o servidor web\\n\\nCaminho selecionado: {0}",
"web_error_browse": "Erro ao procurar diretórios: {0}",
"web_loading_platforms": "Carregando plataformas...",
"web_loading_games": "Carregando jogos...",
"web_no_platforms": "Nenhuma plataforma encontrada",
"web_no_downloads": "Nenhum download em andamento",
"web_history_empty": "Nenhum download concluído",
"web_history_platform": "Plataforma",
"web_history_size": "Tamanho",
"web_history_status_completed": "Concluído",
"web_history_status_error": "Erro",
"web_settings_os": "Sistema operacional",
"web_settings_platforms_count": "Número de plataformas",
"web_settings_show_unsupported": "Mostrar plataformas não suportadas (sistema ausente em es_systems.cfg)",
"web_settings_allow_unknown": "Permitir extensões desconhecidas (não mostrar avisos)",
"web_restart_confirm_title": "Reiniciar aplicação?",
"web_restart_confirm_message": "As configurações foram salvas. Deseja reiniciar a aplicação agora para aplicar as alterações?",
"web_restart_yes": "Sim, reiniciar",
"web_restart_no": "Não, mais tarde",
"web_restart_success": "Reiniciando...",
"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_generating": "Gerando arquivo de suporte...",
"web_support_download": "Baixar arquivo de suporte",
"web_support_error": "Erro ao gerar arquivo de suporte: {0}",
"web_tab_queue": "Fila",
"web_tooltip_queue": "Fila de downloads",
"web_queue_active_download": "⏳ Um download está ativo",
"web_queue_no_active": "✓ Sem downloads ativos",
"web_queue_title": "Fila de Downloads",
"web_queue_empty": "Nenhum item na fila",
"web_queue_clear": "Limpar fila",
"web_queue_cleared": "Fila limpa com sucesso!",
"web_confirm_remove_queue": "Remover este item da fila?",
"web_confirm_clear_queue": "Limpar toda a fila?",
"web_remove": "Remover",
"web_loading": "Carregando...",
"web_sort": "Ordenar por",
"web_sort_name_asc": "A-Z (Nome)",
"web_sort_name_desc": "Z-A (Nome)",
"web_sort_size_asc": "Tamanho +- (Menor primeiro)",
"web_sort_size_desc": "Tamanho -+ (Maior primeiro)",
"accessibility_font_size": "Tamanho da fonte: {0}",
"web_filter_region": "Região",
"web_filter_hide_non_release": "Ocultar Demos/Betas/Protos",
"web_filter_regex_mode": "Ativar pesquisa Regex",
"web_filter_one_rom_per_game": "Uma ROM por jogo",
"web_filter_configure_priority": "Configurar ordem de prioridade das regiões",
"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",
"filter_active": "Filtro ativo",
"filter_games_shown": "{0} jogo(s) exibido(s)"
}

View File

@@ -625,7 +625,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 +639,22 @@ def cancel_all_downloads():
th.join(timeout=0.2)
except Exception:
pass
# Vider la file d'attente des téléchargements
config.download_queue.clear()
config.download_active = False
# Mettre à jour l'historique pour annuler les téléchargements en statut "Queued"
try:
history = load_history()
for entry in history:
if entry.get("status") == "Queued":
entry["status"] = "Canceled"
entry["message"] = _("download_canceled")
logger.info(f"Téléchargement en attente annulé : {entry.get('game_name', '?')}")
save_history(history)
except Exception as e:
logger.error(f"Erreur lors de l'annulation des téléchargements en attente : {e}")
@@ -1055,14 +1072,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 +1114,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
logger.debug(f"Mise à jour finale de progression: {downloaded}/{total_size} octets")
# Si annulé, ne pas continuer avec extraction
if download_cancelled:
if download_canceled:
return
os.chmod(dest_path, 0o644)
@@ -2077,7 +2094,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 +2102,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 +2138,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

@@ -53,7 +53,8 @@ def load_rgsx_settings():
"language": "en",
"music_enabled": True,
"accessibility": {
"font_scale": 1.0
"font_scale": 1.0,
"footer_font_scale": 1.5
},
"display": {
"grid": "3x4",
@@ -338,3 +339,26 @@ def get_language(settings=None):
if settings is None:
settings = load_rgsx_settings()
return settings.get("language", "en")
def load_game_filters():
"""Charge les filtres de jeux depuis rgsx_settings.json."""
try:
settings = load_rgsx_settings()
return settings.get("game_filters", {})
except Exception as e:
logger.error(f"Error loading game filters: {str(e)}")
return {}
def save_game_filters(filters_dict):
"""Sauvegarde les filtres de jeux dans rgsx_settings.json."""
try:
settings = load_rgsx_settings()
settings["game_filters"] = filters_dict
save_rgsx_settings(settings)
logger.debug(f"Game filters saved: {filters_dict}")
return True
except Exception as e:
logger.error(f"Error saving game filters: {str(e)}")
return False

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
"""
Module de scraping pour récupérer les métadonnées des jeux depuis TheGamesDB.net
Module de scraping pour récupérer les métadonnées des jeux depuis TheGamesDB.net API v1
"""
import logging
import requests
@@ -9,138 +9,167 @@ import pygame
logger = logging.getLogger(__name__)
# Mapping des noms de plateformes vers leurs IDs sur TheGamesDB
# Les noms correspondent exactement à ceux utilisés dans systems_list.json
# Clé API publique pour TheGamesDB
API_KEY = "bdbb4a1ce5f1c12c1bcc119aeb4d4923d3887e22ad336d576e9b9e5da5ecaa3c"
API_BASE_URL = "https://api.thegamesdb.net/v1"
# Mapping des noms de plateformes vers leurs IDs sur TheGamesDB API
# Documentation: https://api.thegamesdb.net/#/Platforms
PLATFORM_MAPPING = {
# Noms exacts du systems_list.json
"3DO Interactive Multiplayer": "25",
"3DS": "4912",
"Adventure Vision": "4974",
"Amiga CD32": "4947",
"Amiga CDTV": "4947", # Même ID que CD32
"Amiga OCS ECS": "4911",
"Apple II": "4942",
"Apple IIGS": "4942", # Même famille
"Arcadia 2001": "4963",
"Archimedes": "4944",
"Astrocade": "4968",
"Atari 2600": "22",
"Atari 5200": "26",
"Atari 7800": "27",
"Atari Lynx": "4924",
"Atari ST": "4937",
"Atom": "5014",
"Channel-F": "4928",
"ColecoVision": "31",
"Commodore 64": "40",
"Commodore Plus4": "5007",
"Commodore VIC-20": "4945",
"CreatiVision": "5005",
"Dos (x86)": "1",
"Dreamcast": "16",
"Family Computer Disk System": "4936",
"Final Burn Neo": "23", # Arcade
"FM-TOWNS": "4932",
"Gamate": "5004",
"Game Boy": "4",
"Game Boy Advance": "5",
"Game Boy Color": "41",
"Game Cube": "2",
"Game Gear": "20",
"Game Master": "4948", # Mega Duck
"Game.com": "4940",
"Jaguar": "28",
"Macintosh": "37",
"Master System": "35",
"Mattel Intellivision": "32",
"Mega CD": "21",
"Mega Drive": "36",
"Mega Duck Cougar Boy": "4948",
"MSX1": "4929",
"MSX2+": "4929",
"Namco System 246 256": "23", # Arcade
"Naomi": "23", # Arcade
"Naomi 2": "23", # Arcade
"Neo-Geo CD": "4956",
"Neo-Geo Pocket": "4922",
"Neo-Geo Pocket Color": "4923",
"Neo-Geo": "24",
"Nintendo 64": "3",
"Nintendo 64 Disk Drive": "3",
"Nintendo DS": "8",
"Nintendo DSi": "8",
"Nintendo Entertainment System": "7",
"Odyssey2": "4927",
"PC Engine": "34",
"PC Engine CD": "4955",
"PC Engine SuperGrafx": "34",
"PC-9800": "4934",
"PlayStation": "10",
"PlayStation 2": "11",
"PlayStation 3": "12",
"PlayStation Portable": "13",
"PlayStation Vita": "39",
"Pokemon Mini": "4957",
"PV-1000": "4964",
"Satellaview": "6", # SNES addon
"Saturn": "17",
"ScummVM": "1", # PC
"Sega 32X": "33",
"Sega Chihiro": "23", # Arcade
"Sega Pico": "4958",
"SG-1000": "4949",
"Sharp X1": "4977",
"SuFami Turbo": "6", # SNES addon
"Super A'Can": "4918", # Pas d'ID exact, utilise Virtual Boy
"Super Cassette Vision": "4966",
"Super Nintendo Entertainment System": "6",
"Supervision": "4959",
"Switch (1Fichier)": "4971",
"TI-99": "4953",
"V.Smile": "4988",
"Vectrex": "4939",
"Virtual Boy": "4918",
"Wii": "9",
"Wii (Virtual Console)": "9",
"Wii U": "38",
"Windows (1Fichier)": "1",
"WonderSwan": "4925",
"WonderSwan Color": "4926",
"Xbox": "14",
"Xbox 360": "15",
"ZX Spectrum": "4913",
"Game and Watch": "4950",
"Nintendo Famicom Disk System": "4936",
"3DO Interactive Multiplayer": 25,
"3DS": 4912,
"Adventure Vision": 4974,
"Amiga CD32": 4947,
"Amiga CDTV": 4947,
"Amiga OCS ECS": 4911,
"Apple II": 4942,
"Apple IIGS": 4942,
"Arcadia 2001": 4963,
"Archimedes": 4944,
"Astrocade": 4968,
"Atari 2600": 22,
"Atari 5200": 26,
"Atari 7800": 27,
"Atari Lynx": 4924,
"Atari ST": 4937,
"Atom": 5014,
"Channel-F": 4928,
"ColecoVision": 31,
"Commodore 64": 40,
"Commodore Plus4": 5007,
"Commodore VIC-20": 4945,
"CreatiVision": 5005,
"Dos (x86)": 1,
"Dreamcast": 16,
"Family Computer Disk System": 4936,
"Final Burn Neo": 23,
"FM-TOWNS": 4932,
"Gamate": 5004,
"Game Boy": 4,
"Game Boy Advance": 5,
"Game Boy Color": 41,
"Game Cube": 2,
"Game Gear": 20,
"Game Master": 4948,
"Game.com": 4940,
"Jaguar": 28,
"Macintosh": 37,
"Master System": 35,
"Mattel Intellivision": 32,
"Mega CD": 21,
"Mega Drive": 36,
"Mega Duck Cougar Boy": 4948,
"MSX1": 4929,
"MSX2+": 4929,
"Namco System 246 256": 23,
"Naomi": 23,
"Naomi 2": 23,
"Neo-Geo CD": 4956,
"Neo-Geo Pocket": 4922,
"Neo-Geo Pocket Color": 4923,
"Neo-Geo": 24,
"Nintendo 64": 3,
"Nintendo 64 Disk Drive": 3,
"Nintendo DS": 8,
"Nintendo DSi": 8,
"Nintendo Entertainment System": 7,
"Odyssey2": 4927,
"PC Engine": 34,
"PC Engine CD": 4955,
"PC Engine SuperGrafx": 34,
"PC-9800": 4934,
"PlayStation": 10,
"PlayStation 2": 11,
"PlayStation 3": 12,
"PlayStation Portable": 13,
"PlayStation Vita": 39,
"Pokemon Mini": 4957,
"PV-1000": 4964,
"Satellaview": 6,
"Saturn": 17,
"ScummVM": 1,
"Sega 32X": 33,
"Sega Chihiro": 23,
"Sega Pico": 4958,
"SG-1000": 4949,
"Sharp X1": 4977,
"SuFami Turbo": 6,
"Super A'Can": 4918,
"Super Cassette Vision": 4966,
"Super Nintendo Entertainment System": 6,
"Supervision": 4959,
"Switch (1Fichier)": 4971,
"TI-99": 4953,
"V.Smile": 4988,
"Vectrex": 4939,
"Virtual Boy": 4918,
"Wii": 9,
"Wii (Virtual Console)": 9,
"Wii U": 38,
"Windows (1Fichier)": 1,
"WonderSwan": 4925,
"WonderSwan Color": 4926,
"Xbox": 14,
"Xbox 360": 15,
"ZX Spectrum": 4913,
"Game and Watch": 4950,
"Nintendo Famicom Disk System": 4936,
# Aliases communs (pour compatibilité)
"3DO": "25",
"NES": "7",
"SNES": "6",
"GBA": "5",
"GBC": "41",
"GameCube": "2",
"N64": "3",
"NDS": "8",
"PSX": "10",
"PS1": "10",
"PS2": "11",
"PS3": "12",
"PSP": "13",
"PS Vita": "39",
"Genesis": "18",
"32X": "33",
"Game & Watch": "4950",
"PC-98": "4934",
"TurboGrafx 16": "34",
"TurboGrafx CD": "4955",
"Mega Duck": "4948",
"Amiga": "4911"
"3DO": 25,
"NES": 7,
"SNES": 6,
"GBA": 5,
"GBC": 41,
"GameCube": 2,
"N64": 3,
"NDS": 8,
"PSX": 10,
"PS1": 10,
"PS2": 11,
"PS3": 12,
"PSP": 13,
"PS Vita": 39,
"Genesis": 18,
"32X": 33,
"Game & Watch": 4950,
"PC-98": 4934,
"TurboGrafx 16": 34,
"TurboGrafx CD": 4955,
"Mega Duck": 4948,
"Amiga": 4911
}
def clean_game_name(game_name):
"""
Nettoie le nom du jeu en supprimant les extensions et tags
Args:
game_name (str): Nom brut du jeu
Returns:
str: Nom nettoyé
"""
clean_name = game_name
# Supprimer les extensions communes
extensions = ['.zip', '.7z', '.rar', '.iso', '.chd', '.cue', '.bin', '.gdi', '.cdi',
'.nsp', '.xci', '.wbfs', '.rvz', '.gcz', '.wad', '.3ds', '.cia']
for ext in extensions:
if clean_name.lower().endswith(ext):
clean_name = clean_name[:-len(ext)]
# Supprimer les tags entre parenthèses et crochets
clean_name = re.sub(r'\s*[\(\[].*?[\)\]]', '', clean_name)
return clean_name.strip()
def get_game_metadata(game_name, platform_name):
"""
Récupère les métadonnées complètes d'un jeu depuis TheGamesDB.net
Récupère les métadonnées complètes d'un jeu depuis TheGamesDB.net API
Args:
game_name (str): Nom du jeu à rechercher
@@ -150,100 +179,128 @@ def get_game_metadata(game_name, platform_name):
dict: Dictionnaire contenant les métadonnées ou message d'erreur
Keys: image_url, game_page_url, description, genre, release_date, error
"""
# Nettoyer le nom du jeu
clean_game_name = game_name
for ext in ['.zip', '.7z', '.rar', '.iso', '.chd', '.cue', '.bin', '.gdi', '.cdi']:
if clean_game_name.lower().endswith(ext):
clean_game_name = clean_game_name[:-len(ext)]
clean_game_name = re.sub(r'\s*[\(\[].*?[\)\]]', '', clean_game_name)
clean_game_name = clean_game_name.strip()
logger.info(f"Recherche métadonnées pour: '{clean_game_name}' sur plateforme '{platform_name}'")
clean_name = clean_game_name(game_name)
logger.info(f"Recherche métadonnées pour: '{clean_name}' sur plateforme '{platform_name}'")
# Obtenir l'ID de la plateforme
platform_id = PLATFORM_MAPPING.get(platform_name)
if not platform_id:
logger.warning(f"Plateforme '{platform_name}' non trouvée dans le mapping")
return {"error": f"Plateforme '{platform_name}' non supportée"}
# Construire l'URL de recherche
base_url = "https://thegamesdb.net/search.php"
params = {
"name": clean_game_name,
"platform_id[]": platform_id
}
try:
# Envoyer la requête GET pour la recherche
logger.debug(f"Recherche sur TheGamesDB: {base_url} avec params={params}")
response = requests.get(base_url, params=params, timeout=10)
# Endpoint: Games/ByGameName
# Documentation: https://api.thegamesdb.net/#/Games/GamesbyName
url = f"{API_BASE_URL}/Games/ByGameName"
params = {
"apikey": API_KEY,
"name": clean_name,
"filter[platform]": platform_id,
"fields": "players,publishers,genres,overview,last_updated,rating,platform,coop,youtube,os,processor,ram,hdd,video,sound,alternates",
"include": "boxart"
}
logger.debug(f"Requête API: {url} avec name='{clean_name}', platform={platform_id}")
response = requests.get(url, params=params, timeout=15)
if response.status_code != 200:
logger.error(f"Erreur HTTP {response.status_code}: {response.text}")
return {"error": f"Erreur HTTP {response.status_code}"}
html_content = response.text
data = response.json()
# Trouver la première carte avec class 'card border-primary'
card_start = html_content.find('div class="card border-primary"')
if card_start == -1:
return {"error": "Aucun résultat trouvé"}
# Vérifier si des résultats ont été trouvés
if "data" not in data or "games" not in data["data"] or not data["data"]["games"]:
logger.warning(f"Aucun résultat trouvé pour '{clean_name}'")
return {"error": f"No result found for '{clean_name}'"}
# Extraire l'URL de la page du jeu
href_match = re.search(r'<a href="(\.\/game\.php\?id=\d+)">', html_content[card_start-100:card_start+500])
game_page_url = None
if href_match:
game_page_url = f"https://thegamesdb.net/{href_match.group(1)[2:]}" # Enlever le ./
logger.info(f"Page du jeu trouvée: {game_page_url}")
# Prendre le premier résultat (meilleure correspondance)
games = data["data"]["games"]
game = games[0]
game_id = game.get("id")
# Extraire l'URL de l'image
img_start = html_content.find('<img class="card-img-top"', card_start)
image_url = None
if img_start != -1:
src_match = re.search(r'src="([^"]+)"', html_content[img_start:img_start+200])
if src_match:
image_url = src_match.group(1)
if not image_url.startswith("https://"):
image_url = f"https://thegamesdb.net{image_url}"
logger.info(f"Image trouvée: {image_url}")
logger.info(f"Jeu trouvé: '{game.get('game_title')}' (ID: {game_id})")
# Extraire la date de sortie depuis les résultats de recherche
release_date = None
card_footer_start = html_content.find('class="card-footer', card_start)
if card_footer_start != -1:
# Chercher une date au format YYYY-MM-DD
date_match = re.search(r'<p>(\d{4}-\d{2}-\d{2})</p>', html_content[card_footer_start:card_footer_start+300])
if date_match:
release_date = date_match.group(1)
logger.info(f"Date de sortie trouvée: {release_date}")
# Construire l'URL de la page du jeu
game_page_url = f"https://thegamesdb.net/game.php?id={game_id}"
# Si on a l'URL de la page, récupérer la description et le genre
description = None
# Extraire les métadonnées de base
description = game.get("overview", "").strip() or None
release_date = game.get("release_date", "").strip() or None
# Extraire les genres
genre = None
if game_page_url:
try:
logger.debug(f"Récupération de la page du jeu: {game_page_url}")
game_response = requests.get(game_page_url, timeout=10)
if game_response.status_code == 200:
game_html = game_response.text
# Extraire la description
desc_match = re.search(r'<p class="game-overview">(.*?)</p>', game_html, re.DOTALL)
if desc_match:
description = desc_match.group(1).strip()
# Nettoyer les entités HTML
description = description.replace('&#039;', "'")
description = description.replace('&quot;', '"')
description = description.replace('&amp;', '&')
logger.info(f"Description trouvée ({len(description)} caractères)")
# Extraire le genre
genre_match = re.search(r'<p>Genre\(s\): (.*?)</p>', game_html)
if genre_match:
genre = genre_match.group(1).strip()
logger.info(f"Genre trouvé: {genre}")
if "genres" in game and game["genres"]:
genre_ids = game["genres"]
# Les noms des genres sont dans data.genres
if "genres" in data["data"]:
genre_names = []
for gid in genre_ids:
if str(gid) in data["data"]["genres"]:
genre_names.append(data["data"]["genres"][str(gid)]["name"])
if genre_names:
genre = ", ".join(genre_names)
# Extraire l'image de couverture (boxart)
# Utiliser l'endpoint dédié /v1/Games/Images pour récupérer les images du jeu
image_url = None
try:
images_url = f"{API_BASE_URL}/Games/Images"
images_params = {
"apikey": API_KEY,
"games_id": game_id,
"filter[type]": "boxart"
}
except Exception as e:
logger.warning(f"Erreur lors de la récupération de la page du jeu: {e}")
logger.debug(f"Récupération des images pour game_id={game_id}")
images_response = requests.get(images_url, params=images_params, timeout=10)
if images_response.status_code == 200:
images_data = images_response.json()
# Récupérer l'URL de base
base_url_original = ""
if "data" in images_data and "base_url" in images_data["data"]:
base_url_original = images_data["data"]["base_url"].get("original", "")
# Parcourir les images
if "data" in images_data and "images" in images_data["data"]:
images_dict = images_data["data"]["images"]
# Les images sont organisées par game_id
if str(game_id) in images_dict:
game_images = images_dict[str(game_id)]
# Chercher front boxart en priorité
for img in game_images:
if img.get("type") == "boxart" and img.get("side") == "front":
filename = img.get("filename")
if filename:
image_url = f"{base_url_original}{filename}"
logger.info(f"Image front trouvée: {image_url}")
break
# Si pas de front, prendre n'importe quelle boxart
if not image_url:
for img in game_images:
if img.get("type") == "boxart":
filename = img.get("filename")
if filename:
image_url = f"{base_url_original}{filename}"
logger.info(f"Image boxart trouvée: {image_url}")
break
# Si toujours rien, prendre la première image
if not image_url and game_images:
filename = game_images[0].get("filename")
if filename:
image_url = f"{base_url_original}{filename}"
logger.info(f"Première image trouvée: {image_url}")
else:
logger.warning(f"Erreur lors de la récupération des images: HTTP {images_response.status_code}")
except Exception as img_error:
logger.warning(f"Erreur lors de la récupération des images: {img_error}")
# Construire le résultat
result = {
@@ -254,15 +311,16 @@ def get_game_metadata(game_name, platform_name):
"release_date": release_date
}
# Vérifier qu'on a au moins quelque chose
if not any([image_url, description, genre]):
result["error"] = "Métadonnées incomplètes"
logger.info(f"Métadonnées récupérées: image={bool(image_url)}, desc={bool(description)}, genre={bool(genre)}, date={bool(release_date)}")
return result
except requests.RequestException as e:
logger.error(f"Erreur lors de la requête: {str(e)}")
logger.error(f"Erreur lors de la requête API: {str(e)}")
return {"error": f"Erreur réseau: {str(e)}"}
except Exception as e:
logger.error(f"Erreur inattendue: {str(e)}", exc_info=True)
return {"error": f"Erreur: {str(e)}"}
def download_image_to_surface(image_url):

View File

@@ -0,0 +1,241 @@
/* ===== ACCESSIBILITY & FOCUS MANAGEMENT ===== */
/* Focus visible - show focus for keyboard navigation only */
:focus {
outline: 2px solid transparent;
outline-offset: 2px;
}
:focus-visible {
outline: 3px solid var(--color-primary);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
/* Skip to main content link (keyboard accessible) */
.skip-to-main {
position: absolute;
top: -40px;
left: 0;
background: var(--color-primary);
color: white;
padding: var(--spacing-md) var(--spacing-lg);
text-decoration: none;
z-index: var(--z-tooltip);
border-radius: var(--radius-sm);
}
.skip-to-main:focus {
top: 0;
}
/* Visually hidden but accessible to screen readers */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
/* ===== LOADING SKELETONS ===== */
@keyframes shimmer {
0% {
background-position: -1000px 0;
}
100% {
background-position: 1000px 0;
}
}
.skeleton {
background: linear-gradient(
90deg,
var(--color-bg-secondary) 0%,
var(--color-bg-tertiary) 50%,
var(--color-bg-secondary) 100%
);
background-size: 1000px 100%;
animation: shimmer 2s infinite;
}
.skeleton-card {
background: var(--color-bg-primary);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-lg);
border: 1px solid var(--color-border-light);
}
.skeleton-card::before {
content: '';
display: block;
width: 100%;
height: 200px;
background: var(--color-bg-secondary);
border-radius: var(--radius-md);
margin-bottom: var(--spacing-md);
animation: shimmer 2s infinite;
}
.skeleton-card::after {
content: '';
display: block;
width: 80%;
height: 16px;
background: var(--color-bg-secondary);
border-radius: var(--radius-sm);
animation: shimmer 2s infinite;
}
.skeleton-text {
height: 1em;
background: var(--color-bg-secondary);
border-radius: var(--radius-sm);
margin: var(--spacing-md) 0;
animation: shimmer 2s infinite;
}
.skeleton-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--color-bg-secondary);
display: inline-block;
animation: shimmer 2s infinite;
}
/* Grid of skeleton cards */
.skeleton-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: var(--spacing-lg);
margin-bottom: var(--spacing-2xl);
}
/* ===== BUTTON & LINK ACCESSIBILITY ===== */
button, a[role="button"], input[type="button"], input[type="submit"] {
position: relative;
transition: all var(--transition-base);
border: none;
font-family: inherit;
font-size: inherit;
cursor: pointer;
}
/* Disabled state */
button:disabled,
[role="button"][aria-disabled="true"],
input[type="button"]:disabled,
input[type="submit"]:disabled {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
/* Ensure buttons have minimum touch target (44x44px WCAG) */
button, a[role="button"] {
min-height: 44px;
min-width: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--spacing-md) var(--spacing-lg);
}
/* ===== FORM ACCESSIBILITY ===== */
label {
display: block;
margin-bottom: var(--spacing-sm);
font-weight: 500;
color: var(--color-text-primary);
}
input, select, textarea {
font-family: inherit;
font-size: inherit;
padding: var(--spacing-md);
border: 2px solid var(--color-border);
border-radius: var(--radius-md);
transition: border-color var(--transition-base);
}
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
border-color: var(--color-primary);
outline: none;
}
/* Error state for inputs */
input[aria-invalid="true"],
select[aria-invalid="true"],
textarea[aria-invalid="true"] {
border-color: var(--color-danger);
}
/* ===== MODAL & DIALOG ACCESSIBILITY ===== */
[role="dialog"],
.modal {
z-index: var(--z-modal);
}
[role="dialog"]::backdrop,
.modal-backdrop {
background-color: var(--color-overlay-bg);
z-index: var(--z-modal-backdrop);
}
/* Ensure modal is positioned and styled appropriately */
[role="dialog"] {
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: var(--color-bg-primary);
padding: var(--spacing-2xl);
max-height: 90vh;
overflow-y: auto;
box-shadow: var(--shadow-xl);
}
/* ===== HIGH CONTRAST SUPPORT ===== */
@media (prefers-contrast: more) {
button,
a[role="button"],
input[type="button"],
input[type="submit"] {
border: 3px solid currentColor;
font-weight: bold;
}
input,
select,
textarea {
border-width: 3px;
}
:focus-visible {
outline-width: 4px;
}
}
/* ===== REDUCED MOTION SUPPORT ===== */
@media (prefers-reduced-motion: reduce) {
.skeleton,
.skeleton-card::before,
.skeleton-card::after,
.skeleton-text,
.skeleton-avatar {
animation: none;
background: var(--color-bg-secondary);
}
}

View File

@@ -0,0 +1,542 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #333;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 90%;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
position: relative;
}
header h1 { font-size: 2.5em; margin-bottom: 10px; }
header p { opacity: 0.9; font-size: 1.1em; }
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Navigation mobile dans le header */
.mobile-tabs {
display: none;
justify-content: space-around;
padding: 15px 10px 10px 10px;
gap: 5px;
}
.mobile-tab {
flex: 1;
background: rgba(255,255,255,0.2);
border: none;
color: white;
padding: 12px 5px;
border-radius: 8px;
cursor: pointer;
font-size: 24px;
transition: all 0.3s;
backdrop-filter: blur(10px);
}
.mobile-tab:hover {
background: rgba(255,255,255,0.3);
}
.mobile-tab.active {
background: rgba(255,255,255,0.4);
transform: scale(1.1);
}
.tabs {
display: flex;
background: #f5f5f5;
border-bottom: 2px solid #ddd;
}
.tab {
flex: 1;
padding: 15px;
text-align: center;
cursor: pointer;
background: #f5f5f5;
border: none;
font-size: 16px;
color: #333;
transition: all 0.3s;
}
.tab:hover { background: #e0e0e0; }
.tab.active {
background: white;
border-bottom: 3px solid #667eea;
font-weight: bold;
}
.tab.support-btn {
margin-left: auto;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 20px;
padding: 8px 20px;
}
.tab.support-btn:hover {
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
transform: scale(1.05);
}
.content {
padding: 30px;
min-height: 400px;
}
.loading {
text-align: center;
padding: 50px;
font-size: 1.2em;
color: #666;
}
.platform-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
.platform-card {
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
padding: 20px;
border-radius: 12px;
cursor: pointer;
transition: transform 0.3s, box-shadow 0.3s;
text-align: center;
}
.platform-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0,0,0,0.4);
}
.platform-card img {
width: 200px;
height: 200px;
object-fit: contain;
margin-bottom: 15px;
filter: drop-shadow(0 4px 6px rgba(0,0,0,0.3));
}
.platform-card h3 {
margin-bottom: 10px;
color: white;
font-size: 1.1em;
min-height: 2.5em;
display: flex;
align-items: center;
justify-content: center;
}
.platform-card .count {
background: #667eea;
color: white;
padding: 5px 15px;
border-radius: 20px;
display: inline-block;
margin-top: 10px;
}
.search-box {
margin-bottom: 20px;
position: relative;
}
.search-box input {
width: 100%;
padding: 12px 45px 12px 15px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
background: white;
color: #333;
}
.search-box input:focus {
outline: none;
border-color: #667eea;
}
.search-box .search-icon {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
color: #999;
font-size: 18px;
}
.search-box .clear-search {
position: absolute;
right: 45px;
top: 50%;
transform: translateY(-50%);
background: #dc3545;
color: white;
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
cursor: pointer;
display: none;
font-size: 14px;
line-height: 1;
}
.search-box .clear-search:hover {
background: #c82333;
}
.sort-btn {
background: #e0e0e0;
color: #333;
border: 2px solid #999;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9em;
font-weight: 500;
transition: all 0.2s;
}
.sort-btn:hover {
background: #d0d0d0;
border-color: #666;
}
.sort-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
.filter-section {
margin-top: 12px;
margin-bottom: 12px;
padding: 12px;
background: #f5f5f5;
border-radius: 8px;
}
.filter-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 8px;
}
.filter-row:last-child {
margin-bottom: 0;
}
.filter-label {
font-weight: bold;
margin-right: 4px;
}
.region-btn {
background: #e0e0e0;
color: #333;
border: 2px solid #999;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 0.85em;
font-weight: 500;
transition: all 0.2s;
}
.region-btn:hover {
background: #d0d0d0;
border-color: #666;
}
.region-btn.active {
background: #28a745;
color: white;
border-color: #28a745;
}
.region-btn.excluded {
background: #dc3545;
color: white;
border-color: #dc3545;
}
.filter-checkbox {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
font-size: 0.9em;
color: #333;
}
.filter-checkbox input[type="checkbox"] {
cursor: pointer;
width: 16px;
height: 16px;
}
.games-list {
max-height: 600px;
overflow-y: auto;
}
.game-item {
background: #f9f9f9;
padding: 15px;
margin-bottom: 10px;
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
transition: background 0.2s;
}
.game-item:hover { background: #f0f0f0; }
.game-name { font-weight: 500; flex: 1; }
.game-size {
background: #667eea;
color: white;
padding: 5px 10px;
border-radius: 5px;
font-size: 0.9em;
margin-right: 10px;
}
.download-btn {
background: transparent;
color: #28a745;
border: none;
padding: 8px;
border-radius: 5px;
cursor: pointer;
font-size: 1.5em;
transition: all 0.2s;
min-width: 40px;
}
.download-btn:hover {
background: rgba(40, 167, 69, 0.1);
transform: scale(1.1);
}
.download-btn:disabled {
color: #6c757d;
cursor: not-allowed;
}
.back-btn {
background: #667eea;
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
margin-bottom: 20px;
font-size: 16px;
}
.back-btn:hover { background: #5568d3; }
.info-grid {
display: grid;
gap: 15px;
}
.info-item {
background: #f9f9f9;
padding: 15px;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.info-item strong { display: block; margin-bottom: 5px; color: #667eea; }
.history-item {
background: #f9f9f9;
padding: 15px;
margin-bottom: 10px;
border-radius: 8px;
border-left: 4px solid #28a745;
}
.history-item.error { border-left-color: #dc3545; }
/* Media Queries pour responsive */
@media (max-width: 768px) {
body {
padding: 10px;
}
.container {
border-radius: 0;
}
header {
padding: 20px 20px 10px 20px;
}
header h1 {
font-size: 1.8em;
}
.tabs {
display: none;
}
.mobile-tabs {
display: flex;
}
.content {
padding: 15px;
}
.platform-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
}
.platform-card {
padding: 15px;
}
.platform-card img {
width: 80px;
height: 80px;
}
.platform-card h3 {
font-size: 0.9em;
min-height: 2em;
}
.game-item {
flex-wrap: wrap;
padding: 10px;
}
.game-name {
font-size: 0.9em;
flex: 1 1 100%;
margin-bottom: 8px;
}
.download-btn-group {
display: flex;
gap: 4px;
}
}
/* 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;
}
.platform-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
}
.platform-card {
padding: 10px;
}
.platform-card img {
width: 60px;
height: 60px;
}
.platform-card h3 {
font-size: 0.85em;
}
.platform-card .count {
font-size: 0.8em;
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

@@ -0,0 +1,120 @@
/* ===== THEMING SYSTEM WITH CSS CUSTOM PROPERTIES ===== */
/* This file defines the color scheme and can be easily swapped for alternative themes */
:root {
/* Primary Colors - Brand Identity */
--color-primary: #667eea;
--color-primary-dark: #764ba2;
--color-primary-light: #8b9dff;
--color-primary-rgb: 102, 126, 234;
/* Secondary Colors - Accents */
--color-success: #28a745;
--color-warning: #ffc107;
--color-danger: #dc3545;
--color-info: #17a2b8;
/* Neutral Colors - Layout & Typography */
--color-bg-primary: #ffffff;
--color-bg-secondary: #f5f5f5;
--color-bg-tertiary: #f9f9f9;
--color-text-primary: #333333;
--color-text-secondary: #666666;
--color-text-muted: #999999;
--color-text-inverse: #ffffff;
/* Border & Divider Colors */
--color-border: #dddddd;
--color-border-light: #eeeeee;
--color-divider: #e0e0e0;
/* Semantic Colors */
--color-card-bg: #2c3e50;
--color-card-border: #34495e;
--color-overlay-bg: rgba(0, 0, 0, 0.7);
--color-overlay-light: rgba(0, 0, 0, 0.3);
/* Gradients */
--gradient-primary: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
--gradient-reverse: linear-gradient(135deg, var(--color-primary-dark) 0%, var(--color-primary) 100%);
/* Shadows */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.12);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.4);
--shadow-xl: 0 20px 60px rgba(0, 0, 0, 0.3);
/* Spacing Scale */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 12px;
--spacing-lg: 16px;
--spacing-xl: 20px;
--spacing-2xl: 30px;
--spacing-3xl: 40px;
/* Border Radius */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
/* Transitions */
--transition-fast: 0.2s ease-in-out;
--transition-base: 0.3s ease-in-out;
--transition-slow: 0.5s ease-in-out;
/* Typography */
--font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
--font-size-2xl: 1.5rem;
--font-size-3xl: 2.5rem;
/* Z-index Scale */
--z-dropdown: 100;
--z-sticky: 200;
--z-fixed: 300;
--z-modal-backdrop: 400;
--z-modal: 500;
--z-notification: 600;
--z-tooltip: 700;
}
/* ===== DARK MODE SUPPORT ===== */
@media (prefers-color-scheme: dark) {
:root {
--color-bg-primary: #1a1a1a;
--color-bg-secondary: #2d2d2d;
--color-bg-tertiary: #3a3a3a;
--color-text-primary: #e0e0e0;
--color-text-secondary: #b0b0b0;
--color-text-muted: #808080;
--color-border: #404040;
--color-border-light: #3a3a3a;
}
}
/* ===== HIGH CONTRAST MODE SUPPORT ===== */
@media (prefers-contrast: more) {
:root {
--color-primary: #0043b3;
--color-primary-dark: #002e7a;
--color-text-primary: #000000;
--color-text-secondary: #333333;
--color-border: #000000;
--shadow-lg: 0 2px 4px rgba(0, 0, 0, 0.5);
}
}
/* ===== REDUCED MOTION SUPPORT ===== */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}

View File

@@ -0,0 +1,194 @@
/**
* Accessibility Utilities Module
* Handles keyboard navigation, focus management, and ARIA updates
*/
const A11y = {
/**
* Manage focus trap for modals (keep focus within modal)
*/
trapFocus(element) {
const focusableElements = element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
element.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
});
// Set initial focus to first element
firstElement?.focus();
},
/**
* Restore focus to element after modal closes
*/
savedFocusElement: null,
saveFocus() {
this.savedFocusElement = document.activeElement;
},
restoreFocus() {
if (this.savedFocusElement && this.savedFocusElement.focus) {
this.savedFocusElement.focus();
}
},
/**
* Announce changes to screen readers with live regions
*/
announceToScreenReader(message, priority = 'polite') {
let liveRegion = document.querySelector(`[role="status"][aria-live="${priority}"]`);
if (!liveRegion) {
liveRegion = document.createElement('div');
liveRegion.setAttribute('role', 'status');
liveRegion.setAttribute('aria-live', priority);
liveRegion.className = 'sr-only';
document.body.appendChild(liveRegion);
}
liveRegion.textContent = message;
// Clear after announcement
setTimeout(() => {
liveRegion.textContent = '';
}, 3000);
},
/**
* Handle Enter/Space key on clickable elements
*/
makeKeyboardClickable(element, callback) {
element.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
callback();
}
});
},
/**
* Arrow key navigation for grids
*/
setupGridNavigation(gridSelector) {
const grid = document.querySelector(gridSelector);
if (!grid) return;
const items = Array.from(grid.children);
const colCount = Math.ceil(Math.sqrt(items.length));
items.forEach((item, index) => {
item.setAttribute('tabindex', index === 0 ? '0' : '-1');
item.addEventListener('keydown', (e) => {
let newIndex = index;
switch (e.key) {
case 'ArrowLeft':
newIndex = index === 0 ? items.length - 1 : index - 1;
e.preventDefault();
break;
case 'ArrowRight':
newIndex = index === items.length - 1 ? 0 : index + 1;
e.preventDefault();
break;
case 'ArrowUp':
newIndex = Math.max(0, index - colCount);
e.preventDefault();
break;
case 'ArrowDown':
newIndex = Math.min(items.length - 1, index + colCount);
e.preventDefault();
break;
case 'Home':
newIndex = 0;
e.preventDefault();
break;
case 'End':
newIndex = items.length - 1;
e.preventDefault();
break;
default:
return;
}
items[index].setAttribute('tabindex', '-1');
items[newIndex].setAttribute('tabindex', '0');
items[newIndex].focus();
});
});
},
/**
* Update ARIA attributes for dynamic content
*/
updateAriaLabel(element, label) {
element.setAttribute('aria-label', label);
},
updateAriaLive(element, region = 'polite') {
element.setAttribute('aria-live', region);
element.setAttribute('aria-atomic', 'true');
},
/**
* Set loading state with ARIA
*/
setLoadingState(element, isLoading) {
if (isLoading) {
element.setAttribute('aria-busy', 'true');
element.setAttribute('disabled', 'true');
} else {
element.removeAttribute('aria-busy');
element.removeAttribute('disabled');
}
},
/**
* Create accessible loading skeleton
*/
createSkeletonLoader(containerId, itemCount = 6) {
const container = document.getElementById(containerId);
if (!container) return;
container.innerHTML = '';
for (let i = 0; i < itemCount; i++) {
const skeleton = document.createElement('div');
skeleton.className = 'skeleton-card';
skeleton.setAttribute('aria-hidden', 'true');
container.appendChild(skeleton);
}
},
/**
* Remove skeleton loader
*/
removeSkeletonLoader(containerId) {
const container = document.getElementById(containerId);
if (!container) return;
const skeletons = container.querySelectorAll('.skeleton-card');
skeletons.forEach(s => s.remove());
}
};
// Export for use in app.js
if (typeof module !== 'undefined' && module.exports) {
module.exports = A11y;
}

2456
ports/RGSX/static/js/app.js Normal file

File diff suppressed because it is too large Load Diff

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}")
@@ -1788,31 +2109,31 @@ def handle_xbox(dest_dir, iso_files, url=None):
time.sleep(2)
if config.OPERATING_SYSTEM == "Windows":
# Sur Windows; telecharger le fichier exe
XDVDFS_EXE = config.XDVDFS_EXE
xdvdfs_cmd = [XDVDFS_EXE, "pack"] # Liste avec 2 éléments
XISO_EXE = config.XISO_EXE
extract_xiso_cmd = [XISO_EXE, "-r"] # Liste avec 2 éléments
else:
# Linux/Batocera : télécharger le fichier xdvdfs
XDVDFS_LINUX = config.XDVDFS_LINUX
XISO_LINUX = config.XISO_LINUX
try:
stat_info = os.stat(XDVDFS_LINUX)
stat_info = os.stat(XISO_LINUX)
mode = stat_info.st_mode
logger.debug(f"Permissions de {XDVDFS_LINUX}: {oct(mode)}")
logger.debug(f"Permissions de {XISO_LINUX}: {oct(mode)}")
logger.debug(f"Propriétaire: {stat_info.st_uid}, Groupe: {stat_info.st_gid}")
# Vérifier si le fichier est exécutable
if not os.access(XDVDFS_LINUX, os.X_OK):
logger.error(f"Le fichier {XDVDFS_LINUX} n'est pas exécutable")
if not os.access(XISO_LINUX, os.X_OK):
logger.error(f"Le fichier {XISO_LINUX} n'est pas exécutable")
try:
os.chmod(XDVDFS_LINUX, 0o755)
logger.info(f"Permissions corrigées pour {XDVDFS_LINUX}")
os.chmod(XISO_LINUX, 0o755)
logger.info(f"Permissions corrigées pour {XISO_LINUX}")
except Exception as e:
logger.error(f"Impossible de modifier les permissions: {str(e)}")
return False, "Erreur de permissions sur xdvdfs"
except Exception as e:
logger.error(f"Erreur lors de la vérification des permissions: {str(e)}")
xdvdfs_cmd = [XDVDFS_LINUX, "pack"] # Liste avec 2 éléments
extract_xiso_cmd = [XISO_LINUX, "-r"] # Liste avec 2 éléments
try:
# Utiliser uniquement la liste fournie (nouveaux ISO extraits). Fallback scan uniquement si liste vide.
@@ -1862,16 +2183,21 @@ def handle_xbox(dest_dir, iso_files, url=None):
logger.info(f"Démarrage conversion Xbox: {total} ISO(s)")
for idx, iso_xbox_source in enumerate(iso_files, start=1):
logger.debug(f"Traitement de l'ISO Xbox: {iso_xbox_source}")
xiso_dest = os.path.splitext(iso_xbox_source)[0] + "_xbox.iso"
# Construction de la commande avec des arguments distincts
cmd = xdvdfs_cmd + [iso_xbox_source, xiso_dest]
logger.debug(f"Exécution de la commande: {' '.join(cmd)}")
# extract-xiso -r repackage l'ISO en place
# Il faut exécuter la commande depuis le dossier contenant l'ISO
iso_dir = os.path.dirname(iso_xbox_source)
iso_filename = os.path.basename(iso_xbox_source)
# Utiliser le nom de fichier relatif et définir le répertoire de travail
cmd = extract_xiso_cmd + [iso_filename]
logger.debug(f"Exécution de la commande: {' '.join(cmd)} (cwd: {iso_dir})")
process = subprocess.run(
cmd,
capture_output=True,
text=True
text=True,
cwd=iso_dir
)
if process.returncode != 0:
@@ -1883,27 +2209,34 @@ def handle_xbox(dest_dir, iso_files, url=None):
if url not in config.download_progress:
config.download_progress[url] = {}
config.download_progress[url]["status"] = "Error"
config.download_progress[url]["message"] = {process.stderr}
config.download_progress[url]["message"] = process.stderr
config.download_progress[url]["progress_percent"] = 0
config.needs_redraw = True
if isinstance(config.history, list):
for entry in config.history:
if entry.get("url") == url and entry.get("status") in ("Converting", "Extracting", "Téléchargement", "Downloading"):
entry["status"] = "Error"
entry["message"] = {process.stderr}
entry["message"] = process.stderr
save_history(config.history)
break
except Exception:
pass
return False, err_msg
# Vérifier que l'ISO converti a été créé
if os.path.exists(xiso_dest):
logger.info(f"ISO converti avec succès: {xiso_dest}")
# Remplacer l'ISO original par l'ISO converti
os.remove(iso_xbox_source)
os.rename(xiso_dest, iso_xbox_source)
logger.debug(f"ISO original remplacé par la version convertie")
# Vérifier que l'ISO existe toujours (extract-xiso le modifie en place)
if os.path.exists(iso_xbox_source):
logger.info(f"ISO repackagé avec succès: {iso_xbox_source}")
logger.debug(f"ISO converti au format XISO en place")
# Supprimer le fichier .old créé par extract-xiso (backup)
old_file = iso_xbox_source + ".old"
if os.path.exists(old_file):
try:
os.remove(old_file)
logger.debug(f"Fichier backup .old supprimé: {old_file}")
except Exception as e:
logger.warning(f"Impossible de supprimer le fichier .old: {e}")
# Mise à jour progression de conversion (coarse-grain)
try:
percent = int(idx / total * 100) if total > 0 else 100
@@ -1922,7 +2255,7 @@ def handle_xbox(dest_dir, iso_files, url=None):
except Exception:
pass
else:
err_msg = f"L'ISO converti n'a pas été créé: {xiso_dest}"
err_msg = f"L'ISO source a disparu après conversion: {iso_xbox_source}"
logger.error(err_msg)
try:
if url:
@@ -1971,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
@@ -1985,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,406 +0,0 @@
#!/usr/bin/env python3
import os
import sys
import time
import json
import re
import traceback
from typing import Any, Dict, Tuple, List, Optional
try:
import pygame # type: ignore
except Exception as e:
print("Pygame is required. Install with: pip install pygame")
raise
PROMPTS = [
# Face buttons
"SOUTH_BUTTON - CONFIRM", # A on Xbox
"EAST_BUTTON - CANCEL", # B on Xbox
"WEST_BUTTON - CLEAR HISTORY / SELECT GAMES", # X on Xbox
"NORTH_BUTTON - HISTORY", # Y on Xbox
# Meta
"START - PAUSE",
"SELECT - FILTER",
# D-Pad
"DPAD_UP - MOVE UP",
"DPAD_DOWN - MOVE DOWN",
"DPAD_LEFT - MOVE LEFT",
"DPAD_RIGHT - MOVE RIGHT",
# Bumpers
"LEFT_BUMPER - LB/L1 - Delete last char",
"RIGHT_BUMPER - RB/R1 - Add space",
# Triggers
"LEFT_TRIGGER - LT/L2 - Page +",
"RIGHT_TRIGGER - RT/R2 - Page -",
# Left stick directions
"JOYSTICK_LEFT_UP - MOVE UP",
"JOYSTICK_LEFT_DOWN - MOVE DOWN",
"JOYSTICK_LEFT_LEFT - MOVE LEFT",
"JOYSTICK_LEFT_RIGHT - MOVE RIGHT",
]
INPUT_TIMEOUT_SECONDS = 10 # Temps max par entrée avant "ignored"
# --- Minimal on-screen console (Pygame window) ---
SURFACE = None # type: ignore
FONT = None # type: ignore
LOG_LINES: List[str] = []
MAX_LOG = 300
def init_screen(width: int = 900, height: int = 600) -> None:
global SURFACE, FONT
try:
pygame.display.init()
SURFACE = pygame.display.set_mode((width, height))
pygame.display.set_caption("Controller Tester")
pygame.font.init()
FONT = pygame.font.SysFont("Consolas", 20) or pygame.font.Font(None, 20)
except Exception:
# If display init fails, stay headless but continue
SURFACE = None
FONT = None
def log(msg: str) -> None:
# Print to real console and on-screen log
try:
print(msg)
except Exception:
pass
LOG_LINES.append(str(msg))
if len(LOG_LINES) > MAX_LOG:
del LOG_LINES[: len(LOG_LINES) - MAX_LOG]
draw_log()
def draw_log() -> None:
if SURFACE is None or FONT is None:
return
try:
SURFACE.fill((12, 12, 12))
margin = 12
line_h = FONT.get_height() + 4
# Show the last N lines that fit on screen
max_lines = (SURFACE.get_height() - margin * 2) // line_h
to_draw = LOG_LINES[-max_lines:]
y = margin
for line in to_draw:
surf = FONT.render(line, True, (220, 220, 220))
SURFACE.blit(surf, (margin, y))
y += line_h
pygame.display.flip()
except Exception:
pass
def init_joystick() -> pygame.joystick.Joystick:
pygame.init()
pygame.joystick.init()
if pygame.joystick.get_count() == 0:
log("No joystick detected. Connect a controller and try again.")
sys.exit(1)
js = pygame.joystick.Joystick(0)
js.init()
name = js.get_name()
log(f"Using joystick 0: {name}")
log("")
log(f"Note: each input will auto-ignore after {INPUT_TIMEOUT_SECONDS}s if not present (e.g. missing L2/R2)")
return js
def wait_for_stable(js: pygame.joystick.Joystick, settle_ms: int = 250, deadband: float = 0.05, timeout_ms: int = 2000) -> bool:
"""Wait until axes stop moving (change < deadband) continuously for settle_ms.
Unlike a traditional neutral check, this doesn't assume axes center at 0.
Hats are required to be (0,0) to avoid capturing D-Pad releases.
Returns True if stability achieved, False on timeout.
"""
start = pygame.time.get_ticks()
last = [js.get_axis(i) for i in range(js.get_numaxes())]
stable_since = None
while True:
# Handle window close only (avoid quitting on keyboard here)
for event in pygame.event.get():
if event.type == pygame.QUIT:
log("Window closed. Exiting.")
sys.exit(0)
moved = False
for i in range(js.get_numaxes()):
cur = js.get_axis(i)
if abs(cur - last[i]) > deadband:
moved = True
last[i] = cur
hats_ok = all(js.get_hat(i) == (0, 0) for i in range(js.get_numhats()))
if not moved and hats_ok:
if stable_since is None:
stable_since = pygame.time.get_ticks()
elif pygame.time.get_ticks() - stable_since >= settle_ms:
return True
else:
stable_since = None
if pygame.time.get_ticks() - start > timeout_ms:
return False
draw_log()
pygame.time.wait(10)
def wait_for_event(js: pygame.joystick.Joystick, logical_name: str, axis_threshold: float = 0.6, timeout_sec: int = INPUT_TIMEOUT_SECONDS) -> Tuple[str, Any]:
"""Wait for a joystick event for the given logical control.
Returns a tuple of (kind, data):
- ("button", button_index)
- ("hat", (x, y)) where x,y in {-1,0,1}
- ("axis", {"axis": index, "direction": -1|1})
"""
# Ensure prior motion has settled to avoid capturing a release
wait_for_stable(js)
log("")
deadline = time.time() + max(1, int(timeout_sec))
log(f"Press {logical_name} (Wait {timeout_sec}s to skip/ignore) if not present")
# Flush old events
pygame.event.clear()
while True:
# Update window title with countdown if we have a surface
try:
remaining = int(max(0, deadline - time.time()))
if SURFACE is not None:
pygame.display.set_caption(f"Controller Tester — {logical_name} — {remaining}s left")
except Exception:
pass
for event in pygame.event.get():
# Keyboard helpers
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
log(f"Skipped {logical_name}")
return ("skipped", None)
# No keyboard quit here to avoid accidental exits when using controllers
if event.type == pygame.QUIT:
log("Window closed. Exiting.")
sys.exit(0)
# Buttons
if event.type == pygame.JOYBUTTONDOWN:
log(f"Captured {logical_name}: BUTTON {event.button}")
return ("button", event.button)
# D-Pad (HAT)
if event.type == pygame.JOYHATMOTION:
val = event.value # (x, y)
if val != (0, 0):
log(f"Captured {logical_name}: HAT {val}")
return ("hat", val)
# Axes (sticks, triggers)
if event.type == pygame.JOYAXISMOTION:
axis = event.axis
value = float(event.value)
if abs(value) >= axis_threshold:
direction = 1 if value > 0 else -1
log(f"Captured {logical_name}: AXIS {axis} dir {direction} (raw {value:.2f})")
return ("axis", {"axis": axis, "direction": direction, "raw": value})
draw_log()
# Timeout?
if time.time() >= deadline:
log(f"Ignored {logical_name} (timeout {timeout_sec}s)")
return ("ignored", None)
time.sleep(0.005)
def write_log(path: str, mapping: Dict[str, Tuple[str, Any]], device_name: str) -> None:
lines = []
lines.append("# Controller mapping log\n")
lines.append(f"# Device: {device_name}\n\n")
for name, (kind, data) in mapping.items():
if kind == "button":
lines.append(f"{name} = BUTTON {data}\n")
elif kind == "hat":
lines.append(f"{name} = HAT {data}\n")
elif kind == "axis":
ax = data.get("axis")
direction = data.get("direction")
lines.append(f"{name} = AXIS {ax} dir {direction}\n")
elif kind == "skipped":
lines.append(f"{name} = SKIPPED\n")
elif kind == "ignored":
lines.append(f"{name} = IGNORED\n")
else:
lines.append(f"{name} = UNKNOWN {data}\n")
with open(path, "w", encoding="utf-8") as f:
f.writelines(lines)
log("")
log(f"Saved mapping to: {path}")
# --- JSON preset generation ---
def sanitize_device_name(name: str) -> str:
s = name.strip().lower()
# Replace non-alphanumeric with underscore
s = re.sub(r"[^a-z0-9]+", "_", s)
s = re.sub(r"_+", "_", s).strip("_")
return s or "controller"
def to_json_binding(kind: str, data: Any, display: Optional[str] = None) -> Optional[Dict[str, Any]]:
if kind == "button" and isinstance(data, int):
return {"type": "button", "button": data, **({"display": display} if display else {})}
if kind == "hat" and isinstance(data, (tuple, list)) and len(data) == 2:
val = list(data)
return {"type": "hat", "value": val, **({"display": display} if display else {})}
if kind == "axis" and isinstance(data, dict):
axis = data.get("axis")
direction = data.get("direction")
if isinstance(axis, int) and direction in (-1, 1):
return {"type": "axis", "axis": axis, "direction": int(direction), **({"display": display} if display else {})}
return None
def build_controls_json(mapping: Dict[str, Tuple[str, Any]]) -> Dict[str, Any]:
# Map logical prompts to action keys and preferred display labels
prompt_map = {
"SOUTH_BUTTON - CONFIRM": ("confirm", "A"),
"EAST_BUTTON - CANCEL": ("cancel", "B"),
"WEST_BUTTON - CLEAR HISTORY / SELECT GAMES": ("clear_history", "X"),
"NORTH_BUTTON - HISTORY": ("history", "Y"),
"START - PAUSE": ("start", "Start"),
"SELECT - FILTER": ("filter", "Select"),
"DPAD_UP - MOVE UP": ("up", "↑"),
"DPAD_DOWN - MOVE DOWN": ("down", "↓"),
"DPAD_LEFT - MOVE LEFT": ("left", "←"),
"DPAD_RIGHT - MOVE RIGHT": ("right", "→"),
"LEFT_BUMPER - LB/L1 - Delete last char": ("delete", "LB"),
"RIGHT_BUMPER - RB/R1 - Add space": ("space", "RB"),
# Triggers per prompts: LEFT=page_up, RIGHT=page_down
"LEFT_TRIGGER - LT/L2 - Page +": ("page_up", "LT"),
"RIGHT_TRIGGER - RT/R2 - Page -": ("page_down", "RT"),
# Left stick directions (fallbacks for arrows)
"JOYSTICK_LEFT_UP - MOVE UP": ("up", "J↑"),
"JOYSTICK_LEFT_DOWN - MOVE DOWN": ("down", "J↓"),
"JOYSTICK_LEFT_LEFT - MOVE LEFT": ("left", "J←"),
"JOYSTICK_LEFT_RIGHT - MOVE RIGHT": ("right", "J→"),
}
result: Dict[str, Any] = {}
# First pass: take direct DPAD/face/meta/bumper/trigger bindings
for prompt, (action, disp) in prompt_map.items():
if prompt not in mapping:
continue
kind, data = mapping[prompt]
if kind in ("ignored", "skipped"):
continue
# Prefer DPAD over JOYSTICK for directions: handle fallback later
if action in ("up", "down", "left", "right"):
if prompt.startswith("DPAD_"):
b = to_json_binding(kind, data, disp)
if b:
result[action] = b
# Joystick handled as fallback if DPAD missing
else:
b = to_json_binding(kind, data, disp)
if b:
result[action] = b
# Second pass: fallback to joystick directions if arrows missing
fallbacks = [
("JOYSTICK_LEFT_UP - MOVE UP", "up", "J↑"),
("JOYSTICK_LEFT_DOWN - MOVE DOWN", "down", "J↓"),
("JOYSTICK_LEFT_LEFT - MOVE LEFT", "left", "J←"),
("JOYSTICK_LEFT_RIGHT - MOVE RIGHT", "right", "J→"),
]
for prompt, action, disp in fallbacks:
if action in result:
continue
if prompt in mapping:
kind, data = mapping[prompt]
if kind in ("ignored", "skipped"):
continue
b = to_json_binding(kind, data, disp)
if b:
result[action] = b
return result
def write_controls_json(device_name: str, controls: Dict[str, Any]) -> str:
"""Write the generated controls preset JSON in the same folder as this script.
Also embeds a JSON-safe comment with the device name under the _comment key.
"""
# Same folder as the launched script
base_dir = os.path.dirname(os.path.abspath(__file__))
fname = f"{sanitize_device_name(device_name)}_controller.json"
out_path = os.path.join(base_dir, fname)
# Include the detected device name for auto-preset matching
payload = {"device": device_name}
payload.update(controls)
try:
with open(out_path, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=4)
return out_path
except Exception:
return out_path
def main() -> None:
init_screen()
js = init_joystick()
# Print device basics
try:
log(f"Buttons: {js.get_numbuttons()} | Axes: {js.get_numaxes()} | Hats: {js.get_numhats()}")
except Exception:
pass
mapping: Dict[str, Tuple[str, Any]] = {}
for logical in PROMPTS:
kind, data = wait_for_event(js, logical)
mapping[logical] = (kind, data)
# Short, consistent debounce for all inputs
pygame.event.clear()
pygame.time.wait(150)
log_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "controller_mapping.log")
write_log(log_path, mapping, js.get_name())
# Build and write ready-to-use JSON controls preset
controls = build_controls_json(mapping)
if controls:
out_json = write_controls_json(js.get_name(), controls)
log(f"Saved JSON preset to: {out_json}")
else:
log("No usable inputs captured to build a JSON preset.")
log("Done. Press Q or close the window to exit.")
if __name__ == "__main__":
try:
main()
except SystemExit:
# Allow intentional exits
pass
except Exception:
# Show traceback on screen and wait for window close
tb = traceback.format_exc()
try:
log("")
log("An error occurred:")
for line in tb.splitlines():
log(line)
# Idle until window is closed
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
raise SystemExit(1)
draw_log()
pygame.time.wait(50)
except Exception:
pass
finally:
try:
pygame.joystick.quit()
pygame.quit()
except Exception:
pass

View File

@@ -1,3 +1,3 @@
{
"version": "2.3.1.4"
"version": "2.3.2.9"
}