diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index b4986bd..3818689 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -489,7 +489,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 18; + CURRENT_PROJECT_VERSION = 19; DEVELOPMENT_TEAM = JND55328G8; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -511,7 +511,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 18; + CURRENT_PROJECT_VERSION = 19; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp.RunnerTests; @@ -529,7 +529,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 18; + CURRENT_PROJECT_VERSION = 19; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp.RunnerTests; @@ -545,7 +545,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 18; + CURRENT_PROJECT_VERSION = 19; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp.RunnerTests; @@ -676,7 +676,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 18; + CURRENT_PROJECT_VERSION = 19; DEVELOPMENT_TEAM = JND55328G8; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -699,7 +699,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 18; + CURRENT_PROJECT_VERSION = 19; DEVELOPMENT_TEAM = JND55328G8; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 0e9a6e7..3614340 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 18 + 19 LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/ios/fastlane/report.xml b/ios/fastlane/report.xml index 6b73287..b078525 100644 --- a/ios/fastlane/report.xml +++ b/ios/fastlane/report.xml @@ -5,17 +5,22 @@ - + - + - + + + + + + diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6b66536..a0f10e4 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1097,6 +1097,11 @@ "description": "Filter option for contacts" }, + "repeatersFilter": "Repeaters", + "@repeatersFilter": { + "description": "Filter option for repeaters" + }, + "sarMarkers": "SAR Markers", "@sarMarkers": { "description": "SAR markers section header" diff --git a/lib/l10n/app_hr.arb b/lib/l10n/app_hr.arb index 8370aa8..46ea40e 100644 --- a/lib/l10n/app_hr.arb +++ b/lib/l10n/app_hr.arb @@ -371,6 +371,8 @@ "contactsFilter": "Kontakti", + "repeatersFilter": "Repetitori", + "sarMarkers": "SAR markeri", "foundPerson": "Pronađena osoba", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 80dfa47..a47db9f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1215,6 +1215,12 @@ abstract class AppLocalizations { /// **'Contacts'** String get contactsFilter; + /// Filter option for repeaters + /// + /// In en, this message translates to: + /// **'Repeaters'** + String get repeatersFilter; + /// SAR markers section header /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index afc1933..7640bcb 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -643,6 +643,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get contactsFilter => 'Contacts'; + @override + String get repeatersFilter => 'Repeaters'; + @override String get sarMarkers => 'SAR Markers'; diff --git a/lib/l10n/app_localizations_hr.dart b/lib/l10n/app_localizations_hr.dart index 21ac909..ded3274 100644 --- a/lib/l10n/app_localizations_hr.dart +++ b/lib/l10n/app_localizations_hr.dart @@ -643,6 +643,9 @@ class AppLocalizationsHr extends AppLocalizations { @override String get contactsFilter => 'Kontakti'; + @override + String get repeatersFilter => 'Repetitori'; + @override String get sarMarkers => 'SAR markeri'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 73b103e..92f02d6 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -643,6 +643,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get contactsFilter => 'Stiki'; + @override + String get repeatersFilter => 'Ponavljalniki'; + @override String get sarMarkers => 'SAR označevalci'; diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index f891f98..ffd185b 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -371,6 +371,8 @@ "contactsFilter": "Stiki", + "repeatersFilter": "Ponavljalniki", + "sarMarkers": "SAR označevalci", "foundPerson": "Najdena oseba", diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 4d24918..8fab91d 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -772,6 +772,30 @@ class _SettingsScreenState extends State { Navigator.pop(context); }, ), + RadioListTile( + title: Row( + children: [ + const Text('SAR Navy Blue'), + const SizedBox(width: 8), + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: const Color(0xFF5C9FFF), + shape: BoxShape.circle, + border: Border.all(color: Colors.black26), + ), + ), + ], + ), + subtitle: const Text('Professional/Operations Mode'), + value: AppThemeMode.sarNavyBlue, + groupValue: _selectedTheme, + onChanged: (value) { + _handleThemeChange(value); + Navigator.pop(context); + }, + ), const Divider(), RadioListTile( title: Text(AppLocalizations.of(context)!.autoSystem), diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index 893f897..d942e93 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -5,6 +5,7 @@ enum AppThemeMode { dark, sarRed, sarGreen, + sarNavyBlue, system, } @@ -181,6 +182,65 @@ class AppTheme { ); } + // SAR Navy Blue theme (Professional/Operations tones) + static ThemeData get sarNavyBlueTheme { + return ThemeData( + useMaterial3: true, + colorScheme: const ColorScheme.dark( + brightness: Brightness.dark, + primary: Color(0xFF5C9FFF), // Bright navy blue + onPrimary: Color(0xFF000000), + primaryContainer: Color(0xFF003366), // Dark navy + onPrimaryContainer: Color(0xFFBBDEFF), + secondary: Color(0xFF80B3FF), + onSecondary: Color(0xFF000000), + secondaryContainer: Color(0xFF002244), + onSecondaryContainer: Color(0xFFBBDEFF), + tertiary: Color(0xFF4DB8FF), + onTertiary: Color(0xFF000000), + error: Color(0xFFCF6679), + onError: Color(0xFF000000), + surface: Color(0xFF00111C), // Very dark blue-tinted + onSurface: Color(0xFFE3F2FD), + surfaceContainerHighest: Color(0xFF001A2D), + onSurfaceVariant: Color(0xFFBBDEFF), + outline: Color(0xFF5C9FFF), + ), + scaffoldBackgroundColor: const Color(0xFF00111C), + appBarTheme: const AppBarTheme( + centerTitle: true, + elevation: 0, + backgroundColor: Color(0xFF001A2D), + foregroundColor: Color(0xFFE3F2FD), + ), + cardTheme: CardThemeData( + elevation: 2, + color: const Color(0xFF001A2D), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide( + color: Color(0xFF5C9FFF), + width: 1, + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFF5C9FFF)), + ), + filled: true, + fillColor: const Color(0xFF001A2D), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF5C9FFF), + foregroundColor: const Color(0xFF000000), + ), + ), + ); + } + // Get theme by mode static ThemeData getTheme(AppThemeMode mode, Brightness systemBrightness) { switch (mode) { @@ -192,6 +252,8 @@ class AppTheme { return sarRedTheme; case AppThemeMode.sarGreen: return sarGreenTheme; + case AppThemeMode.sarNavyBlue: + return sarNavyBlueTheme; case AppThemeMode.system: return systemBrightness == Brightness.dark ? darkTheme : lightTheme; } @@ -208,6 +270,8 @@ class AppTheme { return 'SAR Red (Alert)'; case AppThemeMode.sarGreen: return 'SAR Green (Safe)'; + case AppThemeMode.sarNavyBlue: + return 'SAR Navy Blue (Ops)'; case AppThemeMode.system: return 'Auto (System)'; } diff --git a/lib/widgets/map/compass/compass_contact_list.dart b/lib/widgets/map/compass/compass_contact_list.dart index d119412..1af2a5f 100644 --- a/lib/widgets/map/compass/compass_contact_list.dart +++ b/lib/widgets/map/compass/compass_contact_list.dart @@ -6,11 +6,14 @@ import '../../../models/contact.dart'; /// Contact list section for the compass dialog. /// Shows all contacts with location sorted by distance with bearing information. +/// Splits contacts by type: Persons/Team, Repeaters, and Rooms. class CompassContactList extends StatelessWidget { final List contacts; final Position? position; final double? heading; final Contact? selectedContact; + final bool showContacts; + final bool showRepeaters; final ValueChanged onContactTap; const CompassContactList({ @@ -19,6 +22,8 @@ class CompassContactList extends StatelessWidget { required this.position, this.heading, required this.selectedContact, + required this.showContacts, + required this.showRepeaters, required this.onContactTap, }); @@ -32,9 +37,16 @@ class CompassContactList extends StatelessWidget { return Text(AppLocalizations.of(context)!.locationUnavailable); } - // Calculate bearings and distances - final contactsWithBearing = contacts.map((contact) { - if (contact.displayLocation == null) return null; + final l10n = AppLocalizations.of(context)!; + + // Split contacts by type + final persons = >[]; + final repeaters = >[]; + final rooms = >[]; + + // Calculate bearings and distances for each contact + for (final contact in contacts) { + if (contact.displayLocation == null) continue; final bearing = _calculateBearing( position!.latitude, @@ -50,101 +62,160 @@ class CompassContactList extends StatelessWidget { contact.displayLocation!.longitude, ); - return { + final item = { 'contact': contact, 'bearing': bearing, 'distance': distance, }; - }).whereType>().toList(); - // Sort by distance - contactsWithBearing.sort((a, b) => - (a['distance'] as double).compareTo(b['distance'] as double)); + if (contact.isRepeater) { + repeaters.add(item); + } else if (contact.isRoom) { + rooms.add(item); + } else { + persons.add(item); + } + } + + // Sort each list by distance + persons.sort((a, b) => (a['distance'] as double).compareTo(b['distance'] as double)); + repeaters.sort((a, b) => (a['distance'] as double).compareTo(b['distance'] as double)); + rooms.sort((a, b) => (a['distance'] as double).compareTo(b['distance'] as double)); - final l10n = AppLocalizations.of(context)!; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.only(left: 16, bottom: 8), - child: Text( - l10n.nearbyContacts, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ), - ...contactsWithBearing.map((item) { - final contact = item['contact'] as Contact; - final bearing = item['bearing'] as double; - final distance = item['distance'] as double; - - return Container( - margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: selectedContact == contact - ? Theme.of(context).colorScheme.primaryContainer - : Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(8), - border: selectedContact == contact - ? Border.all( - color: Theme.of(context).colorScheme.primary, - width: 2, - ) - : null, - ), - child: ListTile( - dense: true, - leading: contact.roleEmoji != null - ? Text( - contact.roleEmoji!, - style: const TextStyle(fontSize: 24), - ) - : Icon( - Icons.person, - color: Theme.of(context).colorScheme.primary, - size: 24, - ), - title: Text(contact.displayName), - subtitle: Text( - '${_bearingToCardinal(bearing)} • ${_formatDistance(distance)}', - style: Theme.of(context).textTheme.bodySmall, - ), - trailing: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - '${bearing.round()}°', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontWeight: FontWeight.bold, - ), + // Persons/Team section + if (showContacts && persons.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.only(left: 16, bottom: 8, top: 4), + child: Text( + l10n.teamMembers, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, ), - if (heading != null) - Text( - _formatRelativeBearing(bearing, heading!, context), - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: Colors.grey, - ), - ), - ], - ), - onTap: () { - if (selectedContact == contact) { - // Deselect if already selected - onContactTap(null); - } else { - // Select this contact - onContactTap(contact); - } - }, ), - ); - }), + ), + ...persons.map((item) => _buildContactTile( + context, + item, + Icons.groups, + Theme.of(context).colorScheme.primary, + )), + ], + // Repeaters section + if (showRepeaters && repeaters.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.only(left: 16, bottom: 8, top: 12), + child: Text( + l10n.repeaters, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + ...repeaters.map((item) => _buildContactTile( + context, + item, + Icons.router, + Colors.purple, + )), + ], + // Rooms section + if (rooms.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.only(left: 16, bottom: 8, top: 12), + child: Text( + l10n.rooms, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + ...rooms.map((item) => _buildContactTile( + context, + item, + Icons.meeting_room, + Colors.teal, + )), + ], ], ); } + Widget _buildContactTile( + BuildContext context, + Map item, + IconData defaultIcon, + Color iconColor, + ) { + final contact = item['contact'] as Contact; + final bearing = item['bearing'] as double; + final distance = item['distance'] as double; + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: selectedContact == contact + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + border: selectedContact == contact + ? Border.all( + color: Theme.of(context).colorScheme.primary, + width: 2, + ) + : null, + ), + child: ListTile( + dense: true, + leading: contact.roleEmoji != null + ? Text( + contact.roleEmoji!, + style: const TextStyle(fontSize: 24), + ) + : Icon( + defaultIcon, + color: iconColor, + size: 24, + ), + title: Text(contact.displayName), + subtitle: Text( + '${_bearingToCardinal(bearing)} • ${_formatDistance(distance)}', + style: Theme.of(context).textTheme.bodySmall, + ), + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${bearing.round()}°', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + if (heading != null) + Text( + _formatRelativeBearing(bearing, heading!, context), + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Colors.grey, + ), + ), + ], + ), + onTap: () { + if (selectedContact == contact) { + // Deselect if already selected + onContactTap(null); + } else { + // Select this contact + onContactTap(contact); + } + }, + ), + ); + } + // Calculate bearing between two points (in degrees) double _calculateBearing( double lat1, double lon1, double lat2, double lon2) { diff --git a/lib/widgets/map/compass/compass_filters.dart b/lib/widgets/map/compass/compass_filters.dart index 1735008..c14bd45 100644 --- a/lib/widgets/map/compass/compass_filters.dart +++ b/lib/widgets/map/compass/compass_filters.dart @@ -5,10 +5,12 @@ import '../../../l10n/app_localizations.dart'; /// Allows filtering of contacts and SAR marker types. class CompassFilters extends StatefulWidget { final bool showContacts; + final bool showRepeaters; final bool showFoundPerson; final bool showFire; final bool showStagingArea; final ValueChanged onShowContactsChanged; + final ValueChanged onShowRepeatersChanged; final ValueChanged onShowFoundPersonChanged; final ValueChanged onShowFireChanged; final ValueChanged onShowStagingAreaChanged; @@ -17,10 +19,12 @@ class CompassFilters extends StatefulWidget { const CompassFilters({ super.key, required this.showContacts, + required this.showRepeaters, required this.showFoundPerson, required this.showFire, required this.showStagingArea, required this.onShowContactsChanged, + required this.onShowRepeatersChanged, required this.onShowFoundPersonChanged, required this.onShowFireChanged, required this.onShowStagingAreaChanged, @@ -62,6 +66,18 @@ class _CompassFiltersState extends State { }, ), const SizedBox(height: 4), + // Repeaters filter + _CompactFilterItem( + icon: Icons.router, + color: Colors.purple, + label: l10n.repeatersFilter, + value: widget.showRepeaters, + onChanged: (value) { + widget.onShowRepeatersChanged(value); + setDialogState(() {}); + }, + ), + const SizedBox(height: 4), const Divider(height: 8), const SizedBox(height: 4), // SAR Markers section diff --git a/lib/widgets/map/detailed_compass_dialog.dart b/lib/widgets/map/detailed_compass_dialog.dart index e287416..c2e1f30 100644 --- a/lib/widgets/map/detailed_compass_dialog.dart +++ b/lib/widgets/map/detailed_compass_dialog.dart @@ -65,6 +65,7 @@ class _DetailedCompassDialogState extends State { // Visibility toggles bool _showContacts = true; + bool _showRepeaters = false; // Hide repeaters by default bool _showFoundPerson = true; bool _showFire = true; bool _showStagingArea = true; @@ -308,6 +309,7 @@ class _DetailedCompassDialogState extends State { ), CompassFilters( showContacts: _showContacts, + showRepeaters: _showRepeaters, showFoundPerson: _showFoundPerson, showFire: _showFire, showStagingArea: _showStagingArea, @@ -316,6 +318,11 @@ class _DetailedCompassDialogState extends State { _showContacts = value; }); }, + onShowRepeatersChanged: (value) { + setState(() { + _showRepeaters = value; + }); + }, onShowFoundPersonChanged: (value) { setState(() { _showFoundPerson = value; @@ -334,6 +341,7 @@ class _DetailedCompassDialogState extends State { onShowAll: () { setState(() { _showContacts = true; + _showRepeaters = true; _showFoundPerson = true; _showFire = true; _showStagingArea = true; @@ -363,7 +371,10 @@ class _DetailedCompassDialogState extends State { ? [_selectedContact!] : (_selectedSarMarker != null ? [] - : (_showContacts ? widget.contacts : [])), + : widget.contacts.where((c) => + (_showContacts && !c.isRepeater && !c.isRoom) || + (_showRepeaters && c.isRepeater) + ).toList()), sarMarkers: _selectedSarMarker != null ? [_selectedSarMarker!] : (_selectedContact != null @@ -381,12 +392,14 @@ class _DetailedCompassDialogState extends State { _buildSelectedItemDetail(context, heading, position), const SizedBox(height: 12), // Contacts list - if (_showContacts && widget.contacts.isNotEmpty) + if (widget.contacts.isNotEmpty) CompassContactList( contacts: widget.contacts, position: position, heading: heading, selectedContact: _selectedContact, + showContacts: _showContacts, + showRepeaters: _showRepeaters, onContactTap: (contact) { setState(() { _selectedContact = contact;