-
-
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>())); + }); + }); +}