From b980fb80f17de569c48155bb6bc94b005b335dda Mon Sep 17 00:00:00 2001 From: Janez T Date: Tue, 28 Oct 2025 00:14:14 +0100 Subject: [PATCH] feat: Add recent messages overlay to map in fullscreen mode and update localization for recent messages --- ios/fastlane/report.xml | 8 +- lib/l10n/app_de.arb | 4 +- lib/l10n/app_en.arb | 5 + lib/l10n/app_es.arb | 4 +- lib/l10n/app_fr.arb | 7 +- lib/l10n/app_hr.arb | 4 +- lib/l10n/app_it.arb | 7 +- lib/l10n/app_localizations.dart | 6 + lib/l10n/app_localizations_de.dart | 3 + lib/l10n/app_localizations_en.dart | 3 + lib/l10n/app_localizations_es.dart | 3 + lib/l10n/app_localizations_fr.dart | 3 + lib/l10n/app_localizations_hr.dart | 3 + lib/l10n/app_localizations_it.dart | 3 + lib/l10n/app_localizations_sl.dart | 3 + lib/l10n/app_sl.arb | 4 +- lib/screens/map_tab.dart | 30 +++ lib/services/map_marker_service.dart | 15 +- lib/services/protocol/frame_parser.dart | 3 +- lib/widgets/map/map_message_overlay.dart | 253 +++++++++++++++++++++++ lib/widgets/map_markers.dart | 22 +- 21 files changed, 370 insertions(+), 23 deletions(-) create mode 100644 lib/widgets/map/map_message_overlay.dart diff --git a/ios/fastlane/report.xml b/ios/fastlane/report.xml index 0aaa609..4a1c60e 100644 --- a/ios/fastlane/report.xml +++ b/ios/fastlane/report.xml @@ -5,22 +5,22 @@ - + - + - + - + diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 3c51eef..b950780 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -2605,5 +2605,7 @@ "forestRoads": "Waldwege", "showCadastralParcels": "Katasterparzellen anzeigen", "showForestRoads": "Waldwege anzeigen", - "wmsOverlays": "WMS Überlagerungen" + "wmsOverlays": "WMS Überlagerungen", + + "recentMessages": "Aktuelle Nachrichten" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 07bc272..b9af7af 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -3236,5 +3236,10 @@ "wmsOverlays": "WMS Overlays", "@wmsOverlays": { "description": "Section header for WMS overlay layers in layer selector" + }, + + "recentMessages": "Recent Messages", + "@recentMessages": { + "description": "Header for recent messages overlay on map in fullscreen mode" } } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 4268ab6..99033d9 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -2600,5 +2600,7 @@ "forestRoads": "Caminos Forestales", "showCadastralParcels": "Mostrar parcelas catastrales", "showForestRoads": "Mostrar caminos forestales", - "wmsOverlays": "Superposiciones WMS" + "wmsOverlays": "Superposiciones WMS", + + "recentMessages": "Mensajes Recientes" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 8012d79..adfb3a4 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -2605,5 +2605,10 @@ "forestRoads": "Chemins Forestiers", "showCadastralParcels": "Afficher les parcelles cadastrales", "showForestRoads": "Afficher les chemins forestiers", - "wmsOverlays": "Superpositions WMS" + "wmsOverlays": "Superpositions WMS", + + "recentMessages": "Messages Récents", + "@recentMessages": { + "description": "Header for recent messages overlay on map in fullscreen mode" + } } diff --git a/lib/l10n/app_hr.arb b/lib/l10n/app_hr.arb index 85f072d..5f0367e 100644 --- a/lib/l10n/app_hr.arb +++ b/lib/l10n/app_hr.arb @@ -979,5 +979,7 @@ "forestRoads": "Šumske ceste", "showCadastralParcels": "Prikaži katastarske čestice", "showForestRoads": "Prikaži šumske ceste", - "wmsOverlays": "WMS Prekrivanja" + "wmsOverlays": "WMS Prekrivanja", + + "recentMessages": "Nedavne poruke" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index bd59c18..4f802e7 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -2605,5 +2605,10 @@ "forestRoads": "Strade Forestali", "showCadastralParcels": "Mostra particelle catastali", "showForestRoads": "Mostra strade forestali", - "wmsOverlays": "Sovrapposizioni WMS" + "wmsOverlays": "Sovrapposizioni WMS", + + "recentMessages": "Messaggi Recenti", + "@recentMessages": { + "description": "Header for recent messages overlay on map in fullscreen mode" + } } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 0bf76ef..948f00d 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3458,6 +3458,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'WMS Overlays'** String get wmsOverlays; + + /// Header for recent messages overlay on map in fullscreen mode + /// + /// In en, this message translates to: + /// **'Recent Messages'** + String get recentMessages; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 68726fc..bcba4bc 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1929,4 +1929,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get wmsOverlays => 'WMS Überlagerungen'; + + @override + String get recentMessages => 'Aktuelle Nachrichten'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 84152c5..35c9604 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1909,4 +1909,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get wmsOverlays => 'WMS Overlays'; + + @override + String get recentMessages => 'Recent Messages'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 02926b7..07f7af1 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1934,4 +1934,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get wmsOverlays => 'Superposiciones WMS'; + + @override + String get recentMessages => 'Mensajes Recientes'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 6c1665a..dcf3728 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1938,4 +1938,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get wmsOverlays => 'Superpositions WMS'; + + @override + String get recentMessages => 'Messages Récents'; } diff --git a/lib/l10n/app_localizations_hr.dart b/lib/l10n/app_localizations_hr.dart index 079ecb9..02c0720 100644 --- a/lib/l10n/app_localizations_hr.dart +++ b/lib/l10n/app_localizations_hr.dart @@ -1918,4 +1918,7 @@ class AppLocalizationsHr extends AppLocalizations { @override String get wmsOverlays => 'WMS Prekrivanja'; + + @override + String get recentMessages => 'Nedavne poruke'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 319ab8f..ac67aae 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1929,4 +1929,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get wmsOverlays => 'Sovrapposizioni WMS'; + + @override + String get recentMessages => 'Messaggi Recenti'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 3249ad0..a418c9a 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1919,4 +1919,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get wmsOverlays => 'WMS Prekrivanja'; + + @override + String get recentMessages => 'Nedavna sporočila'; } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 40ac6ad..9d9be23 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -979,5 +979,7 @@ "forestRoads": "Gozdne ceste", "showCadastralParcels": "Prikaži katastrske parcele", "showForestRoads": "Prikaži gozdne ceste", - "wmsOverlays": "WMS Prekrivanja" + "wmsOverlays": "WMS Prekrivanja", + + "recentMessages": "Nedavna sporočila" } diff --git a/lib/screens/map_tab.dart b/lib/screens/map_tab.dart index 40b833d..785b01a 100644 --- a/lib/screens/map_tab.dart +++ b/lib/screens/map_tab.dart @@ -36,6 +36,7 @@ import '../widgets/map/drawing_layer.dart'; import '../widgets/map/drawing_toolbar.dart'; import '../widgets/map/location_trail_layer.dart'; import '../widgets/map/trail_controls.dart'; +import '../widgets/map/map_message_overlay.dart'; import '../widgets/messages/sar_update_sheet.dart'; import '../l10n/app_localizations.dart'; import 'map_management_screen.dart'; @@ -1739,6 +1740,35 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { child: const Icon(Icons.fullscreen_exit), ), ), + // Message overlay - right side (only shown in fullscreen mode on large screens) + if (_isFullscreen && MediaQuery.of(context).size.width >= 800) + Positioned( + top: 60, + bottom: 60, + right: 16, + width: 300, + child: Consumer( + builder: (context, messagesProvider, _) { + // Get last 20 non-system messages, sorted chronologically + final recentMessages = messagesProvider.messages + .where((m) => !m.isSystemMessage) + .toList() + ..sort((a, b) => a.sentAt.compareTo(b.sentAt)); + final displayMessages = recentMessages.length > 20 + ? recentMessages.sublist(recentMessages.length - 20) + : recentMessages; + + return MapMessageOverlay( + messages: displayMessages, + onNavigateToMessages: widget.onNavigateToMessages, + onMessageTap: (messageId) { + messagesProvider.navigateToMessage(messageId); + widget.onNavigateToMessages?.call(); + }, + ); + }, + ), + ), // Compass widget - top right (hidden in fullscreen mode) if (!_isFullscreen) Positioned( diff --git a/lib/services/map_marker_service.dart b/lib/services/map_marker_service.dart index 9c29385..bcc0307 100644 --- a/lib/services/map_marker_service.dart +++ b/lib/services/map_marker_service.dart @@ -88,11 +88,16 @@ class MapMarkerService { ], ), padding: const EdgeInsets.all(6), - child: Icon( - getContactMarkerIcon(contact), - color: Colors.white, - size: 18, - ), + child: contact.roleEmoji != null + ? Text( + contact.roleEmoji!, + style: const TextStyle(fontSize: 18), + ) + : Icon( + getContactMarkerIcon(contact), + color: Colors.white, + size: 18, + ), ), const SizedBox(height: 2), // Name label (without emoji) diff --git a/lib/services/protocol/frame_parser.dart b/lib/services/protocol/frame_parser.dart index 21b45ae..e29f024 100644 --- a/lib/services/protocol/frame_parser.dart +++ b/lib/services/protocol/frame_parser.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:typed_data'; import '../../models/contact.dart'; import '../../models/message.dart'; @@ -248,7 +249,7 @@ class FrameParser { String? selfName; if (reader.hasRemaining) { final nameBytes = reader.readRemainingBytes(); - selfName = String.fromCharCodes(nameBytes.takeWhile((b) => b != 0)); + selfName = utf8.decode(nameBytes.takeWhile((b) => b != 0).toList()); } return { diff --git a/lib/widgets/map/map_message_overlay.dart b/lib/widgets/map/map_message_overlay.dart new file mode 100644 index 0000000..aae0174 --- /dev/null +++ b/lib/widgets/map/map_message_overlay.dart @@ -0,0 +1,253 @@ +import 'package:flutter/material.dart'; +import '../../models/message.dart'; +import '../../l10n/app_localizations.dart'; + +/// Message overlay widget for displaying recent messages on the map +/// Only shown in fullscreen mode on large screens (>= 800px width) +class MapMessageOverlay extends StatefulWidget { + final List messages; + final VoidCallback? onNavigateToMessages; + final Function(String messageId)? onMessageTap; + + const MapMessageOverlay({ + super.key, + required this.messages, + this.onNavigateToMessages, + this.onMessageTap, + }); + + @override + State createState() => _MapMessageOverlayState(); +} + +class _MapMessageOverlayState extends State { + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + // Scroll to bottom on initial build + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollToBottom(animate: false); + }); + } + + @override + void didUpdateWidget(MapMessageOverlay oldWidget) { + super.didUpdateWidget(oldWidget); + // Auto-scroll to bottom when new messages arrive + if (widget.messages.length > oldWidget.messages.length) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollToBottom(animate: true); + }); + } + } + + void _scrollToBottom({bool animate = true}) { + if (_scrollController.hasClients) { + if (animate) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } else { + _scrollController.jumpTo(_scrollController.position.maxScrollExtent); + } + } + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + Color _getMessageAccentColor(Message message) { + if (message.isSarMarker) { + return Colors.red; + } else if (message.isChannelMessage) { + return Colors.green; + } else if (message.isContactMessage) { + return Colors.blue; + } + return Colors.grey; + } + + IconData _getMessageIcon(Message message) { + if (message.isSarMarker) { + return Icons.emergency; + } else if (message.isChannelMessage) { + return Icons.campaign; + } else if (message.isContactMessage) { + return Icons.person; + } + return Icons.message; + } + + String _truncateText(String text, int maxLength) { + if (text.length <= maxLength) return text; + return '${text.substring(0, maxLength)}...'; + } + + @override + Widget build(BuildContext context) { + if (widget.messages.isEmpty) { + return const SizedBox.shrink(); + } + + return Container( + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.75), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + // Header + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.3), + borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), + ), + child: Row( + children: [ + const Icon( + Icons.message, + color: Colors.white, + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + AppLocalizations.of(context)!.recentMessages, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + Text( + '${widget.messages.length}', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.7), + fontSize: 12, + ), + ), + ], + ), + ), + // Message list + Expanded( + child: ListView.separated( + controller: _scrollController, + padding: const EdgeInsets.all(8), + itemCount: widget.messages.length, + separatorBuilder: (context, index) => const SizedBox(height: 4), + itemBuilder: (context, index) { + final message = widget.messages[index]; + final accentColor = _getMessageAccentColor(message); + final icon = _getMessageIcon(message); + + return GestureDetector( + onTap: () { + widget.onMessageTap?.call(message.id); + }, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: accentColor.withValues(alpha: 0.3), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Sender and time + Row( + children: [ + Icon( + icon, + color: accentColor, + size: 14, + ), + const SizedBox(width: 6), + Expanded( + child: Text( + message.displaySender, + style: TextStyle( + color: accentColor, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Text( + message.timeAgo, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.5), + fontSize: 10, + ), + ), + ], + ), + const SizedBox(height: 4), + // Message preview + Text( + _truncateText(message.text, 60), + style: const TextStyle( + color: Colors.white, + fontSize: 11, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + // SAR marker indicator + if (message.isSarMarker) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Row( + children: [ + Icon( + Icons.location_on, + color: Colors.red.shade300, + size: 12, + ), + const SizedBox(width: 4), + Text( + message.sarMarkerType?.displayName ?? 'SAR Marker', + style: TextStyle( + color: Colors.red.shade300, + fontSize: 10, + fontStyle: FontStyle.italic, + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/map_markers.dart b/lib/widgets/map_markers.dart index ff6e368..a39073c 100644 --- a/lib/widgets/map_markers.dart +++ b/lib/widgets/map_markers.dart @@ -51,7 +51,7 @@ class MapMarkers { ), ), const SizedBox(height: 2), - // Marker icon + // Marker icon or emoji Container( decoration: BoxDecoration( color: _getContactTypeColor(contact, context), @@ -66,11 +66,16 @@ class MapMarkers { ], ), padding: const EdgeInsets.all(6), - child: Icon( - _getContactTypeIcon(contact), - color: Colors.white, - size: 18, - ), + child: contact.roleEmoji != null + ? Text( + contact.roleEmoji!, + style: const TextStyle(fontSize: 18), + ) + : Icon( + _getContactTypeIcon(contact), + color: Colors.white, + size: 18, + ), ), const SizedBox(height: 2), // Name label (without emoji) @@ -214,7 +219,10 @@ class MapMarkers { builder: (context) => AlertDialog( title: Row( children: [ - Icon(Icons.person, color: Theme.of(context).colorScheme.primary), + if (contact.roleEmoji != null) + Text(contact.roleEmoji!, style: const TextStyle(fontSize: 24)) + else + Icon(Icons.person, color: Theme.of(context).colorScheme.primary), const SizedBox(width: 8), Expanded(child: Text(contact.displayName)), ],