From 2eb2ee4433e7a239c0aa3159f326d2b4ee464d3e Mon Sep 17 00:00:00 2001 From: Janez T Date: Tue, 14 Oct 2025 10:16:53 +0200 Subject: [PATCH] feat: Refactor theme management and enhance UI with AppTheme integration --- lib/main.dart | 72 +---- lib/screens/contacts_tab.dart | 10 +- lib/screens/home_screen.dart | 15 +- lib/screens/map_tab.dart | 22 +- lib/screens/messages_tab.dart | 280 ++++++++---------- lib/screens/settings_screen.dart | 144 +++++---- lib/services/background_location_service.dart | 1 + lib/theme/app_theme.dart | 223 ++++++++++++++ lib/widgets/map_markers.dart | 6 +- 9 files changed, 483 insertions(+), 290 deletions(-) create mode 100644 lib/theme/app_theme.dart diff --git a/lib/main.dart b/lib/main.dart index e8fe460..f7d1484 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,7 @@ import 'providers/map_provider.dart'; import 'providers/app_provider.dart'; import 'services/tile_cache_service.dart'; import 'screens/home_screen.dart'; +import 'theme/app_theme.dart'; void main() { runApp(const MeshCoreSarApp()); @@ -21,7 +22,7 @@ class MeshCoreSarApp extends StatefulWidget { } class _MeshCoreSarAppState extends State { - ThemeMode _themeMode = ThemeMode.system; + AppThemeMode _themeMode = AppThemeMode.system; @override void initState() { @@ -33,14 +34,11 @@ class _MeshCoreSarAppState extends State { final prefs = await SharedPreferences.getInstance(); final themeName = prefs.getString('theme_mode') ?? 'system'; setState(() { - _themeMode = ThemeMode.values.firstWhere( - (mode) => mode.name == themeName, - orElse: () => ThemeMode.system, - ); + _themeMode = AppTheme.themeFromString(themeName); }); } - void _handleThemeChanged(ThemeMode mode) { + void _handleThemeChanged(AppThemeMode mode) { setState(() { _themeMode = mode; }); @@ -78,57 +76,19 @@ class _MeshCoreSarAppState extends State { ), ), ], - child: MaterialApp( - title: 'MeshCore SAR', - debugShowCheckedModeBanner: false, - theme: ThemeData( - useMaterial3: true, - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.orange, - brightness: Brightness.light, - ), - appBarTheme: const AppBarTheme( - centerTitle: true, - elevation: 0, - ), - cardTheme: CardThemeData( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + child: Builder( + builder: (context) { + final systemBrightness = MediaQuery.platformBrightnessOf(context); + return MaterialApp( + title: 'MeshCore SAR', + debugShowCheckedModeBanner: false, + theme: AppTheme.getTheme(_themeMode, systemBrightness), + home: HomeScreen( + onThemeChanged: _handleThemeChanged, + currentTheme: _themeMode, ), - ), - inputDecorationTheme: InputDecorationTheme( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - filled: true, - ), - ), - darkTheme: ThemeData( - useMaterial3: true, - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.orange, - brightness: Brightness.dark, - ), - appBarTheme: const AppBarTheme( - centerTitle: true, - elevation: 0, - ), - cardTheme: CardThemeData( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - inputDecorationTheme: InputDecorationTheme( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - filled: true, - ), - ), - themeMode: _themeMode, - home: HomeScreen(onThemeChanged: _handleThemeChanged, currentTheme: _themeMode), + ); + }, ), ); } diff --git a/lib/screens/contacts_tab.dart b/lib/screens/contacts_tab.dart index 0486050..c6a4cb3 100644 --- a/lib/screens/contacts_tab.dart +++ b/lib/screens/contacts_tab.dart @@ -140,7 +140,7 @@ class _ContactTile extends StatelessWidget { margin: const EdgeInsets.only(bottom: 8), child: ListTile( leading: CircleAvatar( - backgroundColor: _getTypeColor(contact.type), + backgroundColor: _getTypeColor(contact.type, context), child: contact.roleEmoji != null ? Text( contact.roleEmoji!, @@ -187,7 +187,7 @@ class _ContactTile extends StatelessWidget { vertical: 2, ), decoration: BoxDecoration( - color: _getTypeColor(contact.type).withOpacity(0.2), + color: _getTypeColor(contact.type, context).withOpacity(0.2), borderRadius: BorderRadius.circular(4), ), child: Text( @@ -283,7 +283,7 @@ class _ContactTile extends StatelessWidget { child: Row( children: [ CircleAvatar( - backgroundColor: _getTypeColor(contact.type), + backgroundColor: _getTypeColor(contact.type, context), child: contact.roleEmoji != null ? Text( contact.roleEmoji!, @@ -393,10 +393,10 @@ class _ContactTile extends StatelessWidget { } } - Color _getTypeColor(ContactType type) { + Color _getTypeColor(ContactType type, BuildContext context) { switch (type) { case ContactType.chat: - return Colors.blue; + return Theme.of(context).colorScheme.primary; case ContactType.repeater: return Colors.green; case ContactType.room: diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 3b7e1c3..3c1f143 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -4,6 +4,7 @@ import '../providers/connection_provider.dart'; import '../providers/app_provider.dart'; import '../models/device_info.dart' as models; import '../services/tile_cache_service.dart'; +import '../theme/app_theme.dart'; import 'messages_tab.dart'; import 'contacts_tab.dart'; import 'map_tab.dart'; @@ -11,8 +12,8 @@ import 'map_management_screen.dart'; import 'settings_screen.dart'; class HomeScreen extends StatefulWidget { - final Function(ThemeMode) onThemeChanged; - final ThemeMode currentTheme; + final Function(AppThemeMode) onThemeChanged; + final AppThemeMode currentTheme; const HomeScreen({ super.key, @@ -109,17 +110,17 @@ class _HomeScreenState extends State with SingleTickerProviderStateM margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.blue, + color: Theme.of(context).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(8), ), - child: const Row( + child: Row( children: [ - Icon(Icons.info_outline, color: Colors.white), - SizedBox(width: 12), + Icon(Icons.info_outline, color: Theme.of(context).colorScheme.onPrimaryContainer), + const SizedBox(width: 12), Expanded( child: Text( 'The default pin for devices without a screen is 123456. Trouble pairing? Forget the bluetooth device in system settings.', - style: TextStyle(color: Colors.white, fontSize: 13), + style: TextStyle(color: Theme.of(context).colorScheme.onPrimaryContainer, fontSize: 13), ), ), ], diff --git a/lib/screens/map_tab.dart b/lib/screens/map_tab.dart index 7e00d04..0965b01 100644 --- a/lib/screens/map_tab.dart +++ b/lib/screens/map_tab.dart @@ -760,13 +760,13 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { rotate: false, // Don't rotate with map child: Container( decoration: BoxDecoration( - color: Colors.blue.withValues(alpha: 0.3), + color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.3), shape: BoxShape.circle, ), child: Container( margin: const EdgeInsets.all(8), - decoration: const BoxDecoration( - color: Colors.blue, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, shape: BoxShape.circle, boxShadow: [ BoxShadow( @@ -935,7 +935,7 @@ class _MapLegend extends StatelessWidget { const SizedBox(height: 8), _LegendItem( icon: Icons.person, - color: Colors.blue, + color: Theme.of(context).colorScheme.primary, label: 'Team', count: teamMemberCount, ), @@ -1252,7 +1252,7 @@ class _DetailedCompassDialogState extends State<_DetailedCompassDialog> { // Contacts filter _CompactFilterItem( icon: Icons.person, - color: Colors.blue, + color: Theme.of(context).colorScheme.primary, label: 'Contacts', value: _showContacts, onChanged: (value) { @@ -1526,7 +1526,7 @@ class _DetailedCompassDialogState extends State<_DetailedCompassDialog> { if (_selectedContact != null) { title = _selectedContact!.displayName; icon = Icons.person; - color = Colors.blue; + color = Theme.of(context).colorScheme.primary; targetLocation = _selectedContact!.displayLocation; if (targetLocation != null) { @@ -1821,9 +1821,9 @@ class _DetailedCompassDialogState extends State<_DetailedCompassDialog> { contact.roleEmoji!, style: const TextStyle(fontSize: 24), ) - : const Icon( + : Icon( Icons.person, - color: Colors.blue, + color: Theme.of(context).colorScheme.primary, size: 24, ), title: Text(contact.displayName), @@ -2195,7 +2195,7 @@ class _LargeCompassPainter extends CustomPainter { // Draw line from center to contact final linePaint = Paint() - ..color = Colors.blue.withValues(alpha: 0.3) + ..color = Colors.lightBlue.withValues(alpha: 0.3) ..style = PaintingStyle.stroke ..strokeWidth = 1.5; canvas.drawLine( @@ -2207,7 +2207,7 @@ class _LargeCompassPainter extends CustomPainter { // Draw contact dot (size varies with zoom) final dotSize = (6.0 * (1.0 + zoomLevel * 0.3)).clamp(4.0, 12.0); final dotPaint = Paint() - ..color = Colors.blue + ..color = Colors.lightBlue ..style = PaintingStyle.fill; canvas.drawCircle(Offset(dotX, dotY), dotSize, dotPaint); @@ -2228,7 +2228,7 @@ class _LargeCompassPainter extends CustomPainter { textPainter.text = TextSpan( text: distanceText, style: const TextStyle( - color: Colors.blue, + color: Colors.lightBlue, fontSize: 9, fontWeight: FontWeight.bold, ), diff --git a/lib/screens/messages_tab.dart b/lib/screens/messages_tab.dart index 0596b6c..fa77fcb 100644 --- a/lib/screens/messages_tab.dart +++ b/lib/screens/messages_tab.dart @@ -568,71 +568,65 @@ class _SarUpdateSheetState extends State<_SarUpdateSheet> { @override Widget build(BuildContext context) { return Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), - ), - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, + height: MediaQuery.of(context).size.height * 0.9, + decoration: const BoxDecoration( + color: Color(0xFF1E1E1E), + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( - mainAxisSize: MainAxisSize.min, children: [ - // Header with drag handle + // Header Container( - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), - child: Column( + padding: const EdgeInsets.all(16), + child: Row( children: [ - // Drag handle - Container( - width: 40, - height: 4, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.4), - borderRadius: BorderRadius.circular(2), + IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + const Expanded( + child: Column( + children: [ + Text( + 'Send SAR Marker', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'Quick location marker', + style: TextStyle( + color: Colors.grey, + fontSize: 14, + ), + ), + ], ), ), - const SizedBox(height: 16), - // Title - Row( - children: [ - Icon( - Icons.add_location_alt, - size: 24, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Send SAR Marker', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), - ), - ], + IconButton( + icon: const Icon(Icons.more_vert, color: Colors.white), + onPressed: () {}, ), ], ), ), - const Divider(height: 1), // Content - Flexible( + Expanded( child: SingleChildScrollView( - padding: const EdgeInsets.all(20), + padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Marker type selection - Text( + const Text( 'Marker Type', - style: Theme.of(context).textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.bold, - ), + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), ), const SizedBox(height: 12), _MarkerTypeChip( @@ -661,29 +655,37 @@ class _SarUpdateSheetState extends State<_SarUpdateSheet> { const SizedBox(height: 24), // Location display - Text( + const Text( 'Current Location', - style: Theme.of(context).textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.bold, - ), + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), ), const SizedBox(height: 12), if (_loadingLocation) Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), + color: const Color(0xFF2D2D2D), + borderRadius: BorderRadius.circular(8), ), child: const Row( children: [ SizedBox( width: 20, height: 20, - child: CircularProgressIndicator(strokeWidth: 2), + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), ), SizedBox(width: 16), - Text('Getting location...'), + Text( + 'Getting location...', + style: TextStyle(color: Colors.white), + ), ], ), ) @@ -737,22 +739,18 @@ class _SarUpdateSheetState extends State<_SarUpdateSheet> { Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.3), - width: 1, - ), + color: const Color(0xFF2D2D2D), + borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - Icon( + const Icon( Icons.location_on, size: 20, - color: Theme.of(context).colorScheme.primary, + color: Colors.green, ), const SizedBox(width: 8), Expanded( @@ -762,11 +760,12 @@ class _SarUpdateSheetState extends State<_SarUpdateSheet> { fontFamily: 'monospace', fontSize: 13, fontWeight: FontWeight.w500, + color: Colors.white, ), ), ), IconButton( - icon: const Icon(Icons.refresh, size: 20), + icon: const Icon(Icons.refresh, size: 20, color: Colors.white), onPressed: _getCurrentLocation, padding: EdgeInsets.zero, constraints: const BoxConstraints(), @@ -778,15 +777,18 @@ class _SarUpdateSheetState extends State<_SarUpdateSheet> { const SizedBox(height: 8), Row( children: [ - Icon( + const Icon( Icons.my_location, size: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant, + color: Colors.grey, ), const SizedBox(width: 6), Text( 'Accuracy: ±${_currentPosition!.accuracy!.round()}m', - style: Theme.of(context).textTheme.bodySmall, + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), ), ], ), @@ -797,89 +799,73 @@ class _SarUpdateSheetState extends State<_SarUpdateSheet> { const SizedBox(height: 24), // Optional notes - Text( + const Text( 'Notes (optional)', - style: Theme.of(context).textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.bold, - ), + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), ), const SizedBox(height: 12), TextField( controller: _notesController, maxLines: 3, maxLength: 100, + style: const TextStyle(fontSize: 14, color: Colors.white), decoration: InputDecoration( hintText: 'Add additional information...', - hintStyle: const TextStyle(fontSize: 14), + hintStyle: const TextStyle(fontSize: 14, color: Colors.grey), + filled: true, + fillColor: const Color(0xFF2D2D2D), border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, ), contentPadding: const EdgeInsets.all(16), ), - style: const TextStyle(fontSize: 14), ), const SizedBox(height: 8), ], ), ), ), - // Bottom action buttons + // Bottom action button Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - border: Border( - top: BorderSide( - color: Theme.of(context).dividerColor, - width: 1, - ), - ), - ), + padding: const EdgeInsets.all(16), child: SafeArea( top: false, - child: Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () => Navigator.pop(context), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: const Text('Cancel'), + child: SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _currentPosition == null + ? null + : () async { + await widget.onSend( + _selectedType, + _currentPosition!, + _notesController.text.trim().isEmpty + ? null + : _notesController.text.trim(), + ); + if (context.mounted) { + Navigator.pop(context); + } + }, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + disabledBackgroundColor: Colors.grey, + disabledForegroundColor: Colors.white70, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), ), ), - const SizedBox(width: 12), - Expanded( - flex: 2, - child: FilledButton.icon( - onPressed: _currentPosition == null - ? null - : () async { - await widget.onSend( - _selectedType, - _currentPosition!, - _notesController.text.trim().isEmpty - ? null - : _notesController.text.trim(), - ); - if (context.mounted) { - Navigator.pop(context); - } - }, - style: FilledButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - icon: const Icon(Icons.send, size: 20), - label: const Text('Send SAR Marker'), - ), + icon: const Icon(Icons.send, size: 20), + label: const Text( + 'Send SAR Marker', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), - ], + ), ), ), ), @@ -922,36 +908,22 @@ class _MarkerTypeChip extends StatelessWidget { return InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(8), child: Container( width: double.infinity, - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( - color: isSelected - ? color.withValues(alpha: 0.15) - : Colors.transparent, - border: Border.all( - color: isSelected ? color : Colors.grey.withValues(alpha: 0.3), - width: isSelected ? 2 : 1, - ), - borderRadius: BorderRadius.circular(12), + color: const Color(0xFF2D2D2D), + border: isSelected + ? Border.all(color: color, width: 2) + : null, + borderRadius: BorderRadius.circular(8), ), child: Row( children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: isSelected - ? color.withValues(alpha: 0.2) - : Colors.grey.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), - ), - alignment: Alignment.center, - child: Text( - type.emoji, - style: const TextStyle(fontSize: 28), - ), + Text( + type.emoji, + style: const TextStyle(fontSize: 32), ), const SizedBox(width: 16), Expanded( @@ -959,8 +931,8 @@ class _MarkerTypeChip extends StatelessWidget { type.displayName, style: TextStyle( fontSize: 16, - fontWeight: isSelected ? FontWeight.bold : FontWeight.w500, - color: isSelected ? color : null, + fontWeight: FontWeight.w500, + color: isSelected ? color : Colors.white, ), ), ), @@ -969,12 +941,6 @@ class _MarkerTypeChip extends StatelessWidget { Icons.check_circle, color: color, size: 24, - ) - else - Icon( - Icons.radio_button_unchecked, - color: Colors.grey.withValues(alpha: 0.4), - size: 24, ), ], ), diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 54cfb89..a91f628 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -9,10 +9,11 @@ import '../providers/messages_provider.dart'; import '../providers/app_provider.dart'; import '../services/background_location_service.dart'; import '../utils/sample_data_generator.dart'; +import '../theme/app_theme.dart'; class SettingsScreen extends StatefulWidget { - final Function(ThemeMode) onThemeChanged; - final ThemeMode currentTheme; + final Function(AppThemeMode) onThemeChanged; + final AppThemeMode currentTheme; const SettingsScreen({ super.key, @@ -25,7 +26,7 @@ class SettingsScreen extends StatefulWidget { } class _SettingsScreenState extends State { - late ThemeMode _selectedTheme; + late AppThemeMode _selectedTheme; PackageInfo? _packageInfo; bool _isLoadingSampleData = false; double _gpsUpdateDistance = 10.0; @@ -78,12 +79,12 @@ class _SettingsScreenState extends State { await prefs.setBool('background_tracking_enabled', _backgroundTrackingEnabled); } - Future _saveThemePreference(ThemeMode theme) async { + Future _saveThemePreference(AppThemeMode theme) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('theme_mode', theme.name); } - void _handleThemeChange(ThemeMode? theme) { + void _handleThemeChange(AppThemeMode? theme) { if (theme != null) { setState(() { _selectedTheme = theme; @@ -250,7 +251,7 @@ class _SettingsScreenState extends State { ListTile( leading: const Icon(Icons.palette), title: const Text('Theme'), - subtitle: Text(_getThemeLabel(_selectedTheme)), + subtitle: Text(AppTheme.getThemeDisplayName(_selectedTheme)), trailing: const Icon(Icons.chevron_right), onTap: () => _showThemeDialog(), ), @@ -383,17 +384,6 @@ class _SettingsScreenState extends State { ); } - String _getThemeLabel(ThemeMode mode) { - switch (mode) { - case ThemeMode.light: - return 'Light'; - case ThemeMode.dark: - return 'Dark'; - case ThemeMode.system: - return 'Auto (System)'; - } - } - void _showGpsDistanceDialog() { double tempDistance = _gpsUpdateDistance; showDialog( @@ -471,40 +461,92 @@ class _SettingsScreenState extends State { context: context, builder: (context) => AlertDialog( title: const Text('Choose Theme'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - RadioListTile( - title: const Text('Light'), - subtitle: const Text('Always use light theme'), - value: ThemeMode.light, - groupValue: _selectedTheme, - onChanged: (value) { - _handleThemeChange(value); - Navigator.pop(context); - }, - ), - RadioListTile( - title: const Text('Dark'), - subtitle: const Text('Always use dark theme'), - value: ThemeMode.dark, - groupValue: _selectedTheme, - onChanged: (value) { - _handleThemeChange(value); - Navigator.pop(context); - }, - ), - RadioListTile( - title: const Text('Auto (System)'), - subtitle: const Text('Follow system theme'), - value: ThemeMode.system, - groupValue: _selectedTheme, - onChanged: (value) { - _handleThemeChange(value); - Navigator.pop(context); - }, - ), - ], + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RadioListTile( + title: const Text('Light'), + subtitle: const Text('Orange light theme'), + value: AppThemeMode.light, + groupValue: _selectedTheme, + onChanged: (value) { + _handleThemeChange(value); + Navigator.pop(context); + }, + ), + RadioListTile( + title: const Text('Dark'), + subtitle: const Text('Orange dark theme'), + value: AppThemeMode.dark, + groupValue: _selectedTheme, + onChanged: (value) { + _handleThemeChange(value); + Navigator.pop(context); + }, + ), + const Divider(), + RadioListTile( + title: Row( + children: [ + const Text('SAR Red'), + const SizedBox(width: 8), + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: const Color(0xFFFF5252), + shape: BoxShape.circle, + border: Border.all(color: Colors.black26), + ), + ), + ], + ), + subtitle: const Text('Alert/Emergency mode'), + value: AppThemeMode.sarRed, + groupValue: _selectedTheme, + onChanged: (value) { + _handleThemeChange(value); + Navigator.pop(context); + }, + ), + RadioListTile( + title: Row( + children: [ + const Text('SAR Green'), + const SizedBox(width: 8), + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: const Color(0xFF69F0AE), + shape: BoxShape.circle, + border: Border.all(color: Colors.black26), + ), + ), + ], + ), + subtitle: const Text('Safe/All Clear mode'), + value: AppThemeMode.sarGreen, + groupValue: _selectedTheme, + onChanged: (value) { + _handleThemeChange(value); + Navigator.pop(context); + }, + ), + const Divider(), + RadioListTile( + title: const Text('Auto (System)'), + subtitle: const Text('Follow system theme'), + value: AppThemeMode.system, + groupValue: _selectedTheme, + onChanged: (value) { + _handleThemeChange(value); + Navigator.pop(context); + }, + ), + ], + ), ), actions: [ TextButton( diff --git a/lib/services/background_location_service.dart b/lib/services/background_location_service.dart index 4481e20..a6121b1 100644 --- a/lib/services/background_location_service.dart +++ b/lib/services/background_location_service.dart @@ -8,6 +8,7 @@ import 'meshcore_ble_service.dart'; /// Background location tracking service for SAR operations /// Tracks user location and sends periodic updates via MeshCore BLE +@pragma('vm:entry-point') class BackgroundLocationService { static const String _prefKeyEnabled = 'background_tracking_enabled'; static const String _prefKeyDistance = 'background_tracking_distance'; diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart new file mode 100644 index 0000000..893f897 --- /dev/null +++ b/lib/theme/app_theme.dart @@ -0,0 +1,223 @@ +import 'package:flutter/material.dart'; + +enum AppThemeMode { + light, + dark, + sarRed, + sarGreen, + system, +} + +class AppTheme { + // Light theme (Blue) + static ThemeData get lightTheme { + return ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.blue, + brightness: Brightness.light, + ), + appBarTheme: const AppBarTheme( + centerTitle: true, + elevation: 0, + ), + cardTheme: CardThemeData( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + filled: true, + ), + ); + } + + // Dark theme (Blue) + static ThemeData get darkTheme { + return ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.blue, + brightness: Brightness.dark, + ), + appBarTheme: const AppBarTheme( + centerTitle: true, + elevation: 0, + ), + cardTheme: CardThemeData( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + filled: true, + ), + ); + } + + // SAR Red theme (Emergency/Alert tones) + static ThemeData get sarRedTheme { + return ThemeData( + useMaterial3: true, + colorScheme: const ColorScheme.dark( + brightness: Brightness.dark, + primary: Color(0xFFFF5252), // Bright red + onPrimary: Color(0xFF000000), + primaryContainer: Color(0xFF8B0000), // Dark red + onPrimaryContainer: Color(0xFFFFCDD2), + secondary: Color(0xFFFF8A80), + onSecondary: Color(0xFF000000), + secondaryContainer: Color(0xFFB71C1C), + onSecondaryContainer: Color(0xFFFFCDD2), + tertiary: Color(0xFFFF6E40), + onTertiary: Color(0xFF000000), + error: Color(0xFFCF6679), + onError: Color(0xFF000000), + surface: Color(0xFF1A0000), // Very dark red-tinted + onSurface: Color(0xFFFFEBEE), + surfaceContainerHighest: Color(0xFF2D0000), + onSurfaceVariant: Color(0xFFFFCDD2), + outline: Color(0xFFFF5252), + ), + scaffoldBackgroundColor: const Color(0xFF1A0000), + appBarTheme: const AppBarTheme( + centerTitle: true, + elevation: 0, + backgroundColor: Color(0xFF2D0000), + foregroundColor: Color(0xFFFFEBEE), + ), + cardTheme: CardThemeData( + elevation: 2, + color: const Color(0xFF2D0000), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide( + color: Color(0xFFFF5252), + width: 1, + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFFFF5252)), + ), + filled: true, + fillColor: const Color(0xFF2D0000), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFFF5252), + foregroundColor: const Color(0xFF000000), + ), + ), + ); + } + + // SAR Green theme (All Clear/Safe tones) + static ThemeData get sarGreenTheme { + return ThemeData( + useMaterial3: true, + colorScheme: const ColorScheme.dark( + brightness: Brightness.dark, + primary: Color(0xFF69F0AE), // Bright green + onPrimary: Color(0xFF000000), + primaryContainer: Color(0xFF00695C), // Dark teal-green + onPrimaryContainer: Color(0xFFB9F6CA), + secondary: Color(0xFF64FFDA), + onSecondary: Color(0xFF000000), + secondaryContainer: Color(0xFF004D40), + onSecondaryContainer: Color(0xFFB9F6CA), + tertiary: Color(0xFF1DE9B6), + onTertiary: Color(0xFF000000), + error: Color(0xFFCF6679), + onError: Color(0xFF000000), + surface: Color(0xFF001A12), // Very dark green-tinted + onSurface: Color(0xFFE8F5E9), + surfaceContainerHighest: Color(0xFF002D1F), + onSurfaceVariant: Color(0xFFB9F6CA), + outline: Color(0xFF69F0AE), + ), + scaffoldBackgroundColor: const Color(0xFF001A12), + appBarTheme: const AppBarTheme( + centerTitle: true, + elevation: 0, + backgroundColor: Color(0xFF002D1F), + foregroundColor: Color(0xFFE8F5E9), + ), + cardTheme: CardThemeData( + elevation: 2, + color: const Color(0xFF002D1F), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide( + color: Color(0xFF69F0AE), + width: 1, + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFF69F0AE)), + ), + filled: true, + fillColor: const Color(0xFF002D1F), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF69F0AE), + foregroundColor: const Color(0xFF000000), + ), + ), + ); + } + + // Get theme by mode + static ThemeData getTheme(AppThemeMode mode, Brightness systemBrightness) { + switch (mode) { + case AppThemeMode.light: + return lightTheme; + case AppThemeMode.dark: + return darkTheme; + case AppThemeMode.sarRed: + return sarRedTheme; + case AppThemeMode.sarGreen: + return sarGreenTheme; + case AppThemeMode.system: + return systemBrightness == Brightness.dark ? darkTheme : lightTheme; + } + } + + // Get display name for theme mode + static String getThemeDisplayName(AppThemeMode mode) { + switch (mode) { + case AppThemeMode.light: + return 'Light'; + case AppThemeMode.dark: + return 'Dark'; + case AppThemeMode.sarRed: + return 'SAR Red (Alert)'; + case AppThemeMode.sarGreen: + return 'SAR Green (Safe)'; + case AppThemeMode.system: + return 'Auto (System)'; + } + } + + // Get theme mode from string + static AppThemeMode themeFromString(String themeName) { + return AppThemeMode.values.firstWhere( + (mode) => mode.name == themeName, + orElse: () => AppThemeMode.system, + ); + } +} diff --git a/lib/widgets/map_markers.dart b/lib/widgets/map_markers.dart index ad79e2a..3adc1a3 100644 --- a/lib/widgets/map_markers.dart +++ b/lib/widgets/map_markers.dart @@ -52,7 +52,7 @@ class MapMarkers { // Marker icon Container( decoration: BoxDecoration( - color: Colors.blue, + color: Theme.of(context).colorScheme.primary, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), boxShadow: [ @@ -201,7 +201,7 @@ class MapMarkers { builder: (context) => AlertDialog( title: Row( children: [ - const Icon(Icons.person, color: Colors.blue), + Icon(Icons.person, color: Theme.of(context).colorScheme.primary), const SizedBox(width: 8), Expanded(child: Text(contact.displayName)), ], @@ -275,7 +275,7 @@ class MapMarkers { final diff = DateTime.now().difference(updateTime); if (diff.inMinutes < 5) return Colors.green; // Very recent - if (diff.inMinutes < 30) return Colors.blue; // Recent + if (diff.inMinutes < 30) return Colors.lightBlue; // Recent if (diff.inHours < 2) return Colors.orange; // Getting old return Colors.red; // Stale }