diff --git a/VERSION b/VERSION index 7c69a55..a5c4c76 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.7.0 +3.9.0 diff --git a/core/pubspec.yaml b/core/pubspec.yaml index d24a6b3..76d13ad 100644 --- a/core/pubspec.yaml +++ b/core/pubspec.yaml @@ -1,6 +1,6 @@ name: openrom_core description: OpenROM Pure Dart Backend Core & Toolkit Library -version: 3.7.0 +version: 3.9.0 publish_to: 'none' environment: diff --git a/gui/lib/app.dart b/gui/lib/app.dart index 6764ee2..66d1038 100644 --- a/gui/lib/app.dart +++ b/gui/lib/app.dart @@ -1,6 +1,7 @@ // OpenROM — Universal ROM Compression Suite // M5 Dev | GPL v3 +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'controllers/home_controller.dart'; @@ -43,6 +44,8 @@ class _OpenROMAppState extends State { String _outputDir = ''; String _audioCodec = 'flac'; + static const List _mobileScreenIndices = [0, 1, 2, 4]; + @override void initState() { super.initState(); @@ -135,6 +138,19 @@ class _OpenROMAppState extends State { }, ); + final screens = [ + HomeScreen(theme: theme, controller: _homeController), + PatcherScreen(theme: theme), + ToolsScreen(theme: theme), + DreamcastScreen(theme: theme), + settingsWidget, + AboutScreen(theme: theme), + ThemeEditorScreen( + theme: theme, + themeService: widget.themeService, + ), + ]; + return MaterialApp( title: 'OpenROM', debugShowCheckedModeBanner: false, @@ -149,49 +165,106 @@ class _OpenROMAppState extends State { primary: theme.accent, ), ), - home: Scaffold( - backgroundColor: theme.background, - body: Row( - children: [ - Sidebar( - selectedIndex: _selectedIndex, - onDestinationSelected: (index) { - setState(() => _selectedIndex = index); - }, - theme: theme, - ), - Expanded( - child: Column( - children: [ - TopBar( - theme: theme, - fileCount: fileCount, - isConverting: isConverting, - onConvertPressed: _onConvertPressed, - onAddFilesPressed: _onAddFilesPressed, + home: LayoutBuilder( + builder: (context, constraints) { + final isNarrow = constraints.maxWidth < 650; + final isMobile = isNarrow || + defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS; + + if (isMobile) { + final mobileCurrentIndex = + _mobileScreenIndices.contains(_selectedIndex) + ? _mobileScreenIndices.indexOf(_selectedIndex) + : 0; + + return Scaffold( + backgroundColor: theme.background, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(60), + child: TopBar( + theme: theme, + fileCount: fileCount, + isConverting: isConverting, + onConvertPressed: _onConvertPressed, + onAddFilesPressed: _onAddFilesPressed, + ), + ), + body: IndexedStack( + index: _selectedIndex, + children: screens, + ), + bottomNavigationBar: BottomNavigationBar( + currentIndex: mobileCurrentIndex, + onTap: (int index) { + setState(() { + _selectedIndex = _mobileScreenIndices[index]; + }); + }, + backgroundColor: theme.surface, + selectedItemColor: theme.accent, + unselectedItemColor: theme.textSecondary, + type: BottomNavigationBarType.fixed, + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.sync_alt_outlined), + activeIcon: Icon(Icons.sync_alt), + label: 'Convert', ), - Expanded( - child: IndexedStack( - index: _selectedIndex, - children: [ - HomeScreen(theme: theme, controller: _homeController), - PatcherScreen(theme: theme), - ToolsScreen(theme: theme), - DreamcastScreen(theme: theme), - settingsWidget, - AboutScreen(theme: theme), - ThemeEditorScreen( - theme: theme, - themeService: widget.themeService, - ), - ], - ), + BottomNavigationBarItem( + icon: Icon(Icons.healing_outlined), + activeIcon: Icon(Icons.healing), + label: 'Patch', + ), + BottomNavigationBarItem( + icon: Icon(Icons.construction_outlined), + activeIcon: Icon(Icons.construction), + label: 'Tools', + ), + BottomNavigationBarItem( + icon: Icon(Icons.settings_outlined), + activeIcon: Icon(Icons.settings), + label: 'Settings', ), ], ), + ); + } + + return Scaffold( + backgroundColor: theme.background, + body: Row( + children: [ + Sidebar( + selectedIndex: _selectedIndex, + onDestinationSelected: (index) { + setState(() => _selectedIndex = index); + }, + theme: theme, + ), + Expanded( + child: Column( + children: [ + TopBar( + theme: theme, + fileCount: fileCount, + isConverting: isConverting, + onConvertPressed: _onConvertPressed, + onAddFilesPressed: _onAddFilesPressed, + ), + Expanded( + child: IndexedStack( + index: _selectedIndex, + children: screens, + ), + ), + ], + ), + ), + ], ), - ], - ), + ); + }, ), ); } diff --git a/gui/lib/screens/settings_screen.dart b/gui/lib/screens/settings_screen.dart index fc32f7f..9ba1be6 100644 --- a/gui/lib/screens/settings_screen.dart +++ b/gui/lib/screens/settings_screen.dart @@ -8,6 +8,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart'; import '../l10n/app_localizations.dart'; +import "../utils/app_version.dart"; import '../models/theme_config.dart'; import '../providers/locale_provider.dart'; import '../services/theme_service.dart'; @@ -92,7 +93,7 @@ class _SettingsScreenState extends State { _autoUpdateEnabled = prefs.getBool(OnlinePrefs.autoUpdateEnabled) ?? false; - _currentVersion = readAppVersion(); + _currentVersion = AppVersion.current; _raEnabled = prefs.getBool(OnlinePrefs.raEnabled) ?? false; _raUsername = prefs.getString(OnlinePrefs.raUsername) ?? ''; @@ -203,6 +204,7 @@ class _SettingsScreenState extends State { return SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( + mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( @@ -433,7 +435,7 @@ class _SettingsScreenState extends State { physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, - childAspectRatio: 2.5, + childAspectRatio: 2.2, crossAxisSpacing: 16, mainAxisSpacing: 16, ), @@ -454,6 +456,7 @@ class _SettingsScreenState extends State { borderRadius: BorderRadius.circular(t.borderRadius), ), child: Column( + mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -600,6 +603,7 @@ class _SettingsScreenState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Column( + mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ TextFormField( diff --git a/gui/lib/services/core_bridge.dart b/gui/lib/services/core_bridge.dart index 931efff..2b417d4 100644 --- a/gui/lib/services/core_bridge.dart +++ b/gui/lib/services/core_bridge.dart @@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart'; import '../models/errors.dart'; import '../models/conversion_job.dart'; import '../models/rom_file.dart'; +import '../utils/app_version.dart'; typedef ProgressCallback = void Function(double percent); typedef LogCallback = void Function(String log); @@ -559,9 +560,10 @@ class CoreBridge { } static Future getVersion() async { + final fallback = 'OpenROM v${AppVersion.current}'; try { if (!await coreExists()) { - return 'OpenROM v2.2.0'; + return fallback; } final corePath = await getCoreExecutablePath(); final List args = []; @@ -572,9 +574,9 @@ class CoreBridge { } final result = await Process.run(corePath, args); final out = result.stdout.toString().trim(); - return out.isNotEmpty ? out : 'OpenROM v2.2.0'; + return out.isNotEmpty ? out : fallback; } catch (_) { - return 'OpenROM v2.2.0'; + return fallback; } } } diff --git a/gui/lib/services/theme_service.dart b/gui/lib/services/theme_service.dart index 55694ea..a776ff2 100644 --- a/gui/lib/services/theme_service.dart +++ b/gui/lib/services/theme_service.dart @@ -37,6 +37,7 @@ class ThemeService extends ChangeNotifier { 'themes/cyberpunk.json', 'themes/terminal.json', 'themes/minimal.json', + 'themes/retroarch.json', ]; _availableThemes = []; diff --git a/gui/lib/utils/app_version.dart b/gui/lib/utils/app_version.dart new file mode 100644 index 0000000..3ab0c46 --- /dev/null +++ b/gui/lib/utils/app_version.dart @@ -0,0 +1,28 @@ +// OpenROM — Universal ROM Compression Suite +// M5 Dev | GPL v3 + +import 'package:openrom_core/openrom_core.dart'; + +class AppVersion { + /// Default fallback version matching current release tag. + static const String fallback = '3.9.0'; + + /// Returns the resolved app version. + /// Priority: + /// 1. Compile-time environment variable (--dart-define=APP_VERSION=x.y.z) + /// 2. Version file read via `readAppVersion()` from `openrom_core` + /// 3. Fallback string ('3.9.0') + static String get current { + const definedVersion = String.fromEnvironment('APP_VERSION'); + if (definedVersion.isNotEmpty) { + return definedVersion; + } + + final readVer = readAppVersion(); + if (readVer.isNotEmpty && readVer != 'unknown') { + return readVer; + } + + return fallback; + } +} diff --git a/gui/lib/widgets/top_bar.dart b/gui/lib/widgets/top_bar.dart index a938b72..215782b 100644 --- a/gui/lib/widgets/top_bar.dart +++ b/gui/lib/widgets/top_bar.dart @@ -37,18 +37,18 @@ class TopBar extends StatelessWidget { return Container( height: 60, - padding: const EdgeInsets.symmetric(horizontal: 20), + padding: const EdgeInsets.symmetric(horizontal: 16), color: theme.surface, child: Row( children: [ Icon(Icons.hexagon_outlined, color: theme.accent, size: 28), - const SizedBox(width: 10), + const SizedBox(width: 8), Text( l10n.appTitle, style: TextStyle( fontFamily: theme.fontFamily, color: theme.textPrimary, - fontSize: 20, + fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 1.1, ), @@ -59,20 +59,22 @@ class TopBar extends StatelessWidget { icon: const Icon(Icons.add_circle_outline, size: 18), label: Text(l10n.browse), style: ElevatedButton.styleFrom( + minimumSize: const Size(0, 48), backgroundColor: theme.surface, foregroundColor: theme.textPrimary, side: BorderSide(color: theme.accent.withValues(alpha: 0.5)), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(theme.borderRadius), ), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), ), ), - const SizedBox(width: 12), + const SizedBox(width: 8), ElevatedButton( onPressed: (fileCount > 0 && !isConverting) ? onConvertPressed : null, style: ElevatedButton.styleFrom( + minimumSize: const Size(0, 48), backgroundColor: theme.accent, foregroundColor: Colors.white, elevation: isConverting ? 0 : 6, @@ -80,7 +82,7 @@ class TopBar extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(theme.borderRadius), ), - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), child: Text( convertText, diff --git a/gui/pubspec.lock b/gui/pubspec.lock index ce6ead6..cb33dc4 100644 --- a/gui/pubspec.lock +++ b/gui/pubspec.lock @@ -331,7 +331,7 @@ packages: path: "../core" relative: true source: path - version: "3.7.0" + version: "3.9.0" package_config: dependency: transitive description: diff --git a/gui/pubspec.yaml b/gui/pubspec.yaml index 9bdb899..4272820 100644 --- a/gui/pubspec.yaml +++ b/gui/pubspec.yaml @@ -1,7 +1,7 @@ name: gui description: OpenROM — Universal ROM Compression Suite Desktop App publish_to: 'none' -version: 3.7.0+1 +version: 3.9.0+1 environment: sdk: '>=3.0.0 <4.0.0' diff --git a/gui/test/app_version_test.dart b/gui/test/app_version_test.dart new file mode 100644 index 0000000..8f4a5bf --- /dev/null +++ b/gui/test/app_version_test.dart @@ -0,0 +1,15 @@ +// OpenROM — AppVersion Unit Tests +// M5 Dev | GPL v3 + +import 'package:flutter_test/flutter_test.dart'; +import 'package:gui/utils/app_version.dart'; + +void main() { + group('AppVersion Tests', () { + test('AppVersion.current returns dynamic current version string', () { + final version = AppVersion.current; + expect(version, isNotEmpty); + expect(version, equals('3.9.0')); + }); + }); +} diff --git a/gui/test/responsive_layout_test.dart b/gui/test/responsive_layout_test.dart new file mode 100644 index 0000000..a906def --- /dev/null +++ b/gui/test/responsive_layout_test.dart @@ -0,0 +1,34 @@ +// OpenROM — Responsive Layout Widget Tests +// M5 Dev | GPL v3 + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:gui/app.dart'; +import 'package:gui/services/theme_service.dart'; + +void main() { + testWidgets('Renders BottomNavigationBar on narrow screen (< 650px)', + (tester) async { + tester.view.physicalSize = const Size(500, 900); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + + final themeService = ThemeService(); + await tester.pumpWidget(OpenROMApp(themeService: themeService)); + await tester.pumpAndSettle(); + + expect(find.byType(BottomNavigationBar), findsOneWidget); + expect(find.text('Convert'), findsOneWidget); + expect(find.text('Patch'), findsOneWidget); + expect(find.text('Tools'), findsOneWidget); + expect(find.text('Settings'), findsOneWidget); + + // Tap Patch tab with warnIfMissed false for BottomNavigationBar item taps + await tester.tap(find.text('Patch'), warnIfMissed: false); + await tester.pumpAndSettle(); + + // Tap Settings tab + await tester.tap(find.text('Settings'), warnIfMissed: false); + await tester.pumpAndSettle(); + }); +} diff --git a/gui/themes/retroarch.json b/gui/themes/retroarch.json new file mode 100644 index 0000000..a0751e4 --- /dev/null +++ b/gui/themes/retroarch.json @@ -0,0 +1,15 @@ +{ + "name": "RetroArch Ozone", + "background": "#16161e", + "surface": "#20202c", + "accent": "#00e5ff", + "text_primary": "#ffffff", + "text_secondary": "#a0a0b0", + "sidebar_bg": "#12121a", + "card_bg": "#20202c", + "terminal_bg": "#101018", + "terminal_text": "#76ff03", + "font_family": "Roboto", + "border_radius": 8, + "layout": "sidebar_left" +}