feat: Add drawing tools localization and update SAR message handling

This commit is contained in:
Janez T
2025-10-23 19:33:35 +02:00
parent 98cda8c976
commit 200e506df8
19 changed files with 129 additions and 21 deletions

View File

@@ -614,6 +614,11 @@
"description": "Aktion zum Löschen einer Kartenzeichnung" "description": "Aktion zum Löschen einer Kartenzeichnung"
}, },
"drawingTools": "Zeichenwerkzeuge",
"@drawingTools": {
"description": "Zeichenwerkzeuge Abschnitt oder Menütitel"
},
"drawLine": "Linie zeichnen", "drawLine": "Linie zeichnen",
"@drawLine": { "@drawLine": {
"description": "Kartenzeichnungsmodus: Linie" "description": "Kartenzeichnungsmodus: Linie"

View File

@@ -614,6 +614,11 @@
"description": "Action to delete a map drawing" "description": "Action to delete a map drawing"
}, },
"drawingTools": "Drawing Tools",
"@drawingTools": {
"description": "Drawing tools section or menu title"
},
"drawLine": "Draw Line", "drawLine": "Draw Line",
"@drawLine": { "@drawLine": {
"description": "Map drawing mode: line" "description": "Map drawing mode: line"

View File

@@ -614,6 +614,11 @@
"description": "Acción para eliminar un dibujo del mapa" "description": "Acción para eliminar un dibujo del mapa"
}, },
"drawingTools": "Herramientas de dibujo",
"@drawingTools": {
"description": "Sección de herramientas de dibujo o título del menú"
},
"drawLine": "Dibujar línea", "drawLine": "Dibujar línea",
"@drawLine": { "@drawLine": {
"description": "Modo de dibujo del mapa: línea" "description": "Modo de dibujo del mapa: línea"

View File

@@ -614,6 +614,11 @@
"description": "Action pour supprimer un dessin de carte" "description": "Action pour supprimer un dessin de carte"
}, },
"drawingTools": "Outils de dessin",
"@drawingTools": {
"description": "Section des outils de dessin ou titre du menu"
},
"drawLine": "Tracer une ligne", "drawLine": "Tracer une ligne",
"@drawLine": { "@drawLine": {
"description": "Mode de dessin de carte : ligne" "description": "Mode de dessin de carte : ligne"

View File

@@ -213,6 +213,8 @@
"deleteDrawing": "Izbriši crtež", "deleteDrawing": "Izbriši crtež",
"drawingTools": "Alati za crtanje",
"drawLine": "Nacrtaj liniju", "drawLine": "Nacrtaj liniju",
"drawLineDesc": "Nacrtaj slobodnu liniju na karti", "drawLineDesc": "Nacrtaj slobodnu liniju na karti",

View File

@@ -614,6 +614,11 @@
"description": "Azione per eliminare un disegno sulla mappa" "description": "Azione per eliminare un disegno sulla mappa"
}, },
"drawingTools": "Strumenti di disegno",
"@drawingTools": {
"description": "Sezione strumenti di disegno o titolo del menu"
},
"drawLine": "Disegna Linea", "drawLine": "Disegna Linea",
"@drawLine": { "@drawLine": {
"description": "Modalità disegno mappa: linea" "description": "Modalità disegno mappa: linea"

View File

@@ -749,6 +749,12 @@ abstract class AppLocalizations {
/// **'Delete Drawing'** /// **'Delete Drawing'**
String get deleteDrawing; String get deleteDrawing;
/// Drawing tools section or menu title
///
/// In en, this message translates to:
/// **'Drawing Tools'**
String get drawingTools;
/// Map drawing mode: line /// Map drawing mode: line
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

@@ -374,6 +374,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get deleteDrawing => 'Zeichnung löschen'; String get deleteDrawing => 'Zeichnung löschen';
@override
String get drawingTools => 'Zeichenwerkzeuge';
@override @override
String get drawLine => 'Linie zeichnen'; String get drawLine => 'Linie zeichnen';

View File

@@ -371,6 +371,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get deleteDrawing => 'Delete Drawing'; String get deleteDrawing => 'Delete Drawing';
@override
String get drawingTools => 'Drawing Tools';
@override @override
String get drawLine => 'Draw Line'; String get drawLine => 'Draw Line';

View File

@@ -372,6 +372,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get deleteDrawing => 'Eliminar dibujo'; String get deleteDrawing => 'Eliminar dibujo';
@override
String get drawingTools => 'Herramientas de dibujo';
@override @override
String get drawLine => 'Dibujar línea'; String get drawLine => 'Dibujar línea';

View File

@@ -374,6 +374,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get deleteDrawing => 'Supprimer le dessin'; String get deleteDrawing => 'Supprimer le dessin';
@override
String get drawingTools => 'Outils de dessin';
@override @override
String get drawLine => 'Tracer une ligne'; String get drawLine => 'Tracer une ligne';

View File

@@ -371,6 +371,9 @@ class AppLocalizationsHr extends AppLocalizations {
@override @override
String get deleteDrawing => 'Izbriši crtež'; String get deleteDrawing => 'Izbriši crtež';
@override
String get drawingTools => 'Alati za crtanje';
@override @override
String get drawLine => 'Nacrtaj liniju'; String get drawLine => 'Nacrtaj liniju';

View File

@@ -373,6 +373,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get deleteDrawing => 'Elimina Disegno'; String get deleteDrawing => 'Elimina Disegno';
@override
String get drawingTools => 'Strumenti di disegno';
@override @override
String get drawLine => 'Disegna Linea'; String get drawLine => 'Disegna Linea';

View File

@@ -371,6 +371,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get deleteDrawing => 'Izbriši risbo'; String get deleteDrawing => 'Izbriši risbo';
@override
String get drawingTools => 'Orodja za risanje';
@override @override
String get drawLine => 'Nariši črto'; String get drawLine => 'Nariši črto';

View File

@@ -213,6 +213,8 @@
"deleteDrawing": "Izbriši risbo", "deleteDrawing": "Izbriši risbo",
"drawingTools": "Orodja za risanje",
"drawLine": "Nariši črto", "drawLine": "Nariši črto",
"drawLineDesc": "Nariši prosto črto na zemljevidu", "drawLineDesc": "Nariši prosto črto na zemljevidu",

View File

@@ -49,7 +49,6 @@ class Message {
// SAR marker data (if this is a SAR message) // SAR marker data (if this is a SAR message)
final bool isSarMarker; final bool isSarMarker;
final SarMarkerType? sarMarkerType;
final LatLng? sarGpsCoordinates; final LatLng? sarGpsCoordinates;
final String? sarNotes; // Optional message/notes for SAR marker final String? sarNotes; // Optional message/notes for SAR marker
final String? sarCustomEmoji; // Custom emoji for unknown SAR marker types final String? sarCustomEmoji; // Custom emoji for unknown SAR marker types
@@ -91,7 +90,6 @@ class Message {
required this.senderTimestamp, required this.senderTimestamp,
required this.text, required this.text,
this.isSarMarker = false, this.isSarMarker = false,
this.sarMarkerType,
this.sarGpsCoordinates, this.sarGpsCoordinates,
this.sarNotes, this.sarNotes,
this.sarCustomEmoji, this.sarCustomEmoji,
@@ -112,6 +110,28 @@ class Message {
this.firstEchoAt, this.firstEchoAt,
}); });
/// Get SAR marker type by inferring from message content
/// Returns the type inferred from sarCustomEmoji or by parsing the message text
SarMarkerType? get sarMarkerType {
if (!isSarMarker) return null;
// If we have a custom emoji stored, infer type from it
if (sarCustomEmoji != null && sarCustomEmoji!.isNotEmpty) {
return SarMarkerType.fromEmoji(sarCustomEmoji!);
}
// Otherwise, parse the message text to extract the emoji
final trimmed = text.trim();
if (!trimmed.startsWith('S:')) return null;
// Extract emoji from format: S:<emoji>:... or S:<emoji>:<colorIndex>:...
final parts = trimmed.split(':');
if (parts.length < 3) return null;
final emoji = parts[1];
return SarMarkerType.fromEmoji(emoji);
}
/// Get sender public key as hex string /// Get sender public key as hex string
String? get senderKeyShort { String? get senderKeyShort {
if (senderPublicKeyPrefix == null) return null; if (senderPublicKeyPrefix == null) return null;
@@ -288,7 +308,6 @@ class Message {
int? senderTimestamp, int? senderTimestamp,
String? text, String? text,
bool? isSarMarker, bool? isSarMarker,
SarMarkerType? sarMarkerType,
LatLng? sarGpsCoordinates, LatLng? sarGpsCoordinates,
String? sarNotes, String? sarNotes,
String? sarCustomEmoji, String? sarCustomEmoji,
@@ -319,7 +338,6 @@ class Message {
senderTimestamp: senderTimestamp ?? this.senderTimestamp, senderTimestamp: senderTimestamp ?? this.senderTimestamp,
text: text ?? this.text, text: text ?? this.text,
isSarMarker: isSarMarker ?? this.isSarMarker, isSarMarker: isSarMarker ?? this.isSarMarker,
sarMarkerType: sarMarkerType ?? this.sarMarkerType,
sarGpsCoordinates: sarGpsCoordinates ?? this.sarGpsCoordinates, sarGpsCoordinates: sarGpsCoordinates ?? this.sarGpsCoordinates,
sarNotes: sarNotes ?? this.sarNotes, sarNotes: sarNotes ?? this.sarNotes,
sarCustomEmoji: sarCustomEmoji ?? this.sarCustomEmoji, sarCustomEmoji: sarCustomEmoji ?? this.sarCustomEmoji,

View File

@@ -2,7 +2,6 @@ import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../models/message.dart'; import '../models/message.dart';
import '../models/sar_marker.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
/// Service for persisting messages to local storage /// Service for persisting messages to local storage
@@ -111,7 +110,6 @@ class MessageStorageService {
'senderTimestamp': message.senderTimestamp, 'senderTimestamp': message.senderTimestamp,
'text': message.text, 'text': message.text,
'isSarMarker': message.isSarMarker, 'isSarMarker': message.isSarMarker,
'sarMarkerType': message.sarMarkerType?.name,
'sarGpsLat': message.sarGpsCoordinates?.latitude, 'sarGpsLat': message.sarGpsCoordinates?.latitude,
'sarGpsLon': message.sarGpsCoordinates?.longitude, 'sarGpsLon': message.sarGpsCoordinates?.longitude,
'receivedAtMillis': message.receivedAt.millisecondsSinceEpoch, 'receivedAtMillis': message.receivedAt.millisecondsSinceEpoch,
@@ -148,12 +146,6 @@ class MessageStorageService {
senderTimestamp: json['senderTimestamp'] as int, senderTimestamp: json['senderTimestamp'] as int,
text: json['text'] as String, text: json['text'] as String,
isSarMarker: json['isSarMarker'] as bool? ?? false, isSarMarker: json['isSarMarker'] as bool? ?? false,
sarMarkerType: json['sarMarkerType'] != null
? SarMarkerType.values.firstWhere(
(e) => e.name == json['sarMarkerType'],
orElse: () => SarMarkerType.unknown,
)
: null,
sarGpsCoordinates: sarGpsCoordinates:
json['sarGpsLat'] != null && json['sarGpsLon'] != null json['sarGpsLat'] != null && json['sarGpsLon'] != null
? LatLng(json['sarGpsLat'] as double, json['sarGpsLon'] as double) ? LatLng(json['sarGpsLat'] as double, json['sarGpsLon'] as double)

View File

@@ -1,10 +1,10 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import '../models/contact_telemetry.dart'; import '../models/contact_telemetry.dart';
import '../models/message.dart'; import '../models/message.dart';
import '../models/sar_marker.dart';
import '../l10n/app_localizations.dart'; import '../l10n/app_localizations.dart';
/// Generates sample data for testing/demo purposes /// Generates sample data for testing/demo purposes
@@ -148,8 +148,8 @@ class SampleDataGenerator {
text: 'S:🧑:${lat.toStringAsFixed(5)},${lon.toStringAsFixed(5)}', text: 'S:🧑:${lat.toStringAsFixed(5)},${lon.toStringAsFixed(5)}',
receivedAt: timestamp, receivedAt: timestamp,
isSarMarker: true, isSarMarker: true,
sarMarkerType: SarMarkerType.foundPerson,
sarGpsCoordinates: LatLng(lat, lon), sarGpsCoordinates: LatLng(lat, lon),
sarCustomEmoji: '🧑',
senderName: l10n.sampleTeamMember, senderName: l10n.sampleTeamMember,
)); ));
messageId++; messageId++;
@@ -177,8 +177,8 @@ class SampleDataGenerator {
text: 'S:🔥:${lat.toStringAsFixed(5)},${lon.toStringAsFixed(5)}', text: 'S:🔥:${lat.toStringAsFixed(5)},${lon.toStringAsFixed(5)}',
receivedAt: timestamp, receivedAt: timestamp,
isSarMarker: true, isSarMarker: true,
sarMarkerType: SarMarkerType.fire,
sarGpsCoordinates: LatLng(lat, lon), sarGpsCoordinates: LatLng(lat, lon),
sarCustomEmoji: '🔥',
senderName: l10n.sampleScout, senderName: l10n.sampleScout,
)); ));
messageId++; messageId++;
@@ -206,8 +206,8 @@ class SampleDataGenerator {
text: 'S:🏕️:${lat.toStringAsFixed(5)},${lon.toStringAsFixed(5)}', text: 'S:🏕️:${lat.toStringAsFixed(5)},${lon.toStringAsFixed(5)}',
receivedAt: timestamp, receivedAt: timestamp,
isSarMarker: true, isSarMarker: true,
sarMarkerType: SarMarkerType.stagingArea,
sarGpsCoordinates: LatLng(lat, lon), sarGpsCoordinates: LatLng(lat, lon),
sarCustomEmoji: '🏕️',
senderName: l10n.sampleBase, senderName: l10n.sampleBase,
)); ));
messageId++; messageId++;
@@ -242,8 +242,8 @@ class SampleDataGenerator {
text: 'S:📦:${lat.toStringAsFixed(5)},${lon.toStringAsFixed(5)}${notes[i % notes.length]}', text: 'S:📦:${lat.toStringAsFixed(5)},${lon.toStringAsFixed(5)}${notes[i % notes.length]}',
receivedAt: timestamp, receivedAt: timestamp,
isSarMarker: true, isSarMarker: true,
sarMarkerType: SarMarkerType.object,
sarGpsCoordinates: LatLng(lat, lon), sarGpsCoordinates: LatLng(lat, lon),
sarCustomEmoji: '📦',
senderName: l10n.sampleSearcher, senderName: l10n.sampleSearcher,
)); ));
messageId++; messageId++;
@@ -252,6 +252,51 @@ class SampleDataGenerator {
return messages; return messages;
} }
/// Generate sample map drawings
static List<dynamic> generateDrawings({
required LatLng centerLocation,
required AppLocalizations l10n,
}) {
final drawings = <dynamic>[];
final now = DateTime.now();
// Generate a line drawing (e.g., search path)
final linePoints = <LatLng>[
LatLng(centerLocation.latitude + 0.002, centerLocation.longitude - 0.003),
LatLng(centerLocation.latitude + 0.004, centerLocation.longitude - 0.002),
LatLng(centerLocation.latitude + 0.005, centerLocation.longitude + 0.001),
LatLng(centerLocation.latitude + 0.003, centerLocation.longitude + 0.003),
];
drawings.add({
'type': 'line',
'id': 'sample_line_${now.millisecondsSinceEpoch}',
'color': Colors.blue.toARGB32(),
'createdAt': now.subtract(const Duration(minutes: 15)).toIso8601String(),
'points': linePoints.map((p) => {'lat': p.latitude, 'lon': p.longitude}).toList(),
'sender': l10n.sampleTeamMember,
});
// Generate a rectangle drawing (e.g., search area)
drawings.add({
'type': 'rectangle',
'id': 'sample_rect_${now.millisecondsSinceEpoch + 1}',
'color': Colors.red.toARGB32(),
'createdAt': now.subtract(const Duration(minutes: 10)).toIso8601String(),
'topLeft': {
'lat': centerLocation.latitude - 0.003,
'lon': centerLocation.longitude - 0.004,
},
'bottomRight': {
'lat': centerLocation.latitude - 0.001,
'lon': centerLocation.longitude - 0.001,
},
'sender': l10n.sampleScout,
});
return drawings;
}
/// Generate sample channel messages for public channels /// Generate sample channel messages for public channels
static List<Message> generateChannelMessages({ static List<Message> generateChannelMessages({
LatLng? centerLocation, LatLng? centerLocation,

View File

@@ -117,12 +117,9 @@ class SarMessageParser {
return message.copyWith( return message.copyWith(
isSarMarker: true, isSarMarker: true,
sarMarkerType: sarInfo.type,
sarGpsCoordinates: sarInfo.location, sarGpsCoordinates: sarInfo.location,
sarNotes: sarInfo.notes, // Extract and store notes sarNotes: sarInfo.notes, // Extract and store notes
sarCustomEmoji: sarInfo.type == SarMarkerType.unknown sarCustomEmoji: sarInfo.emoji, // Always store emoji for type inference
? sarInfo.emoji // Preserve custom emoji for unknown types
: null,
sarColorIndex: sarInfo.colorIndex, // Store color index sarColorIndex: sarInfo.colorIndex, // Store color index
); );
} }