Compare commits

...

7 Commits

Author SHA1 Message Date
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
22 changed files with 599 additions and 230 deletions

2
.gitignore vendored
View File

@@ -21,3 +21,5 @@ Info.txt
# Docker test data
data/
docker-compose.test.yml
config/

View File

@@ -5,28 +5,29 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
p7zip-full \
unrar-free \
curl \
rsync \
gosu \
&& rm -rf /var/lib/apt/lists/*
# Create required directories
RUN mkdir -p /userdata/saves/ports/rgsx \
&& mkdir -p /userdata/roms/ports \
&& mkdir -p /app
# Create app directory
RUN mkdir -p /app
# Copy RGSX application files to /app (will be copied to volume at runtime)
# Copy RGSX application files to /app
COPY ports/RGSX/ /app/RGSX/
# Copy entrypoint script
COPY docker-entrypoint.sh /usr/local/bin/
COPY docker/docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Install Python dependencies
# pygame is imported in some modules even in headless mode, so we include it
RUN pip install --no-cache-dir requests pygame
# Set environment to headless mode
ENV RGSX_HEADLESS=1
# Set environment variables for Docker mode
# These tell RGSX to use /config for settings and /data for ROMs
ENV RGSX_HEADLESS=1 \
RGSX_APP_DIR=/app \
RGSX_CONFIG_DIR=/config \
RGSX_DATA_DIR=/data
# Expose web interface port
EXPOSE 5000
@@ -35,6 +36,9 @@ EXPOSE 5000
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:5000/ || exit 1
# Entrypoint copies app to volume, then runs command
# Set working directory
WORKDIR /app/RGSX
# Entrypoint handles user permissions and directory setup
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["python", "rgsx_web.py", "--host", "0.0.0.0", "--port", "5000"]

View File

@@ -5,19 +5,16 @@ Run RGSX as a web-only service without the Pygame UI. Perfect for homelab/server
## Quick Start
```bash
# Build the image
docker build -t rgsx .
# Using docker-compose (recommended)
docker-compose up -d
# Run with docker
# Or build and run manually
docker build -f docker/Dockerfile -t rgsx .
docker run -d \
--name rgsx \
-p 5000:5000 \
-e PUID=99 \
-e PGID=100 \
-e RGSX_HEADLESS=1 \
-v ./data/saves:/userdata/saves/ports/rgsx \
-v ./data/roms:/userdata/roms/ports \
-v ./data/logs:/userdata/roms/ports/RGSX/logs \
-v ./config:/config \
-v ./data:/data \
rgsx
# Access the web interface
@@ -28,65 +25,63 @@ open http://localhost:5000
- Runs RGSX web server in headless mode (no Pygame UI)
- Web interface accessible from any browser
- ROMs and settings persist in `./data/` volumes
- Container restarts automatically
- Config persists in `/config` volume (settings, metadata, history)
- ROMs download to `/data/roms/{platform}/` and extract there
- Environment variables pre-configured (no manual setup needed)
## Environment Variables
**Pre-configured in the container (no need to set these):**
- `RGSX_HEADLESS=1` - Runs in headless mode
- `RGSX_CONFIG_DIR=/config` - Config location
- `RGSX_DATA_DIR=/data` - Data location
**Optional (only if needed):**
- `PUID` - User ID for file ownership (default: root)
- `PGID` - Group ID for file ownership (default: root)
## Configuration
### Docker Compose
See `docker-compose.example.yml` for a complete example configuration.
### User Permissions (Important!)
**For SMB mounts (Unraid, Windows shares):**
Don't set PUID/PGID. The container runs as root, and the SMB server maps files to your authenticated user.
```bash
docker run \
-e RGSX_HEADLESS=1 \
...
```
- Don't set PUID/PGID
- The container runs as root, and the SMB server maps files to your authenticated user
**For NFS/local storage:**
- Set PUID and PGID to match your host user (files will be owned by that user)
- Find your user ID: `id -u` and `id -g`
Set PUID and PGID to match your host user. Files will be owned by that user.
### Volumes
```bash
docker run \
-e PUID=1000 \
-e PGID=1000 \
-e RGSX_HEADLESS=1 \
...
```
Two volumes are used:
**Find your user ID:**
```bash
id -u # Your UID
id -g # Your GID
```
**`/config`** - Configuration and metadata
- `rgsx_settings.json` - Settings
- `games/` - Platform game database files (JSON)
- `images/` - Game cover art
- `history.json` - Download history
- `logs/` - Application logs
- `*.txt` - API keys
### Change Port
```bash
docker run -p 8080:5000 ... # Access on port 8080
```
### Custom ROM Location
Map to your existing ROM collection:
```bash
docker run -v /your/existing/roms:/userdata/roms/ports ...
```
**`/data`** - ROM storage
- `roms/` - ROMs by platform (snes/, nes/, psx/, etc.) - downloads extract here
### API Keys
Add your download service API keys to `./data/saves/`:
Add your download service API keys to `./config/`:
```bash
# Add your API key (just the key, no extra text)
echo "YOUR_KEY_HERE" > ./data/saves/1FichierAPI.txt
echo "YOUR_KEY_HERE" > ./config/1FichierAPI.txt
# Optional: AllDebrid/RealDebrid fallbacks
echo "YOUR_KEY" > ./data/saves/AllDebridAPI.txt
echo "YOUR_KEY" > ./data/saves/RealDebridAPI.txt
# Optional: AllDebrid/RealDebrid
echo "YOUR_KEY" > ./config/AllDebridAPI.txt
echo "YOUR_KEY" > ./config/RealDebridAPI.txt
# Restart to apply
docker restart rgsx
@@ -112,14 +107,29 @@ docker stop rgsx && docker rm rgsx
## Directory Structure
**On Host:**
```
RGSX/
├── data/ # Created on first run
│ ├── saves/ # Settings, history, API keys
│ ├── roms/ # Downloaded ROMs
── logs/ # Application logs
├── Dockerfile
└── docker-compose.yml
./
├── config/ # Config volume (created on first run)
│ ├── rgsx_settings.json
│ ├── games/ # Platform game database (JSON)
── images/ # Platform images
│ ├── logs/ # Application logs
│ └── *.txt # API keys (1FichierAPI.txt, etc.)
└── data/
└── roms/ # ROMs by platform
├── snes/
├── n64/
└── ...
```
**In Container:**
```
/app/RGSX/ # Application code
/config/ # Mapped to ./config on host
└── games/, images/, logs/, etc.
/data/ # Mapped to ./data on host
└── roms/ # ROM downloads go here
```
## How It Works
@@ -137,22 +147,15 @@ The container creates files with the UID/GID specified by PUID/PGID environment
docker run -e PUID=1000 -e PGID=1000 ...
```
**Changed PUID/PGID and container won't start:**
**Changed PUID/PGID and permission errors:**
When you change PUID/PGID, old files with different ownership will cause rsync to fail. You MUST fix ownership on the storage server:
Fix ownership of your volumes:
```bash
# On your NAS/Unraid (via SSH), either:
# Option 1: Delete old files (easiest)
rm -rf /mnt/user/roms/rgsx/roms/ports/RGSX/*
# Option 2: Change ownership to new PUID/PGID
chown -R 1000:1000 /mnt/user/roms/rgsx/roms/ports/RGSX/
# Fix ownership to match new PUID/PGID
sudo chown -R 1000:1000 ./config ./data
```
Then restart the container.
**Port already in use:**
```bash
docker run -p 8080:5000 ... # Use port 8080 instead

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

@@ -1,10 +1,12 @@
#!/bin/bash
set -e
echo "=== RGSX Docker Container Startup ==="
# If PUID/PGID are set, create user and run as that user
# If not set, run as root (works for SMB mounts)
if [ -n "$PUID" ] && [ -n "$PGID" ]; then
echo "=== Creating user with PUID=$PUID, PGID=$PGID ==="
echo "Creating user with PUID=$PUID, PGID=$PGID..."
# Create group if it doesn't exist
if ! getent group $PGID >/dev/null 2>&1; then
@@ -16,43 +18,28 @@ if [ -n "$PUID" ] && [ -n "$PGID" ]; then
useradd -u $PUID -g $PGID -m -s /bin/bash rgsx
fi
# Fix ownership of app files
chown -R $PUID:$PGID /app /userdata 2>/dev/null || true
echo "=== Running as user $(id -un $PUID) (UID=$PUID, GID=$PGID) ==="
echo "Running as user $(id -un $PUID) (UID=$PUID, GID=$PGID)"
RUN_USER="gosu rgsx"
else
echo "=== Running as root (no PUID/PGID set) - for SMB mounts ==="
echo "Running as root (no PUID/PGID set) - suitable for SMB mounts"
RUN_USER=""
fi
# Always sync RGSX app code to the mounted volume (for updates)
echo "Syncing RGSX app code to /userdata/roms/ports/RGSX..."
$RUN_USER mkdir -p /userdata/roms/ports/RGSX
# Create necessary directories
# /config needs logs directory, app will create others (like images/, games/) as needed
# /data needs roms directory
echo "Setting up directories..."
$RUN_USER mkdir -p /config/logs
$RUN_USER mkdir -p /data/roms
# Try rsync
if ! $RUN_USER rsync -av --delete /app/RGSX/ /userdata/roms/ports/RGSX/ 2>&1; then
echo ""
echo "=========================================="
echo "WARNING: rsync partially failed!"
echo "=========================================="
echo "Some files may not have synced. Container will continue for debugging."
echo ""
if [ -n "$PUID" ] && [ -n "$PGID" ]; then
echo "If using SMB, try removing PUID/PGID to run as root"
fi
echo ""
# Fix ownership of volumes if PUID/PGID are set
if [ -n "$PUID" ] && [ -n "$PGID" ]; then
echo "Setting ownership on volumes..."
chown -R $PUID:$PGID /config /data 2>/dev/null || true
fi
echo "RGSX app code sync attempted."
# Create Batocera folder structure only if folders don't exist
$RUN_USER mkdir -p /userdata/saves/ports/rgsx/images
$RUN_USER mkdir -p /userdata/saves/ports/rgsx/games
$RUN_USER mkdir -p /userdata/roms/ports/RGSX/logs
# Create default settings with show_unsupported_platforms enabled if config doesn't exist
SETTINGS_FILE="/userdata/saves/ports/rgsx/rgsx_settings.json"
SETTINGS_FILE="/config/rgsx_settings.json"
if [ ! -f "$SETTINGS_FILE" ]; then
echo "Creating default settings with all platforms visible..."
$RUN_USER bash -c "cat > '$SETTINGS_FILE' << 'EOF'
@@ -60,11 +47,15 @@ if [ ! -f "$SETTINGS_FILE" ]; then
\"show_unsupported_platforms\": true
}
EOF"
echo "Default settings created!"
echo "Default settings created at $SETTINGS_FILE"
fi
# Run the command
cd /userdata/roms/ports/RGSX
echo "=== Starting RGSX Web Server ==="
echo "Config directory: /config"
echo "ROMs directory: /data/roms"
echo "======================================"
# Run the command from the working directory (/app/RGSX set in Dockerfile)
if [ -z "$RUN_USER" ]; then
exec "$@"
else

View File

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

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

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.5"
app_version = "2.3.1.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
@@ -412,6 +502,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

@@ -1415,7 +1415,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 = 9 # layout, font size, footer font size, font family, unsupported, unknown, hide premium, filter, back
if is_input_matched(event, "up"):
config.pause_display_selection = (sel - 1) % total
config.needs_redraw = True
@@ -1439,14 +1439,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 popup au lieu de redémarrer
try:
config.menu_state = "restart_popup"
config.popup_message = _("popup_restarting") if _ else "Restarting..."
config.popup_timer = 2000
restart_application(2000)
restart_msg = _("popup_layout_changed_restart").format(new_cols, new_rows) if _ else f"Layout changed to {new_cols}x{new_rows}. Please restart the app to apply changes."
show_toast(restart_msg, duration=3000)
except Exception as e:
logger.error(f"Erreur restart après layout: {e}")
logger.error(f"Erreur toast après 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 +1464,28 @@ 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")):
opts = getattr(config, 'footer_font_scale_options', [0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0])
idx = getattr(config, 'current_footer_font_scale_index', 3)
idx = max(0, idx-1) if is_input_matched(event, "left") else min(len(opts)-1, idx+1)
if idx != getattr(config, 'current_footer_font_scale_index', 3):
config.current_footer_font_scale_index = idx
scale = opts[idx]
config.accessibility_settings["footer_font_scale"] = scale
try:
save_accessibility_settings(config.accessibility_settings)
except Exception as e:
logger.error(f"Erreur sauvegarde footer font scale: {e}")
try:
init_footer_font_func = getattr(config, 'init_footer_font', None)
if callable(init_footer_font_func):
init_footer_font_func()
except Exception as e:
logger.error(f"Erreur init footer font: {e}")
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,8 +1518,8 @@ 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")):
# 4 unsupported toggle
elif sel == 4 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)
@@ -1511,8 +1529,8 @@ def handle_controls(event, sources, joystick, screen):
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")):
# 5 allow unknown extensions
elif sel == 5 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
try:
current = get_allow_unknown_extensions()
new_val = set_allow_unknown_extensions(not current)
@@ -1521,8 +1539,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")):
# 6 hide premium systems
elif sel == 6 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)
@@ -1531,15 +1549,15 @@ def handle_controls(event, sources, joystick, screen):
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")):
# 7 filter platforms
elif sel == 7 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")):
# 8 back
elif sel == 8 and (is_input_matched(event, "confirm")):
config.menu_state = "pause_menu"
config.last_state_change_time = pygame.time.get_ticks()
config.needs_redraw = True

View File

@@ -90,7 +90,34 @@ def get_help_icon_surface(action_name: str, size: int):
def _render_icons_line(actions, text, target_col_width, font, text_color, icon_size=28, icon_gap=8, icon_text_gap=12):
"""Compose une ligne avec une rangée d'icônes (actions) et un texte à droite.
Renvoie un pygame.Surface prêt à être blité, limité à target_col_width.
Si aucun joystick n'est détecté, affiche les touches clavier entre [ ] au lieu des icônes.
"""
# Si aucun joystick détecté, afficher les touches clavier entre crochets au lieu des icônes
if not getattr(config, 'joystick', True):
# Mode clavier : afficher [Touche] : Description
action_labels = []
for a in actions:
label = get_control_display(a, a.upper())
action_labels.append(f"[{label}]")
# Combiner les labels avec le texte
full_text = " ".join(action_labels) + " : " + text
try:
lines = wrap_text(full_text, font, target_col_width)
except Exception:
lines = [full_text]
line_surfs = [font.render(l, True, text_color) for l in lines]
width = max((s.get_width() for s in line_surfs), default=1)
height = sum(s.get_height() for s in line_surfs) + max(0, (len(line_surfs) - 1)) * 4
surf = pygame.Surface((width, height), pygame.SRCALPHA)
y = 0
for s in line_surfs:
surf.blit(s, (0, y))
y += s.get_height() + 4
return surf
# Mode joystick : afficher les icônes normalement
# Charger icônes (ignorer celles manquantes)
icon_surfs = []
for a in actions:
@@ -643,7 +670,10 @@ def draw_platform_grid(screen):
if total_pages > 1:
page_indicator_text = _("platform_page").format(config.current_page + 1, total_pages)
page_indicator = config.small_font.render(page_indicator_text, True, THEME_COLORS["text"])
page_rect = page_indicator.get_rect(center=(config.screen_width // 2, config.screen_height - margin_bottom // 2))
# Positionner au-dessus du footer (réserver ~80px pour le footer)
footer_reserved_height = 80
page_y = config.screen_height - footer_reserved_height - page_indicator.get_height() - 10
page_rect = page_indicator.get_rect(center=(config.screen_width // 2, page_y))
screen.blit(page_indicator, page_rect)
# Calculer une seule fois la pulsation pour les éléments sélectionnés
@@ -1494,6 +1524,9 @@ def draw_controls(screen, menu_state, current_music_name=None, music_popup_start
control_parts = []
start_button = get_control_display('start', 'START')
# Si aucun joystick, afficher la touche entre crochets
if not getattr(config, 'joystick', True):
start_button = f"[{start_button}]"
start_text = i18n("controls_action_start")
control_parts.append(f"RGSX v{config.app_version} - {start_button} : {start_text}")
@@ -1540,6 +1573,15 @@ def draw_controls(screen, menu_state, current_music_name=None, music_popup_start
max_width = config.screen_width - 40
icon_surfs = []
# Calculer la taille des icônes en fonction du footer_font_scale
footer_scale = config.accessibility_settings.get("footer_font_scale", 1.0)
base_icon_size = 20
scaled_icon_size = int(base_icon_size * footer_scale)
base_icon_gap = 6
scaled_icon_gap = int(base_icon_gap * footer_scale)
base_icon_text_gap = 10
scaled_icon_text_gap = int(base_icon_text_gap * footer_scale)
for line_data in icon_lines:
if isinstance(line_data, tuple) and len(line_data) >= 2:
if line_data[0] == "icons_combined":
@@ -1550,7 +1592,7 @@ def draw_controls(screen, menu_state, current_music_name=None, music_popup_start
for action_tuple in all_controls:
_, actions, label = action_tuple
try:
surf = _render_icons_line(actions, label, max_width - x_pos - 10, config.font, THEME_COLORS["text"], icon_size=20, icon_gap=6, icon_text_gap=10)
surf = _render_icons_line(actions, label, max_width - x_pos - 10, config.tiny_font, THEME_COLORS["text"], icon_size=scaled_icon_size, icon_gap=scaled_icon_gap, icon_text_gap=scaled_icon_text_gap)
if x_pos + surf.get_width() > max_width - 10:
break # Pas assez de place
combined_surf.blit(surf, (x_pos, (50 - surf.get_height()) // 2))
@@ -1565,14 +1607,14 @@ def draw_controls(screen, menu_state, current_music_name=None, music_popup_start
elif line_data[0] == "icons" and len(line_data) == 3:
_, actions, label = line_data
try:
surf = _render_icons_line(actions, label, max_width, config.font, THEME_COLORS["text"], icon_size=20, icon_gap=6, icon_text_gap=10)
surf = _render_icons_line(actions, label, max_width, config.tiny_font, THEME_COLORS["text"], icon_size=scaled_icon_size, icon_gap=scaled_icon_gap, icon_text_gap=scaled_icon_text_gap)
icon_surfs.append(surf)
except Exception:
text_surface = config.font.render(f"{label}", True, THEME_COLORS["text"])
text_surface = config.tiny_font.render(f"{label}", True, THEME_COLORS["text"])
icon_surfs.append(text_surface)
else:
# Texte simple (pour la ligne platform)
text_surface = config.font.render(line_data, True, THEME_COLORS["text"])
text_surface = config.tiny_font.render(line_data, True, THEME_COLORS["text"])
icon_surfs.append(text_surface)
# Calculer hauteur totale
@@ -1954,6 +1996,11 @@ def draw_pause_display_menu(screen, selected_index):
cur_idx = getattr(config, 'current_font_scale_index', 1)
font_value = f"{opts[cur_idx]}x"
font_txt = f"{_('submenu_display_font_size') if _ else 'Font Size'}: < {font_value} >"
# Footer font size
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])
footer_cur_idx = getattr(config, 'current_footer_font_scale_index', 3)
footer_font_value = f"{footer_opts[footer_cur_idx]}x"
footer_font_txt = f"{_('accessibility_footer_font_size').split(':')[0] if _ else 'Footer Font Size'}: < {footer_font_value} >"
# Font family
current_family = get_font_family()
# Nom user-friendly
@@ -1985,11 +2032,12 @@ def draw_pause_display_menu(screen, selected_index):
hide_premium_txt = f"{hide_premium_label}: < {status_hide_premium} >"
filter_txt = _("submenu_display_filter_platforms") if _ else "Filter Platforms"
back_txt = _("menu_back") if _ else "Back"
options = [layout_txt, font_txt, font_family_txt, unsupported_txt, unknown_txt, hide_premium_txt, filter_txt, back_txt]
options = [layout_txt, font_txt, footer_font_txt, font_family_txt, unsupported_txt, unknown_txt, hide_premium_txt, filter_txt, back_txt]
_draw_submenu_generic(screen, _("menu_display"), options, selected_index)
instruction_keys = [
"instruction_display_layout",
"instruction_display_font_size",
"instruction_display_footer_font_size",
"instruction_display_font_family",
"instruction_display_show_unsupported",
"instruction_display_unknown_ext",

View File

@@ -190,6 +190,7 @@
"instruction_generic_back": "Zum vorherigen Menü zurückkehren",
"instruction_display_layout": "Rasterabmessungen (Spalten × Zeilen) durchschalten",
"instruction_display_font_size": "Schriftgröße für bessere Lesbarkeit anpassen",
"instruction_display_footer_font_size": "Fußzeilen-Textgröße anpassen (Version & Steuerelemente)",
"instruction_display_font_family": "Zwischen verfügbaren Schriftarten wechseln",
"instruction_display_show_unsupported": "Nicht in es_systems.cfg definierte Systeme anzeigen/ausblenden",
"instruction_display_unknown_ext": "Warnung für in es_systems.cfg fehlende Dateiendungen an-/abschalten",
@@ -361,5 +362,7 @@
"filter_all": "Alles auswählen",
"filter_none": "Alles abwählen",
"filter_apply": "Filter anwenden",
"filter_back": "Zurück"
"filter_back": "Zurück",
"accessibility_footer_font_size": "Fußzeilen-Schriftgröße: {0}",
"popup_layout_changed_restart": "Layout geändert auf {0}x{1}. Bitte starten Sie die App neu."
}

View File

@@ -192,6 +192,7 @@
"instruction_generic_back": "Return to the previous menu",
"instruction_display_layout": "Cycle grid dimensions (columns × rows)",
"instruction_display_font_size": "Adjust text scale for readability",
"instruction_display_footer_font_size": "Adjust footer text scale (version & controls display)",
"instruction_display_font_family": "Switch between available font families",
"instruction_display_show_unsupported": "Show/hide systems not defined in es_systems.cfg",
"instruction_display_unknown_ext": "Enable/disable warning for file extensions absent from es_systems.cfg",
@@ -361,5 +362,7 @@
"filter_all": "Check All",
"filter_none": "Uncheck All",
"filter_apply": "Apply Filter",
"filter_back": "Back"
"filter_back": "Back",
"accessibility_footer_font_size": "Footer font size: {0}",
"popup_layout_changed_restart": "Layout changed to {0}x{1}. Please restart the app to apply."
}

View File

@@ -192,6 +192,7 @@
"instruction_generic_back": "Volver al menú anterior",
"instruction_display_layout": "Alternar dimensiones de la cuadrícula (columnas × filas)",
"instruction_display_font_size": "Ajustar tamaño del texto para mejor legibilidad",
"instruction_display_footer_font_size": "Ajustar el tamaño del texto del pie de página (versión y controles)",
"instruction_display_font_family": "Cambiar entre familias de fuentes disponibles",
"instruction_display_show_unsupported": "Mostrar/ocultar sistemas no definidos en es_systems.cfg",
"instruction_display_unknown_ext": "Activar/desactivar aviso para extensiones no presentes en es_systems.cfg",
@@ -361,5 +362,7 @@
"filter_all": "Marcar todo",
"filter_none": "Desmarcar todo",
"filter_apply": "Aplicar filtro",
"filter_back": "Volver"
"filter_back": "Volver",
"accessibility_footer_font_size": "Tamaño fuente pie de página: {0}",
"popup_layout_changed_restart": "Diseño cambiado a {0}x{1}. Reinicie la app para aplicar."
}

View File

@@ -192,6 +192,7 @@
"instruction_generic_back": "Revenir au menu précédent",
"instruction_display_layout": "Changer les dimensions de la grille",
"instruction_display_font_size": "Ajuster la taille du texte pour la lisibilité",
"instruction_display_footer_font_size": "Ajuster la taille du texte du pied de page (version et contrôles)",
"instruction_display_font_family": "Basculer entre les polices disponibles",
"instruction_display_show_unsupported": "Afficher/masquer systèmes absents de es_systems.cfg",
"instruction_display_unknown_ext": "Avertir ou non pour extensions absentes de es_systems.cfg",
@@ -361,5 +362,7 @@
"filter_all": "Tout cocher",
"filter_none": "Tout décocher",
"filter_apply": "Appliquer filtre",
"filter_back": "Retour"
"filter_back": "Retour",
"accessibility_footer_font_size": "Taille police pied de page : {0}",
"popup_layout_changed_restart": "Disposition changée en {0}x{1}. Veuillez redémarrer l'app pour appliquer."
}

View File

@@ -189,6 +189,7 @@
"instruction_generic_back": "Tornare al menu precedente",
"instruction_display_layout": "Scorrere dimensioni griglia (colonne × righe)",
"instruction_display_font_size": "Regolare dimensione testo per leggibilità",
"instruction_display_footer_font_size": "Regola dimensione testo piè di pagina (versione e controlli)",
"instruction_display_font_family": "Cambiare famiglia di font disponibile",
"instruction_display_show_unsupported": "Mostrare/nascondere sistemi non definiti in es_systems.cfg",
"instruction_display_unknown_ext": "Attivare/disattivare avviso per estensioni assenti in es_systems.cfg",
@@ -361,5 +362,7 @@
"filter_all": "Seleziona tutto",
"filter_none": "Deseleziona tutto",
"filter_apply": "Applica filtro",
"filter_back": "Indietro"
"filter_back": "Indietro",
"accessibility_footer_font_size": "Dimensione carattere piè di pagina: {0}",
"popup_layout_changed_restart": "Layout cambiato in {0}x{1}. Riavviare l'app per applicare."
}

View File

@@ -191,6 +191,7 @@
"instruction_generic_back": "Voltar ao menu anterior",
"instruction_display_layout": "Alternar dimensões da grade (colunas × linhas)",
"instruction_display_font_size": "Ajustar tamanho do texto para legibilidade",
"instruction_display_footer_font_size": "Ajustar tamanho do texto do rodapé (versão e controles)",
"instruction_display_font_family": "Alternar entre famílias de fontes disponíveis",
"instruction_display_show_unsupported": "Mostrar/ocultar sistemas não definidos em es_systems.cfg",
"instruction_display_unknown_ext": "Ativar/desativar aviso para extensões ausentes em es_systems.cfg",
@@ -361,5 +362,7 @@
"filter_all": "Marcar tudo",
"filter_none": "Desmarcar tudo",
"filter_apply": "Aplicar filtro",
"filter_back": "Voltar"
"filter_back": "Voltar",
"accessibility_footer_font_size": "Tamanho fonte rodapé: {0}",
"popup_layout_changed_restart": "Layout alterado para {0}x{1}. Reinicie o app para aplicar."
}

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",

View File

@@ -212,7 +212,7 @@ def get_game_metadata(game_name, platform_name):
# Vérifier si des résultats ont été trouvés
if "data" not in data or "games" not in data["data"] or not data["data"]["games"]:
logger.warning(f"Aucun résultat trouvé pour '{clean_name}'")
return {"error": "Aucun résultat trouvé"}
return {"error": f"No result found for '{clean_name}'"}
# Prendre le premier résultat (meilleure correspondance)
games = data["data"]["games"]

View File

@@ -1788,31 +1788,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 +1862,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 +1888,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 +1934,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:

View File

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