mirror of
https://github.com/RetroGameSets/RGSX.git
synced 2026-03-19 16:26:00 +01:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c5e307112 | ||
|
|
f9d95b9a2d | ||
|
|
2033eb2f76 | ||
|
|
61b615f4c7 | ||
|
|
f1c4955670 | ||
|
|
03d64d4401 | ||
|
|
5569238e55 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -21,3 +21,5 @@ Info.txt
|
||||
# Docker test data
|
||||
data/
|
||||
|
||||
docker-compose.test.yml
|
||||
config/
|
||||
|
||||
@@ -5,28 +5,29 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
p7zip-full \
|
||||
unrar-free \
|
||||
curl \
|
||||
rsync \
|
||||
gosu \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create required directories
|
||||
RUN mkdir -p /userdata/saves/ports/rgsx \
|
||||
&& mkdir -p /userdata/roms/ports \
|
||||
&& mkdir -p /app
|
||||
# Create app directory
|
||||
RUN mkdir -p /app
|
||||
|
||||
# Copy RGSX application files to /app (will be copied to volume at runtime)
|
||||
# Copy RGSX application files to /app
|
||||
COPY ports/RGSX/ /app/RGSX/
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY docker-entrypoint.sh /usr/local/bin/
|
||||
COPY docker/docker-entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
# Install Python dependencies
|
||||
# pygame is imported in some modules even in headless mode, so we include it
|
||||
RUN pip install --no-cache-dir requests pygame
|
||||
|
||||
# Set environment to headless mode
|
||||
ENV RGSX_HEADLESS=1
|
||||
# Set environment variables for Docker mode
|
||||
# These tell RGSX to use /config for settings and /data for ROMs
|
||||
ENV RGSX_HEADLESS=1 \
|
||||
RGSX_APP_DIR=/app \
|
||||
RGSX_CONFIG_DIR=/config \
|
||||
RGSX_DATA_DIR=/data
|
||||
|
||||
# Expose web interface port
|
||||
EXPOSE 5000
|
||||
@@ -35,6 +36,9 @@ EXPOSE 5000
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
CMD curl -f http://localhost:5000/ || exit 1
|
||||
|
||||
# Entrypoint copies app to volume, then runs command
|
||||
# Set working directory
|
||||
WORKDIR /app/RGSX
|
||||
|
||||
# Entrypoint handles user permissions and directory setup
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
CMD ["python", "rgsx_web.py", "--host", "0.0.0.0", "--port", "5000"]
|
||||
|
||||
@@ -5,19 +5,16 @@ Run RGSX as a web-only service without the Pygame UI. Perfect for homelab/server
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Build the image
|
||||
docker build -t rgsx .
|
||||
# Using docker-compose (recommended)
|
||||
docker-compose up -d
|
||||
|
||||
# Run with docker
|
||||
# Or build and run manually
|
||||
docker build -f docker/Dockerfile -t rgsx .
|
||||
docker run -d \
|
||||
--name rgsx \
|
||||
-p 5000:5000 \
|
||||
-e PUID=99 \
|
||||
-e PGID=100 \
|
||||
-e RGSX_HEADLESS=1 \
|
||||
-v ./data/saves:/userdata/saves/ports/rgsx \
|
||||
-v ./data/roms:/userdata/roms/ports \
|
||||
-v ./data/logs:/userdata/roms/ports/RGSX/logs \
|
||||
-v ./config:/config \
|
||||
-v ./data:/data \
|
||||
rgsx
|
||||
|
||||
# Access the web interface
|
||||
@@ -28,65 +25,63 @@ open http://localhost:5000
|
||||
|
||||
- Runs RGSX web server in headless mode (no Pygame UI)
|
||||
- Web interface accessible from any browser
|
||||
- ROMs and settings persist in `./data/` volumes
|
||||
- Container restarts automatically
|
||||
- Config persists in `/config` volume (settings, metadata, history)
|
||||
- ROMs download to `/data/roms/{platform}/` and extract there
|
||||
- Environment variables pre-configured (no manual setup needed)
|
||||
|
||||
## Environment Variables
|
||||
|
||||
**Pre-configured in the container (no need to set these):**
|
||||
- `RGSX_HEADLESS=1` - Runs in headless mode
|
||||
- `RGSX_CONFIG_DIR=/config` - Config location
|
||||
- `RGSX_DATA_DIR=/data` - Data location
|
||||
|
||||
**Optional (only if needed):**
|
||||
- `PUID` - User ID for file ownership (default: root)
|
||||
- `PGID` - Group ID for file ownership (default: root)
|
||||
|
||||
## Configuration
|
||||
|
||||
### Docker Compose
|
||||
|
||||
See `docker-compose.example.yml` for a complete example configuration.
|
||||
|
||||
### User Permissions (Important!)
|
||||
|
||||
**For SMB mounts (Unraid, Windows shares):**
|
||||
|
||||
Don't set PUID/PGID. The container runs as root, and the SMB server maps files to your authenticated user.
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-e RGSX_HEADLESS=1 \
|
||||
...
|
||||
```
|
||||
- Don't set PUID/PGID
|
||||
- The container runs as root, and the SMB server maps files to your authenticated user
|
||||
|
||||
**For NFS/local storage:**
|
||||
- Set PUID and PGID to match your host user (files will be owned by that user)
|
||||
- Find your user ID: `id -u` and `id -g`
|
||||
|
||||
Set PUID and PGID to match your host user. Files will be owned by that user.
|
||||
### Volumes
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-e PUID=1000 \
|
||||
-e PGID=1000 \
|
||||
-e RGSX_HEADLESS=1 \
|
||||
...
|
||||
```
|
||||
Two volumes are used:
|
||||
|
||||
**Find your user ID:**
|
||||
```bash
|
||||
id -u # Your UID
|
||||
id -g # Your GID
|
||||
```
|
||||
**`/config`** - Configuration and metadata
|
||||
- `rgsx_settings.json` - Settings
|
||||
- `games/` - Platform game database files (JSON)
|
||||
- `images/` - Game cover art
|
||||
- `history.json` - Download history
|
||||
- `logs/` - Application logs
|
||||
- `*.txt` - API keys
|
||||
|
||||
### Change Port
|
||||
|
||||
```bash
|
||||
docker run -p 8080:5000 ... # Access on port 8080
|
||||
```
|
||||
|
||||
### Custom ROM Location
|
||||
|
||||
Map to your existing ROM collection:
|
||||
```bash
|
||||
docker run -v /your/existing/roms:/userdata/roms/ports ...
|
||||
```
|
||||
**`/data`** - ROM storage
|
||||
- `roms/` - ROMs by platform (snes/, nes/, psx/, etc.) - downloads extract here
|
||||
|
||||
### API Keys
|
||||
|
||||
Add your download service API keys to `./data/saves/`:
|
||||
Add your download service API keys to `./config/`:
|
||||
|
||||
```bash
|
||||
# Add your API key (just the key, no extra text)
|
||||
echo "YOUR_KEY_HERE" > ./data/saves/1FichierAPI.txt
|
||||
echo "YOUR_KEY_HERE" > ./config/1FichierAPI.txt
|
||||
|
||||
# Optional: AllDebrid/RealDebrid fallbacks
|
||||
echo "YOUR_KEY" > ./data/saves/AllDebridAPI.txt
|
||||
echo "YOUR_KEY" > ./data/saves/RealDebridAPI.txt
|
||||
# Optional: AllDebrid/RealDebrid
|
||||
echo "YOUR_KEY" > ./config/AllDebridAPI.txt
|
||||
echo "YOUR_KEY" > ./config/RealDebridAPI.txt
|
||||
|
||||
# Restart to apply
|
||||
docker restart rgsx
|
||||
@@ -112,14 +107,29 @@ docker stop rgsx && docker rm rgsx
|
||||
|
||||
## Directory Structure
|
||||
|
||||
**On Host:**
|
||||
```
|
||||
RGSX/
|
||||
├── data/ # Created on first run
|
||||
│ ├── saves/ # Settings, history, API keys
|
||||
│ ├── roms/ # Downloaded ROMs
|
||||
│ └── logs/ # Application logs
|
||||
├── Dockerfile
|
||||
└── docker-compose.yml
|
||||
./
|
||||
├── config/ # Config volume (created on first run)
|
||||
│ ├── rgsx_settings.json
|
||||
│ ├── games/ # Platform game database (JSON)
|
||||
│ ├── images/ # Platform images
|
||||
│ ├── logs/ # Application logs
|
||||
│ └── *.txt # API keys (1FichierAPI.txt, etc.)
|
||||
└── data/
|
||||
└── roms/ # ROMs by platform
|
||||
├── snes/
|
||||
├── n64/
|
||||
└── ...
|
||||
```
|
||||
|
||||
**In Container:**
|
||||
```
|
||||
/app/RGSX/ # Application code
|
||||
/config/ # Mapped to ./config on host
|
||||
└── games/, images/, logs/, etc.
|
||||
/data/ # Mapped to ./data on host
|
||||
└── roms/ # ROM downloads go here
|
||||
```
|
||||
|
||||
## How It Works
|
||||
@@ -137,22 +147,15 @@ The container creates files with the UID/GID specified by PUID/PGID environment
|
||||
docker run -e PUID=1000 -e PGID=1000 ...
|
||||
```
|
||||
|
||||
**Changed PUID/PGID and container won't start:**
|
||||
**Changed PUID/PGID and permission errors:**
|
||||
|
||||
When you change PUID/PGID, old files with different ownership will cause rsync to fail. You MUST fix ownership on the storage server:
|
||||
Fix ownership of your volumes:
|
||||
|
||||
```bash
|
||||
# On your NAS/Unraid (via SSH), either:
|
||||
|
||||
# Option 1: Delete old files (easiest)
|
||||
rm -rf /mnt/user/roms/rgsx/roms/ports/RGSX/*
|
||||
|
||||
# Option 2: Change ownership to new PUID/PGID
|
||||
chown -R 1000:1000 /mnt/user/roms/rgsx/roms/ports/RGSX/
|
||||
# Fix ownership to match new PUID/PGID
|
||||
sudo chown -R 1000:1000 ./config ./data
|
||||
```
|
||||
|
||||
Then restart the container.
|
||||
|
||||
**Port already in use:**
|
||||
```bash
|
||||
docker run -p 8080:5000 ... # Use port 8080 instead
|
||||
|
||||
29
docker/docker-compose.example.yml
Normal file
29
docker/docker-compose.example.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
rgsx:
|
||||
# Option 1: Build from source
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/Dockerfile
|
||||
|
||||
# Option 2: Use pre-built image (push to your own registry)
|
||||
# image: your-registry/rgsx:latest
|
||||
|
||||
container_name: rgsx
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5000:5000"
|
||||
volumes:
|
||||
- ./config:/config
|
||||
- ./data:/data
|
||||
# Optional: Set PUID/PGID for NFS/local storage (not needed for SMB mounts)
|
||||
# environment:
|
||||
# - PUID=1000
|
||||
# - PGID=1000
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:5000/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
@@ -1,10 +1,12 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== RGSX Docker Container Startup ==="
|
||||
|
||||
# If PUID/PGID are set, create user and run as that user
|
||||
# If not set, run as root (works for SMB mounts)
|
||||
if [ -n "$PUID" ] && [ -n "$PGID" ]; then
|
||||
echo "=== Creating user with PUID=$PUID, PGID=$PGID ==="
|
||||
echo "Creating user with PUID=$PUID, PGID=$PGID..."
|
||||
|
||||
# Create group if it doesn't exist
|
||||
if ! getent group $PGID >/dev/null 2>&1; then
|
||||
@@ -16,43 +18,28 @@ if [ -n "$PUID" ] && [ -n "$PGID" ]; then
|
||||
useradd -u $PUID -g $PGID -m -s /bin/bash rgsx
|
||||
fi
|
||||
|
||||
# Fix ownership of app files
|
||||
chown -R $PUID:$PGID /app /userdata 2>/dev/null || true
|
||||
|
||||
echo "=== Running as user $(id -un $PUID) (UID=$PUID, GID=$PGID) ==="
|
||||
echo "Running as user $(id -un $PUID) (UID=$PUID, GID=$PGID)"
|
||||
RUN_USER="gosu rgsx"
|
||||
else
|
||||
echo "=== Running as root (no PUID/PGID set) - for SMB mounts ==="
|
||||
echo "Running as root (no PUID/PGID set) - suitable for SMB mounts"
|
||||
RUN_USER=""
|
||||
fi
|
||||
|
||||
# Always sync RGSX app code to the mounted volume (for updates)
|
||||
echo "Syncing RGSX app code to /userdata/roms/ports/RGSX..."
|
||||
$RUN_USER mkdir -p /userdata/roms/ports/RGSX
|
||||
# Create necessary directories
|
||||
# /config needs logs directory, app will create others (like images/, games/) as needed
|
||||
# /data needs roms directory
|
||||
echo "Setting up directories..."
|
||||
$RUN_USER mkdir -p /config/logs
|
||||
$RUN_USER mkdir -p /data/roms
|
||||
|
||||
# Try rsync
|
||||
if ! $RUN_USER rsync -av --delete /app/RGSX/ /userdata/roms/ports/RGSX/ 2>&1; then
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "WARNING: rsync partially failed!"
|
||||
echo "=========================================="
|
||||
echo "Some files may not have synced. Container will continue for debugging."
|
||||
echo ""
|
||||
if [ -n "$PUID" ] && [ -n "$PGID" ]; then
|
||||
echo "If using SMB, try removing PUID/PGID to run as root"
|
||||
fi
|
||||
echo ""
|
||||
# Fix ownership of volumes if PUID/PGID are set
|
||||
if [ -n "$PUID" ] && [ -n "$PGID" ]; then
|
||||
echo "Setting ownership on volumes..."
|
||||
chown -R $PUID:$PGID /config /data 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "RGSX app code sync attempted."
|
||||
|
||||
# Create Batocera folder structure only if folders don't exist
|
||||
$RUN_USER mkdir -p /userdata/saves/ports/rgsx/images
|
||||
$RUN_USER mkdir -p /userdata/saves/ports/rgsx/games
|
||||
$RUN_USER mkdir -p /userdata/roms/ports/RGSX/logs
|
||||
|
||||
# Create default settings with show_unsupported_platforms enabled if config doesn't exist
|
||||
SETTINGS_FILE="/userdata/saves/ports/rgsx/rgsx_settings.json"
|
||||
SETTINGS_FILE="/config/rgsx_settings.json"
|
||||
if [ ! -f "$SETTINGS_FILE" ]; then
|
||||
echo "Creating default settings with all platforms visible..."
|
||||
$RUN_USER bash -c "cat > '$SETTINGS_FILE' << 'EOF'
|
||||
@@ -60,11 +47,15 @@ if [ ! -f "$SETTINGS_FILE" ]; then
|
||||
\"show_unsupported_platforms\": true
|
||||
}
|
||||
EOF"
|
||||
echo "Default settings created!"
|
||||
echo "Default settings created at $SETTINGS_FILE"
|
||||
fi
|
||||
|
||||
# Run the command
|
||||
cd /userdata/roms/ports/RGSX
|
||||
echo "=== Starting RGSX Web Server ==="
|
||||
echo "Config directory: /config"
|
||||
echo "ROMs directory: /data/roms"
|
||||
echo "======================================"
|
||||
|
||||
# Run the command from the working directory (/app/RGSX set in Dockerfile)
|
||||
if [ -z "$RUN_USER" ]; then
|
||||
exec "$@"
|
||||
else
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
BIN
ports/RGSX/assets/progs/extract-xiso_linux
Normal file
BIN
ports/RGSX/assets/progs/extract-xiso_linux
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/progs/extract-xiso_win.exe
Normal file
BIN
ports/RGSX/assets/progs/extract-xiso_win.exe
Normal file
Binary file not shown.
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2.3.1.5"
|
||||
"version": "2.3.1.9"
|
||||
}
|
||||
Reference in New Issue
Block a user