mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Update project version to 19, add repeaters filter and theme support, and enhance contact list display
This commit is contained in:
@@ -489,7 +489,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 18;
|
CURRENT_PROJECT_VERSION = 19;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -511,7 +511,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 18;
|
CURRENT_PROJECT_VERSION = 19;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp.RunnerTests;
|
||||||
@@ -529,7 +529,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 18;
|
CURRENT_PROJECT_VERSION = 19;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp.RunnerTests;
|
||||||
@@ -545,7 +545,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 18;
|
CURRENT_PROJECT_VERSION = 19;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp.RunnerTests;
|
||||||
@@ -676,7 +676,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 18;
|
CURRENT_PROJECT_VERSION = 19;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -699,7 +699,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 18;
|
CURRENT_PROJECT_VERSION = 19;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>18</string>
|
<string>19</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
|
|||||||
@@ -5,17 +5,22 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000203">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000227">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.287931">
|
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.240778">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: build_app" time="31.633362">
|
<testcase classname="fastlane.lanes" name="2: build_app" time="91.431808">
|
||||||
|
|
||||||
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
|
<testcase classname="fastlane.lanes" name="3: upload_to_testflight" time="328.922766">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -1097,6 +1097,11 @@
|
|||||||
"description": "Filter option for contacts"
|
"description": "Filter option for contacts"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"repeatersFilter": "Repeaters",
|
||||||
|
"@repeatersFilter": {
|
||||||
|
"description": "Filter option for repeaters"
|
||||||
|
},
|
||||||
|
|
||||||
"sarMarkers": "SAR Markers",
|
"sarMarkers": "SAR Markers",
|
||||||
"@sarMarkers": {
|
"@sarMarkers": {
|
||||||
"description": "SAR markers section header"
|
"description": "SAR markers section header"
|
||||||
|
|||||||
@@ -371,6 +371,8 @@
|
|||||||
|
|
||||||
"contactsFilter": "Kontakti",
|
"contactsFilter": "Kontakti",
|
||||||
|
|
||||||
|
"repeatersFilter": "Repetitori",
|
||||||
|
|
||||||
"sarMarkers": "SAR markeri",
|
"sarMarkers": "SAR markeri",
|
||||||
|
|
||||||
"foundPerson": "Pronađena osoba",
|
"foundPerson": "Pronađena osoba",
|
||||||
|
|||||||
@@ -1215,6 +1215,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'Contacts'**
|
/// **'Contacts'**
|
||||||
String get contactsFilter;
|
String get contactsFilter;
|
||||||
|
|
||||||
|
/// Filter option for repeaters
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Repeaters'**
|
||||||
|
String get repeatersFilter;
|
||||||
|
|
||||||
/// SAR markers section header
|
/// SAR markers section header
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -643,6 +643,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contactsFilter => 'Contacts';
|
String get contactsFilter => 'Contacts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get repeatersFilter => 'Repeaters';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sarMarkers => 'SAR Markers';
|
String get sarMarkers => 'SAR Markers';
|
||||||
|
|
||||||
|
|||||||
@@ -643,6 +643,9 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contactsFilter => 'Kontakti';
|
String get contactsFilter => 'Kontakti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get repeatersFilter => 'Repetitori';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sarMarkers => 'SAR markeri';
|
String get sarMarkers => 'SAR markeri';
|
||||||
|
|
||||||
|
|||||||
@@ -643,6 +643,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contactsFilter => 'Stiki';
|
String get contactsFilter => 'Stiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get repeatersFilter => 'Ponavljalniki';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sarMarkers => 'SAR označevalci';
|
String get sarMarkers => 'SAR označevalci';
|
||||||
|
|
||||||
|
|||||||
@@ -371,6 +371,8 @@
|
|||||||
|
|
||||||
"contactsFilter": "Stiki",
|
"contactsFilter": "Stiki",
|
||||||
|
|
||||||
|
"repeatersFilter": "Ponavljalniki",
|
||||||
|
|
||||||
"sarMarkers": "SAR označevalci",
|
"sarMarkers": "SAR označevalci",
|
||||||
|
|
||||||
"foundPerson": "Najdena oseba",
|
"foundPerson": "Najdena oseba",
|
||||||
|
|||||||
@@ -772,6 +772,30 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
RadioListTile<AppThemeMode>(
|
||||||
|
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(),
|
const Divider(),
|
||||||
RadioListTile<AppThemeMode>(
|
RadioListTile<AppThemeMode>(
|
||||||
title: Text(AppLocalizations.of(context)!.autoSystem),
|
title: Text(AppLocalizations.of(context)!.autoSystem),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ enum AppThemeMode {
|
|||||||
dark,
|
dark,
|
||||||
sarRed,
|
sarRed,
|
||||||
sarGreen,
|
sarGreen,
|
||||||
|
sarNavyBlue,
|
||||||
system,
|
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
|
// Get theme by mode
|
||||||
static ThemeData getTheme(AppThemeMode mode, Brightness systemBrightness) {
|
static ThemeData getTheme(AppThemeMode mode, Brightness systemBrightness) {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
@@ -192,6 +252,8 @@ class AppTheme {
|
|||||||
return sarRedTheme;
|
return sarRedTheme;
|
||||||
case AppThemeMode.sarGreen:
|
case AppThemeMode.sarGreen:
|
||||||
return sarGreenTheme;
|
return sarGreenTheme;
|
||||||
|
case AppThemeMode.sarNavyBlue:
|
||||||
|
return sarNavyBlueTheme;
|
||||||
case AppThemeMode.system:
|
case AppThemeMode.system:
|
||||||
return systemBrightness == Brightness.dark ? darkTheme : lightTheme;
|
return systemBrightness == Brightness.dark ? darkTheme : lightTheme;
|
||||||
}
|
}
|
||||||
@@ -208,6 +270,8 @@ class AppTheme {
|
|||||||
return 'SAR Red (Alert)';
|
return 'SAR Red (Alert)';
|
||||||
case AppThemeMode.sarGreen:
|
case AppThemeMode.sarGreen:
|
||||||
return 'SAR Green (Safe)';
|
return 'SAR Green (Safe)';
|
||||||
|
case AppThemeMode.sarNavyBlue:
|
||||||
|
return 'SAR Navy Blue (Ops)';
|
||||||
case AppThemeMode.system:
|
case AppThemeMode.system:
|
||||||
return 'Auto (System)';
|
return 'Auto (System)';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,14 @@ import '../../../models/contact.dart';
|
|||||||
|
|
||||||
/// Contact list section for the compass dialog.
|
/// Contact list section for the compass dialog.
|
||||||
/// Shows all contacts with location sorted by distance with bearing information.
|
/// Shows all contacts with location sorted by distance with bearing information.
|
||||||
|
/// Splits contacts by type: Persons/Team, Repeaters, and Rooms.
|
||||||
class CompassContactList extends StatelessWidget {
|
class CompassContactList extends StatelessWidget {
|
||||||
final List<Contact> contacts;
|
final List<Contact> contacts;
|
||||||
final Position? position;
|
final Position? position;
|
||||||
final double? heading;
|
final double? heading;
|
||||||
final Contact? selectedContact;
|
final Contact? selectedContact;
|
||||||
|
final bool showContacts;
|
||||||
|
final bool showRepeaters;
|
||||||
final ValueChanged<Contact?> onContactTap;
|
final ValueChanged<Contact?> onContactTap;
|
||||||
|
|
||||||
const CompassContactList({
|
const CompassContactList({
|
||||||
@@ -19,6 +22,8 @@ class CompassContactList extends StatelessWidget {
|
|||||||
required this.position,
|
required this.position,
|
||||||
this.heading,
|
this.heading,
|
||||||
required this.selectedContact,
|
required this.selectedContact,
|
||||||
|
required this.showContacts,
|
||||||
|
required this.showRepeaters,
|
||||||
required this.onContactTap,
|
required this.onContactTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -32,9 +37,16 @@ class CompassContactList extends StatelessWidget {
|
|||||||
return Text(AppLocalizations.of(context)!.locationUnavailable);
|
return Text(AppLocalizations.of(context)!.locationUnavailable);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate bearings and distances
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final contactsWithBearing = contacts.map((contact) {
|
|
||||||
if (contact.displayLocation == null) return null;
|
// Split contacts by type
|
||||||
|
final persons = <Map<String, dynamic>>[];
|
||||||
|
final repeaters = <Map<String, dynamic>>[];
|
||||||
|
final rooms = <Map<String, dynamic>>[];
|
||||||
|
|
||||||
|
// Calculate bearings and distances for each contact
|
||||||
|
for (final contact in contacts) {
|
||||||
|
if (contact.displayLocation == null) continue;
|
||||||
|
|
||||||
final bearing = _calculateBearing(
|
final bearing = _calculateBearing(
|
||||||
position!.latitude,
|
position!.latitude,
|
||||||
@@ -50,101 +62,160 @@ class CompassContactList extends StatelessWidget {
|
|||||||
contact.displayLocation!.longitude,
|
contact.displayLocation!.longitude,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
final item = {
|
||||||
'contact': contact,
|
'contact': contact,
|
||||||
'bearing': bearing,
|
'bearing': bearing,
|
||||||
'distance': distance,
|
'distance': distance,
|
||||||
};
|
};
|
||||||
}).whereType<Map<String, dynamic>>().toList();
|
|
||||||
|
|
||||||
// Sort by distance
|
if (contact.isRepeater) {
|
||||||
contactsWithBearing.sort((a, b) =>
|
repeaters.add(item);
|
||||||
(a['distance'] as double).compareTo(b['distance'] as double));
|
} 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(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
// Persons/Team section
|
||||||
padding: const EdgeInsets.only(left: 16, bottom: 8),
|
if (showContacts && persons.isNotEmpty) ...[
|
||||||
child: Text(
|
Padding(
|
||||||
l10n.nearbyContacts,
|
padding: const EdgeInsets.only(left: 16, bottom: 8, top: 4),
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
child: Text(
|
||||||
fontWeight: FontWeight.bold,
|
l10n.teamMembers,
|
||||||
),
|
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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
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<String, dynamic> 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)
|
// Calculate bearing between two points (in degrees)
|
||||||
double _calculateBearing(
|
double _calculateBearing(
|
||||||
double lat1, double lon1, double lat2, double lon2) {
|
double lat1, double lon1, double lat2, double lon2) {
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import '../../../l10n/app_localizations.dart';
|
|||||||
/// Allows filtering of contacts and SAR marker types.
|
/// Allows filtering of contacts and SAR marker types.
|
||||||
class CompassFilters extends StatefulWidget {
|
class CompassFilters extends StatefulWidget {
|
||||||
final bool showContacts;
|
final bool showContacts;
|
||||||
|
final bool showRepeaters;
|
||||||
final bool showFoundPerson;
|
final bool showFoundPerson;
|
||||||
final bool showFire;
|
final bool showFire;
|
||||||
final bool showStagingArea;
|
final bool showStagingArea;
|
||||||
final ValueChanged<bool> onShowContactsChanged;
|
final ValueChanged<bool> onShowContactsChanged;
|
||||||
|
final ValueChanged<bool> onShowRepeatersChanged;
|
||||||
final ValueChanged<bool> onShowFoundPersonChanged;
|
final ValueChanged<bool> onShowFoundPersonChanged;
|
||||||
final ValueChanged<bool> onShowFireChanged;
|
final ValueChanged<bool> onShowFireChanged;
|
||||||
final ValueChanged<bool> onShowStagingAreaChanged;
|
final ValueChanged<bool> onShowStagingAreaChanged;
|
||||||
@@ -17,10 +19,12 @@ class CompassFilters extends StatefulWidget {
|
|||||||
const CompassFilters({
|
const CompassFilters({
|
||||||
super.key,
|
super.key,
|
||||||
required this.showContacts,
|
required this.showContacts,
|
||||||
|
required this.showRepeaters,
|
||||||
required this.showFoundPerson,
|
required this.showFoundPerson,
|
||||||
required this.showFire,
|
required this.showFire,
|
||||||
required this.showStagingArea,
|
required this.showStagingArea,
|
||||||
required this.onShowContactsChanged,
|
required this.onShowContactsChanged,
|
||||||
|
required this.onShowRepeatersChanged,
|
||||||
required this.onShowFoundPersonChanged,
|
required this.onShowFoundPersonChanged,
|
||||||
required this.onShowFireChanged,
|
required this.onShowFireChanged,
|
||||||
required this.onShowStagingAreaChanged,
|
required this.onShowStagingAreaChanged,
|
||||||
@@ -62,6 +66,18 @@ class _CompassFiltersState extends State<CompassFilters> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
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 Divider(height: 8),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
// SAR Markers section
|
// SAR Markers section
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ class _DetailedCompassDialogState extends State<DetailedCompassDialog> {
|
|||||||
|
|
||||||
// Visibility toggles
|
// Visibility toggles
|
||||||
bool _showContacts = true;
|
bool _showContacts = true;
|
||||||
|
bool _showRepeaters = false; // Hide repeaters by default
|
||||||
bool _showFoundPerson = true;
|
bool _showFoundPerson = true;
|
||||||
bool _showFire = true;
|
bool _showFire = true;
|
||||||
bool _showStagingArea = true;
|
bool _showStagingArea = true;
|
||||||
@@ -308,6 +309,7 @@ class _DetailedCompassDialogState extends State<DetailedCompassDialog> {
|
|||||||
),
|
),
|
||||||
CompassFilters(
|
CompassFilters(
|
||||||
showContacts: _showContacts,
|
showContacts: _showContacts,
|
||||||
|
showRepeaters: _showRepeaters,
|
||||||
showFoundPerson: _showFoundPerson,
|
showFoundPerson: _showFoundPerson,
|
||||||
showFire: _showFire,
|
showFire: _showFire,
|
||||||
showStagingArea: _showStagingArea,
|
showStagingArea: _showStagingArea,
|
||||||
@@ -316,6 +318,11 @@ class _DetailedCompassDialogState extends State<DetailedCompassDialog> {
|
|||||||
_showContacts = value;
|
_showContacts = value;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
onShowRepeatersChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_showRepeaters = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
onShowFoundPersonChanged: (value) {
|
onShowFoundPersonChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_showFoundPerson = value;
|
_showFoundPerson = value;
|
||||||
@@ -334,6 +341,7 @@ class _DetailedCompassDialogState extends State<DetailedCompassDialog> {
|
|||||||
onShowAll: () {
|
onShowAll: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_showContacts = true;
|
_showContacts = true;
|
||||||
|
_showRepeaters = true;
|
||||||
_showFoundPerson = true;
|
_showFoundPerson = true;
|
||||||
_showFire = true;
|
_showFire = true;
|
||||||
_showStagingArea = true;
|
_showStagingArea = true;
|
||||||
@@ -363,7 +371,10 @@ class _DetailedCompassDialogState extends State<DetailedCompassDialog> {
|
|||||||
? [_selectedContact!]
|
? [_selectedContact!]
|
||||||
: (_selectedSarMarker != null
|
: (_selectedSarMarker != null
|
||||||
? []
|
? []
|
||||||
: (_showContacts ? widget.contacts : [])),
|
: widget.contacts.where((c) =>
|
||||||
|
(_showContacts && !c.isRepeater && !c.isRoom) ||
|
||||||
|
(_showRepeaters && c.isRepeater)
|
||||||
|
).toList()),
|
||||||
sarMarkers: _selectedSarMarker != null
|
sarMarkers: _selectedSarMarker != null
|
||||||
? [_selectedSarMarker!]
|
? [_selectedSarMarker!]
|
||||||
: (_selectedContact != null
|
: (_selectedContact != null
|
||||||
@@ -381,12 +392,14 @@ class _DetailedCompassDialogState extends State<DetailedCompassDialog> {
|
|||||||
_buildSelectedItemDetail(context, heading, position),
|
_buildSelectedItemDetail(context, heading, position),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
// Contacts list
|
// Contacts list
|
||||||
if (_showContacts && widget.contacts.isNotEmpty)
|
if (widget.contacts.isNotEmpty)
|
||||||
CompassContactList(
|
CompassContactList(
|
||||||
contacts: widget.contacts,
|
contacts: widget.contacts,
|
||||||
position: position,
|
position: position,
|
||||||
heading: heading,
|
heading: heading,
|
||||||
selectedContact: _selectedContact,
|
selectedContact: _selectedContact,
|
||||||
|
showContacts: _showContacts,
|
||||||
|
showRepeaters: _showRepeaters,
|
||||||
onContactTap: (contact) {
|
onContactTap: (contact) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedContact = contact;
|
_selectedContact = contact;
|
||||||
|
|||||||
Reference in New Issue
Block a user