-
-
Notifications
You must be signed in to change notification settings - Fork 14
commit 2ce9c83
abduznik edited this page May 23, 2026
·
1 revision
Commit: 2ce9c83b4ee8b9ddd9bb675b60094e6d035c58a1
Author: abduznik
Date: 2026-05-04
Why: Routine maintenance, dependency updates, version bumps, or tooling changes.
CHANGELOG.md | 31 ++++++++
README.md | 15 ++--
agent.md | 10 ++-
lib/core/romm/romm_service.dart | 8 +-
test/core/emulator/linux_strategies_test.dart | 106 ++++++++++++++++++++++++++
test/unit/romm_service_test.dart | 91 ++++++++++++++++++++++
6 files changed, 250 insertions(+), 11 deletions(-)
CHANGELOG.mdREADME.mdagent.mdlib/core/romm/romm_service.darttest/core/emulator/linux_strategies_test.darttest/unit/romm_service_test.dart
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..0da1b93
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,31 @@
+# Changelog
+
+## [0.4.0] - 2026-05-04
+
+### Added
+- **Steam Deck & Linux Support**: Full integration with **EmuDeck** and **RetroDECK** environments.
+ - Automatic detection of EmuDeck/RetroDECK folder structures.
+ - Support for SteamOS-specific launcher scripts (`.sh` files).
+ - High-precision path resolution for emulator saves, including Flatpak sandboxes and EmuDeck symlinks.
+- **Serial Background Sync**: Implementation of a background queue for game save backups. Offline saves are now automatically synchronized to RomM when a connection is restored.
+- **Recently Added Widget**: Optimized "Recently Added" section on the home screen, now sorted by RomM ID for true chronological discovery.
+- **Automated Linux Validation**: Comprehensive unit test suite for Linux path resolution strategies to ensure stability across SteamOS updates.
+
+### Fixed
+- **ROM Scanning**: Resolved issues with PS3 and Nintendo Switch ROM scanning and name normalization.
+- **Download Reliability**: Fixed filesystem access errors (errno 5) during game downloads on certain OS configurations.
+- **UI/UX Polishing**: Improved alignment and visual consistency in the Settings screen and Game Detail views.
+
+### Changed
+- Refactored Linux strategy logic into isolated, testable classes (`EmuDeckStrategy`, `RetroDeckStrategy`).
+- Optimized metadata caching for faster offline library browsing.
+
+---
+
+## [0.3.0] - 2026-04-20
+
+### Added
+- **macOS Texture Processing**: Support for Ryujinx asset processing and texture conversion.
+- **Multi-Platform Native Support**: Initial support for macOS and Windows.
+- **Save Sync**: Bidirectional sync for major emulators.
+- **BIOS Management**: Automatic BIOS placement and downloading.
diff --git a/README.md b/README.md
index a4aee6c..b688ecc 100644
--- a/README.md
+++ b/README.md
@@ -23,24 +23,25 @@ Freegosy is a solo passion project — built and maintained in my spare time, wi
No pressure at all — the app is and will always be free.
-## Current Features (v0.3.x)
+## Current Features (v0.4.0)
-- **Native Multi-Platform Support**: Full support for **macOS** (ARM64/Intel), **Windows**, and **Linux** (including **Steam Deck/EmuDeck** integration).
+- **Native Multi-Platform Support**: Full support for **macOS** (ARM64/Intel), **Windows**, and **Linux** (including **Steam Deck/EmuDeck** and **RetroDECK** integration).
- **Enhanced Offline Mode**: Persistent metadata caching for browsing and launching your collection without a connection.
- **RomM Integration**:
- Browse and filter your entire library with server-side pagination.
- - **New**: Instant "Downloaded" games filter with background deep collection scanning.
+ - Instant "Downloaded" games filter with background deep collection scanning.
- Download ROMs directly via HTTP with real-time progress tracking.
- Personal game properties support (rating, status, completion).
- **Advanced Emulator Management**:
- Download, update, and uninstall emulators directly from Settings.
- Automatic extraction of `.zip`, `.7z`, `.dmg`, `.tar.gz`, `.tar.xz`, and `.AppImage`.
- Smart binary detection and canonical naming across all platforms.
- - **New**: Dynamic architecture selection for RPCS3 on macOS (ARM64 vs x64).
+ - Dynamic architecture selection for RPCS3 on macOS (ARM64 vs x64).
+ - **New**: Linux Environment Strategies (Default, EmuDeck, RetroDECK) with automatic path detection.
- **BIOS Management**: Fetch and download BIOS files directly from RomM and automatically place them in the correct directory for each emulator.
- **Save Sync**:
- Bidirectional local-to-cloud save synchronization with RomM.
- - **New**: Local Backup History (create instant restore points before experimenting).
+ - Local Backup History (create instant restore points before experimenting).
- **New**: Serial Background Sync Queue (offline backups silently push to RomM automatically when you reconnect).
- Optimized for EmuDeck's platform-specific save structure.
- **Refined UI/UX**:
@@ -50,9 +51,9 @@ No pressure at all — the app is and will always be free.
- **Multi-Disc Support**: Integrated picker for multi-file games.
## Calling All Testers!
-I am currently searching for testers on **macOS** and **Windows** to help polish the experience.
+I am currently searching for testers on **macOS**, **Windows**, and **Linux (Steam Deck)** to help polish the experience.
-- **Future Plans**: Steam Deck/Linux support is next, followed by **Android** for a truly unified app.
+- **Future Plans**: **Android** support is next for a truly unified app experience.
- **Get Involved**: If you're interested in testing an early release, reach out via GitHub or join the community discussions.
## About RomM
diff --git a/agent.md b/agent.md
index d534a3f..48c92ab 100644
--- a/agent.md
+++ b/agent.md
@@ -50,10 +50,16 @@ Freegosy is a cross-platform Flutter app for browsing a RomM library, downloadin
### Core — Emulator
- `lib/core/emulator/emulator_strategy.dart` — Abstract base class for launch logic.
- `lib/core/emulator/emulator_registry_data.dart` — Static definitions for emulator downloads and filters.
-- `lib/core/emulator/strategy_registry.dart` — Registry for emulator strategies with conflict detection.
+- `lib/core/emulator/strategy_registry.dart` — Registry for emulator strategies with conflict detection. Added OS-based strategy filtering.
- `lib/core/emulator/emulator_download_service.dart` — Downloads emulators from direct URLs or GitHub.
- `lib/core/emulator/github_release_service.dart` — Resolves latest GitHub release assets.
-- `lib/core/emulator/strategies/` — Specific implementations for each emulator (RetroArch, Dolphin, Eden, RPCS3, PCSX2, Azahar, Cemu, DuckStation, Flycast, melonDS, PPSSPP, mGBA, MAME, Xemu, Xenia, Windows).
+- `lib/core/emulator/strategies/` — Specific implementations for each emulator (RetroArch, Dolphin, Eden, Ryujinx, RPCS3, PCSX2, Azahar, Cemu, DuckStation, Flycast, melonDS, PPSSPP, mGBA, MAME, Xemu, Xenia, Windows).
+- `lib/core/emulator/linux_strategies/` — SteamOS/Linux environment strategies.
+ - `linux_environment_strategy.dart` — Interface for ROM/Save/Tool path resolution on Linux.
+ - `emudeck_strategy.dart` — EmuDeck-specific resolution (SD card detection, symlink saves).
+ - `retrodeck_strategy.dart` — RetroDECK Flatpak resolution.
+ - `native_linux_strategy.dart` — Default Linux directory structure.
+ - `linux_native_game_service.dart` — Proton/Steam prefix path resolution for native PC games on Linux.
### Core — Extraction
- `lib/core/extraction/extraction_service.dart` — Unified extraction for .zip, .7z, .dmg, .tar.gz, .tar.xz, and .exe. Sanitizes macOS .app bundles.
diff --git a/lib/core/romm/romm_service.dart b/lib/core/romm/romm_service.dart
index d6bd79c..2d36b4d 100644
--- a/lib/core/romm/romm_service.dart
+++ b/lib/core/romm/romm_service.dart
@@ -29,8 +29,8 @@ class RommService {
static String _normalizeBaseUrl(String url) =>
url.endsWith('/') ? url.substring(0, url.length - 1) : url;
- RommService(this._config)
- : _dio = Dio(BaseOptions(
+ RommService(this._config, {Dio? dio})
+ : _dio = dio ?? Dio(BaseOptions(
baseUrl: _normalizeBaseUrl(_config.baseUrl),
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 5),
@@ -41,6 +41,10 @@ class RommService {
)),
_authOptions = _computeAuthOptions(_config) {
+ if (dio != null) {
+ _dio.options.baseUrl = _normalizeBaseUrl(_config.baseUrl);
+ }
+
if (kDebugMode || io.Platform.isLinux || io.Platform.isMacOS) {
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
diff --git a/test/core/emulator/linux_strategies_test.dart b/test/core/emulator/linux_strategies_test.dart
new file mode 100644
index 0000000..3b05502
--- /dev/null
+++ b/test/core/emulator/linux_strategies_test.dart
@@ -0,0 +1,106 @@
+import 'dart:io';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:path/path.dart' as p;
+import 'package:freegosy/core/emulator/linux_strategies/emudeck_strategy.dart';
+import 'package:freegosy/core/emulator/linux_strategies/retrodeck_strategy.dart';
+
+void main() {
+ group('Linux Environment Strategies Path Resolution', () {
+ late Directory tempHome;
+ late Directory emudeckRoot;
+ late Directory retrodeckVarConfig;
+
+ late EmuDeckStrategy emudeckStrategy;
+ late RetroDeckStrategy retrodeckStrategy;
+
+ setUp(() async {
+ // Create a fake home directory
+ tempHome = await Directory.systemTemp.createTemp('freegosy_test_home_');
+
+ // --- Setup Mock EmuDeck Structure ---
+ // EmuDeck normally lives in ~/Emulation or an SD card
+ emudeckRoot = Directory(p.join(tempHome.path, 'Emulation'));
+ await emudeckRoot.create();
+
+ await Directory(p.join(emudeckRoot.path, 'roms')).create();
+ await Directory(p.join(emudeckRoot.path, 'tools', 'launchers')).create(recursive: true);
+ await Directory(p.join(emudeckRoot.path, 'bios')).create();
+
+ // Mock EmuDeck Save symlink structure for Cemu
+ final cemuSaves = Directory(p.join(emudeckRoot.path, 'saves', 'Cemu', 'saves'));
+ await cemuSaves.create(recursive: true);
+
+ // Create a dummy launch script
+ final launcher = File(p.join(emudeckRoot.path, 'tools', 'launchers', 'cemu.sh'));
+ await launcher.writeAsString('#!/bin/bash');
+
+ // --- Setup Mock RetroDECK Structure ---
+ await Directory(p.join(tempHome.path, 'retrodeck', 'roms')).create(recursive: true);
+ await Directory(p.join(tempHome.path, 'retrodeck', 'tools')).create(recursive: true);
+
+ // RetroDECK uses Flatpak standard paths
+ retrodeckVarConfig = Directory(p.join(tempHome.path, '.var', 'app', 'net.retrodeck.retrodeck', 'config'));
+ await Directory(p.join(retrodeckVarConfig.path, 'bios')).create(recursive: true);
+
+ // Mock RetroDECK save structure for PCSX2
+ await Directory(p.join(retrodeckVarConfig.path, 'PCSX2', 'saves')).create(recursive: true);
+
+ // Initialize strategies
+ emudeckStrategy = EmuDeckStrategy();
+ retrodeckStrategy = RetroDeckStrategy();
+ });
+
+ tearDown(() async {
+ // Clean up the fake file tree
+ if (await tempHome.exists()) {
+ await tempHome.delete(recursive: true);
+ }
+ });
+
+ group('EmuDeckStrategy', () {
+ test('Resolves ROMs root correctly', () {
+ // EmuDeck Strategy checks for Emulation/roms
+ final result = emudeckStrategy.getRomsRoot(tempHome.path, null, tempHome.path);
+ expect(result, p.join(tempHome.path, 'Emulation', 'roms'));
+ });
+
+ test('Resolves Tools root correctly', () {
+ // EmuDeck Strategy checks for Emulation/tools
+ final result = emudeckStrategy.getEmulatorsRoot(tempHome.path, null, tempHome.path);
+ expect(result, p.join(tempHome.path, 'Emulation', 'tools'));
+ });
+
+ test('Resolves specific save symlink directory correctly', () {
+ // We mocked 'Cemu' and its 'saves' subfolder, which EmuDeckStrategy should prioritize
+ final result = emudeckStrategy.getEmulatorAppSupportDirectory(tempHome.path, 'cemu', tempHome.path);
+ expect(result, p.join(tempHome.path, 'Emulation', 'saves', 'Cemu', 'saves'));
+ });
+
+ test('Finds EmuDeck specific launcher script', () async {
+ // EmuDeck map maps 'cemu' to 'cemu.sh'
+ final result = await emudeckStrategy.findExecutable('cemu', 'Cemu.AppImage', tempHome.path, tempHome.path);
+ expect(result, p.join(tempHome.path, 'Emulation', 'tools', 'launchers', 'cemu.sh'));
+ });
+ });
+
+ group('RetroDeckStrategy', () {
+ test('Resolves ROMs root correctly', () {
+ // RetroDECK ROMs are in ~/retrodeck/roms
+ final result = retrodeckStrategy.getRomsRoot(tempHome.path, null, null);
+ expect(result, p.join(tempHome.path, 'retrodeck', 'roms'));
+ });
+
+ test('Resolves Flatpak BIOS directory correctly', () {
+ // RetroDECK bios are in ~/.var/app/net.retrodeck.retrodeck/config/bios
+ final result = retrodeckStrategy.getBiosPath(tempHome.path, null);
+ expect(result, p.join(tempHome.path, '.var', 'app', 'net.retrodeck.retrodeck', 'config', 'bios'));
+ });
+
+ test('Resolves Flatpak Save directory correctly', () {
+ // We mocked PCSX2 with a 'saves' subfolder inside the flatpak config
+ final result = retrodeckStrategy.getEmulatorAppSupportDirectory(tempHome.path, 'pcsx2', null);
+ expect(result, p.join(retrodeckVarConfig.path, 'PCSX2', 'saves'));
+ });
+ });
+ });
+}
diff --git a/test/unit/romm_service_test.dart b/test/unit/romm_service_test.dart
new file mode 100644
index 0000000..7caedfb
--- /dev/null
+++ b/test/unit/romm_service_test.dart
@@ -0,0 +1,91 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:dio/dio.dart';
+import 'package:http_mock_adapter/http_mock_adapter.dart';
+import 'package:freegosy/core/romm/romm_models.dart';
+import 'package:freegosy/core/romm/romm_service.dart';
+
+void main() {
+ late RommService rommService;
+ late Dio dio;
+ late DioAdapter dioAdapter;
+
+ const String testBaseUrl = 'https://romm.example.com';
+ const String testApiKey = 'test_api_key_12345';
+
+ setUp(() {
+ dio = Dio(BaseOptions(baseUrl: testBaseUrl));
+ dioAdapter = DioAdapter(dio: dio);
+
+ final config = RomMConfig(
+ baseUrl: testBaseUrl,
+ username: '',
+ password: '',
+ apiKey: testApiKey,
+ );
+
+ rommService = RommService(config, dio: dio);
+ });
+
+ group('RommService API Authentication', () {
+ test('sends API Key in both Authorization and X-Api-Key headers', () async {
+ // Setup mock response
+ dioAdapter.onGet(
+ '/api/platforms',
+ (server) => server.reply(200, {'items': []}),
+ headers: {
+ 'Authorization': 'Bearer $testApiKey',
+ 'X-Api-Key': testApiKey,
+ },
+ );
+
+ // Call the API
+ final platforms = await rommService.getPlatforms();
+
+ // Verify results
+ expect(platforms, isEmpty);
+ });
+
+ test('getPlatforms correctly parses platforms list', () async {
+ // Mock data
+ final mockData = {
+ 'items': [
+ {
+ 'id': 1,
+ 'name': 'Nintendo Switch',
+ 'slug': 'switch',
+ 'display_name': 'Switch',
+ 'rom_count': 10,
+ },
+ {
+ 'id': 2,
+ 'name': 'PlayStation 2',
+ 'slug': 'ps2',
+ 'display_name': 'PS2',
+ 'rom_count': 5,
+ }
+ ]
+ };
+
+ dioAdapter.onGet(
+ '/api/platforms',
+ (server) => server.reply(200, mockData),
+ );
+
+ final platforms = await rommService.getPlatforms();
+
+ expect(platforms.length, 2);
+ expect(platforms[0].id, 1);
+ expect(platforms[0].name, 'Nintendo Switch');
+ expect(platforms[1].slug, 'ps2');
+ });
+
+ test('getPlatforms throws DioException on error response', () async {
+ dioAdapter.onGet(
+ '/api/platforms',
+ (server) => server.reply(401, {'message': 'Unauthorized'}),
+ );
+
+ expect(() => rommService.getPlatforms(), throwsA(isA<DioException>()));
+ });
+ });
+}