mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Enhance SAR message handling with color indexing and localization
- Added `sarColorIndex` to `Message` and `SarMarker` models to support color indexing for SAR markers. - Introduced a standard color palette in `SarTemplate` and methods to retrieve colors based on index. - Updated `SarTemplate` to include localization for template names. - Modified `SarUpdateSheet` and message sending logic to include color index in SAR messages. - Enhanced `SarMessageParser` to support both old and new message formats with color index. - Updated UI components to reflect changes in SAR marker color handling based on the new color index. - Improved sample data generation to utilize localized strings for better testing and demonstration.
This commit is contained in:
@@ -878,8 +878,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
builder: (context) => SarUpdateSheet(
|
||||
prePopulatedPosition: position,
|
||||
allowLocationUpdate: false, // Don't allow changing to current location
|
||||
onSend: (emoji, name, position, roomPublicKey, sendToChannel) async {
|
||||
await _sendSarMessage(emoji, name, position, roomPublicKey, sendToChannel);
|
||||
onSend: (emoji, name, position, roomPublicKey, sendToChannel, colorIndex) async {
|
||||
await _sendSarMessage(emoji, name, position, roomPublicKey, sendToChannel, colorIndex);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -891,6 +891,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
Position position,
|
||||
Uint8List? roomPublicKey,
|
||||
bool sendToChannel,
|
||||
int colorIndex,
|
||||
) async {
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
final messagesProvider = context.read<MessagesProvider>();
|
||||
@@ -918,9 +919,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
}
|
||||
|
||||
try {
|
||||
// Format: S:<emoji>:<latitude>,<longitude>:<name>
|
||||
// New format: S:<emoji>:<colorIndex>:<latitude>,<longitude>:<name>
|
||||
// Round coordinates to 5 decimal places (~1m accuracy) since most GPS is only that accurate
|
||||
final sarMessage = 'S:$emoji:${position.latitude.toStringAsFixed(5)},${position.longitude.toStringAsFixed(5)}:$name';
|
||||
final sarMessage = 'S:$emoji:$colorIndex:${position.latitude.toStringAsFixed(5)},${position.longitude.toStringAsFixed(5)}:$name';
|
||||
|
||||
if (sendToChannel) {
|
||||
// Create message ID
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import '../providers/messages_provider.dart';
|
||||
@@ -64,8 +65,9 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
// Check for navigation request whenever dependencies change
|
||||
// Reload saved destination and check for navigation request whenever dependencies change
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_loadSavedDestination();
|
||||
_checkForNavigationRequest();
|
||||
});
|
||||
}
|
||||
@@ -387,13 +389,14 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => SarUpdateSheet(
|
||||
onSend: (emoji, name, position, roomPublicKey, sendToChannel) async {
|
||||
onSend: (emoji, name, position, roomPublicKey, sendToChannel, colorIndex) async {
|
||||
await _sendSarMessage(
|
||||
emoji,
|
||||
name,
|
||||
position,
|
||||
roomPublicKey,
|
||||
sendToChannel,
|
||||
colorIndex,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -406,6 +409,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
Position position,
|
||||
Uint8List? roomPublicKey,
|
||||
bool sendToChannel,
|
||||
int colorIndex,
|
||||
) async {
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
final messagesProvider = context.read<MessagesProvider>();
|
||||
@@ -423,10 +427,10 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
}
|
||||
|
||||
try {
|
||||
// Format: S:<emoji>:<latitude>,<longitude>:<name>
|
||||
// New format: S:<emoji>:<colorIndex>:<latitude>,<longitude>:<name>
|
||||
// Round coordinates to 5 decimal places (~1m accuracy) since most GPS is only that accurate
|
||||
final sarMessage =
|
||||
'S:$emoji:${position.latitude.toStringAsFixed(5)},${position.longitude.toStringAsFixed(5)}:$name';
|
||||
'S:$emoji:$colorIndex:${position.latitude.toStringAsFixed(5)},${position.longitude.toStringAsFixed(5)}:$name';
|
||||
|
||||
if (sendToChannel) {
|
||||
// Create message ID
|
||||
@@ -551,9 +555,51 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
}
|
||||
|
||||
List<Message> _getFilteredMessages(MessagesProvider messagesProvider) {
|
||||
// Show ALL messages regardless of recipient selection
|
||||
// The recipient selector only controls where NEW messages are sent
|
||||
return messagesProvider.getRecentMessages(count: 100);
|
||||
// Get all recent messages
|
||||
final allMessages = messagesProvider.getRecentMessages(count: 100);
|
||||
|
||||
// If public channel is selected, show ALL messages
|
||||
if (_destinationType ==
|
||||
MessageDestinationPreferences.destinationTypeChannel &&
|
||||
_selectedRecipient == null) {
|
||||
return allMessages;
|
||||
}
|
||||
|
||||
// If a contact or room is selected, filter by recipient
|
||||
if ((_destinationType == MessageDestinationPreferences.destinationTypeContact ||
|
||||
_destinationType ==
|
||||
MessageDestinationPreferences.destinationTypeRoom) &&
|
||||
_selectedRecipient != null) {
|
||||
return allMessages.where((message) {
|
||||
// Include messages sent TO this recipient
|
||||
if (message.recipientPublicKey != null &&
|
||||
message.recipientPublicKey!.length >= 6 &&
|
||||
_selectedRecipient!.publicKey.length >= 6) {
|
||||
// Compare first 6 bytes (public key prefix)
|
||||
final recipientPrefix = message.recipientPublicKey!.sublist(0, 6);
|
||||
final selectedPrefix = _selectedRecipient!.publicKey.sublist(0, 6);
|
||||
if (_publicKeysMatch(recipientPrefix, selectedPrefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Include messages received FROM this recipient
|
||||
if (message.senderPublicKeyPrefix != null &&
|
||||
message.senderPublicKeyPrefix!.length >= 6 &&
|
||||
_selectedRecipient!.publicKey.length >= 6) {
|
||||
final senderPrefix = message.senderPublicKeyPrefix!.sublist(0, 6);
|
||||
final selectedPrefix = _selectedRecipient!.publicKey.sublist(0, 6);
|
||||
if (_publicKeysMatch(senderPrefix, selectedPrefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Default: show all messages (fallback case)
|
||||
return allMessages;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -831,6 +877,8 @@ class _MessageBubble extends StatelessWidget {
|
||||
contact: roomContact, // Include contact for path status logging
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (!sentSuccessfully) {
|
||||
messagesProvider.markMessageFailed(retryMessageId);
|
||||
ToastLogger.error(context, 'Failed to resend message');
|
||||
@@ -845,9 +893,12 @@ class _MessageBubble extends StatelessWidget {
|
||||
messageId: retryMessageId,
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
ToastLogger.info(context, 'Retrying message...');
|
||||
}
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
ToastLogger.error(context, 'Retry failed: $e');
|
||||
}
|
||||
}
|
||||
@@ -899,6 +950,16 @@ class _MessageBubble extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
// Share location option (only for SAR markers with GPS coordinates)
|
||||
if (message.isSarMarker && message.sarGpsCoordinates != null)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.share_location),
|
||||
title: Text(AppLocalizations.of(context)!.shareLocation),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_shareLocation(context);
|
||||
},
|
||||
),
|
||||
// Delete message option
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete, color: Colors.red),
|
||||
@@ -982,6 +1043,52 @@ class _MessageBubble extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _shareLocation(BuildContext context) {
|
||||
if (message.sarGpsCoordinates == null) {
|
||||
ToastLogger.error(context, 'No GPS coordinates available');
|
||||
return;
|
||||
}
|
||||
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final coords = message.sarGpsCoordinates!;
|
||||
|
||||
// Get SAR marker type emoji/name
|
||||
String markerInfo = '';
|
||||
if (message.sarMarkerType != null) {
|
||||
markerInfo = message.sarMarkerType!.emoji;
|
||||
if (message.sarNotes != null && message.sarNotes!.isNotEmpty) {
|
||||
markerInfo += ' ${message.sarNotes}';
|
||||
}
|
||||
} else if (message.sarCustomEmoji != null) {
|
||||
markerInfo = message.sarCustomEmoji!;
|
||||
if (message.sarNotes != null && message.sarNotes!.isNotEmpty) {
|
||||
markerInfo += ' ${message.sarNotes}';
|
||||
}
|
||||
}
|
||||
|
||||
// Format coordinates with 6 decimal places (≈0.1m precision)
|
||||
final lat = coords.latitude.toStringAsFixed(6);
|
||||
final lon = coords.longitude.toStringAsFixed(6);
|
||||
|
||||
// Build share text
|
||||
final shareText = l10n.shareLocationText(
|
||||
markerInfo,
|
||||
lat,
|
||||
lon,
|
||||
'https://www.google.com/maps/search/?api=1&query=$lat,$lon',
|
||||
);
|
||||
|
||||
// Share the location
|
||||
SharePlus.instance.share(
|
||||
ShareParams(
|
||||
text: shareText,
|
||||
subject: l10n.sarLocationShare,
|
||||
),
|
||||
);
|
||||
|
||||
ToastLogger.success(context, l10n.locationShared);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isSarMarker = message.isSarMarker;
|
||||
|
||||
@@ -81,7 +81,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!.deleteTemplate),
|
||||
content: Text(
|
||||
AppLocalizations.of(context)!.deleteTemplateConfirmation(template.name),
|
||||
AppLocalizations.of(context)!.deleteTemplateConfirmation(template.getLocalizedName(context)),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -351,7 +351,7 @@ class _TemplateListItem extends StatelessWidget {
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!.deleteTemplate),
|
||||
content: Text(
|
||||
AppLocalizations.of(context)!.deleteTemplateConfirmation(template.name),
|
||||
AppLocalizations.of(context)!.deleteTemplateConfirmation(template.getLocalizedName(context)),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -394,7 +394,7 @@ class _TemplateListItem extends StatelessWidget {
|
||||
title: Row(
|
||||
children: [
|
||||
Text(
|
||||
template.name,
|
||||
template.getLocalizedName(context),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
|
||||
@@ -178,15 +178,20 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Get localization
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
// Generate sample data
|
||||
final contacts = SampleDataGenerator.generateContacts(
|
||||
centerLocation: centerLocation,
|
||||
l10n: l10n,
|
||||
teamMemberCount: 5,
|
||||
channelCount: 2,
|
||||
);
|
||||
|
||||
final sarMessages = SampleDataGenerator.generateSarMarkerMessages(
|
||||
centerLocation: centerLocation,
|
||||
l10n: l10n,
|
||||
foundPersonCount: 2,
|
||||
fireCount: 1,
|
||||
stagingCount: 1,
|
||||
@@ -195,6 +200,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
final channelMessages = SampleDataGenerator.generateChannelMessages(
|
||||
centerLocation: centerLocation,
|
||||
l10n: l10n,
|
||||
generalChannelMessages: 8,
|
||||
emergencyChannelMessages: 5,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user