mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Enhance message handling for drawings
- Added properties to the Message model to track drawing status and associated drawing ID. - Updated AppProvider to mark messages as drawings and link them to their respective drawing IDs. - Enhanced DrawingProvider to manage received drawings and their visibility. - Implemented navigation to drawings in MapProvider, including dynamic zoom levels based on drawing size. - Updated MessagesProvider to parse drawing messages and manage linked drawings. - Enhanced MessagesTab to support drawing message interactions, including navigation and coordinate copying. - Created DrawingMinimapPreview widget for visual representation of drawings in message bubbles. - Improved DrawingToolbar to handle sharing of drawings, ensuring only unshared drawings are sent. - Added utility functions in DrawingMessageParser for extracting drawing metadata.
This commit is contained in:
@@ -2508,5 +2508,18 @@
|
||||
"sampleEchoSupport": "Unterstützung",
|
||||
"sampleBaseCommand": "Basis-Kommando",
|
||||
"sampleFieldCoordinator": "Feldkoordinator",
|
||||
"sampleMedicalTeam": "Medizinisches Team"
|
||||
"sampleMedicalTeam": "Medizinisches Team",
|
||||
|
||||
"mapDrawing": "Kartenzeichnung",
|
||||
"drawingShared": "Kartenzeichnung",
|
||||
"lineDrawing": "Linie",
|
||||
"rectangleDrawing": "Rechteck",
|
||||
"navigateToDrawing": "Zur Zeichnung navigieren",
|
||||
"hideFromMap": "Von Karte ausblenden",
|
||||
"copyCoordinates": "Koordinaten kopieren",
|
||||
"coordinatesCopiedToClipboard": "Koordinaten in Zwischenablage kopiert",
|
||||
"drawingHidden": "Zeichnung von Karte ausgeblendet",
|
||||
"alreadyShared": "{count} bereits geteilt",
|
||||
"newDrawingsShared": "{count} neue Zeichnung(en) geteilt",
|
||||
"drawingTools": "Zeichenwerkzeuge"
|
||||
}
|
||||
|
||||
@@ -2990,5 +2990,73 @@
|
||||
"sampleMedicalTeam": "Medical Team",
|
||||
"@sampleMedicalTeam": {
|
||||
"description": "Sample team name"
|
||||
},
|
||||
|
||||
"mapDrawing": "Map Drawing",
|
||||
"@mapDrawing": {
|
||||
"description": "Label for map drawing messages"
|
||||
},
|
||||
|
||||
"navigateToDrawing": "Navigate to Drawing",
|
||||
"@navigateToDrawing": {
|
||||
"description": "Option to navigate to drawing on map"
|
||||
},
|
||||
|
||||
"copyCoordinates": "Copy Coordinates",
|
||||
"@copyCoordinates": {
|
||||
"description": "Option to copy coordinates to clipboard"
|
||||
},
|
||||
|
||||
"hideFromMap": "Hide from Map",
|
||||
"@hideFromMap": {
|
||||
"description": "Option to hide drawing from map"
|
||||
},
|
||||
|
||||
"lineDrawing": "Line Drawing",
|
||||
"@lineDrawing": {
|
||||
"description": "Label for line type drawings"
|
||||
},
|
||||
|
||||
"rectangleDrawing": "Rectangle Drawing",
|
||||
"@rectangleDrawing": {
|
||||
"description": "Label for rectangle type drawings"
|
||||
},
|
||||
|
||||
"coordinatesCopiedToClipboard": "Coordinates copied to clipboard",
|
||||
"@coordinatesCopiedToClipboard": {
|
||||
"description": "Success message when coordinates are copied"
|
||||
},
|
||||
|
||||
"drawingShared": "Map Drawing",
|
||||
"@drawingShared": {
|
||||
"description": "Label for shared drawing notifications"
|
||||
},
|
||||
|
||||
"drawingHidden": "Drawing hidden from map",
|
||||
"@drawingHidden": {
|
||||
"description": "Success message when drawing is hidden"
|
||||
},
|
||||
|
||||
"alreadyShared": "{count, plural, =1{1 already shared} other{{count} already shared}}",
|
||||
"@alreadyShared": {
|
||||
"description": "Message showing how many drawings were already shared",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"newDrawingsShared": "Shared {count} new drawing{plural}",
|
||||
"@newDrawingsShared": {
|
||||
"description": "Success message after sharing new drawings",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
},
|
||||
"plural": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2503,5 +2503,18 @@
|
||||
"sampleEchoSupport": "Apoyo",
|
||||
"sampleBaseCommand": "Comando de Base",
|
||||
"sampleFieldCoordinator": "Coordinador de Campo",
|
||||
"sampleMedicalTeam": "Equipo Médico"
|
||||
"sampleMedicalTeam": "Equipo Médico",
|
||||
|
||||
"mapDrawing": "Dibujo del Mapa",
|
||||
"drawingShared": "Dibujo del Mapa",
|
||||
"lineDrawing": "Línea",
|
||||
"rectangleDrawing": "Rectángulo",
|
||||
"navigateToDrawing": "Navegar al Dibujo",
|
||||
"hideFromMap": "Ocultar del Mapa",
|
||||
"copyCoordinates": "Copiar Coordenadas",
|
||||
"coordinatesCopiedToClipboard": "Coordenadas copiadas al portapapeles",
|
||||
"drawingHidden": "Dibujo ocultado del mapa",
|
||||
"alreadyShared": "{count} ya compartido",
|
||||
"newDrawingsShared": "{count} nuevo(s) dibujo(s) compartido(s)",
|
||||
"drawingTools": "Herramientas de Dibujo"
|
||||
}
|
||||
|
||||
@@ -2508,5 +2508,18 @@
|
||||
"sampleEchoSupport": "Soutien",
|
||||
"sampleBaseCommand": "Commandement de Base",
|
||||
"sampleFieldCoordinator": "Coordinateur de Terrain",
|
||||
"sampleMedicalTeam": "Équipe Médicale"
|
||||
"sampleMedicalTeam": "Équipe Médicale",
|
||||
|
||||
"mapDrawing": "Dessin de Carte",
|
||||
"drawingShared": "Dessin de Carte",
|
||||
"lineDrawing": "Ligne",
|
||||
"rectangleDrawing": "Rectangle",
|
||||
"navigateToDrawing": "Naviguer vers le Dessin",
|
||||
"hideFromMap": "Masquer de la Carte",
|
||||
"copyCoordinates": "Copier les Coordonnées",
|
||||
"coordinatesCopiedToClipboard": "Coordonnées copiées dans le presse-papiers",
|
||||
"drawingHidden": "Dessin masqué de la carte",
|
||||
"alreadyShared": "{count} déjà partagé",
|
||||
"newDrawingsShared": "{count} nouveau(x) dessin(s) partagé(s)",
|
||||
"drawingTools": "Outils de Dessin"
|
||||
}
|
||||
|
||||
@@ -918,5 +918,19 @@
|
||||
"sampleEchoSupport": "Podrška",
|
||||
"sampleBaseCommand": "Zapovjedništvo Baze",
|
||||
"sampleFieldCoordinator": "Terenski Koordinator",
|
||||
"sampleMedicalTeam": "Medicinski Tim"
|
||||
"sampleMedicalTeam": "Medicinski Tim",
|
||||
|
||||
"mapDrawing": "Crtež karte",
|
||||
"navigateToDrawing": "Navigiraj do crteža",
|
||||
"copyCoordinates": "Kopiraj koordinate",
|
||||
"hideFromMap": "Sakrij s karte",
|
||||
"lineDrawing": "Linijski crtež",
|
||||
"rectangleDrawing": "Pravokutni crtež",
|
||||
"coordinatesCopiedToClipboard": "Koordinate kopirane u međuspremnik",
|
||||
|
||||
"drawingShared": "Crtež karte",
|
||||
"drawingHidden": "Crtež sakriven s karte",
|
||||
"alreadyShared": "{count, plural, =1{1 već podijeljeno} other{{count} već podijeljeno}}",
|
||||
"newDrawingsShared": "Podijeljeno {count} novi{plural} crtež{plural}",
|
||||
"drawingTools": "Alati za crtanje"
|
||||
}
|
||||
|
||||
@@ -2508,5 +2508,18 @@
|
||||
"sampleEchoSupport": "Supporto",
|
||||
"sampleBaseCommand": "Comando di Base",
|
||||
"sampleFieldCoordinator": "Coordinatore sul Campo",
|
||||
"sampleMedicalTeam": "Team Medico"
|
||||
"sampleMedicalTeam": "Team Medico",
|
||||
|
||||
"mapDrawing": "Disegno della Mappa",
|
||||
"drawingShared": "Disegno della Mappa",
|
||||
"lineDrawing": "Linea",
|
||||
"rectangleDrawing": "Rettangolo",
|
||||
"navigateToDrawing": "Naviga al Disegno",
|
||||
"hideFromMap": "Nascondi dalla Mappa",
|
||||
"copyCoordinates": "Copia Coordinate",
|
||||
"coordinatesCopiedToClipboard": "Coordinate copiate negli appunti",
|
||||
"drawingHidden": "Disegno nascosto dalla mappa",
|
||||
"alreadyShared": "{count} già condiviso",
|
||||
"newDrawingsShared": "{count} nuovo(i) disegno(i) condiviso(i)",
|
||||
"drawingTools": "Strumenti di Disegno"
|
||||
}
|
||||
|
||||
@@ -3188,6 +3188,72 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'Medical Team'**
|
||||
String get sampleMedicalTeam;
|
||||
|
||||
/// Label for map drawing messages
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Map Drawing'**
|
||||
String get mapDrawing;
|
||||
|
||||
/// Option to navigate to drawing on map
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Navigate to Drawing'**
|
||||
String get navigateToDrawing;
|
||||
|
||||
/// Option to copy coordinates to clipboard
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Copy Coordinates'**
|
||||
String get copyCoordinates;
|
||||
|
||||
/// Option to hide drawing from map
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Hide from Map'**
|
||||
String get hideFromMap;
|
||||
|
||||
/// Label for line type drawings
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Line Drawing'**
|
||||
String get lineDrawing;
|
||||
|
||||
/// Label for rectangle type drawings
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Rectangle Drawing'**
|
||||
String get rectangleDrawing;
|
||||
|
||||
/// Success message when coordinates are copied
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Coordinates copied to clipboard'**
|
||||
String get coordinatesCopiedToClipboard;
|
||||
|
||||
/// Label for shared drawing notifications
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Map Drawing'**
|
||||
String get drawingShared;
|
||||
|
||||
/// Success message when drawing is hidden
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Drawing hidden from map'**
|
||||
String get drawingHidden;
|
||||
|
||||
/// Message showing how many drawings were already shared
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count, plural, =1{1 already shared} other{{count} already shared}}'**
|
||||
String alreadyShared(int count);
|
||||
|
||||
/// Success message after sharing new drawings
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Shared {count} new drawing{plural}'**
|
||||
String newDrawingsShared(int count, String plural);
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -1782,4 +1782,42 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get sampleMedicalTeam => 'Medizinisches Team';
|
||||
|
||||
@override
|
||||
String get mapDrawing => 'Kartenzeichnung';
|
||||
|
||||
@override
|
||||
String get navigateToDrawing => 'Zur Zeichnung navigieren';
|
||||
|
||||
@override
|
||||
String get copyCoordinates => 'Koordinaten kopieren';
|
||||
|
||||
@override
|
||||
String get hideFromMap => 'Von Karte ausblenden';
|
||||
|
||||
@override
|
||||
String get lineDrawing => 'Linie';
|
||||
|
||||
@override
|
||||
String get rectangleDrawing => 'Rechteck';
|
||||
|
||||
@override
|
||||
String get coordinatesCopiedToClipboard =>
|
||||
'Koordinaten in Zwischenablage kopiert';
|
||||
|
||||
@override
|
||||
String get drawingShared => 'Kartenzeichnung';
|
||||
|
||||
@override
|
||||
String get drawingHidden => 'Zeichnung von Karte ausgeblendet';
|
||||
|
||||
@override
|
||||
String alreadyShared(int count) {
|
||||
return '$count bereits geteilt';
|
||||
}
|
||||
|
||||
@override
|
||||
String newDrawingsShared(int count, String plural) {
|
||||
return '$count neue Zeichnung(en) geteilt';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1758,4 +1758,47 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get sampleMedicalTeam => 'Medical Team';
|
||||
|
||||
@override
|
||||
String get mapDrawing => 'Map Drawing';
|
||||
|
||||
@override
|
||||
String get navigateToDrawing => 'Navigate to Drawing';
|
||||
|
||||
@override
|
||||
String get copyCoordinates => 'Copy Coordinates';
|
||||
|
||||
@override
|
||||
String get hideFromMap => 'Hide from Map';
|
||||
|
||||
@override
|
||||
String get lineDrawing => 'Line Drawing';
|
||||
|
||||
@override
|
||||
String get rectangleDrawing => 'Rectangle Drawing';
|
||||
|
||||
@override
|
||||
String get coordinatesCopiedToClipboard => 'Coordinates copied to clipboard';
|
||||
|
||||
@override
|
||||
String get drawingShared => 'Map Drawing';
|
||||
|
||||
@override
|
||||
String get drawingHidden => 'Drawing hidden from map';
|
||||
|
||||
@override
|
||||
String alreadyShared(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count already shared',
|
||||
one: '1 already shared',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String newDrawingsShared(int count, String plural) {
|
||||
return 'Shared $count new drawing$plural';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,7 +373,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get deleteDrawing => 'Eliminar dibujo';
|
||||
|
||||
@override
|
||||
String get drawingTools => 'Herramientas de dibujo';
|
||||
String get drawingTools => 'Herramientas de Dibujo';
|
||||
|
||||
@override
|
||||
String get drawLine => 'Dibujar línea';
|
||||
@@ -1783,4 +1783,42 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get sampleMedicalTeam => 'Equipo Médico';
|
||||
|
||||
@override
|
||||
String get mapDrawing => 'Dibujo del Mapa';
|
||||
|
||||
@override
|
||||
String get navigateToDrawing => 'Navegar al Dibujo';
|
||||
|
||||
@override
|
||||
String get copyCoordinates => 'Copiar Coordenadas';
|
||||
|
||||
@override
|
||||
String get hideFromMap => 'Ocultar del Mapa';
|
||||
|
||||
@override
|
||||
String get lineDrawing => 'Línea';
|
||||
|
||||
@override
|
||||
String get rectangleDrawing => 'Rectángulo';
|
||||
|
||||
@override
|
||||
String get coordinatesCopiedToClipboard =>
|
||||
'Coordenadas copiadas al portapapeles';
|
||||
|
||||
@override
|
||||
String get drawingShared => 'Dibujo del Mapa';
|
||||
|
||||
@override
|
||||
String get drawingHidden => 'Dibujo ocultado del mapa';
|
||||
|
||||
@override
|
||||
String alreadyShared(int count) {
|
||||
return '$count ya compartido';
|
||||
}
|
||||
|
||||
@override
|
||||
String newDrawingsShared(int count, String plural) {
|
||||
return '$count nuevo(s) dibujo(s) compartido(s)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,7 +375,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get deleteDrawing => 'Supprimer le dessin';
|
||||
|
||||
@override
|
||||
String get drawingTools => 'Outils de dessin';
|
||||
String get drawingTools => 'Outils de Dessin';
|
||||
|
||||
@override
|
||||
String get drawLine => 'Tracer une ligne';
|
||||
@@ -1789,4 +1789,42 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get sampleMedicalTeam => 'Équipe Médicale';
|
||||
|
||||
@override
|
||||
String get mapDrawing => 'Dessin de Carte';
|
||||
|
||||
@override
|
||||
String get navigateToDrawing => 'Naviguer vers le Dessin';
|
||||
|
||||
@override
|
||||
String get copyCoordinates => 'Copier les Coordonnées';
|
||||
|
||||
@override
|
||||
String get hideFromMap => 'Masquer de la Carte';
|
||||
|
||||
@override
|
||||
String get lineDrawing => 'Ligne';
|
||||
|
||||
@override
|
||||
String get rectangleDrawing => 'Rectangle';
|
||||
|
||||
@override
|
||||
String get coordinatesCopiedToClipboard =>
|
||||
'Coordonnées copiées dans le presse-papiers';
|
||||
|
||||
@override
|
||||
String get drawingShared => 'Dessin de Carte';
|
||||
|
||||
@override
|
||||
String get drawingHidden => 'Dessin masqué de la carte';
|
||||
|
||||
@override
|
||||
String alreadyShared(int count) {
|
||||
return '$count déjà partagé';
|
||||
}
|
||||
|
||||
@override
|
||||
String newDrawingsShared(int count, String plural) {
|
||||
return '$count nouveau(x) dessin(s) partagé(s)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1766,4 +1766,48 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get sampleMedicalTeam => 'Medicinski Tim';
|
||||
|
||||
@override
|
||||
String get mapDrawing => 'Crtež karte';
|
||||
|
||||
@override
|
||||
String get navigateToDrawing => 'Navigiraj do crteža';
|
||||
|
||||
@override
|
||||
String get copyCoordinates => 'Kopiraj koordinate';
|
||||
|
||||
@override
|
||||
String get hideFromMap => 'Sakrij s karte';
|
||||
|
||||
@override
|
||||
String get lineDrawing => 'Linijski crtež';
|
||||
|
||||
@override
|
||||
String get rectangleDrawing => 'Pravokutni crtež';
|
||||
|
||||
@override
|
||||
String get coordinatesCopiedToClipboard =>
|
||||
'Koordinate kopirane u međuspremnik';
|
||||
|
||||
@override
|
||||
String get drawingShared => 'Crtež karte';
|
||||
|
||||
@override
|
||||
String get drawingHidden => 'Crtež sakriven s karte';
|
||||
|
||||
@override
|
||||
String alreadyShared(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count već podijeljeno',
|
||||
one: '1 već podijeljeno',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String newDrawingsShared(int count, String plural) {
|
||||
return 'Podijeljeno $count novi$plural crtež$plural';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,7 +374,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get deleteDrawing => 'Elimina Disegno';
|
||||
|
||||
@override
|
||||
String get drawingTools => 'Strumenti di disegno';
|
||||
String get drawingTools => 'Strumenti di Disegno';
|
||||
|
||||
@override
|
||||
String get drawLine => 'Disegna Linea';
|
||||
@@ -1781,4 +1781,41 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get sampleMedicalTeam => 'Team Medico';
|
||||
|
||||
@override
|
||||
String get mapDrawing => 'Disegno della Mappa';
|
||||
|
||||
@override
|
||||
String get navigateToDrawing => 'Naviga al Disegno';
|
||||
|
||||
@override
|
||||
String get copyCoordinates => 'Copia Coordinate';
|
||||
|
||||
@override
|
||||
String get hideFromMap => 'Nascondi dalla Mappa';
|
||||
|
||||
@override
|
||||
String get lineDrawing => 'Linea';
|
||||
|
||||
@override
|
||||
String get rectangleDrawing => 'Rettangolo';
|
||||
|
||||
@override
|
||||
String get coordinatesCopiedToClipboard => 'Coordinate copiate negli appunti';
|
||||
|
||||
@override
|
||||
String get drawingShared => 'Disegno della Mappa';
|
||||
|
||||
@override
|
||||
String get drawingHidden => 'Disegno nascosto dalla mappa';
|
||||
|
||||
@override
|
||||
String alreadyShared(int count) {
|
||||
return '$count già condiviso';
|
||||
}
|
||||
|
||||
@override
|
||||
String newDrawingsShared(int count, String plural) {
|
||||
return '$count nuovo(i) disegno(i) condiviso(i)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1769,4 +1769,47 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get sampleMedicalTeam => 'Medicinska Ekipa';
|
||||
|
||||
@override
|
||||
String get mapDrawing => 'Risba zemljevida';
|
||||
|
||||
@override
|
||||
String get navigateToDrawing => 'Navigiraj do risbe';
|
||||
|
||||
@override
|
||||
String get copyCoordinates => 'Kopiraj koordinate';
|
||||
|
||||
@override
|
||||
String get hideFromMap => 'Skrij z zemljevida';
|
||||
|
||||
@override
|
||||
String get lineDrawing => 'Linijska risba';
|
||||
|
||||
@override
|
||||
String get rectangleDrawing => 'Pravokotna risba';
|
||||
|
||||
@override
|
||||
String get coordinatesCopiedToClipboard => 'Koordinate kopirane v odložišče';
|
||||
|
||||
@override
|
||||
String get drawingShared => 'Risba zemljevida';
|
||||
|
||||
@override
|
||||
String get drawingHidden => 'Risba skrita z zemljevida';
|
||||
|
||||
@override
|
||||
String alreadyShared(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count že deljeno',
|
||||
one: '1 že deljeno',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String newDrawingsShared(int count, String plural) {
|
||||
return 'Deljeno $count nov$plural risb$plural';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -918,5 +918,19 @@
|
||||
"sampleEchoSupport": "Podpora",
|
||||
"sampleBaseCommand": "Poveljstvo Baze",
|
||||
"sampleFieldCoordinator": "Terenski Koordinator",
|
||||
"sampleMedicalTeam": "Medicinska Ekipa"
|
||||
"sampleMedicalTeam": "Medicinska Ekipa",
|
||||
|
||||
"mapDrawing": "Risba zemljevida",
|
||||
"navigateToDrawing": "Navigiraj do risbe",
|
||||
"copyCoordinates": "Kopiraj koordinate",
|
||||
"hideFromMap": "Skrij z zemljevida",
|
||||
"lineDrawing": "Linijska risba",
|
||||
"rectangleDrawing": "Pravokotna risba",
|
||||
"coordinatesCopiedToClipboard": "Koordinate kopirane v odložišče",
|
||||
|
||||
"drawingShared": "Risba zemljevida",
|
||||
"drawingHidden": "Risba skrita z zemljevida",
|
||||
"alreadyShared": "{count, plural, =1{1 že deljeno} other{{count} že deljeno}}",
|
||||
"newDrawingsShared": "Deljeno {count} nov{plural} risb{plural}",
|
||||
"drawingTools": "Orodja za risanje"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
/// Drawing shape type
|
||||
@@ -72,6 +73,9 @@ abstract class MapDrawing {
|
||||
final String? senderName; // Name of sender (null if local drawing)
|
||||
final bool isReceived; // True if drawing was received from another node
|
||||
final String? messageId; // ID of the source message (for navigation)
|
||||
final bool isShared; // Whether drawing has been broadcast over mesh
|
||||
final bool isSent; // Whether this is a sent drawing (vs received)
|
||||
final bool isHidden; // Temporary visibility toggle (session only, not persisted)
|
||||
|
||||
MapDrawing({
|
||||
required this.id,
|
||||
@@ -81,6 +85,9 @@ abstract class MapDrawing {
|
||||
this.senderName,
|
||||
this.isReceived = false,
|
||||
this.messageId,
|
||||
this.isShared = false,
|
||||
this.isSent = false,
|
||||
this.isHidden = false,
|
||||
});
|
||||
|
||||
/// Convert to JSON for persistence
|
||||
@@ -145,6 +152,12 @@ abstract class MapDrawing {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the center point of the drawing
|
||||
LatLng getCenter();
|
||||
|
||||
/// Get the bounds of the drawing
|
||||
LatLngBounds getBounds();
|
||||
}
|
||||
|
||||
/// Line drawing on map
|
||||
@@ -159,6 +172,9 @@ class LineDrawing extends MapDrawing {
|
||||
super.senderName,
|
||||
super.isReceived,
|
||||
super.messageId,
|
||||
super.isShared,
|
||||
super.isSent,
|
||||
super.isHidden,
|
||||
}) : super(type: DrawingShapeType.line);
|
||||
|
||||
@override
|
||||
@@ -169,6 +185,8 @@ class LineDrawing extends MapDrawing {
|
||||
'color': color.toARGB32(),
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'points': points.map((p) => {'lat': p.latitude, 'lon': p.longitude}).toList(),
|
||||
'isShared': isShared,
|
||||
// Note: isHidden is not persisted - it's session-only
|
||||
};
|
||||
}
|
||||
|
||||
@@ -176,11 +194,15 @@ class LineDrawing extends MapDrawing {
|
||||
Map<String, dynamic> toNetworkJson() {
|
||||
// Ultra-compact format: t=type (0=line, 1=rect), c=color index (0-7), p=points
|
||||
// Points are encoded as flat array [lat1,lon1,lat2,lon2,...]
|
||||
// Coordinates rounded to 5 decimal places (~1m precision, like SAR markers)
|
||||
// Sender is fetched from packet metadata, not included in JSON
|
||||
return {
|
||||
't': type.index,
|
||||
'c': DrawingColors.colorToIndex(color),
|
||||
'p': points.expand((p) => [p.latitude, p.longitude]).toList(),
|
||||
'p': points.expand((p) => [
|
||||
double.parse(p.latitude.toStringAsFixed(5)),
|
||||
double.parse(p.longitude.toStringAsFixed(5)),
|
||||
]).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -196,6 +218,7 @@ class LineDrawing extends MapDrawing {
|
||||
points: points,
|
||||
senderName: senderName,
|
||||
isReceived: senderName != null, // Mark as received if sender is present
|
||||
isShared: json['isShared'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -219,6 +242,7 @@ class LineDrawing extends MapDrawing {
|
||||
senderName: senderName,
|
||||
isReceived: true,
|
||||
messageId: messageId, // Link to source message
|
||||
isShared: false, // Received drawings are not marked as shared
|
||||
);
|
||||
}
|
||||
|
||||
@@ -231,6 +255,41 @@ class LineDrawing extends MapDrawing {
|
||||
points: points ?? this.points,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
LatLng getCenter() {
|
||||
if (points.isEmpty) return LatLng(0, 0);
|
||||
if (points.length == 1) return points[0];
|
||||
|
||||
// Calculate center as average of all points
|
||||
double sumLat = 0;
|
||||
double sumLon = 0;
|
||||
for (final point in points) {
|
||||
sumLat += point.latitude;
|
||||
sumLon += point.longitude;
|
||||
}
|
||||
return LatLng(sumLat / points.length, sumLon / points.length);
|
||||
}
|
||||
|
||||
@override
|
||||
LatLngBounds getBounds() {
|
||||
if (points.isEmpty) return LatLngBounds(LatLng(0, 0), LatLng(0, 0));
|
||||
if (points.length == 1) return LatLngBounds(points[0], points[0]);
|
||||
|
||||
double minLat = points[0].latitude;
|
||||
double maxLat = points[0].latitude;
|
||||
double minLon = points[0].longitude;
|
||||
double maxLon = points[0].longitude;
|
||||
|
||||
for (final point in points) {
|
||||
if (point.latitude < minLat) minLat = point.latitude;
|
||||
if (point.latitude > maxLat) maxLat = point.latitude;
|
||||
if (point.longitude < minLon) minLon = point.longitude;
|
||||
if (point.longitude > maxLon) maxLon = point.longitude;
|
||||
}
|
||||
|
||||
return LatLngBounds(LatLng(minLat, minLon), LatLng(maxLat, maxLon));
|
||||
}
|
||||
}
|
||||
|
||||
/// Rectangle drawing on map
|
||||
@@ -247,6 +306,9 @@ class RectangleDrawing extends MapDrawing {
|
||||
super.senderName,
|
||||
super.isReceived,
|
||||
super.messageId,
|
||||
super.isShared,
|
||||
super.isSent,
|
||||
super.isHidden,
|
||||
}) : super(type: DrawingShapeType.rectangle);
|
||||
|
||||
/// Get all corner points for rendering
|
||||
@@ -267,17 +329,25 @@ class RectangleDrawing extends MapDrawing {
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'topLeft': {'lat': topLeft.latitude, 'lon': topLeft.longitude},
|
||||
'bottomRight': {'lat': bottomRight.latitude, 'lon': bottomRight.longitude},
|
||||
'isShared': isShared,
|
||||
// Note: isHidden is not persisted - it's session-only
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toNetworkJson() {
|
||||
// Ultra-compact format: t=type (0=line, 1=rect), c=color index (0-7), b=bounds [lat1,lon1,lat2,lon2]
|
||||
// Coordinates rounded to 5 decimal places (~1m precision, like SAR markers)
|
||||
// Sender is fetched from packet metadata, not included in JSON
|
||||
return {
|
||||
't': type.index,
|
||||
'c': DrawingColors.colorToIndex(color),
|
||||
'b': [topLeft.latitude, topLeft.longitude, bottomRight.latitude, bottomRight.longitude],
|
||||
'b': [
|
||||
double.parse(topLeft.latitude.toStringAsFixed(5)),
|
||||
double.parse(topLeft.longitude.toStringAsFixed(5)),
|
||||
double.parse(bottomRight.latitude.toStringAsFixed(5)),
|
||||
double.parse(bottomRight.longitude.toStringAsFixed(5)),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -294,6 +364,7 @@ class RectangleDrawing extends MapDrawing {
|
||||
bottomRight: LatLng(bottomRightJson['lat'] as double, bottomRightJson['lon'] as double),
|
||||
senderName: senderName,
|
||||
isReceived: senderName != null, // Mark as received if sender is present
|
||||
isShared: json['isShared'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -314,6 +385,7 @@ class RectangleDrawing extends MapDrawing {
|
||||
senderName: senderName,
|
||||
isReceived: true,
|
||||
messageId: messageId, // Link to source message
|
||||
isShared: false, // Received drawings are not marked as shared
|
||||
);
|
||||
}
|
||||
|
||||
@@ -330,4 +402,19 @@ class RectangleDrawing extends MapDrawing {
|
||||
bottomRight: bottomRight ?? this.bottomRight,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
LatLng getCenter() {
|
||||
// Center is the midpoint between top-left and bottom-right
|
||||
return LatLng(
|
||||
(topLeft.latitude + bottomRight.latitude) / 2,
|
||||
(topLeft.longitude + bottomRight.longitude) / 2,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
LatLngBounds getBounds() {
|
||||
// Bounds are simply the two corners
|
||||
return LatLngBounds(topLeft, bottomRight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,10 @@ class Message {
|
||||
final int echoCount; // Number of times message was detected being rebroadcast
|
||||
final DateTime? firstEchoAt; // When first echo was detected
|
||||
|
||||
// Drawing message tracking
|
||||
final bool isDrawing; // Whether this message contains a map drawing
|
||||
final String? drawingId; // ID of the associated drawing (for navigation)
|
||||
|
||||
Message({
|
||||
required this.id,
|
||||
required this.messageType,
|
||||
@@ -108,6 +112,8 @@ class Message {
|
||||
this.isRead = false,
|
||||
this.echoCount = 0,
|
||||
this.firstEchoAt,
|
||||
this.isDrawing = false,
|
||||
this.drawingId,
|
||||
});
|
||||
|
||||
/// Get SAR marker type by inferring from message content
|
||||
@@ -298,6 +304,22 @@ class Message {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Get drawing metadata from message text (returns null if not a drawing)
|
||||
/// Extracts basic info for display in message bubbles
|
||||
Map<String, dynamic>? get drawingMetadata {
|
||||
if (!isDrawing || !text.startsWith('D:')) return null;
|
||||
|
||||
try {
|
||||
// Return basic metadata (actual parsing happens in DrawingMessageParser)
|
||||
return {
|
||||
'hasDrawing': true,
|
||||
'drawingId': drawingId,
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Message copyWith({
|
||||
String? id,
|
||||
MessageType? messageType,
|
||||
@@ -326,6 +348,8 @@ class Message {
|
||||
bool? isRead,
|
||||
int? echoCount,
|
||||
DateTime? firstEchoAt,
|
||||
bool? isDrawing,
|
||||
String? drawingId,
|
||||
}) {
|
||||
return Message(
|
||||
id: id ?? this.id,
|
||||
@@ -356,6 +380,8 @@ class Message {
|
||||
isRead: isRead ?? this.isRead,
|
||||
echoCount: echoCount ?? this.echoCount,
|
||||
firstEchoAt: firstEchoAt ?? this.firstEchoAt,
|
||||
isDrawing: isDrawing ?? this.isDrawing,
|
||||
drawingId: drawingId ?? this.drawingId,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -150,10 +150,16 @@ class AppProvider with ChangeNotifier {
|
||||
debugPrint(' Drawing linked to message ID: ${message.id}');
|
||||
drawingProvider.addReceivedDrawing(drawing);
|
||||
|
||||
// Add the original drawing message to chat (not a modified info message)
|
||||
// Update message to mark as drawing and link to drawing ID
|
||||
final updatedMessage = message.copyWith(
|
||||
isDrawing: true,
|
||||
drawingId: drawing.id,
|
||||
);
|
||||
|
||||
// Add the drawing message to chat with drawing metadata
|
||||
// This allows users to click on the drawing message to navigate to it
|
||||
messagesProvider.addMessage(
|
||||
message,
|
||||
updatedMessage,
|
||||
contactLookup: (name) => '',
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -31,9 +31,17 @@ class DrawingProvider with ChangeNotifier {
|
||||
Color get selectedColor => _selectedColor;
|
||||
bool get showReceivedDrawings => _showReceivedDrawings;
|
||||
bool get showSarMarkers => _showSarMarkers;
|
||||
List<MapDrawing> get drawings => _showReceivedDrawings
|
||||
? List.unmodifiable(_drawings)
|
||||
: List.unmodifiable(_drawings.where((d) => !d.isReceived).toList());
|
||||
List<MapDrawing> get drawings {
|
||||
// Filter out hidden drawings first
|
||||
var visibleDrawings = _drawings.where((d) => !d.isHidden);
|
||||
|
||||
// Then filter by received status if needed
|
||||
if (!_showReceivedDrawings) {
|
||||
visibleDrawings = visibleDrawings.where((d) => !d.isReceived);
|
||||
}
|
||||
|
||||
return List.unmodifiable(visibleDrawings.toList());
|
||||
}
|
||||
MapDrawing? get currentDrawing => _currentDrawing;
|
||||
List<LatLng> get currentLinePoints => List.unmodifiable(_currentLinePoints);
|
||||
LatLng? get rectangleStartPoint => _rectangleStartPoint;
|
||||
@@ -281,7 +289,155 @@ class DrawingProvider with ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
_drawings.add(drawing);
|
||||
// Mark as received when adding
|
||||
final receivedDrawing = _createReceivedCopy(drawing);
|
||||
_drawings.add(receivedDrawing);
|
||||
_saveDrawings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Create a copy of a drawing marked as received
|
||||
MapDrawing _createReceivedCopy(MapDrawing drawing) {
|
||||
if (drawing is LineDrawing) {
|
||||
return LineDrawing(
|
||||
id: drawing.id,
|
||||
color: drawing.color,
|
||||
createdAt: drawing.createdAt,
|
||||
points: drawing.points,
|
||||
senderName: drawing.senderName,
|
||||
isReceived: true,
|
||||
messageId: drawing.messageId,
|
||||
isShared: drawing.isShared,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: drawing.isHidden,
|
||||
);
|
||||
} else if (drawing is RectangleDrawing) {
|
||||
return RectangleDrawing(
|
||||
id: drawing.id,
|
||||
color: drawing.color,
|
||||
createdAt: drawing.createdAt,
|
||||
topLeft: drawing.topLeft,
|
||||
bottomRight: drawing.bottomRight,
|
||||
senderName: drawing.senderName,
|
||||
isReceived: true,
|
||||
messageId: drawing.messageId,
|
||||
isShared: drawing.isShared,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: drawing.isHidden,
|
||||
);
|
||||
}
|
||||
return drawing;
|
||||
}
|
||||
|
||||
/// Get a drawing by its ID
|
||||
MapDrawing? getDrawingById(String id) {
|
||||
try {
|
||||
return _drawings.firstWhere((d) => d.id == id);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all unshared drawings (local drawings not yet sent)
|
||||
List<MapDrawing> getUnsharedDrawings() {
|
||||
return _drawings.where((d) => !d.isShared && !d.isReceived).toList();
|
||||
}
|
||||
|
||||
/// Mark a drawing as shared
|
||||
void markDrawingAsShared(String id) {
|
||||
final index = _drawings.indexWhere((d) => d.id == id);
|
||||
if (index != -1) {
|
||||
final drawing = _drawings[index];
|
||||
|
||||
// Create a copy with isShared = true
|
||||
if (drawing is LineDrawing) {
|
||||
_drawings[index] = LineDrawing(
|
||||
id: drawing.id,
|
||||
color: drawing.color,
|
||||
createdAt: drawing.createdAt,
|
||||
points: drawing.points,
|
||||
senderName: drawing.senderName,
|
||||
isReceived: drawing.isReceived,
|
||||
messageId: drawing.messageId,
|
||||
isShared: true,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: drawing.isHidden,
|
||||
);
|
||||
} else if (drawing is RectangleDrawing) {
|
||||
_drawings[index] = RectangleDrawing(
|
||||
id: drawing.id,
|
||||
color: drawing.color,
|
||||
createdAt: drawing.createdAt,
|
||||
topLeft: drawing.topLeft,
|
||||
bottomRight: drawing.bottomRight,
|
||||
senderName: drawing.senderName,
|
||||
isReceived: drawing.isReceived,
|
||||
messageId: drawing.messageId,
|
||||
isShared: true,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: drawing.isHidden,
|
||||
);
|
||||
}
|
||||
|
||||
_saveDrawings();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle visibility of a drawing (doesn't save to storage)
|
||||
void toggleDrawingVisibility(String id) {
|
||||
final index = _drawings.indexWhere((d) => d.id == id);
|
||||
if (index != -1) {
|
||||
final drawing = _drawings[index];
|
||||
|
||||
// Create a copy with toggled isHidden flag
|
||||
if (drawing is LineDrawing) {
|
||||
_drawings[index] = LineDrawing(
|
||||
id: drawing.id,
|
||||
color: drawing.color,
|
||||
createdAt: drawing.createdAt,
|
||||
points: drawing.points,
|
||||
senderName: drawing.senderName,
|
||||
isReceived: drawing.isReceived,
|
||||
messageId: drawing.messageId,
|
||||
isShared: drawing.isShared,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: !drawing.isHidden,
|
||||
);
|
||||
} else if (drawing is RectangleDrawing) {
|
||||
_drawings[index] = RectangleDrawing(
|
||||
id: drawing.id,
|
||||
color: drawing.color,
|
||||
createdAt: drawing.createdAt,
|
||||
topLeft: drawing.topLeft,
|
||||
bottomRight: drawing.bottomRight,
|
||||
senderName: drawing.senderName,
|
||||
isReceived: drawing.isReceived,
|
||||
messageId: drawing.messageId,
|
||||
isShared: drawing.isShared,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: !drawing.isHidden,
|
||||
);
|
||||
}
|
||||
|
||||
// Don't save to storage - visibility toggle is temporary
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a drawing and its linked message
|
||||
void removeDrawingAndMessage(String drawingId, dynamic messagesProvider) {
|
||||
final drawing = getDrawingById(drawingId);
|
||||
if (drawing == null) return;
|
||||
|
||||
// Remove the drawing
|
||||
_drawings.removeWhere((d) => d.id == drawingId);
|
||||
|
||||
// If the drawing has a linked message, remove it too
|
||||
if (drawing.messageId != null && messagesProvider != null) {
|
||||
messagesProvider.deleteMessage(drawing.messageId!);
|
||||
}
|
||||
|
||||
_saveDrawings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -44,6 +44,56 @@ class MapProvider with ChangeNotifier {
|
||||
// Don't notify listeners to avoid rebuilds
|
||||
}
|
||||
|
||||
/// Navigate to a drawing by its ID
|
||||
void navigateToDrawing(String drawingId, dynamic drawingProvider) {
|
||||
// Find the drawing in the provider
|
||||
final drawings = drawingProvider.drawings as List;
|
||||
final drawing = drawings.cast<dynamic>().firstWhere(
|
||||
(d) => d.id == drawingId,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
if (drawing == null) {
|
||||
debugPrint('⚠️ [MapProvider] Drawing $drawingId not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use MapDrawing's built-in getCenter and getBounds methods
|
||||
final center = drawing.getCenter();
|
||||
final bounds = drawing.getBounds();
|
||||
|
||||
// Calculate appropriate zoom level based on bounds
|
||||
// For larger drawings, use lower zoom to fit the whole drawing
|
||||
// For smaller drawings, use higher zoom for better detail
|
||||
final latDiff = (bounds.north - bounds.south).abs();
|
||||
final lonDiff = (bounds.east - bounds.west).abs();
|
||||
final maxDiff = latDiff > lonDiff ? latDiff : lonDiff;
|
||||
|
||||
// Zoom scale: smaller drawings get higher zoom
|
||||
// 0.001 degrees (~100m) -> zoom 17
|
||||
// 0.005 degrees (~500m) -> zoom 16
|
||||
// 0.01 degrees (~1km) -> zoom 15
|
||||
// 0.05 degrees (~5km) -> zoom 13
|
||||
// 0.1 degrees (~10km) -> zoom 12
|
||||
double zoom = 15.0;
|
||||
if (maxDiff < 0.001) {
|
||||
zoom = 17.0;
|
||||
} else if (maxDiff < 0.005) {
|
||||
zoom = 16.0;
|
||||
} else if (maxDiff < 0.01) {
|
||||
zoom = 15.0;
|
||||
} else if (maxDiff < 0.05) {
|
||||
zoom = 13.0;
|
||||
} else if (maxDiff < 0.1) {
|
||||
zoom = 12.0;
|
||||
} else {
|
||||
zoom = 10.0;
|
||||
}
|
||||
|
||||
debugPrint('🗺️ [MapProvider] Navigating to drawing: ${drawing.type.name}, zoom: $zoom');
|
||||
navigateToLocation(location: center, zoom: zoom, animate: true);
|
||||
}
|
||||
|
||||
void updateZoom(double zoom) {
|
||||
_targetZoom = zoom;
|
||||
notifyListeners();
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../models/sar_marker.dart';
|
||||
import '../services/message_storage_service.dart';
|
||||
import '../services/notification_service.dart';
|
||||
import '../utils/sar_message_parser.dart';
|
||||
import '../utils/drawing_message_parser.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import 'helpers/message_retry_manager.dart';
|
||||
|
||||
@@ -139,7 +140,25 @@ class MessagesProvider with ChangeNotifier {
|
||||
String Function(String name)? contactLookup,
|
||||
}) {
|
||||
// Always enhance message with SAR parser to detect SAR markers
|
||||
final enhancedMessage = SarMessageParser.enhanceMessage(message);
|
||||
var enhancedMessage = SarMessageParser.enhanceMessage(message);
|
||||
|
||||
// Check if it's a drawing message (D:...) and not already marked
|
||||
// Don't overwrite if already set by the sender (preserves correct drawing ID)
|
||||
if (DrawingMessageParser.isDrawingMessage(enhancedMessage.text) &&
|
||||
!enhancedMessage.isDrawing) {
|
||||
// Parse the drawing to get its ID
|
||||
final drawing = DrawingMessageParser.parseDrawingMessage(
|
||||
enhancedMessage.text,
|
||||
senderName: enhancedMessage.senderName,
|
||||
messageId: enhancedMessage.id,
|
||||
);
|
||||
|
||||
// Mark message as drawing and link to drawing ID
|
||||
enhancedMessage = enhancedMessage.copyWith(
|
||||
isDrawing: true,
|
||||
drawingId: drawing?.id,
|
||||
);
|
||||
}
|
||||
|
||||
// For channel messages with sender name, try to link with contact
|
||||
Message finalMessage = enhancedMessage;
|
||||
@@ -497,6 +516,23 @@ class MessagesProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a drawing message and its linked drawing
|
||||
void deleteDrawingMessage(String messageId, dynamic drawingProvider) {
|
||||
final message = _messages.firstWhere(
|
||||
(m) => m.id == messageId,
|
||||
orElse: () => throw Exception('Message not found'),
|
||||
);
|
||||
|
||||
// If the message has a linked drawing, remove it
|
||||
if (message.drawingId != null && drawingProvider != null) {
|
||||
// Remove the drawing (DrawingProvider will handle removing this message)
|
||||
drawingProvider.removeDrawing(message.drawingId!);
|
||||
} else {
|
||||
// No linked drawing, just delete the message
|
||||
deleteMessage(messageId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all messages
|
||||
void clearMessages() {
|
||||
_messages.clear();
|
||||
@@ -585,7 +621,25 @@ class MessagesProvider with ChangeNotifier {
|
||||
);
|
||||
|
||||
// Always enhance message with SAR parser to detect SAR markers
|
||||
final enhancedMessage = SarMessageParser.enhanceMessage(message);
|
||||
var enhancedMessage = SarMessageParser.enhanceMessage(message);
|
||||
|
||||
// Check if it's a drawing message (D:...) and not already marked
|
||||
// Don't overwrite if already set by the sender (preserves correct drawing ID)
|
||||
if (DrawingMessageParser.isDrawingMessage(enhancedMessage.text) &&
|
||||
!enhancedMessage.isDrawing) {
|
||||
// Parse the drawing to get its ID
|
||||
final drawing = DrawingMessageParser.parseDrawingMessage(
|
||||
enhancedMessage.text,
|
||||
senderName: enhancedMessage.senderName,
|
||||
messageId: enhancedMessage.id,
|
||||
);
|
||||
|
||||
// Mark message as drawing and link to drawing ID
|
||||
enhancedMessage = enhancedMessage.copyWith(
|
||||
isDrawing: true,
|
||||
drawingId: drawing?.id,
|
||||
);
|
||||
}
|
||||
|
||||
// Check for duplicates (shouldn't happen for sent messages, but be safe)
|
||||
if (_isDuplicate(enhancedMessage)) {
|
||||
|
||||
@@ -7,12 +7,15 @@ import '../providers/messages_provider.dart';
|
||||
import '../providers/contacts_provider.dart';
|
||||
import '../providers/map_provider.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../providers/drawing_provider.dart';
|
||||
import '../models/message.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../models/sar_marker.dart';
|
||||
import '../models/map_drawing.dart';
|
||||
import '../widgets/messages/sar_update_sheet.dart';
|
||||
import '../widgets/messages/recipient_selector_sheet.dart';
|
||||
import '../widgets/contacts/direct_message_sheet.dart';
|
||||
import '../widgets/drawing_minimap_preview.dart';
|
||||
import '../services/message_destination_preferences.dart';
|
||||
import '../utils/toast_logger.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
@@ -676,6 +679,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
return _MessageBubble(
|
||||
message: message,
|
||||
isHighlighted: isHighlighted,
|
||||
onNavigateToMap: widget.onNavigateToMap,
|
||||
onTap:
|
||||
message.isSarMarker &&
|
||||
message.sarGpsCoordinates != null
|
||||
@@ -688,6 +692,18 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
);
|
||||
widget.onNavigateToMap();
|
||||
}
|
||||
: message.isDrawing && message.drawingId != null
|
||||
? () {
|
||||
final mapProvider = context
|
||||
.read<MapProvider>();
|
||||
final drawingProvider = context
|
||||
.read<DrawingProvider>();
|
||||
mapProvider.navigateToDrawing(
|
||||
message.drawingId!,
|
||||
drawingProvider,
|
||||
);
|
||||
widget.onNavigateToMap();
|
||||
}
|
||||
: null,
|
||||
);
|
||||
},
|
||||
@@ -809,11 +825,13 @@ class _MessageBubble extends StatelessWidget {
|
||||
final Message message;
|
||||
final VoidCallback? onTap;
|
||||
final bool isHighlighted;
|
||||
final VoidCallback? onNavigateToMap;
|
||||
|
||||
const _MessageBubble({
|
||||
required this.message,
|
||||
this.onTap,
|
||||
this.isHighlighted = false,
|
||||
this.onNavigateToMap,
|
||||
});
|
||||
|
||||
/// Helper method to compare two public keys for equality
|
||||
@@ -957,6 +975,36 @@ class _MessageBubble extends StatelessWidget {
|
||||
_shareLocation(context);
|
||||
},
|
||||
),
|
||||
// Navigate to drawing option (only for drawing messages)
|
||||
if (message.isDrawing && message.drawingId != null)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.map),
|
||||
title: Text(AppLocalizations.of(context)!.navigateToDrawing),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_navigateToDrawing(context);
|
||||
},
|
||||
),
|
||||
// Copy coordinates option (only for drawing messages)
|
||||
if (message.isDrawing && message.drawingId != null)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.copy),
|
||||
title: Text(AppLocalizations.of(context)!.copyCoordinates),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_copyDrawingCoordinates(context);
|
||||
},
|
||||
),
|
||||
// Hide from map option (only for drawing messages)
|
||||
if (message.isDrawing && message.drawingId != null)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.visibility_off),
|
||||
title: Text(AppLocalizations.of(context)!.hideFromMap),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_hideDrawingFromMap(context);
|
||||
},
|
||||
),
|
||||
// Delete message option
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete, color: Colors.red),
|
||||
@@ -1013,6 +1061,85 @@ class _MessageBubble extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToDrawing(BuildContext context) {
|
||||
if (message.drawingId == null) {
|
||||
ToastLogger.error(context, 'No drawing ID available');
|
||||
return;
|
||||
}
|
||||
|
||||
final mapProvider = context.read<MapProvider>();
|
||||
final drawingProvider = context.read<DrawingProvider>();
|
||||
mapProvider.navigateToDrawing(message.drawingId!, drawingProvider);
|
||||
onNavigateToMap?.call();
|
||||
}
|
||||
|
||||
void _copyDrawingCoordinates(BuildContext context) {
|
||||
if (message.drawingId == null) {
|
||||
ToastLogger.error(context, 'No drawing ID available');
|
||||
return;
|
||||
}
|
||||
|
||||
final drawingProvider = context.read<DrawingProvider>();
|
||||
final drawing = drawingProvider.drawings
|
||||
.cast<MapDrawing?>()
|
||||
.firstWhere(
|
||||
(d) => d?.id == message.drawingId,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
if (drawing == null) {
|
||||
ToastLogger.error(context, 'Drawing not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate center coordinates
|
||||
String coordinates;
|
||||
if (drawing is LineDrawing) {
|
||||
// For line drawings, calculate center from all points
|
||||
if (drawing.points.isEmpty) {
|
||||
ToastLogger.error(context, 'No coordinates available');
|
||||
return;
|
||||
}
|
||||
double sumLat = 0;
|
||||
double sumLon = 0;
|
||||
for (final point in drawing.points) {
|
||||
sumLat += point.latitude;
|
||||
sumLon += point.longitude;
|
||||
}
|
||||
final centerLat = sumLat / drawing.points.length;
|
||||
final centerLon = sumLon / drawing.points.length;
|
||||
coordinates = '${centerLat.toStringAsFixed(5)}, ${centerLon.toStringAsFixed(5)}';
|
||||
} else if (drawing is RectangleDrawing) {
|
||||
// For rectangle drawings, calculate center from bounds
|
||||
final centerLat = (drawing.topLeft.latitude + drawing.bottomRight.latitude) / 2;
|
||||
final centerLon = (drawing.topLeft.longitude + drawing.bottomRight.longitude) / 2;
|
||||
coordinates = '${centerLat.toStringAsFixed(5)}, ${centerLon.toStringAsFixed(5)}';
|
||||
} else {
|
||||
ToastLogger.error(context, 'Unknown drawing type');
|
||||
return;
|
||||
}
|
||||
|
||||
Clipboard.setData(ClipboardData(text: coordinates));
|
||||
ToastLogger.success(
|
||||
context,
|
||||
AppLocalizations.of(context)!.coordinatesCopiedToClipboard,
|
||||
);
|
||||
}
|
||||
|
||||
void _hideDrawingFromMap(BuildContext context) {
|
||||
if (message.drawingId == null) {
|
||||
ToastLogger.error(context, 'No drawing ID available');
|
||||
return;
|
||||
}
|
||||
|
||||
final drawingProvider = context.read<DrawingProvider>();
|
||||
drawingProvider.removeDrawing(message.drawingId!);
|
||||
ToastLogger.success(
|
||||
context,
|
||||
AppLocalizations.of(context)!.drawingHidden,
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteConfirmation(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
showDialog(
|
||||
@@ -1029,6 +1156,13 @@ class _MessageBubble extends StatelessWidget {
|
||||
onPressed: () {
|
||||
final messagesProvider = context.read<MessagesProvider>();
|
||||
messagesProvider.deleteMessage(message.id);
|
||||
|
||||
// Also delete the drawing if this is a drawing message
|
||||
if (message.isDrawing && message.drawingId != null) {
|
||||
final drawingProvider = context.read<DrawingProvider>();
|
||||
drawingProvider.removeDrawing(message.drawingId!);
|
||||
}
|
||||
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
@@ -1210,6 +1344,10 @@ class _MessageBubble extends StatelessWidget {
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: isSarMarker
|
||||
? _getSarMarkerColor(context, isDarkMode)
|
||||
: message.isDrawing
|
||||
? (isDarkMode
|
||||
? Theme.of(context).colorScheme.primary.withValues(alpha: 0.15)
|
||||
: Theme.of(context).colorScheme.primary.withValues(alpha: 0.08))
|
||||
: _getMessageBubbleColor(context, isOwnMessage, isDarkMode),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: isHighlighted
|
||||
@@ -1222,6 +1360,11 @@ class _MessageBubble extends StatelessWidget {
|
||||
color: _getSarMarkerBorderColor(context, isDarkMode),
|
||||
width: 2,
|
||||
)
|
||||
: message.isDrawing
|
||||
? Border.all(
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.4),
|
||||
width: 2,
|
||||
)
|
||||
: isOwnMessage
|
||||
? Border.all(
|
||||
color: Theme.of(
|
||||
@@ -1245,12 +1388,12 @@ class _MessageBubble extends StatelessWidget {
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: isSarMarker
|
||||
: isSarMarker || message.isDrawing
|
||||
? [
|
||||
BoxShadow(
|
||||
color: _getSarMarkerBorderColor(
|
||||
context,
|
||||
isDarkMode,
|
||||
color: (isSarMarker
|
||||
? _getSarMarkerBorderColor(context, isDarkMode)
|
||||
: Theme.of(context).colorScheme.primary
|
||||
).withValues(alpha: 0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
@@ -1309,6 +1452,37 @@ class _MessageBubble extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
)
|
||||
else if (message.isDrawing)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.draw,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.mapDrawing,
|
||||
style: Theme.of(context).textTheme.labelSmall
|
||||
?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
if (isOwnMessage)
|
||||
Icon(
|
||||
@@ -1409,6 +1583,96 @@ class _MessageBubble extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
]
|
||||
// Drawing message content
|
||||
else if (message.isDrawing && message.drawingId != null)
|
||||
Consumer<DrawingProvider>(
|
||||
builder: (context, drawingProvider, child) {
|
||||
// Find the drawing by ID
|
||||
final drawing = drawingProvider.drawings
|
||||
.cast<MapDrawing?>()
|
||||
.firstWhere(
|
||||
(d) => d?.id == message.drawingId,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
if (drawing == null) {
|
||||
return Text(
|
||||
message.text,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
);
|
||||
}
|
||||
|
||||
// Determine drawing type label
|
||||
final String drawingTypeLabel;
|
||||
if (drawing is LineDrawing) {
|
||||
drawingTypeLabel = AppLocalizations.of(context)!.lineDrawing;
|
||||
} else if (drawing is RectangleDrawing) {
|
||||
drawingTypeLabel = AppLocalizations.of(context)!.rectangleDrawing;
|
||||
} else {
|
||||
drawingTypeLabel = AppLocalizations.of(context)!.drawing;
|
||||
}
|
||||
|
||||
// Get color name
|
||||
final colorName = DrawingColors.colorToName(drawing.color);
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Minimap preview
|
||||
DrawingMinimapPreview(drawing: drawing),
|
||||
const SizedBox(width: 12),
|
||||
// Drawing info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
drawingTypeLabel,
|
||||
style: Theme.of(context).textTheme.titleSmall
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: drawing.color,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.black26,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
colorName,
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (drawing.senderName != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'From: ${drawing.senderName}',
|
||||
style: Theme.of(context).textTheme.labelSmall
|
||||
?.copyWith(fontStyle: FontStyle.italic),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
// Regular message content
|
||||
else
|
||||
Text(message.text, style: Theme.of(context).textTheme.bodyMedium),
|
||||
|
||||
@@ -49,4 +49,113 @@ class DrawingMessageParser {
|
||||
final jsonStr = jsonEncode(json);
|
||||
return '$prefix$jsonStr';
|
||||
}
|
||||
|
||||
/// Get drawing type display name from drawing message text
|
||||
/// Returns "Line" or "Rectangle", or null if parsing fails
|
||||
static String? getDrawingTypeDisplay(String text) {
|
||||
if (!isDrawingMessage(text)) return null;
|
||||
|
||||
try {
|
||||
final jsonStr = text.substring(prefix.length);
|
||||
final json = jsonDecode(jsonStr) as Map<String, dynamic>;
|
||||
final typeNum = json['t'] as int?;
|
||||
|
||||
if (typeNum == null) return null;
|
||||
|
||||
switch (typeNum) {
|
||||
case 0:
|
||||
return 'Line';
|
||||
case 1:
|
||||
return 'Rectangle';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get color name from drawing message text
|
||||
/// Returns color name like "Red", "Blue", etc., or null if parsing fails
|
||||
static String? getColorName(String text) {
|
||||
if (!isDrawingMessage(text)) return null;
|
||||
|
||||
try {
|
||||
final jsonStr = text.substring(prefix.length);
|
||||
final json = jsonDecode(jsonStr) as Map<String, dynamic>;
|
||||
final colorIndex = json['c'] as int?;
|
||||
|
||||
if (colorIndex == null) return null;
|
||||
|
||||
// Color mapping from DrawingColor enum
|
||||
const colorNames = [
|
||||
'Red', // 0
|
||||
'Blue', // 1
|
||||
'Green', // 2
|
||||
'Yellow', // 3
|
||||
'Orange', // 4
|
||||
'Purple', // 5
|
||||
'Pink', // 6
|
||||
'Cyan', // 7
|
||||
];
|
||||
|
||||
if (colorIndex >= 0 && colorIndex < colorNames.length) {
|
||||
return colorNames[colorIndex];
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get drawing metadata for display in message bubbles
|
||||
/// Returns map with type, color, and pointCount, or null if parsing fails
|
||||
static Map<String, dynamic>? getDrawingMetadata(String text) {
|
||||
if (!isDrawingMessage(text)) return null;
|
||||
|
||||
try {
|
||||
final jsonStr = text.substring(prefix.length);
|
||||
final json = jsonDecode(jsonStr) as Map<String, dynamic>;
|
||||
|
||||
final typeNum = json['t'] as int?;
|
||||
final colorIndex = json['c'] as int?;
|
||||
|
||||
if (typeNum == null || colorIndex == null) return null;
|
||||
|
||||
// Get type display name
|
||||
String type;
|
||||
int? pointCount;
|
||||
|
||||
switch (typeNum) {
|
||||
case 0: // Line
|
||||
type = 'Line';
|
||||
final points = json['p'] as List?;
|
||||
pointCount = points != null ? points.length ~/ 2 : null;
|
||||
break;
|
||||
case 1: // Rectangle
|
||||
type = 'Rectangle';
|
||||
pointCount = 4; // Rectangles always have 4 corners
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get color name
|
||||
const colorNames = [
|
||||
'Red', 'Blue', 'Green', 'Yellow', 'Orange', 'Purple', 'Pink', 'Cyan',
|
||||
];
|
||||
final color = colorIndex >= 0 && colorIndex < colorNames.length
|
||||
? colorNames[colorIndex]
|
||||
: 'Unknown';
|
||||
|
||||
return {
|
||||
'type': type,
|
||||
'color': color,
|
||||
'pointCount': pointCount,
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
135
lib/widgets/drawing_minimap_preview.dart
Normal file
135
lib/widgets/drawing_minimap_preview.dart
Normal file
@@ -0,0 +1,135 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import '../models/map_drawing.dart';
|
||||
|
||||
/// Minimap preview widget for map drawings
|
||||
/// Renders a small 80x80px preview of a drawing on a simple gray background
|
||||
class DrawingMinimapPreview extends StatelessWidget {
|
||||
final MapDrawing drawing;
|
||||
|
||||
const DrawingMinimapPreview({
|
||||
super.key,
|
||||
required this.drawing,
|
||||
});
|
||||
|
||||
/// Calculate bounds for the drawing to fit in the preview
|
||||
LatLngBounds _calculateBounds() {
|
||||
if (drawing is LineDrawing) {
|
||||
final lineDrawing = drawing as LineDrawing;
|
||||
if (lineDrawing.points.isEmpty) {
|
||||
// Fallback to default bounds if no points
|
||||
return LatLngBounds(
|
||||
const LatLng(0, 0),
|
||||
const LatLng(0.01, 0.01),
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate bounds from points
|
||||
double minLat = lineDrawing.points.first.latitude;
|
||||
double maxLat = lineDrawing.points.first.latitude;
|
||||
double minLon = lineDrawing.points.first.longitude;
|
||||
double maxLon = lineDrawing.points.first.longitude;
|
||||
|
||||
for (final point in lineDrawing.points) {
|
||||
if (point.latitude < minLat) minLat = point.latitude;
|
||||
if (point.latitude > maxLat) maxLat = point.latitude;
|
||||
if (point.longitude < minLon) minLon = point.longitude;
|
||||
if (point.longitude > maxLon) maxLon = point.longitude;
|
||||
}
|
||||
|
||||
// Add padding (10% on each side)
|
||||
final latPadding = (maxLat - minLat) * 0.1;
|
||||
final lonPadding = (maxLon - minLon) * 0.1;
|
||||
|
||||
return LatLngBounds(
|
||||
LatLng(minLat - latPadding, minLon - lonPadding),
|
||||
LatLng(maxLat + latPadding, maxLon + lonPadding),
|
||||
);
|
||||
} else if (drawing is RectangleDrawing) {
|
||||
final rectDrawing = drawing as RectangleDrawing;
|
||||
|
||||
// Add padding (10% on each side)
|
||||
final latDiff = (rectDrawing.bottomRight.latitude - rectDrawing.topLeft.latitude).abs();
|
||||
final lonDiff = (rectDrawing.bottomRight.longitude - rectDrawing.topLeft.longitude).abs();
|
||||
final latPadding = latDiff * 0.1;
|
||||
final lonPadding = lonDiff * 0.1;
|
||||
|
||||
return LatLngBounds(
|
||||
LatLng(
|
||||
rectDrawing.topLeft.latitude - latPadding,
|
||||
rectDrawing.topLeft.longitude - lonPadding,
|
||||
),
|
||||
LatLng(
|
||||
rectDrawing.bottomRight.latitude + latPadding,
|
||||
rectDrawing.bottomRight.longitude + lonPadding,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback to default bounds
|
||||
return LatLngBounds(
|
||||
const LatLng(0, 0),
|
||||
const LatLng(0.01, 0.01),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bounds = _calculateBounds();
|
||||
|
||||
return Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade400,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
child: FlutterMap(
|
||||
options: MapOptions(
|
||||
initialCameraFit: CameraFit.bounds(
|
||||
bounds: bounds,
|
||||
padding: const EdgeInsets.all(8),
|
||||
),
|
||||
interactionOptions: const InteractionOptions(
|
||||
flags: InteractiveFlag.none, // Disable all interactions
|
||||
),
|
||||
),
|
||||
children: [
|
||||
// Simple gray background (no tiles for performance)
|
||||
Container(color: Colors.grey.shade300),
|
||||
|
||||
// Render the drawing
|
||||
if (drawing is LineDrawing)
|
||||
PolylineLayer(
|
||||
polylines: [
|
||||
Polyline(
|
||||
points: (drawing as LineDrawing).points,
|
||||
strokeWidth: 3.0,
|
||||
color: drawing.color,
|
||||
),
|
||||
],
|
||||
)
|
||||
else if (drawing is RectangleDrawing)
|
||||
PolygonLayer(
|
||||
polygons: [
|
||||
Polygon(
|
||||
points: (drawing as RectangleDrawing).corners,
|
||||
color: drawing.color.withValues(alpha: 0.3),
|
||||
borderColor: drawing.color,
|
||||
borderStrokeWidth: 3.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import '../../providers/contacts_provider.dart';
|
||||
import '../../providers/messages_provider.dart';
|
||||
import '../../models/map_drawing.dart';
|
||||
import '../../models/contact.dart';
|
||||
import '../../models/message.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
/// Toolbar for drawing controls on the map
|
||||
@@ -376,33 +377,19 @@ class DrawingToolbar extends StatelessWidget {
|
||||
);
|
||||
|
||||
if (!connectionProvider.deviceInfo.isConnected) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.deviceNotConnected),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get device name for sender identification
|
||||
final senderName = connectionProvider.deviceInfo.selfName ?? 'Unknown';
|
||||
|
||||
// Filter drawings (only share local drawings, not received ones)
|
||||
final localDrawings = drawingProvider.drawings
|
||||
.where((d) => !d.isReceived)
|
||||
.toList();
|
||||
// Filter drawings - only share unshared local drawings
|
||||
final unsharedDrawings = drawingProvider.getUnsharedDrawings();
|
||||
|
||||
debugPrint(' Local drawings count: ${localDrawings.length}');
|
||||
debugPrint(' Unshared drawings count: ${unsharedDrawings.length}');
|
||||
debugPrint(' Total drawings count: ${drawingProvider.drawings.length}');
|
||||
|
||||
if (localDrawings.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.noLocalDrawings),
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
if (unsharedDrawings.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -429,8 +416,8 @@ class DrawingToolbar extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.shareDrawingsCount(
|
||||
localDrawings.length,
|
||||
localDrawings.length > 1 ? 's' : '',
|
||||
unsharedDrawings.length,
|
||||
unsharedDrawings.length > 1 ? 's' : '',
|
||||
),
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -451,7 +438,7 @@ class DrawingToolbar extends StatelessWidget {
|
||||
// Share BEFORE popping the navigator
|
||||
await _shareDrawingsToChannel(
|
||||
sheetContext,
|
||||
localDrawings,
|
||||
unsharedDrawings,
|
||||
connectionProvider,
|
||||
senderName,
|
||||
);
|
||||
@@ -476,7 +463,7 @@ class DrawingToolbar extends StatelessWidget {
|
||||
// Share BEFORE popping the navigator
|
||||
await _shareDrawingsToRoom(
|
||||
sheetContext,
|
||||
localDrawings,
|
||||
unsharedDrawings,
|
||||
connectionProvider,
|
||||
senderName,
|
||||
room,
|
||||
@@ -516,9 +503,20 @@ class DrawingToolbar extends StatelessWidget {
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final messagesProvider = Provider.of<MessagesProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
int successCount = 0;
|
||||
int alreadyShared = 0;
|
||||
|
||||
for (final drawing in drawings) {
|
||||
// Skip if already shared
|
||||
if (drawing.isShared) {
|
||||
alreadyShared++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
debugPrint(' Creating message for drawing ${drawing.id}...');
|
||||
// Sender name is no longer included in JSON - will be extracted from packet metadata
|
||||
@@ -526,13 +524,46 @@ class DrawingToolbar extends StatelessWidget {
|
||||
debugPrint(
|
||||
' Message created (${message.length} chars): ${message.substring(0, message.length > 100 ? 100 : message.length)}...',
|
||||
);
|
||||
|
||||
// Create message ID and timestamp
|
||||
final messageId = '${DateTime.now().millisecondsSinceEpoch}_channel_drawing_sent';
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
// Get current device's public key (first 6 bytes)
|
||||
final devicePublicKey = connectionProvider.deviceInfo.publicKey;
|
||||
final senderPublicKeyPrefix = devicePublicKey?.sublist(0, 6);
|
||||
|
||||
// Create sent message object
|
||||
final sentMessage = Message(
|
||||
id: messageId,
|
||||
messageType: MessageType.channel,
|
||||
senderPublicKeyPrefix: senderPublicKeyPrefix,
|
||||
pathLen: 0,
|
||||
textType: MessageTextType.plain,
|
||||
senderTimestamp: timestamp,
|
||||
text: message,
|
||||
receivedAt: DateTime.now(),
|
||||
deliveryStatus: MessageDeliveryStatus.sending,
|
||||
channelIdx: 0,
|
||||
isDrawing: true,
|
||||
drawingId: drawing.id,
|
||||
);
|
||||
|
||||
// Add to messages list with "sending" status
|
||||
messagesProvider.addSentMessage(sentMessage);
|
||||
|
||||
debugPrint(' Sending to channel 0...');
|
||||
await connectionProvider.sendChannelMessage(
|
||||
channelIdx: 0,
|
||||
text: message,
|
||||
messageId: messageId,
|
||||
);
|
||||
debugPrint(' ✅ Sent successfully');
|
||||
|
||||
// Mark as shared after successful send
|
||||
drawingProvider.markDrawingAsShared(drawing.id);
|
||||
successCount++;
|
||||
|
||||
// Small delay between messages to avoid overwhelming the device
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
} catch (e, stackTrace) {
|
||||
@@ -541,36 +572,7 @@ class DrawingToolbar extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint(' Share complete: $successCount/${drawings.length} sent');
|
||||
debugPrint(' Context mounted after send: ${context.mounted}');
|
||||
|
||||
if (!context.mounted) {
|
||||
debugPrint('❌ Context not mounted, cannot show snackbar');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add informational message to chat
|
||||
final messagesProvider = Provider.of<MessagesProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
messagesProvider.logSystemMessage(
|
||||
text:
|
||||
'📤 ${l10n.drawingsSentToPublicChannel(drawings.length, drawings.length > 1 ? 's' : '')}',
|
||||
level: 'info',
|
||||
);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
l10n.drawingsSharedToPublicChannel(successCount, drawings.length),
|
||||
),
|
||||
backgroundColor: successCount == drawings.length
|
||||
? Colors.green
|
||||
: Colors.orange,
|
||||
),
|
||||
);
|
||||
debugPrint(' Share complete: $successCount/${drawings.length} sent, $alreadyShared already shared');
|
||||
}
|
||||
|
||||
/// Share drawings to a specific room
|
||||
@@ -596,9 +598,20 @@ class DrawingToolbar extends StatelessWidget {
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final messagesProvider = Provider.of<MessagesProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
int successCount = 0;
|
||||
int alreadyShared = 0;
|
||||
|
||||
for (final drawing in drawings) {
|
||||
// Skip if already shared
|
||||
if (drawing.isShared) {
|
||||
alreadyShared++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
debugPrint(' Creating message for drawing ${drawing.id}...');
|
||||
// Sender name is no longer included in JSON - will be extracted from packet metadata
|
||||
@@ -606,13 +619,46 @@ class DrawingToolbar extends StatelessWidget {
|
||||
debugPrint(
|
||||
' Message created (${message.length} chars): ${message.substring(0, message.length > 100 ? 100 : message.length)}...',
|
||||
);
|
||||
|
||||
// Create message ID and timestamp
|
||||
final messageId = '${DateTime.now().millisecondsSinceEpoch}_contact_drawing_sent';
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
// Get current device's public key (first 6 bytes)
|
||||
final devicePublicKey = connectionProvider.deviceInfo.publicKey;
|
||||
final senderPublicKeyPrefix = devicePublicKey?.sublist(0, 6);
|
||||
|
||||
// Create sent message object
|
||||
final sentMessage = Message(
|
||||
id: messageId,
|
||||
messageType: MessageType.contact,
|
||||
senderPublicKeyPrefix: senderPublicKeyPrefix,
|
||||
pathLen: 0,
|
||||
textType: MessageTextType.plain,
|
||||
senderTimestamp: timestamp,
|
||||
text: message,
|
||||
receivedAt: DateTime.now(),
|
||||
deliveryStatus: MessageDeliveryStatus.sending,
|
||||
recipientPublicKey: room.publicKey,
|
||||
isDrawing: true,
|
||||
drawingId: drawing.id,
|
||||
);
|
||||
|
||||
// Add to messages list with "sending" status
|
||||
messagesProvider.addSentMessage(sentMessage);
|
||||
|
||||
debugPrint(' Sending to room ${room.advName}...');
|
||||
await connectionProvider.sendTextMessage(
|
||||
contactPublicKey: room.publicKey,
|
||||
text: message,
|
||||
messageId: messageId,
|
||||
);
|
||||
debugPrint(' ✅ Sent successfully');
|
||||
|
||||
// Mark as shared after successful send
|
||||
drawingProvider.markDrawingAsShared(drawing.id);
|
||||
successCount++;
|
||||
|
||||
// Small delay between messages to avoid overwhelming the device
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
} catch (e, stackTrace) {
|
||||
@@ -623,44 +669,6 @@ class DrawingToolbar extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint(' Share complete: $successCount/${drawings.length} sent');
|
||||
debugPrint(' Context mounted after send: ${context.mounted}');
|
||||
|
||||
if (!context.mounted) {
|
||||
debugPrint('❌ Context not mounted, cannot show snackbar');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add informational message to chat
|
||||
final messagesProvider = Provider.of<MessagesProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final l10nRoom = AppLocalizations.of(context)!;
|
||||
messagesProvider.logSystemMessage(
|
||||
text:
|
||||
'📤 ${l10nRoom.sentDrawingsToRoom(
|
||||
drawings.length,
|
||||
drawings.length > 1 ? 's' : '',
|
||||
room.advName,
|
||||
)}',
|
||||
level: 'info',
|
||||
);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
l10nRoom.sharedDrawingsToRoom(
|
||||
successCount,
|
||||
drawings.length,
|
||||
drawings.length > 1 ? 's' : '',
|
||||
room.advName,
|
||||
),
|
||||
),
|
||||
backgroundColor: successCount == drawings.length
|
||||
? Colors.green
|
||||
: Colors.orange,
|
||||
),
|
||||
);
|
||||
debugPrint(' Share complete: $successCount/${drawings.length} sent, $alreadyShared already shared');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user