feat: Add recent messages overlay to map in fullscreen mode and update localization for recent messages

This commit is contained in:
Janez T
2025-10-28 00:14:14 +01:00
parent f6155913a1
commit b980fb80f1
21 changed files with 370 additions and 23 deletions

View File

@@ -5,22 +5,22 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000222">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000198">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.252463">
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.259253">
</testcase>
<testcase classname="fastlane.lanes" name="2: build_app" time="118.067928">
<testcase classname="fastlane.lanes" name="2: build_app" time="120.957994">
</testcase>
<testcase classname="fastlane.lanes" name="3: upload_to_testflight" time="295.388945">
<testcase classname="fastlane.lanes" name="3: upload_to_testflight" time="318.329185">
</testcase>

View File

@@ -2605,5 +2605,7 @@
"forestRoads": "Waldwege",
"showCadastralParcels": "Katasterparzellen anzeigen",
"showForestRoads": "Waldwege anzeigen",
"wmsOverlays": "WMS Überlagerungen"
"wmsOverlays": "WMS Überlagerungen",
"recentMessages": "Aktuelle Nachrichten"
}

View File

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

View File

@@ -2600,5 +2600,7 @@
"forestRoads": "Caminos Forestales",
"showCadastralParcels": "Mostrar parcelas catastrales",
"showForestRoads": "Mostrar caminos forestales",
"wmsOverlays": "Superposiciones WMS"
"wmsOverlays": "Superposiciones WMS",
"recentMessages": "Mensajes Recientes"
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1929,4 +1929,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get wmsOverlays => 'WMS Überlagerungen';
@override
String get recentMessages => 'Aktuelle Nachrichten';
}

View File

@@ -1909,4 +1909,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get wmsOverlays => 'WMS Overlays';
@override
String get recentMessages => 'Recent Messages';
}

View File

@@ -1934,4 +1934,7 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get wmsOverlays => 'Superposiciones WMS';
@override
String get recentMessages => 'Mensajes Recientes';
}

View File

@@ -1938,4 +1938,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get wmsOverlays => 'Superpositions WMS';
@override
String get recentMessages => 'Messages Récents';
}

View File

@@ -1918,4 +1918,7 @@ class AppLocalizationsHr extends AppLocalizations {
@override
String get wmsOverlays => 'WMS Prekrivanja';
@override
String get recentMessages => 'Nedavne poruke';
}

View File

@@ -1929,4 +1929,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get wmsOverlays => 'Sovrapposizioni WMS';
@override
String get recentMessages => 'Messaggi Recenti';
}

View File

@@ -1919,4 +1919,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get wmsOverlays => 'WMS Prekrivanja';
@override
String get recentMessages => 'Nedavna sporočila';
}

View File

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

View File

@@ -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<MapTab> 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<MessagesProvider>(
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(

View File

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

View File

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

View File

@@ -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<Message> messages;
final VoidCallback? onNavigateToMessages;
final Function(String messageId)? onMessageTap;
const MapMessageOverlay({
super.key,
required this.messages,
this.onNavigateToMessages,
this.onMessageTap,
});
@override
State<MapMessageOverlay> createState() => _MapMessageOverlayState();
}
class _MapMessageOverlayState extends State<MapMessageOverlay> {
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,
),
),
],
),
),
],
),
),
);
},
),
),
],
),
);
}
}

View File

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