From 9d923f8118164158e51afab1c2c08e9346e1e72f Mon Sep 17 00:00:00 2001 From: Janez T Date: Thu, 23 Oct 2025 20:50:14 +0200 Subject: [PATCH] Add drawing sharing functionality and localization updates - Implemented sharing options for drawings, including sharing to a public channel and specific rooms. - Added confirmation dialogs for deleting drawings and sharing drawings. - Enhanced the drawing toolbar to display a list of user's drawings with options to share or delete. - Updated localization files for English, Spanish, French, Croatian, Italian, German, Slovenian, and other languages to include new strings related to drawing sharing and management. - Introduced syncing of drawing messages with the DrawingProvider to restore missing drawings. - Improved the drawing minimap preview to display a small preview of drawings in the toolbar. --- lib/l10n/app_de.arb | 12 +- lib/l10n/app_en.arb | 55 +++ lib/l10n/app_es.arb | 12 +- lib/l10n/app_fr.arb | 12 +- lib/l10n/app_hr.arb | 12 +- lib/l10n/app_it.arb | 12 +- lib/l10n/app_localizations.dart | 60 ++++ lib/l10n/app_localizations_de.dart | 34 ++ lib/l10n/app_localizations_en.dart | 33 ++ lib/l10n/app_localizations_es.dart | 35 ++ lib/l10n/app_localizations_fr.dart | 35 ++ lib/l10n/app_localizations_hr.dart | 34 ++ lib/l10n/app_localizations_it.dart | 35 ++ lib/l10n/app_localizations_sl.dart | 33 ++ lib/l10n/app_sl.arb | 12 +- lib/providers/app_provider.dart | 5 + lib/providers/messages_provider.dart | 100 +++++- lib/screens/map_tab.dart | 11 +- lib/screens/messages_tab.dart | 15 +- lib/widgets/drawing_minimap_preview.dart | 41 ++- lib/widgets/map/drawing_toolbar.dart | 404 +++++++++++++++++++++-- 21 files changed, 941 insertions(+), 61 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index e5967d7..bbd27d4 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -2521,5 +2521,15 @@ "drawingHidden": "Zeichnung von Karte ausgeblendet", "alreadyShared": "{count} bereits geteilt", "newDrawingsShared": "{count} neue Zeichnung(en) geteilt", - "drawingTools": "Zeichenwerkzeuge" + "drawingTools": "Zeichenwerkzeuge", + "shareDrawing": "Zeichnung teilen", + "shareWithAllNearbyDevices": "Mit allen Geräten in der Nähe teilen", + "shareToRoom": "In Raum teilen", + "sendToPersistentStorage": "An persistenten Raum-Speicher senden", + "deleteDrawingConfirm": "Möchten Sie diese Zeichnung wirklich löschen?", + "drawingDeleted": "Zeichnung gelöscht", + "yourDrawingsCount": "Ihre Zeichnungen ({count})", + "shared": "Geteilt", + "line": "Linie", + "rectangle": "Rechteck" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e8ced8a..173a337 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -3058,5 +3058,60 @@ "type": "String" } } + }, + + "shareDrawing": "Share Drawing", + "@shareDrawing": { + "description": "Title for share single drawing dialog" + }, + + "shareWithAllNearbyDevices": "Share with all nearby devices", + "@shareWithAllNearbyDevices": { + "description": "Subtitle for public channel sharing option" + }, + + "shareToRoom": "Share to Room", + "@shareToRoom": { + "description": "Header for room sharing section" + }, + + "sendToPersistentStorage": "Send to persistent room storage", + "@sendToPersistentStorage": { + "description": "Subtitle for room sharing option" + }, + + "deleteDrawingConfirm": "Are you sure you want to delete this drawing?", + "@deleteDrawingConfirm": { + "description": "Confirmation message for deleting a single drawing" + }, + + "drawingDeleted": "Drawing deleted", + "@drawingDeleted": { + "description": "Success message after deleting a drawing" + }, + + "yourDrawingsCount": "Your Drawings ({count})", + "@yourDrawingsCount": { + "description": "Header showing count of user's drawings", + "placeholders": { + "count": { + "type": "int" + } + } + }, + + "shared": "Shared", + "@shared": { + "description": "Status label for shared drawings" + }, + + "line": "Line", + "@line": { + "description": "Line drawing type label" + }, + + "rectangle": "Rectangle", + "@rectangle": { + "description": "Rectangle drawing type label" } } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index bda833e..5ea2ebc 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -2516,5 +2516,15 @@ "drawingHidden": "Dibujo ocultado del mapa", "alreadyShared": "{count} ya compartido", "newDrawingsShared": "{count} nuevo(s) dibujo(s) compartido(s)", - "drawingTools": "Herramientas de Dibujo" + "drawingTools": "Herramientas de Dibujo", + "shareDrawing": "Compartir Dibujo", + "shareWithAllNearbyDevices": "Compartir con todos los dispositivos cercanos", + "shareToRoom": "Compartir en Sala", + "sendToPersistentStorage": "Enviar a almacenamiento persistente de sala", + "deleteDrawingConfirm": "¿Está seguro de que desea eliminar este dibujo?", + "drawingDeleted": "Dibujo eliminado", + "yourDrawingsCount": "Sus Dibujos ({count})", + "shared": "Compartido", + "line": "Línea", + "rectangle": "Rectángulo" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 051bc5c..e21bf10 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -2521,5 +2521,15 @@ "drawingHidden": "Dessin masqué de la carte", "alreadyShared": "{count} déjà partagé", "newDrawingsShared": "{count} nouveau(x) dessin(s) partagé(s)", - "drawingTools": "Outils de Dessin" + "drawingTools": "Outils de Dessin", + "shareDrawing": "Partager le Dessin", + "shareWithAllNearbyDevices": "Partager avec tous les appareils à proximité", + "shareToRoom": "Partager dans la Salle", + "sendToPersistentStorage": "Envoyer au stockage persistant de la salle", + "deleteDrawingConfirm": "Êtes-vous sûr de vouloir supprimer ce dessin?", + "drawingDeleted": "Dessin supprimé", + "yourDrawingsCount": "Vos Dessins ({count})", + "shared": "Partagé", + "line": "Ligne", + "rectangle": "Rectangle" } diff --git a/lib/l10n/app_hr.arb b/lib/l10n/app_hr.arb index 549f6ec..3df7f9d 100644 --- a/lib/l10n/app_hr.arb +++ b/lib/l10n/app_hr.arb @@ -932,5 +932,15 @@ "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" + "drawingTools": "Alati za crtanje", + "shareDrawing": "Podijeli Crtež", + "shareWithAllNearbyDevices": "Podijeli sa svim obližnjim uređajima", + "shareToRoom": "Podijeli u Sobu", + "sendToPersistentStorage": "Pošalji u trajnu pohranu sobe", + "deleteDrawingConfirm": "Jeste li sigurni da želite izbrisati ovaj crtež?", + "drawingDeleted": "Crtež izbrisan", + "yourDrawingsCount": "Vaši Crteži ({count})", + "shared": "Podijeljeno", + "line": "Linija", + "rectangle": "Pravokutnik" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index bd5bb4a..969349b 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -2521,5 +2521,15 @@ "drawingHidden": "Disegno nascosto dalla mappa", "alreadyShared": "{count} già condiviso", "newDrawingsShared": "{count} nuovo(i) disegno(i) condiviso(i)", - "drawingTools": "Strumenti di Disegno" + "drawingTools": "Strumenti di Disegno", + "shareDrawing": "Condividi Disegno", + "shareWithAllNearbyDevices": "Condividi con tutti i dispositivi vicini", + "shareToRoom": "Condividi nella Stanza", + "sendToPersistentStorage": "Invia allo storage persistente della stanza", + "deleteDrawingConfirm": "Sei sicuro di voler eliminare questo disegno?", + "drawingDeleted": "Disegno eliminato", + "yourDrawingsCount": "I Tuoi Disegni ({count})", + "shared": "Condiviso", + "line": "Linea", + "rectangle": "Rettangolo" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index f3e73a7..d8539df 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3254,6 +3254,66 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Shared {count} new drawing{plural}'** String newDrawingsShared(int count, String plural); + + /// Title for share single drawing dialog + /// + /// In en, this message translates to: + /// **'Share Drawing'** + String get shareDrawing; + + /// Subtitle for public channel sharing option + /// + /// In en, this message translates to: + /// **'Share with all nearby devices'** + String get shareWithAllNearbyDevices; + + /// Header for room sharing section + /// + /// In en, this message translates to: + /// **'Share to Room'** + String get shareToRoom; + + /// Subtitle for room sharing option + /// + /// In en, this message translates to: + /// **'Send to persistent room storage'** + String get sendToPersistentStorage; + + /// Confirmation message for deleting a single drawing + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete this drawing?'** + String get deleteDrawingConfirm; + + /// Success message after deleting a drawing + /// + /// In en, this message translates to: + /// **'Drawing deleted'** + String get drawingDeleted; + + /// Header showing count of user's drawings + /// + /// In en, this message translates to: + /// **'Your Drawings ({count})'** + String yourDrawingsCount(int count); + + /// Status label for shared drawings + /// + /// In en, this message translates to: + /// **'Shared'** + String get shared; + + /// Line drawing type label + /// + /// In en, this message translates to: + /// **'Line'** + String get line; + + /// Rectangle drawing type label + /// + /// In en, this message translates to: + /// **'Rectangle'** + String get rectangle; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 508912d..623b5b3 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1820,4 +1820,38 @@ class AppLocalizationsDe extends AppLocalizations { String newDrawingsShared(int count, String plural) { return '$count neue Zeichnung(en) geteilt'; } + + @override + String get shareDrawing => 'Zeichnung teilen'; + + @override + String get shareWithAllNearbyDevices => + 'Mit allen Geräten in der Nähe teilen'; + + @override + String get shareToRoom => 'In Raum teilen'; + + @override + String get sendToPersistentStorage => 'An persistenten Raum-Speicher senden'; + + @override + String get deleteDrawingConfirm => + 'Möchten Sie diese Zeichnung wirklich löschen?'; + + @override + String get drawingDeleted => 'Zeichnung gelöscht'; + + @override + String yourDrawingsCount(int count) { + return 'Ihre Zeichnungen ($count)'; + } + + @override + String get shared => 'Geteilt'; + + @override + String get line => 'Linie'; + + @override + String get rectangle => 'Rechteck'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index ede5e65..9dd83cf 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1801,4 +1801,37 @@ class AppLocalizationsEn extends AppLocalizations { String newDrawingsShared(int count, String plural) { return 'Shared $count new drawing$plural'; } + + @override + String get shareDrawing => 'Share Drawing'; + + @override + String get shareWithAllNearbyDevices => 'Share with all nearby devices'; + + @override + String get shareToRoom => 'Share to Room'; + + @override + String get sendToPersistentStorage => 'Send to persistent room storage'; + + @override + String get deleteDrawingConfirm => + 'Are you sure you want to delete this drawing?'; + + @override + String get drawingDeleted => 'Drawing deleted'; + + @override + String yourDrawingsCount(int count) { + return 'Your Drawings ($count)'; + } + + @override + String get shared => 'Shared'; + + @override + String get line => 'Line'; + + @override + String get rectangle => 'Rectangle'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index d5af833..5417f7a 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1821,4 +1821,39 @@ class AppLocalizationsEs extends AppLocalizations { String newDrawingsShared(int count, String plural) { return '$count nuevo(s) dibujo(s) compartido(s)'; } + + @override + String get shareDrawing => 'Compartir Dibujo'; + + @override + String get shareWithAllNearbyDevices => + 'Compartir con todos los dispositivos cercanos'; + + @override + String get shareToRoom => 'Compartir en Sala'; + + @override + String get sendToPersistentStorage => + 'Enviar a almacenamiento persistente de sala'; + + @override + String get deleteDrawingConfirm => + '¿Está seguro de que desea eliminar este dibujo?'; + + @override + String get drawingDeleted => 'Dibujo eliminado'; + + @override + String yourDrawingsCount(int count) { + return 'Sus Dibujos ($count)'; + } + + @override + String get shared => 'Compartido'; + + @override + String get line => 'Línea'; + + @override + String get rectangle => 'Rectángulo'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 0262cd5..92dfefd 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1827,4 +1827,39 @@ class AppLocalizationsFr extends AppLocalizations { String newDrawingsShared(int count, String plural) { return '$count nouveau(x) dessin(s) partagé(s)'; } + + @override + String get shareDrawing => 'Partager le Dessin'; + + @override + String get shareWithAllNearbyDevices => + 'Partager avec tous les appareils à proximité'; + + @override + String get shareToRoom => 'Partager dans la Salle'; + + @override + String get sendToPersistentStorage => + 'Envoyer au stockage persistant de la salle'; + + @override + String get deleteDrawingConfirm => + 'Êtes-vous sûr de vouloir supprimer ce dessin?'; + + @override + String get drawingDeleted => 'Dessin supprimé'; + + @override + String yourDrawingsCount(int count) { + return 'Vos Dessins ($count)'; + } + + @override + String get shared => 'Partagé'; + + @override + String get line => 'Ligne'; + + @override + String get rectangle => 'Rectangle'; } diff --git a/lib/l10n/app_localizations_hr.dart b/lib/l10n/app_localizations_hr.dart index b5f6a65..0834150 100644 --- a/lib/l10n/app_localizations_hr.dart +++ b/lib/l10n/app_localizations_hr.dart @@ -1810,4 +1810,38 @@ class AppLocalizationsHr extends AppLocalizations { String newDrawingsShared(int count, String plural) { return 'Podijeljeno $count novi$plural crtež$plural'; } + + @override + String get shareDrawing => 'Podijeli Crtež'; + + @override + String get shareWithAllNearbyDevices => + 'Podijeli sa svim obližnjim uređajima'; + + @override + String get shareToRoom => 'Podijeli u Sobu'; + + @override + String get sendToPersistentStorage => 'Pošalji u trajnu pohranu sobe'; + + @override + String get deleteDrawingConfirm => + 'Jeste li sigurni da želite izbrisati ovaj crtež?'; + + @override + String get drawingDeleted => 'Crtež izbrisan'; + + @override + String yourDrawingsCount(int count) { + return 'Vaši Crteži ($count)'; + } + + @override + String get shared => 'Podijeljeno'; + + @override + String get line => 'Linija'; + + @override + String get rectangle => 'Pravokutnik'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index e5baeae..0c5e79f 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1818,4 +1818,39 @@ class AppLocalizationsIt extends AppLocalizations { String newDrawingsShared(int count, String plural) { return '$count nuovo(i) disegno(i) condiviso(i)'; } + + @override + String get shareDrawing => 'Condividi Disegno'; + + @override + String get shareWithAllNearbyDevices => + 'Condividi con tutti i dispositivi vicini'; + + @override + String get shareToRoom => 'Condividi nella Stanza'; + + @override + String get sendToPersistentStorage => + 'Invia allo storage persistente della stanza'; + + @override + String get deleteDrawingConfirm => + 'Sei sicuro di voler eliminare questo disegno?'; + + @override + String get drawingDeleted => 'Disegno eliminato'; + + @override + String yourDrawingsCount(int count) { + return 'I Tuoi Disegni ($count)'; + } + + @override + String get shared => 'Condiviso'; + + @override + String get line => 'Linea'; + + @override + String get rectangle => 'Rettangolo'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index dfa0cad..bce6b09 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1812,4 +1812,37 @@ class AppLocalizationsSl extends AppLocalizations { String newDrawingsShared(int count, String plural) { return 'Deljeno $count nov$plural risb$plural'; } + + @override + String get shareDrawing => 'Deli Risbo'; + + @override + String get shareWithAllNearbyDevices => 'Deli z vsemi bližnjimi napravami'; + + @override + String get shareToRoom => 'Deli v Sobo'; + + @override + String get sendToPersistentStorage => 'Pošlji v trajno shrambo sobe'; + + @override + String get deleteDrawingConfirm => + 'Ali ste prepričani, da želite izbrisati to risbo?'; + + @override + String get drawingDeleted => 'Risba izbrisana'; + + @override + String yourDrawingsCount(int count) { + return 'Vaše Risbe ($count)'; + } + + @override + String get shared => 'Deljeno'; + + @override + String get line => 'Črta'; + + @override + String get rectangle => 'Pravokotnik'; } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 54a82b3..1d36936 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -932,5 +932,15 @@ "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" + "drawingTools": "Orodja za risanje", + "shareDrawing": "Deli Risbo", + "shareWithAllNearbyDevices": "Deli z vsemi bližnjimi napravami", + "shareToRoom": "Deli v Sobo", + "sendToPersistentStorage": "Pošlji v trajno shrambo sobe", + "deleteDrawingConfirm": "Ali ste prepričani, da želite izbrisati to risbo?", + "drawingDeleted": "Risba izbrisana", + "yourDrawingsCount": "Vaše Risbe ({count})", + "shared": "Deljeno", + "line": "Črta", + "rectangle": "Pravokotnik" } diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index 2fc5826..2b2538a 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -322,6 +322,11 @@ class AppProvider with ChangeNotifier { debugPrint('📍 [AppProvider] Starting location tracking after successful initialization'); await _startLocationTracking(); + // Sync drawing messages with DrawingProvider + // This restores any drawings that may be missing from storage + debugPrint('🎨 [AppProvider] Syncing drawing messages with DrawingProvider...'); + messagesProvider.syncDrawingsWithProvider(drawingProvider); + notifyListeners(); } catch (e) { debugPrint('Initialization error: $e'); diff --git a/lib/providers/messages_provider.dart b/lib/providers/messages_provider.dart index b73291d..47fcf7e 100644 --- a/lib/providers/messages_provider.dart +++ b/lib/providers/messages_provider.dart @@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart'; import '../models/message.dart'; import '../models/contact.dart'; import '../models/sar_marker.dart'; +import '../models/map_drawing.dart'; import '../services/message_storage_service.dart'; import '../services/notification_service.dart'; import '../utils/sar_message_parser.dart'; @@ -109,7 +110,32 @@ class MessagesProvider with ChangeNotifier { for (final message in storedMessages) { // Re-enhance each message to ensure SAR markers are properly detected // This handles cases where messages were stored before enhancement logic - final enhancedMessage = SarMessageParser.enhanceMessage(message); + var enhancedMessage = SarMessageParser.enhanceMessage(message); + + // Check if it's a drawing message (D:...) and not already marked + // This handles cases where messages were stored before drawing detection + if (DrawingMessageParser.isDrawingMessage(enhancedMessage.text) && + !enhancedMessage.isDrawing) { + debugPrint( + '🎨 [MessagesProvider] Detected drawing message during initialization: ${enhancedMessage.id}', + ); + // 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, + ); + debugPrint( + ' Drawing ID: ${enhancedMessage.drawingId}, isDrawing: ${enhancedMessage.isDrawing}', + ); + } + _messages.add(enhancedMessage); // Extract SAR markers @@ -132,6 +158,78 @@ class MessagesProvider with ChangeNotifier { } } + /// Sync drawing messages with DrawingProvider + /// This restores drawings that may be missing from DrawingProvider storage + /// Should be called after both providers are initialized + void syncDrawingsWithProvider(dynamic drawingProvider) { + debugPrint('🔄 [MessagesProvider] Syncing drawings with DrawingProvider...'); + int restoredCount = 0; + + for (final message in _messages) { + if (!message.isDrawing || message.drawingId == null) continue; + + // Check if drawing exists in DrawingProvider + final existingDrawing = drawingProvider.getDrawingById(message.drawingId!); + if (existingDrawing != null) { + continue; // Drawing already exists + } + + // Drawing is missing, reconstruct from message text + debugPrint('🔧 [MessagesProvider] Restoring missing drawing: ${message.drawingId}'); + final drawing = DrawingMessageParser.parseDrawingMessage( + message.text, + senderName: message.senderName, + messageId: message.id, + ); + + if (drawing == null) { + debugPrint('⚠️ [MessagesProvider] Failed to parse drawing from message ${message.id}'); + continue; + } + + // The parsed drawing has a new generated ID, but we need to use the original ID + // Create a copy with the correct ID from the message + final restoredDrawing = _createDrawingWithId(drawing, message.drawingId!); + + if (restoredDrawing != null) { + drawingProvider.addReceivedDrawing(restoredDrawing); + restoredCount++; + debugPrint('✅ [MessagesProvider] Restored drawing ${message.drawingId}'); + } + } + + debugPrint('✅ [MessagesProvider] Sync complete: restored $restoredCount drawings'); + } + + /// Create a copy of a drawing with a specific ID + dynamic _createDrawingWithId(dynamic drawing, String targetId) { + if (drawing is LineDrawing) { + return LineDrawing( + id: targetId, + color: drawing.color, + createdAt: drawing.createdAt, + points: drawing.points, + senderName: drawing.senderName, + isReceived: drawing.isReceived, + messageId: drawing.messageId, + isShared: drawing.isShared, + ); + } else if (drawing is RectangleDrawing) { + return RectangleDrawing( + id: targetId, + color: drawing.color, + createdAt: drawing.createdAt, + topLeft: drawing.topLeft, + bottomRight: drawing.bottomRight, + senderName: drawing.senderName, + isReceived: drawing.isReceived, + messageId: drawing.messageId, + isShared: drawing.isShared, + ); + } + return null; + } + /// Add a message /// If [contactLookup] function is provided, it will be used to match channel /// message senders with known contacts by name diff --git a/lib/screens/map_tab.dart b/lib/screens/map_tab.dart index 28fb179..724a95b 100644 --- a/lib/screens/map_tab.dart +++ b/lib/screens/map_tab.dart @@ -1417,8 +1417,15 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { right: 16, child: Column( children: [ - // Drawing toolbar - const DrawingToolbar(), + // Drawing toolbar (hidden in simple mode) + Consumer( + builder: (context, appProvider, _) { + if (appProvider.isSimpleMode) { + return const SizedBox.shrink(); + } + return const DrawingToolbar(); + }, + ), // Hide other buttons when in drawing mode if (!drawingProvider.isDrawing) ...[ const SizedBox(height: 8), diff --git a/lib/screens/messages_tab.dart b/lib/screens/messages_tab.dart index 4dfcb10..917a091 100644 --- a/lib/screens/messages_tab.dart +++ b/lib/screens/messages_tab.dart @@ -1596,15 +1596,16 @@ class _MessageBubble extends StatelessWidget { else if (message.isDrawing && message.drawingId != null) Consumer( builder: (context, drawingProvider, child) { - // Find the drawing by ID - final drawing = drawingProvider.drawings - .cast() - .firstWhere( - (d) => d?.id == message.drawingId, - orElse: () => null, - ); + // Find the drawing by ID (bypasses visibility filters) + final drawing = drawingProvider.getDrawingById(message.drawingId!); if (drawing == null) { + debugPrint( + '⚠️ [MessageBubble] Drawing not found: ${message.drawingId}', + ); + debugPrint( + ' Total drawings in provider: ${drawingProvider.drawings.length}', + ); return Text( message.text, style: Theme.of(context).textTheme.bodyMedium, diff --git a/lib/widgets/drawing_minimap_preview.dart b/lib/widgets/drawing_minimap_preview.dart index e8f5b23..7cda08a 100644 --- a/lib/widgets/drawing_minimap_preview.dart +++ b/lib/widgets/drawing_minimap_preview.dart @@ -1,25 +1,27 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/flutter_map.dart' as flutter_map; 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 +/// Renders a small 80x80px preview of a drawing on a map background class DrawingMinimapPreview extends StatelessWidget { final MapDrawing drawing; + final Widget? tileLayer; const DrawingMinimapPreview({ super.key, required this.drawing, + this.tileLayer, }); /// Calculate bounds for the drawing to fit in the preview - LatLngBounds _calculateBounds() { + flutter_map.LatLngBounds _calculateBounds() { if (drawing is LineDrawing) { final lineDrawing = drawing as LineDrawing; if (lineDrawing.points.isEmpty) { // Fallback to default bounds if no points - return LatLngBounds( + return flutter_map.LatLngBounds( const LatLng(0, 0), const LatLng(0.01, 0.01), ); @@ -42,7 +44,7 @@ class DrawingMinimapPreview extends StatelessWidget { final latPadding = (maxLat - minLat) * 0.1; final lonPadding = (maxLon - minLon) * 0.1; - return LatLngBounds( + return flutter_map.LatLngBounds( LatLng(minLat - latPadding, minLon - lonPadding), LatLng(maxLat + latPadding, maxLon + lonPadding), ); @@ -55,7 +57,7 @@ class DrawingMinimapPreview extends StatelessWidget { final latPadding = latDiff * 0.1; final lonPadding = lonDiff * 0.1; - return LatLngBounds( + return flutter_map.LatLngBounds( LatLng( rectDrawing.topLeft.latitude - latPadding, rectDrawing.topLeft.longitude - lonPadding, @@ -68,7 +70,7 @@ class DrawingMinimapPreview extends StatelessWidget { } // Fallback to default bounds - return LatLngBounds( + return flutter_map.LatLngBounds( const LatLng(0, 0), const LatLng(0.01, 0.01), ); @@ -91,25 +93,28 @@ class DrawingMinimapPreview extends StatelessWidget { ), child: ClipRRect( borderRadius: BorderRadius.circular(7), - child: FlutterMap( - options: MapOptions( - initialCameraFit: CameraFit.bounds( + child: flutter_map.FlutterMap( + options: flutter_map.MapOptions( + initialCameraFit: flutter_map.CameraFit.bounds( bounds: bounds, padding: const EdgeInsets.all(8), ), - interactionOptions: const InteractionOptions( - flags: InteractiveFlag.none, // Disable all interactions + interactionOptions: const flutter_map.InteractionOptions( + flags: flutter_map.InteractiveFlag.none, // Disable all interactions ), ), children: [ - // Simple gray background (no tiles for performance) - Container(color: Colors.grey.shade300), + // Use provided tile layer or fallback to gray background + if (tileLayer != null) + tileLayer! + else + Container(color: Colors.grey.shade300), // Render the drawing if (drawing is LineDrawing) - PolylineLayer( + flutter_map.PolylineLayer( polylines: [ - Polyline( + flutter_map.Polyline( points: (drawing as LineDrawing).points, strokeWidth: 3.0, color: drawing.color, @@ -117,9 +122,9 @@ class DrawingMinimapPreview extends StatelessWidget { ], ) else if (drawing is RectangleDrawing) - PolygonLayer( + flutter_map.PolygonLayer( polygons: [ - Polygon( + flutter_map.Polygon( points: (drawing as RectangleDrawing).corners, color: drawing.color.withValues(alpha: 0.3), borderColor: drawing.color, diff --git a/lib/widgets/map/drawing_toolbar.dart b/lib/widgets/map/drawing_toolbar.dart index 1e7c6f1..5a86d4c 100644 --- a/lib/widgets/map/drawing_toolbar.dart +++ b/lib/widgets/map/drawing_toolbar.dart @@ -9,6 +9,7 @@ import '../../models/contact.dart'; import '../../models/message.dart'; import '../../l10n/app_localizations.dart'; import '../../utils/toast_logger.dart'; +import '../drawing_minimap_preview.dart'; /// Toolbar for drawing controls on the map class DrawingToolbar extends StatelessWidget { @@ -161,9 +162,12 @@ class DrawingToolbar extends StatelessWidget { /// Show drawing mode selection menu void _showDrawingMenu(BuildContext context, DrawingProvider drawingProvider) { + // Capture root context for use after modal closes + final rootContext = context; + showModalBottomSheet( context: context, - builder: (context) => SingleChildScrollView( + builder: (sheetContext) => SingleChildScrollView( child: Container( padding: const EdgeInsets.symmetric(vertical: 16), child: Column( @@ -176,8 +180,8 @@ class DrawingToolbar extends StatelessWidget { const Icon(Icons.edit), const SizedBox(width: 12), Text( - AppLocalizations.of(context)!.drawingTools, - style: Theme.of(context).textTheme.titleMedium?.copyWith( + AppLocalizations.of(sheetContext)!.drawingTools, + style: Theme.of(sheetContext).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), @@ -187,19 +191,19 @@ class DrawingToolbar extends StatelessWidget { const Divider(), ListTile( leading: const Icon(Icons.show_chart), - title: Text(AppLocalizations.of(context)!.drawLine), - subtitle: Text(AppLocalizations.of(context)!.drawLineDesc), + title: Text(AppLocalizations.of(sheetContext)!.drawLine), + subtitle: Text(AppLocalizations.of(sheetContext)!.drawLineDesc), onTap: () { - Navigator.pop(context); + Navigator.pop(sheetContext); drawingProvider.setDrawingMode(DrawingMode.line); }, ), ListTile( leading: const Icon(Icons.crop_square), - title: Text(AppLocalizations.of(context)!.drawRectangle), - subtitle: Text(AppLocalizations.of(context)!.drawRectangleDesc), + title: Text(AppLocalizations.of(sheetContext)!.drawRectangle), + subtitle: Text(AppLocalizations.of(sheetContext)!.drawRectangleDesc), onTap: () { - Navigator.pop(context); + Navigator.pop(sheetContext); drawingProvider.setDrawingMode(DrawingMode.rectangle); }, ), @@ -211,11 +215,11 @@ class DrawingToolbar extends StatelessWidget { ? Icons.visibility : Icons.visibility_off, ), - title: Text(AppLocalizations.of(context)!.showReceivedDrawings), + title: Text(AppLocalizations.of(sheetContext)!.showReceivedDrawings), subtitle: Text( drawingProvider.showReceivedDrawings - ? AppLocalizations.of(context)!.showingAllDrawings - : AppLocalizations.of(context)!.showingOnlyYourDrawings, + ? AppLocalizations.of(sheetContext)!.showingAllDrawings + : AppLocalizations.of(sheetContext)!.showingOnlyYourDrawings, ), value: drawingProvider.showReceivedDrawings, onChanged: (value) { @@ -229,50 +233,188 @@ class DrawingToolbar extends StatelessWidget { ? Icons.pin_drop : Icons.pin_drop_outlined, ), - title: Text(AppLocalizations.of(context)!.showSarMarkers), + title: Text(AppLocalizations.of(sheetContext)!.showSarMarkers), subtitle: Text( drawingProvider.showSarMarkers - ? AppLocalizations.of(context)!.showingSarMarkers - : AppLocalizations.of(context)!.hidingSarMarkers, + ? AppLocalizations.of(sheetContext)!.showingSarMarkers + : AppLocalizations.of(sheetContext)!.hidingSarMarkers, ), value: drawingProvider.showSarMarkers, onChanged: (value) { drawingProvider.toggleSarMarkers(); }, ), + // Drawings list with individual actions if (drawingProvider.drawings.isNotEmpty) ...[ + const Divider(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + const Icon(Icons.layers, size: 20), + const SizedBox(width: 8), + Text( + AppLocalizations.of(sheetContext)!.yourDrawingsCount(drawingProvider.drawings.length), + style: Theme.of(sheetContext).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ...drawingProvider.drawings.map((drawing) { + final isShared = drawing.isShared; + final typeStr = drawing is LineDrawing + ? AppLocalizations.of(sheetContext)!.line + : AppLocalizations.of(sheetContext)!.rectangle; + final colorName = DrawingColors.colorToName(drawing.color); + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey.shade300, + width: 1, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + // Minimap preview + DrawingMinimapPreview(drawing: drawing), + const SizedBox(width: 12), + // Drawing info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + typeStr, + style: Theme.of(sheetContext).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Container( + width: 12, + height: 12, + 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(sheetContext).textTheme.labelSmall, + ), + if (isShared) ...[ + const SizedBox(width: 8), + Icon( + Icons.check_circle, + size: 14, + color: Colors.green.shade700, + ), + const SizedBox(width: 4), + Text( + AppLocalizations.of(sheetContext)!.shared, + style: Theme.of(sheetContext).textTheme.labelSmall?.copyWith( + color: Colors.green.shade700, + fontWeight: FontWeight.w600, + ), + ), + ], + ], + ), + ], + ), + ), + // Action buttons + Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Share button + if (!isShared) + IconButton( + icon: const Icon(Icons.share, size: 20), + onPressed: () async { + Navigator.pop(sheetContext); + await Future.delayed(const Duration(milliseconds: 100)); + if (rootContext.mounted) { + _showShareSingleDrawingDialog( + rootContext, + drawingProvider, + drawing, + ); + } + }, + tooltip: 'Share', + color: Colors.blue, + padding: const EdgeInsets.all(4), + constraints: const BoxConstraints(), + ), + const SizedBox(width: 4), + // Delete button + IconButton( + icon: const Icon(Icons.delete, size: 20), + onPressed: () { + Navigator.pop(sheetContext); + _showDeleteSingleDrawingDialog( + rootContext, + drawingProvider, + drawing, + ); + }, + tooltip: 'Delete', + color: Colors.red, + padding: const EdgeInsets.all(4), + constraints: const BoxConstraints(), + ), + ], + ), + ], + ), + ); + }).toList(), const Divider(), ListTile( leading: const Icon(Icons.share, color: Colors.blue), - title: Text(AppLocalizations.of(context)!.shareDrawings), + title: Text(AppLocalizations.of(sheetContext)!.shareDrawings), subtitle: Text( - AppLocalizations.of(context)!.broadcastDrawingsToTeam( + AppLocalizations.of(sheetContext)!.broadcastDrawingsToTeam( drawingProvider.drawings.length, drawingProvider.drawings.length > 1 ? 's' : '', ), ), onTap: () async { - Navigator.pop(context); + Navigator.pop(sheetContext); // Small delay to ensure first bottom sheet is fully closed // before opening the second one await Future.delayed(const Duration(milliseconds: 100)); - if (context.mounted) { - _showShareDrawingsDialog(context, drawingProvider); + if (rootContext.mounted) { + _showShareDrawingsDialog(rootContext, drawingProvider); } }, ), ListTile( leading: const Icon(Icons.delete_sweep, color: Colors.red), - title: Text(AppLocalizations.of(context)!.clearAllDrawings), + title: Text(AppLocalizations.of(sheetContext)!.clearAllDrawings), subtitle: Text( - AppLocalizations.of(context)!.removeAllDrawings( + AppLocalizations.of(sheetContext)!.removeAllDrawings( drawingProvider.drawings.length, drawingProvider.drawings.length > 1 ? 's' : '', ), ), onTap: () { - Navigator.pop(context); - _showClearAllDialog(context, drawingProvider); + Navigator.pop(sheetContext); + _showClearAllDialog(rootContext, drawingProvider); }, ), ], @@ -359,9 +501,6 @@ class DrawingToolbar extends StatelessWidget { ) { debugPrint('🎨 [DrawingToolbar] _showShareDrawingsDialog called'); - // Capture the root context BEFORE showing the modal - final rootContext = context; - // Read providers BEFORE any async operations or dialogs // This ensures we have the correct BuildContext final connectionProvider = Provider.of( @@ -686,4 +825,215 @@ class DrawingToolbar extends StatelessWidget { debugPrint(' Share complete: $successCount/${drawings.length} sent, $alreadyShared already shared'); } + + /// Show share dialog for a single drawing + void _showShareSingleDrawingDialog( + BuildContext context, + DrawingProvider drawingProvider, + MapDrawing drawing, + ) { + debugPrint('🎨 [DrawingToolbar] _showShareSingleDrawingDialog called for ${drawing.id}'); + + // Read providers BEFORE any async operations or dialogs + final connectionProvider = Provider.of( + context, + listen: false, + ); + final contactsProvider = Provider.of( + context, + listen: false, + ); + + if (!connectionProvider.deviceInfo.isConnected) { + debugPrint(' ❌ Not connected - showing error toast'); + if (context.mounted) { + ToastLogger.error( + context, + AppLocalizations.of(context)!.notConnectedToDevice, + ); + } + return; + } + + // Get device name for sender identification + final senderName = connectionProvider.deviceInfo.selfName ?? 'Unknown'; + + // Get available rooms + final rooms = contactsProvider.rooms; + debugPrint(' Available rooms: ${rooms.length}'); + + // Capture root context for provider access after modal closes + final rootContext = context; + + debugPrint(' Showing modal bottom sheet...'); + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (sheetContext) => SingleChildScrollView( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + const Icon(Icons.share), + const SizedBox(width: 12), + Expanded( + child: Text( + AppLocalizations.of(sheetContext)!.shareDrawing, + style: Theme.of(sheetContext).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + const Divider(), + // Option: Send to Public Channel + ListTile( + leading: const Icon(Icons.public, color: Colors.blue), + title: Text(AppLocalizations.of(sheetContext)!.publicChannel), + subtitle: Text(AppLocalizations.of(sheetContext)!.shareWithAllNearbyDevices), + onTap: () async { + Navigator.pop(sheetContext); + await _shareDrawingsToChannel( + rootContext, + [drawing], + connectionProvider, + senderName, + ); + }, + ), + // Option: Send to specific room + if (rooms.isNotEmpty) ...[ + const Divider(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text( + AppLocalizations.of(sheetContext)!.shareToRoom, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.grey, + ), + ), + ), + ...rooms.map((room) { + return ListTile( + leading: const Icon(Icons.meeting_room, color: Colors.orange), + title: Text(room.advName), + subtitle: Text(AppLocalizations.of(sheetContext)!.sendToPersistentStorage), + onTap: () async { + Navigator.pop(sheetContext); + await _shareDrawingsToRoom( + rootContext, + [drawing], + connectionProvider, + senderName, + room, + ); + }, + ); + }), + ], + ], + ), + ), + ), + ); + } + + /// Show delete confirmation dialog for a single drawing + void _showDeleteSingleDrawingDialog( + BuildContext context, + DrawingProvider drawingProvider, + MapDrawing drawing, + ) { + final typeStr = drawing is LineDrawing + ? AppLocalizations.of(context)!.line + : AppLocalizations.of(context)!.rectangle; + final colorName = DrawingColors.colorToName(drawing.color); + + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(AppLocalizations.of(dialogContext)!.deleteDrawing), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(AppLocalizations.of(dialogContext)!.deleteDrawingConfirm), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + DrawingMinimapPreview(drawing: drawing), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + typeStr, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: drawing.color, + shape: BoxShape.circle, + border: Border.all( + color: Colors.black26, + width: 1, + ), + ), + ), + const SizedBox(width: 6), + Text( + colorName, + style: const TextStyle(fontSize: 12), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(AppLocalizations.of(dialogContext)!.cancel), + ), + TextButton( + onPressed: () { + Navigator.pop(dialogContext); + drawingProvider.removeDrawing(drawing.id); + ToastLogger.success(context, AppLocalizations.of(context)!.drawingDeleted); + }, + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: Text(AppLocalizations.of(dialogContext)!.delete), + ), + ], + ), + ); + } }