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:
Janez T
2025-10-23 19:17:13 +02:00
parent b70eed1bf2
commit 98cda8c976
28 changed files with 2734 additions and 270 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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,
),

View File

@@ -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,
);