-
-
Notifications
You must be signed in to change notification settings - Fork 14
commit 481ba0f
abduznik edited this page May 23, 2026
·
1 revision
Commit: 481ba0f1bb0314dfc9f7fb856ad97da58b0377a0
Author: abduznik
Date: 2026-04-28
Why: Fixes a bug or regression in the existing codebase.
.github/workflows/release.yml | 6 +-
lib/core/romm/romm_service.dart | 10 +++-
lib/providers/download_provider.dart | 2 -
lib/ui/screens/onboarding_screen.dart | 101 +++++++++++++++++++++++++---------
lib/ui/screens/settings_screen.dart | 92 +++++++++++++++++++++++++------
5 files changed, 165 insertions(+), 46 deletions(-)
.github/workflows/release.ymllib/core/romm/romm_service.dartlib/providers/download_provider.dartlib/ui/screens/onboarding_screen.dartlib/ui/screens/settings_screen.dart
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 3034b81..0a27c18 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -7,6 +7,10 @@ on:
description: 'Version tag (e.g. v1.0.0)'
required: true
default: 'v1.0.0'
+ prerelease:
+ description: 'Is Pre-release?'
+ type: boolean
+ default: false
build_linux:
description: 'Build Linux?'
type: boolean
@@ -186,6 +190,6 @@ jobs:
artifacts/freegosy-macos.dmg
fail_on_unmatched_files: false
draft: false
- prerelease: false
+ prerelease: ${{ github.event.inputs.prerelease == 'true' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/lib/core/romm/romm_service.dart b/lib/core/romm/romm_service.dart
index e031153..6b9bcdb 100644
--- a/lib/core/romm/romm_service.dart
+++ b/lib/core/romm/romm_service.dart
@@ -350,10 +350,16 @@ class RommService {
}
final baseUrl = _normalizeBaseUrl(_config.baseUrl);
final name = game.fileName ?? game.fsName ?? game.name;
- String encoded = Uri.encodeComponent(name);
+ String encoded = Uri.encodeComponent(name)
+ .replaceAll("'", "%27")
+ .replaceAll("(", "%28")
+ .replaceAll(")", "%29");
if (encoded.length > 100) {
final ext = p.extension(name); final stem = p.basenameWithoutExtension(name);
- encoded = Uri.encodeComponent('${stem.substring(0, min(stem.length, 50))}$ext');
+ encoded = Uri.encodeComponent('${stem.substring(0, min(stem.length, 50))}$ext')
+ .replaceAll("'", "%27")
+ .replaceAll("(", "%28")
+ .replaceAll(")", "%29");
}
return '$baseUrl/api/roms/${game.id}/content/$encoded';
}
diff --git a/lib/providers/download_provider.dart b/lib/providers/download_provider.dart
index aac0528..9e01f0c 100644
--- a/lib/providers/download_provider.dart
+++ b/lib/providers/download_provider.dart
@@ -22,7 +22,6 @@ final downloadServiceProvider = FutureProvider<DownloadService?>((ref) async {
connectTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(hours: 4), // Allow up to 4 hours for large ROMs
headers: {
- 'Accept-Encoding': 'identity', // Disable compression for large downloads to avoid proxy issues
'User-Agent': 'Freegosy/${AppConstants.version}',
},
));
@@ -53,7 +52,6 @@ final emulatorDownloadServiceProvider =
connectTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(minutes: 30),
headers: {
- 'Accept-Encoding': 'identity',
'User-Agent': 'Freegosy/${AppConstants.version}',
},
));
diff --git a/lib/ui/screens/onboarding_screen.dart b/lib/ui/screens/onboarding_screen.dart
index f7e8c63..37282e8 100644
--- a/lib/ui/screens/onboarding_screen.dart
+++ b/lib/ui/screens/onboarding_screen.dart
@@ -1,5 +1,6 @@
import 'dart:io' as io;
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:file_picker/file_picker.dart';
import 'package:path/path.dart' as p;
@@ -32,6 +33,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
// Step 2: Storage Config
String? _romsRoot;
String? _emusRoot;
+ String? _presetRoot;
String _linuxPreset = 'default';
bool _isStorageInitialized = false;
@@ -49,6 +51,12 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
final romsRoot = prefs.getString('romsRootPath');
final emusRoot = prefs.getString('emulatorsRootPath');
final linuxPreset = prefs.getString('linuxSyncPreset') ?? 'default';
+ String? presetRoot;
+ if (linuxPreset == 'emudeck') {
+ presetRoot = prefs.getString('emudeckRootPath');
+ } else if (linuxPreset == 'retrodeck') {
+ presetRoot = prefs.getString('retrodeckRootPath');
+ }
if (!mounted) return;
@@ -60,6 +68,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
if (romsRoot != null) _romsRoot = romsRoot;
if (emusRoot != null) _emusRoot = emusRoot;
_linuxPreset = linuxPreset;
+ if (presetRoot != null) _presetRoot = presetRoot;
if (romsRoot != null || emusRoot != null) {
_isStorageInitialized = true;
}
@@ -128,14 +137,25 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
await prefs.setString('rommBaseUrl', _baseUrlController.text.trim());
await SecureStorageService.write('rommApiKey', _apiKeyController.text.trim(), prefs);
+ if (io.Platform.isLinux && _linuxPreset != 'default' && _presetRoot != null) {
+ if (_linuxPreset == 'emudeck') {
+ _romsRoot = p.join(_presetRoot!, 'roms');
+ _emusRoot = p.join(_presetRoot!, 'tools', 'launchers');
+ await prefs.setString('emudeckRootPath', _presetRoot!);
+ } else if (_linuxPreset == 'retrodeck') {
+ _romsRoot = p.join(_presetRoot!, 'roms');
+ _emusRoot = p.join(_presetRoot!, 'tools');
+ await prefs.setString('retrodeckRootPath', _presetRoot!);
+ }
+ await prefs.setString('linuxSyncPreset', _linuxPreset);
+ } else if (io.Platform.isLinux) {
+ await prefs.setString('linuxSyncPreset', 'default');
+ }
+
// Save Storage Config
if (_romsRoot != null) await prefs.setString('romsRootPath', _romsRoot!);
if (_emusRoot != null) await prefs.setString('emulatorsRootPath', _emusRoot!);
- if (io.Platform.isLinux) {
- await prefs.setString('linuxSyncPreset', _linuxPreset);
- }
-
// Invalidate providers to trigger reload
ref.invalidate(rommConfigProvider);
ref.invalidate(rommServiceProvider);
@@ -261,19 +281,39 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
const SizedBox(height: 40),
TextField(
controller: _baseUrlController,
- decoration: const InputDecoration(
+ decoration: InputDecoration(
labelText: 'Server URL',
hintText: 'http://your-ip:8080',
- prefixIcon: Icon(Icons.dns),
+ prefixIcon: const Icon(Icons.dns),
+ suffixIcon: IconButton(
+ icon: const Icon(Icons.paste),
+ onPressed: () async {
+ final data = await Clipboard.getData(Clipboard.kTextPlain);
+ if (data != null && data.text != null) {
+ _baseUrlController.text = data.text!;
+ }
+ },
+ tooltip: 'Paste from clipboard',
+ ),
),
),
const SizedBox(height: 20),
TextField(
controller: _apiKeyController,
- decoration: const InputDecoration(
+ decoration: InputDecoration(
labelText: 'API Key',
hintText: 'Found in RomM User Settings',
- prefixIcon: Icon(Icons.key),
+ prefixIcon: const Icon(Icons.key),
+ suffixIcon: IconButton(
+ icon: const Icon(Icons.paste),
+ onPressed: () async {
+ final data = await Clipboard.getData(Clipboard.kTextPlain);
+ if (data != null && data.text != null) {
+ _apiKeyController.text = data.text!;
+ }
+ },
+ tooltip: 'Paste from clipboard',
+ ),
),
obscureText: true,
),
@@ -367,23 +407,34 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
const SizedBox(height: 32),
],
- _buildPathSelector(
- label: 'ROMs Directory',
- currentPath: _romsRoot ?? 'Loading...',
- onTap: () async {
- final path = await FilePicker.platform.getDirectoryPath();
- if (path != null) setState(() => _romsRoot = path);
- },
- ),
- const SizedBox(height: 24),
- _buildPathSelector(
- label: 'Emulators Directory',
- currentPath: _emusRoot ?? 'Loading...',
- onTap: () async {
- final path = await FilePicker.platform.getDirectoryPath();
- if (path != null) setState(() => _emusRoot = path);
- },
- ),
+ if (io.Platform.isLinux && _linuxPreset != 'default') ...[
+ _buildPathSelector(
+ label: '${_linuxPreset == 'emudeck' ? 'EmuDeck' : 'RetroDeck'} Installation Root',
+ currentPath: _presetRoot ?? 'Select root directory...',
+ onTap: () async {
+ final path = await FilePicker.platform.getDirectoryPath();
+ if (path != null) setState(() => _presetRoot = path);
+ },
+ ),
+ ] else ...[
+ _buildPathSelector(
+ label: 'ROMs Directory',
+ currentPath: _romsRoot ?? 'Loading...',
+ onTap: () async {
+ final path = await FilePicker.platform.getDirectoryPath();
+ if (path != null) setState(() => _romsRoot = path);
+ },
+ ),
+ const SizedBox(height: 24),
+ _buildPathSelector(
+ label: 'Emulators Directory',
+ currentPath: _emusRoot ?? 'Loading...',
+ onTap: () async {
+ final path = await FilePicker.platform.getDirectoryPath();
+ if (path != null) setState(() => _emusRoot = path);
+ },
+ ),
+ ],
],
),
);
diff --git a/lib/ui/screens/settings_screen.dart b/lib/ui/screens/settings_screen.dart
index 7619b64..5938745 100644
--- a/lib/ui/screens/settings_screen.dart
+++ b/lib/ui/screens/settings_screen.dart
@@ -1,3 +1,4 @@
+import 'dart:io' as io;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -236,22 +237,81 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
// --- Storage Section ---
Widget _buildStorageSection(DirectoryService directoryService) {
+ final prefs = ref.read(sharedPreferencesProvider);
+ final preset = directoryService.linuxSyncPreset;
+
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Text('Storage', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
- _buildPathRow(
- label: 'ROMs Directory',
- currentPath: directoryService.romsRootPath,
- onChanged: (p) async { if (p != null) { await directoryService.setRomsRoot(p); ref.invalidate(directoryServiceProvider); } },
- onReset: () async { await directoryService.resetRomsRoot(); ref.invalidate(directoryServiceProvider); },
- ),
- const SizedBox(height: 12),
- _buildPathRow(
- label: 'Emulators Directory',
- currentPath: directoryService.emulatorsRootPath,
- onChanged: (p) async { if (p != null) { await directoryService.setEmulatorsRoot(p); ref.invalidate(directoryServiceProvider); } },
- onReset: () async { await directoryService.resetEmulatorsRoot(); ref.invalidate(directoryServiceProvider); },
- ),
+
+ if (io.Platform.isLinux) ...[
+ const Text('Linux App Layout', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
+ const SizedBox(height: 8),
+ DropdownButtonFormField<String>(
+ initialValue: preset,
+ decoration: const InputDecoration(border: OutlineInputBorder()),
+ items: const [
+ DropdownMenuItem(value: 'default', child: Text('Manual / Native')),
+ DropdownMenuItem(value: 'emudeck', child: Text('EmuDeck')),
+ DropdownMenuItem(value: 'retrodeck', child: Text('RetroDeck')),
+ ],
+ onChanged: (val) async {
+ if (val != null) {
+ await directoryService.setLinuxSyncPreset(val);
+ ref.invalidate(directoryServiceProvider);
+ }
+ },
+ ),
+ const SizedBox(height: 16),
+ ],
+
+ if (io.Platform.isLinux && preset != 'default') ...[
+ _buildPathRow(
+ label: '${preset == 'emudeck' ? 'EmuDeck' : 'RetroDeck'} Installation Root',
+ currentPath: preset == 'emudeck'
+ ? (directoryService.emudeckRootPath ?? 'Not set')
+ : (prefs.getString('retrodeckRootPath') ?? 'Not set'),
+ onChanged: (p) async {
+ if (p != null) {
+ if (preset == 'emudeck') {
+ await directoryService.setEmudeckRoot(p);
+ } else {
+ await prefs.setString('retrodeckRootPath', p);
+ // Trigger re-init so paths compute
+ await directoryService.initialize();
+ }
+ ref.invalidate(directoryServiceProvider);
+ }
+ },
+ ),
+ const SizedBox(height: 12),
+ // Read-only views of the computed paths
+ _buildPathRow(
+ label: 'Computed ROMs Directory',
+ currentPath: directoryService.romsRootPath,
+ onChanged: null,
+ ),
+ const SizedBox(height: 12),
+ _buildPathRow(
+ label: 'Computed Emulators Directory',
+ currentPath: directoryService.emulatorsRootPath,
+ onChanged: null,
+ ),
+ ] else ...[
+ _buildPathRow(
+ label: 'ROMs Directory',
+ currentPath: directoryService.romsRootPath,
+ onChanged: (p) async { if (p != null) { await directoryService.setRomsRoot(p); ref.invalidate(directoryServiceProvider); } },
+ onReset: () async { await directoryService.resetRomsRoot(); ref.invalidate(directoryServiceProvider); },
+ ),
+ const SizedBox(height: 12),
+ _buildPathRow(
+ label: 'Emulators Directory',
+ currentPath: directoryService.emulatorsRootPath,
+ onChanged: (p) async { if (p != null) { await directoryService.setEmulatorsRoot(p); ref.invalidate(directoryServiceProvider); } },
+ onReset: () async { await directoryService.resetEmulatorsRoot(); ref.invalidate(directoryServiceProvider); },
+ ),
+ ],
const SizedBox(height: 16),
OutlinedButton.icon(
onPressed: () => SystemUtils.openDirectory(directoryService.romsRootPath),
@@ -267,13 +327,13 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
]);
}
- Widget _buildPathRow({required String label, required String currentPath, required Function(String?) onChanged, VoidCallback? onReset}) {
+ Widget _buildPathRow({required String label, required String currentPath, required Function(String?)? onChanged, VoidCallback? onReset}) {
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(label, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
Row(children: [
Expanded(child: Text(currentPath, overflow: TextOverflow.ellipsis)),
- IconButton(onPressed: onReset, icon: const Icon(Icons.restore)),
- ElevatedButton(onPressed: () async => onChanged(await FilePicker.platform.getDirectoryPath()), child: const Text('Change')),
+ if (onReset != null) IconButton(onPressed: onReset, icon: const Icon(Icons.restore)),
+ if (onChanged != null) ElevatedButton(onPressed: () async => onChanged(await FilePicker.platform.getDirectoryPath()), child: const Text('Change')),
]),
]);
}