Refactor SAR marker handling and add template management

- Updated CompassSarList and DetailedCompassDialog to use marker.displayName instead of marker.type.displayName.
- Enhanced DrawingLayer and DrawingMarkersLayer to support simple mode for drawing visibility and interaction.
- Added toggle switches in DrawingToolbar for showing/hiding received drawings and SAR markers.
- Modified MapMarkers to utilize custom emojis and display names for markers.
- Introduced RecipientSelectorSheet for selecting message recipients with search functionality.
- Refactored SarUpdateSheet to use SAR templates instead of marker types, allowing for emoji and name customization.
- Created SarTemplateEditDialog for adding and editing SAR templates with color selection and preview.
This commit is contained in:
Janez T
2025-10-21 23:44:49 +02:00
parent e9d516b749
commit 021ce21cbe
60 changed files with 8736 additions and 1413 deletions

View File

@@ -207,6 +207,16 @@
"description": "Beschreibung für die Einstellung der RX/TX-Indikatoren"
},
"simpleMode": "Einfacher Modus",
"@simpleMode": {
"description": "Einstellung zum Aktivieren des einfachen Modus"
},
"simpleModeDescription": "Nicht wesentliche Informationen in Nachrichten und Kontakten ausblenden",
"@simpleModeDescription": {
"description": "Beschreibung für die Einstellung des einfachen Modus"
},
"language": "Sprache",
"@language": {
"description": "Beschriftung der Spracheinstellung"
@@ -514,7 +524,7 @@
"description": "Beschriftung der Aktualisieren-Schaltfläche"
},
"sendDirectMessage": "Direktnachricht senden",
"sendDirectMessage": "Senden",
"@sendDirectMessage": {
"description": "Aktion zum Senden einer Direktnachricht an Kontakt"
},
@@ -634,6 +644,36 @@
"description": "Aktion zum Löschen aller lokalen Zeichnungen"
},
"showReceivedDrawings": "Empfangene Zeichnungen anzeigen",
"@showReceivedDrawings": {
"description": "Umschalter zum Ein-/Ausblenden empfangener Zeichnungen von anderen Teammitgliedern"
},
"showingAllDrawings": "Alle Zeichnungen werden angezeigt",
"@showingAllDrawings": {
"description": "Untertitel, wenn empfangene Zeichnungen sichtbar sind"
},
"showingOnlyYourDrawings": "Nur Ihre Zeichnungen werden angezeigt",
"@showingOnlyYourDrawings": {
"description": "Untertitel, wenn empfangene Zeichnungen ausgeblendet sind"
},
"showSarMarkers": "SAR-Markierungen anzeigen",
"@showSarMarkers": {
"description": "Umschalter zum Ein-/Ausblenden von SAR-Markierungen auf der Karte"
},
"showingSarMarkers": "SAR-Markierungen werden angezeigt",
"@showingSarMarkers": {
"description": "Untertitel, wenn SAR-Markierungen sichtbar sind"
},
"hidingSarMarkers": "SAR-Markierungen ausgeblendet",
"@hidingSarMarkers": {
"description": "Untertitel, wenn SAR-Markierungen ausgeblendet sind"
},
"clearAll": "Alle löschen",
"@clearAll": {
"description": "Beschriftung der Alle-löschen-Schaltfläche"
@@ -762,6 +802,11 @@
"description": "Beschriftung für Standortbereich"
},
"myLocation": "Mein Standort",
"@myLocation": {
"description": "Schaltflächenbeschriftung zum Einfügen der aktuellen GPS-Position"
},
"fromMap": "Von Karte",
"@fromMap": {
"description": "Badge, das anzeigt, dass der Standort vom Kartentippen stammt"
@@ -1125,6 +1170,21 @@
"description": "Beschriftung der GPS-Genauigkeit"
},
"distance": "Entfernung",
"@distance": {
"description": "Entfernungsbeschriftung im Kompass"
},
"bearing": "Peilung",
"@bearing": {
"description": "Peilungsbeschriftung im Kompass"
},
"direction": "Richtung",
"@direction": {
"description": "Richtungsbeschriftung im Kompass"
},
"filterMarkers": "Markierungen filtern",
"@filterMarkers": {
"description": "Titel für Markierungen-filtern-Dialog"
@@ -1538,6 +1598,21 @@
"description": "Name der ESRI-Satellitenbildebene"
},
"googleHybrid": "Google Hybrid",
"@googleHybrid": {
"description": "Name der Google Hybrid-Ebene (Satellit + Beschriftungen)"
},
"googleRoadmap": "Google Straßenkarte",
"@googleRoadmap": {
"description": "Name der Google Straßenkarten-Ebene"
},
"googleTerrain": "Google Gelände",
"@googleTerrain": {
"description": "Name der Google Gelände-Ebene (topografisch)"
},
"downloadVisibleArea": "Sichtbaren Bereich herunterladen",
"@downloadVisibleArea": {
"description": "Tooltip für Schaltfläche zum Herunterladen des sichtbaren Bereichs"
@@ -1673,6 +1748,11 @@
"description": "Infonachricht, wenn Nachricht gelöscht wird"
},
"copyText": "Text kopieren",
"textCopiedToClipboard": "Text in Zwischenablage kopiert",
"deleteMessage": "Nachricht löschen",
"deleteMessageConfirmation": "Möchten Sie diese Nachricht wirklich löschen?",
"refreshedContacts": "Kontakte aktualisiert",
"@refreshedContacts": {
"description": "Erfolgsmeldung, wenn Kontakte aktualisiert werden"
@@ -1901,6 +1981,111 @@
"description": "Fehlermeldung, wenn MBTiles-Löschung fehlschlägt"
},
"importExportCachedTiles": "Import/Export gecachter Kacheln",
"@importExportCachedTiles": {
"description": "Titel für Import/Export-Bereich"
},
"importExportDescription": "Sichern, teilen und wiederherstellen Sie heruntergeladene Kartenkacheln zwischen Geräten",
"@importExportDescription": {
"description": "Beschreibung der Import/Export-Funktionalität"
},
"exportTilesToFile": "Kacheln in Datei exportieren",
"@exportTilesToFile": {
"description": "Schaltfläche zum Exportieren von Kacheln"
},
"importTilesFromFile": "Kacheln aus Datei importieren",
"@importTilesFromFile": {
"description": "Schaltfläche zum Importieren von Kacheln"
},
"selectExportLocation": "Exportspeicherort wählen",
"@selectExportLocation": {
"description": "Titel für Export-Dateiauswahl"
},
"selectImportFile": "Kachel-Archiv auswählen",
"@selectImportFile": {
"description": "Titel für Import-Dateiauswahl"
},
"exportingTiles": "Exportiere Kacheln...",
"@exportingTiles": {
"description": "Statusmeldung während des Exports"
},
"importingTiles": "Importiere Kacheln...",
"@importingTiles": {
"description": "Statusmeldung während des Imports"
},
"exportSuccess": "{count} Kacheln erfolgreich exportiert",
"@exportSuccess": {
"description": "Erfolgsmeldung nach Export",
"placeholders": {
"count": {
"type": "int"
}
}
},
"importSuccess": "{count} Speicher erfolgreich importiert",
"@importSuccess": {
"description": "Erfolgsmeldung nach Import",
"placeholders": {
"count": {
"type": "int"
}
}
},
"exportFailed": "Export fehlgeschlagen: {error}",
"@exportFailed": {
"description": "Fehlermeldung bei Export-Fehler",
"placeholders": {
"error": {
"type": "String"
}
}
},
"importFailed": "Import fehlgeschlagen: {error}",
"@importFailed": {
"description": "Fehlermeldung bei Import-Fehler",
"placeholders": {
"error": {
"type": "String"
}
}
},
"exportNote": "Erstellt eine komprimierte Archivdatei (.fmtc), die auf anderen Geräten geteilt und importiert werden kann.",
"@exportNote": {
"description": "Hinweis zur Export-Funktionalität"
},
"importNote": "Importiert Kartenkacheln aus einer zuvor exportierten Archivdatei. Kacheln werden mit dem vorhandenen Cache zusammengeführt.",
"@importNote": {
"description": "Hinweis zur Import-Funktionalität"
},
"noTilesToExport": "Keine Kacheln zum Exportieren verfügbar",
"@noTilesToExport": {
"description": "Meldung wenn Cache leer ist"
},
"archiveContainsStores": "Archiv enthält {count} Speicher",
"@archiveContainsStores": {
"description": "Information über Archivinhalte",
"placeholders": {
"count": {
"type": "int"
}
}
},
"vectorTiles": "Vektor-Tiles",
"@vectorTiles": {
"description": "Beschriftung für Vektor-Tile-Typ"
@@ -2139,5 +2324,63 @@
"type": "String"
}
}
}
},
"sarTemplates": "SAR Templates",
"manageSarTemplates": "Manage cursor on target templates",
"addTemplate": "Add Template",
"editTemplate": "Edit Template",
"deleteTemplate": "Delete Template",
"templateName": "Template Name",
"templateNameHint": "e.g. Found Person",
"templateEmoji": "Emoji",
"emojiRequired": "Emoji ist erforderlich",
"nameRequired": "Name ist erforderlich",
"templateDescription": "Description (Optional)",
"templateDescriptionHint": "Add additional context...",
"templateColor": "Color",
"previewFormat": "Preview (SAR Message Format)",
"importFromClipboard": "Import",
"exportToClipboard": "Export",
"deleteTemplateConfirmation": "Delete template '{name}'?",
"templateAdded": "Template added",
"templateUpdated": "Template updated",
"templateDeleted": "Template deleted",
"templatesImported": "{count, plural, =0{No templates imported} =1{Imported 1 template} other{Imported {count} templates}}",
"templatesExported": "{count, plural, =1{Exported 1 template to clipboard} other{Exported {count} templates to clipboard}}",
"importFailed": "Import failed: {error}",
"exportFailed": "Export failed: {error}",
"resetToDefaults": "Reset to Defaults",
"resetToDefaultsConfirmation": "This will delete all custom templates and restore the 4 default templates. Continue?",
"reset": "Reset",
"resetComplete": "Templates reset to defaults",
"noTemplates": "No templates available",
"tapAddToCreate": "Tap + to create your first template",
"ok": "OK",
"delete": "Löschen",
"permissionsSection": "Berechtigungen",
"locationPermission": "Standortberechtigung",
"checking": "Überprüfen...",
"locationPermissionGrantedAlways": "Erteilt (Immer)",
"locationPermissionGrantedWhileInUse": "Erteilt (Während der Nutzung)",
"locationPermissionDeniedTapToRequest": "Verweigert - Tippen zum Anfragen",
"locationPermissionPermanentlyDeniedOpenSettings": "Dauerhaft verweigert - Einstellungen öffnen",
"locationPermissionDialogContent": "Die Standortberechtigung wurde dauerhaft verweigert. Bitte aktivieren Sie sie in Ihren Geräteeinstellungen, um GPS-Tracking und Standortfreigabe zu nutzen.",
"openSettings": "Einstellungen öffnen",
"locationPermissionGranted": "Standortberechtigung erteilt!",
"locationPermissionRequiredForGps": "Die Standortberechtigung ist erforderlich für GPS-Tracking und Standortfreigabe.",
"locationPermissionAlreadyGranted": "Die Standortberechtigung wurde bereits erteilt.",
"sarNavyBlue": "SAR Navy Blau",
"sarNavyBlueDescription": "Professionell/Einsatzmodus",
"selectRecipient": "Empfänger auswählen",
"broadcastToAllNearby": "An alle in der Nähe senden",
"searchRecipients": "Empfänger suchen...",
"noContactsFound": "Keine Kontakte gefunden",
"noRoomsFound": "Keine Räume gefunden",
"noContactsOrRoomsAvailable": "Keine Kontakte oder Räume verfügbar",
"messagesWillBeSentToPublicChannel": "Nachrichten werden an öffentlichen Kanal gesendet",
"newMessage": "Neue Nachricht",
"channel": "Kanal"
}

View File

@@ -207,6 +207,16 @@
"description": "Description for RX/TX indicators setting"
},
"simpleMode": "Simple Mode",
"@simpleMode": {
"description": "Setting to enable simple mode"
},
"simpleModeDescription": "Hide non-essential information in messages and contacts",
"@simpleModeDescription": {
"description": "Description for simple mode setting"
},
"language": "Language",
"@language": {
"description": "Language setting label"
@@ -514,7 +524,7 @@
"description": "Refresh button label"
},
"sendDirectMessage": "Send Direct Message",
"sendDirectMessage": "Send",
"@sendDirectMessage": {
"description": "Action to send direct message to contact"
},
@@ -634,6 +644,36 @@
"description": "Action to clear all local drawings"
},
"showReceivedDrawings": "Show Received Drawings",
"@showReceivedDrawings": {
"description": "Toggle to show/hide received drawings from other team members"
},
"showingAllDrawings": "Showing all drawings",
"@showingAllDrawings": {
"description": "Subtitle when received drawings are visible"
},
"showingOnlyYourDrawings": "Showing only your drawings",
"@showingOnlyYourDrawings": {
"description": "Subtitle when received drawings are hidden"
},
"showSarMarkers": "Show SAR Markers",
"@showSarMarkers": {
"description": "Toggle to show/hide SAR markers on map"
},
"showingSarMarkers": "Showing SAR markers",
"@showingSarMarkers": {
"description": "Subtitle when SAR markers are visible"
},
"hidingSarMarkers": "Hiding SAR markers",
"@hidingSarMarkers": {
"description": "Subtitle when SAR markers are hidden"
},
"clearAll": "Clear All",
"@clearAll": {
"description": "Clear all button label"
@@ -762,6 +802,11 @@
"description": "Label for location section"
},
"myLocation": "My Location",
"@myLocation": {
"description": "Button label to insert current GPS location"
},
"fromMap": "From Map",
"@fromMap": {
"description": "Badge showing location is from map tap"
@@ -1125,6 +1170,21 @@
"description": "GPS accuracy label"
},
"distance": "Distance",
"@distance": {
"description": "Distance label in compass"
},
"bearing": "Bearing",
"@bearing": {
"description": "Bearing label in compass"
},
"direction": "Direction",
"@direction": {
"description": "Direction label in compass"
},
"filterMarkers": "Filter Markers",
"@filterMarkers": {
"description": "Title for filter markers dialog"
@@ -1538,6 +1598,21 @@
"description": "ESRI Satellite imagery layer name"
},
"googleHybrid": "Google Hybrid",
"@googleHybrid": {
"description": "Google Hybrid layer name (satellite + labels)"
},
"googleRoadmap": "Google Roadmap",
"@googleRoadmap": {
"description": "Google Roadmap layer name (street map)"
},
"googleTerrain": "Google Terrain",
"@googleTerrain": {
"description": "Google Terrain layer name (topographic)"
},
"downloadVisibleArea": "Download visible area",
"@downloadVisibleArea": {
"description": "Tooltip for download visible area button"
@@ -1673,6 +1748,26 @@
"description": "Info message when message is deleted"
},
"copyText": "Copy text",
"@copyText": {
"description": "Option to copy message text to clipboard"
},
"textCopiedToClipboard": "Text copied to clipboard",
"@textCopiedToClipboard": {
"description": "Success message when text is copied"
},
"deleteMessage": "Delete message",
"@deleteMessage": {
"description": "Dialog title for deleting a message"
},
"deleteMessageConfirmation": "Are you sure you want to delete this message?",
"@deleteMessageConfirmation": {
"description": "Confirmation text for message deletion"
},
"refreshedContacts": "Refreshed contacts",
"@refreshedContacts": {
"description": "Success message when contacts are refreshed"
@@ -1901,6 +1996,111 @@
"description": "Error message when MBTiles deletion fails"
},
"importExportCachedTiles": "Import/Export Cached Tiles",
"@importExportCachedTiles": {
"description": "Title for import/export section"
},
"importExportDescription": "Backup, share, and restore downloaded map tiles between devices",
"@importExportDescription": {
"description": "Description for import/export functionality"
},
"exportTilesToFile": "Export Tiles to File",
"@exportTilesToFile": {
"description": "Button to export tiles to archive file"
},
"importTilesFromFile": "Import Tiles from File",
"@importTilesFromFile": {
"description": "Button to import tiles from archive file"
},
"selectExportLocation": "Select Export Location",
"@selectExportLocation": {
"description": "Title for export file picker"
},
"selectImportFile": "Select Tile Archive",
"@selectImportFile": {
"description": "Title for import file picker"
},
"exportingTiles": "Exporting tiles...",
"@exportingTiles": {
"description": "Status message during export"
},
"importingTiles": "Importing tiles...",
"@importingTiles": {
"description": "Status message during import"
},
"exportSuccess": "Exported {count} tiles successfully",
"@exportSuccess": {
"description": "Success message after export",
"placeholders": {
"count": {
"type": "int"
}
}
},
"importSuccess": "Imported {count} stores successfully",
"@importSuccess": {
"description": "Success message after import",
"placeholders": {
"count": {
"type": "int"
}
}
},
"exportFailed": "Export failed: {error}",
"@exportFailed": {
"description": "Error message when export fails",
"placeholders": {
"error": {
"type": "String"
}
}
},
"importFailed": "Import failed: {error}",
"@importFailed": {
"description": "Error message when import fails",
"placeholders": {
"error": {
"type": "String"
}
}
},
"exportNote": "Creates a compressed archive (.fmtc) file that can be shared and imported on other devices.",
"@exportNote": {
"description": "Note about export functionality"
},
"importNote": "Imports map tiles from a previously exported archive file. Tiles will be merged with existing cache.",
"@importNote": {
"description": "Note about import functionality"
},
"noTilesToExport": "No tiles available to export",
"@noTilesToExport": {
"description": "Message when cache is empty"
},
"archiveContainsStores": "Archive contains {count} stores",
"@archiveContainsStores": {
"description": "Information about archive contents",
"placeholders": {
"count": {
"type": "int"
}
}
},
"vectorTiles": "Vector Tiles",
"@vectorTiles": {
"description": "Label for vector tile type"
@@ -2139,5 +2339,305 @@
"type": "String"
}
}
},
"sarTemplates": "SAR Templates",
"@sarTemplates": {
"description": "SAR templates menu title"
},
"manageSarTemplates": "Manage cursor on target templates",
"@manageSarTemplates": {
"description": "Subtitle for SAR templates settings"
},
"addTemplate": "Add Template",
"@addTemplate": {
"description": "Button to add new SAR template"
},
"editTemplate": "Edit Template",
"@editTemplate": {
"description": "Dialog title for editing template"
},
"deleteTemplate": "Delete Template",
"@deleteTemplate": {
"description": "Action to delete template"
},
"templateName": "Template Name",
"@templateName": {
"description": "Label for template name field"
},
"templateNameHint": "e.g. Found Person",
"@templateNameHint": {
"description": "Hint text for template name"
},
"templateEmoji": "Emoji",
"@templateEmoji": {
"description": "Label for template emoji field"
},
"emojiRequired": "Emoji is required",
"@emojiRequired": {
"description": "Validation error when emoji field is empty"
},
"nameRequired": "Name is required",
"@nameRequired": {
"description": "Validation error when name field is empty"
},
"templateDescription": "Description (Optional)",
"@templateDescription": {
"description": "Label for template description field"
},
"templateDescriptionHint": "Add additional context...",
"@templateDescriptionHint": {
"description": "Hint text for template description"
},
"templateColor": "Color",
"@templateColor": {
"description": "Label for template color picker"
},
"previewFormat": "Preview (SAR Message Format)",
"@previewFormat": {
"description": "Label for format preview"
},
"importFromClipboard": "Import",
"@importFromClipboard": {
"description": "Button to import templates from clipboard"
},
"exportToClipboard": "Export",
"@exportToClipboard": {
"description": "Button to export templates to clipboard"
},
"deleteTemplateConfirmation": "Delete template '{name}'?",
"@deleteTemplateConfirmation": {
"description": "Confirmation message for template deletion",
"placeholders": {
"name": {
"type": "String"
}
}
},
"templateAdded": "Template added",
"@templateAdded": {
"description": "Success message when template is added"
},
"templateUpdated": "Template updated",
"@templateUpdated": {
"description": "Success message when template is updated"
},
"templateDeleted": "Template deleted",
"@templateDeleted": {
"description": "Success message when template is deleted"
},
"templatesImported": "{count, plural, =0{No templates imported} =1{Imported 1 template} other{Imported {count} templates}}",
"@templatesImported": {
"description": "Success message after importing templates",
"placeholders": {
"count": {
"type": "int"
}
}
},
"templatesExported": "{count, plural, =1{Exported 1 template to clipboard} other{Exported {count} templates to clipboard}}",
"@templatesExported": {
"description": "Success message after exporting templates",
"placeholders": {
"count": {
"type": "int"
}
}
},
"importFailed": "Import failed: {error}",
"@importFailed": {
"description": "Error message when import fails",
"placeholders": {
"error": {
"type": "String"
}
}
},
"exportFailed": "Export failed: {error}",
"@exportFailed": {
"description": "Error message when export fails",
"placeholders": {
"error": {
"type": "String"
}
}
},
"resetToDefaults": "Reset to Defaults",
"@resetToDefaults": {
"description": "Action to reset templates to defaults"
},
"resetToDefaultsConfirmation": "This will delete all custom templates and restore the 4 default templates. Continue?",
"@resetToDefaultsConfirmation": {
"description": "Confirmation message for reset to defaults"
},
"reset": "Reset",
"@reset": {
"description": "Reset button label"
},
"resetComplete": "Templates reset to defaults",
"@resetComplete": {
"description": "Success message after reset"
},
"noTemplates": "No templates available",
"@noTemplates": {
"description": "Message when no templates exist"
},
"tapAddToCreate": "Tap + to create your first template",
"@tapAddToCreate": {
"description": "Helper text when no templates exist"
},
"ok": "OK",
"@ok": {
"description": "OK button label"
},
"delete": "Delete",
"@delete": {
"description": "Delete button label"
},
"permissionsSection": "Permissions",
"@permissionsSection": {
"description": "Permissions section header"
},
"locationPermission": "Location Permission",
"@locationPermission": {
"description": "Location permission label"
},
"checking": "Checking...",
"@checking": {
"description": "Loading state indicator"
},
"locationPermissionGrantedAlways": "Granted (Always)",
"@locationPermissionGrantedAlways": {
"description": "Location permission status: granted always"
},
"locationPermissionGrantedWhileInUse": "Granted (While In Use)",
"@locationPermissionGrantedWhileInUse": {
"description": "Location permission status: granted while in use"
},
"locationPermissionDeniedTapToRequest": "Denied - Tap to request",
"@locationPermissionDeniedTapToRequest": {
"description": "Location permission status: denied, user can request"
},
"locationPermissionPermanentlyDeniedOpenSettings": "Permanently Denied - Open Settings",
"@locationPermissionPermanentlyDeniedOpenSettings": {
"description": "Location permission status: permanently denied"
},
"locationPermissionDialogContent": "Location permission is permanently denied. Please enable it in your device settings to use GPS tracking and location sharing features.",
"@locationPermissionDialogContent": {
"description": "Content for location permission dialog when permanently denied"
},
"openSettings": "Open Settings",
"@openSettings": {
"description": "Button to open device settings"
},
"locationPermissionGranted": "Location permission granted!",
"@locationPermissionGranted": {
"description": "Success message when location permission is granted"
},
"locationPermissionRequiredForGps": "Location permission is required for GPS tracking and location sharing.",
"@locationPermissionRequiredForGps": {
"description": "Info message about location permission requirement"
},
"locationPermissionAlreadyGranted": "Location permission is already granted.",
"@locationPermissionAlreadyGranted": {
"description": "Info message when permission is already granted"
},
"sarNavyBlue": "SAR Navy Blue",
"@sarNavyBlue": {
"description": "SAR Navy Blue theme name"
},
"sarNavyBlueDescription": "Professional/Operations Mode",
"@sarNavyBlueDescription": {
"description": "Description for SAR Navy Blue theme"
},
"selectRecipient": "Select Recipient",
"@selectRecipient": {
"description": "Title for recipient selector sheet"
},
"broadcastToAllNearby": "Broadcast to all nearby",
"@broadcastToAllNearby": {
"description": "Subtitle for public channel option"
},
"searchRecipients": "Search recipients...",
"@searchRecipients": {
"description": "Placeholder text for recipient search field"
},
"noContactsFound": "No contacts found",
"@noContactsFound": {
"description": "Message when no contacts match search"
},
"noRoomsFound": "No rooms found",
"@noRoomsFound": {
"description": "Message when no rooms match search"
},
"noContactsOrRoomsAvailable": "No contacts or rooms available",
"@noContactsOrRoomsAvailable": {
"description": "Message when no contacts or rooms exist"
},
"messagesWillBeSentToPublicChannel": "Messages will be sent to public channel",
"@messagesWillBeSentToPublicChannel": {
"description": "Info message when only public channel is available"
},
"newMessage": "New message",
"@newMessage": {
"description": "Notification title for new message"
},
"channel": "Channel",
"@channel": {
"description": "Channel label in notifications"
}
}

View File

@@ -207,6 +207,16 @@
"description": "Descripción de la configuración de indicadores RX/TX"
},
"simpleMode": "Modo Simple",
"@simpleMode": {
"description": "Configuración para habilitar el modo simple"
},
"simpleModeDescription": "Ocultar información no esencial en mensajes y contactos",
"@simpleModeDescription": {
"description": "Descripción de la configuración del modo simple"
},
"language": "Idioma",
"@language": {
"description": "Etiqueta de la configuración de idioma"
@@ -514,7 +524,7 @@
"description": "Etiqueta del botón de actualizar"
},
"sendDirectMessage": "Enviar mensaje directo",
"sendDirectMessage": "Enviar",
"@sendDirectMessage": {
"description": "Acción para enviar mensaje directo al contacto"
},
@@ -634,6 +644,36 @@
"description": "Acción para borrar todos los dibujos locales"
},
"showReceivedDrawings": "Mostrar dibujos recibidos",
"@showReceivedDrawings": {
"description": "Alternar para mostrar/ocultar dibujos recibidos de otros miembros del equipo"
},
"showingAllDrawings": "Mostrando todos los dibujos",
"@showingAllDrawings": {
"description": "Subtítulo cuando los dibujos recibidos son visibles"
},
"showingOnlyYourDrawings": "Mostrando solo tus dibujos",
"@showingOnlyYourDrawings": {
"description": "Subtítulo cuando los dibujos recibidos están ocultos"
},
"showSarMarkers": "Mostrar marcadores SAR",
"@showSarMarkers": {
"description": "Alternar para mostrar/ocultar marcadores SAR en el mapa"
},
"showingSarMarkers": "Mostrando marcadores SAR",
"@showingSarMarkers": {
"description": "Subtítulo cuando los marcadores SAR son visibles"
},
"hidingSarMarkers": "Ocultando marcadores SAR",
"@hidingSarMarkers": {
"description": "Subtítulo cuando los marcadores SAR están ocultos"
},
"clearAll": "Borrar todo",
"@clearAll": {
"description": "Etiqueta del botón de borrar todo"
@@ -762,6 +802,11 @@
"description": "Etiqueta para la sección de ubicación"
},
"myLocation": "Mi ubicación",
"@myLocation": {
"description": "Etiqueta del botón para insertar la ubicación GPS actual"
},
"fromMap": "Desde el mapa",
"@fromMap": {
"description": "Insignia que muestra que la ubicación es desde un toque en el mapa"
@@ -1125,6 +1170,21 @@
"description": "Etiqueta de precisión GPS"
},
"distance": "Distancia",
"@distance": {
"description": "Etiqueta de distancia en la brújula"
},
"bearing": "Rumbo",
"@bearing": {
"description": "Etiqueta de rumbo en la brújula"
},
"direction": "Dirección",
"@direction": {
"description": "Etiqueta de dirección en la brújula"
},
"filterMarkers": "Filtrar marcadores",
"@filterMarkers": {
"description": "Título del diálogo de filtrar marcadores"
@@ -1538,6 +1598,21 @@
"description": "Nombre de capa de imágenes de satélite ESRI"
},
"googleHybrid": "Google Híbrido",
"@googleHybrid": {
"description": "Nombre de capa Google Híbrido (satélite + etiquetas)"
},
"googleRoadmap": "Google Mapa de Carreteras",
"@googleRoadmap": {
"description": "Nombre de capa Google Mapa de Carreteras"
},
"googleTerrain": "Google Terreno",
"@googleTerrain": {
"description": "Nombre de capa Google Terreno (topográfico)"
},
"downloadVisibleArea": "Descargar área visible",
"@downloadVisibleArea": {
"description": "Tooltip para el botón de descargar área visible"
@@ -1673,6 +1748,11 @@
"description": "Mensaje de información cuando se elimina mensaje"
},
"copyText": "Copiar texto",
"textCopiedToClipboard": "Texto copiado al portapapeles",
"deleteMessage": "Eliminar mensaje",
"deleteMessageConfirmation": "¿Está seguro de que desea eliminar este mensaje?",
"refreshedContacts": "Contactos actualizados",
"@refreshedContacts": {
"description": "Mensaje de éxito cuando se actualizan contactos"
@@ -1901,6 +1981,111 @@
"description": "Mensaje de error cuando falla la eliminación de MBTiles"
},
"importExportCachedTiles": "Importar/Exportar teselas en caché",
"@importExportCachedTiles": {
"description": "Título para sección de importar/exportar"
},
"importExportDescription": "Realice copias de seguridad, comparta y restaure teselas de mapas descargadas entre dispositivos",
"@importExportDescription": {
"description": "Descripción de funcionalidad de importar/exportar"
},
"exportTilesToFile": "Exportar teselas a archivo",
"@exportTilesToFile": {
"description": "Botón para exportar teselas"
},
"importTilesFromFile": "Importar teselas desde archivo",
"@importTilesFromFile": {
"description": "Botón para importar teselas"
},
"selectExportLocation": "Seleccionar ubicación de exportación",
"@selectExportLocation": {
"description": "Título para selector de archivo de exportación"
},
"selectImportFile": "Seleccionar archivo de teselas",
"@selectImportFile": {
"description": "Título para selector de archivo de importación"
},
"exportingTiles": "Exportando teselas...",
"@exportingTiles": {
"description": "Mensaje de estado durante exportación"
},
"importingTiles": "Importando teselas...",
"@importingTiles": {
"description": "Mensaje de estado durante importación"
},
"exportSuccess": "{count} teselas exportadas exitosamente",
"@exportSuccess": {
"description": "Mensaje de éxito después de exportar",
"placeholders": {
"count": {
"type": "int"
}
}
},
"importSuccess": "{count} almacenes importados exitosamente",
"@importSuccess": {
"description": "Mensaje de éxito después de importar",
"placeholders": {
"count": {
"type": "int"
}
}
},
"exportFailed": "Error en exportación: {error}",
"@exportFailed": {
"description": "Mensaje de error cuando falla exportación",
"placeholders": {
"error": {
"type": "String"
}
}
},
"importFailed": "Error en importación: {error}",
"@importFailed": {
"description": "Mensaje de error cuando falla importación",
"placeholders": {
"error": {
"type": "String"
}
}
},
"exportNote": "Crea un archivo comprimido (.fmtc) que se puede compartir e importar en otros dispositivos.",
"@exportNote": {
"description": "Nota sobre funcionalidad de exportación"
},
"importNote": "Importa teselas de mapa desde un archivo previamente exportado. Las teselas se fusionarán con la caché existente.",
"@importNote": {
"description": "Nota sobre funcionalidad de importación"
},
"noTilesToExport": "No hay teselas para exportar",
"@noTilesToExport": {
"description": "Mensaje cuando caché está vacío"
},
"archiveContainsStores": "El archivo contiene {count} almacenes",
"@archiveContainsStores": {
"description": "Información sobre contenido del archivo",
"placeholders": {
"count": {
"type": "int"
}
}
},
"vectorTiles": "Teselas vectoriales",
"@vectorTiles": {
"description": "Etiqueta del tipo de tesela vectorial"
@@ -2134,5 +2319,63 @@
"type": "String"
}
}
}
},
"sarTemplates": "SAR Templates",
"manageSarTemplates": "Manage cursor on target templates",
"addTemplate": "Add Template",
"editTemplate": "Edit Template",
"deleteTemplate": "Delete Template",
"templateName": "Template Name",
"templateNameHint": "e.g. Found Person",
"templateEmoji": "Emoji",
"emojiRequired": "Se requiere emoji",
"nameRequired": "Se requiere nombre",
"templateDescription": "Description (Optional)",
"templateDescriptionHint": "Add additional context...",
"templateColor": "Color",
"previewFormat": "Preview (SAR Message Format)",
"importFromClipboard": "Import",
"exportToClipboard": "Export",
"deleteTemplateConfirmation": "Delete template '{name}'?",
"templateAdded": "Template added",
"templateUpdated": "Template updated",
"templateDeleted": "Template deleted",
"templatesImported": "{count, plural, =0{No templates imported} =1{Imported 1 template} other{Imported {count} templates}}",
"templatesExported": "{count, plural, =1{Exported 1 template to clipboard} other{Exported {count} templates to clipboard}}",
"importFailed": "Import failed: {error}",
"exportFailed": "Export failed: {error}",
"resetToDefaults": "Reset to Defaults",
"resetToDefaultsConfirmation": "This will delete all custom templates and restore the 4 default templates. Continue?",
"reset": "Reset",
"resetComplete": "Templates reset to defaults",
"noTemplates": "No templates available",
"tapAddToCreate": "Tap + to create your first template",
"ok": "OK",
"delete": "Eliminar",
"permissionsSection": "Permisos",
"locationPermission": "Permiso de ubicación",
"checking": "Comprobando...",
"locationPermissionGrantedAlways": "Concedido (Siempre)",
"locationPermissionGrantedWhileInUse": "Concedido (Durante el uso)",
"locationPermissionDeniedTapToRequest": "Denegado - Toca para solicitar",
"locationPermissionPermanentlyDeniedOpenSettings": "Denegado permanentemente - Abrir ajustes",
"locationPermissionDialogContent": "El permiso de ubicación está permanentemente denegado. Por favor, actívalo en la configuración de tu dispositivo para usar el rastreo GPS y compartir ubicación.",
"openSettings": "Abrir ajustes",
"locationPermissionGranted": "¡Permiso de ubicación concedido!",
"locationPermissionRequiredForGps": "El permiso de ubicación es necesario para el rastreo GPS y compartir ubicación.",
"locationPermissionAlreadyGranted": "El permiso de ubicación ya está concedido.",
"sarNavyBlue": "SAR Azul Marino",
"sarNavyBlueDescription": "Modo Profesional/Operaciones",
"selectRecipient": "Seleccionar destinatario",
"broadcastToAllNearby": "Transmitir a todos cercanos",
"searchRecipients": "Buscar destinatarios...",
"noContactsFound": "No se encontraron contactos",
"noRoomsFound": "No se encontraron salas",
"noContactsOrRoomsAvailable": "No hay contactos o salas disponibles",
"messagesWillBeSentToPublicChannel": "Los mensajes se enviarán al canal público",
"newMessage": "Nuevo mensaje",
"channel": "Canal"
}

View File

@@ -207,6 +207,16 @@
"description": "Description du paramètre des indicateurs RX/TX"
},
"simpleMode": "Mode Simple",
"@simpleMode": {
"description": "Paramètre pour activer le mode simple"
},
"simpleModeDescription": "Masquer les informations non essentielles dans les messages et les contacts",
"@simpleModeDescription": {
"description": "Description du paramètre du mode simple"
},
"language": "Langue",
"@language": {
"description": "Libellé du paramètre de langue"
@@ -514,7 +524,7 @@
"description": "Libellé du bouton Actualiser"
},
"sendDirectMessage": "Envoyer un message direct",
"sendDirectMessage": "Envoyer",
"@sendDirectMessage": {
"description": "Action pour envoyer un message direct au contact"
},
@@ -634,6 +644,36 @@
"description": "Action pour effacer tous les dessins locaux"
},
"showReceivedDrawings": "Afficher les dessins reçus",
"@showReceivedDrawings": {
"description": "Basculer pour afficher/masquer les dessins reçus des autres membres de l'équipe"
},
"showingAllDrawings": "Affichage de tous les dessins",
"@showingAllDrawings": {
"description": "Sous-titre lorsque les dessins reçus sont visibles"
},
"showingOnlyYourDrawings": "Affichage uniquement de vos dessins",
"@showingOnlyYourDrawings": {
"description": "Sous-titre lorsque les dessins reçus sont masqués"
},
"showSarMarkers": "Afficher les marqueurs SAR",
"@showSarMarkers": {
"description": "Basculer pour afficher/masquer les marqueurs SAR sur la carte"
},
"showingSarMarkers": "Affichage des marqueurs SAR",
"@showingSarMarkers": {
"description": "Sous-titre lorsque les marqueurs SAR sont visibles"
},
"hidingSarMarkers": "Masquage des marqueurs SAR",
"@hidingSarMarkers": {
"description": "Sous-titre lorsque les marqueurs SAR sont masqués"
},
"clearAll": "Tout effacer",
"@clearAll": {
"description": "Libellé du bouton Tout effacer"
@@ -762,6 +802,11 @@
"description": "Libellé de la section de position"
},
"myLocation": "Ma position",
"@myLocation": {
"description": "Libellé du bouton pour insérer la position GPS actuelle"
},
"fromMap": "Depuis la carte",
"@fromMap": {
"description": "Badge indiquant que la position provient d'un clic sur la carte"
@@ -1125,6 +1170,21 @@
"description": "Libellé de la précision GPS"
},
"distance": "Distance",
"@distance": {
"description": "Libellé de la distance dans la boussole"
},
"bearing": "Relèvement",
"@bearing": {
"description": "Libellé du relèvement dans la boussole"
},
"direction": "Direction",
"@direction": {
"description": "Libellé de la direction dans la boussole"
},
"filterMarkers": "Filtrer les marqueurs",
"@filterMarkers": {
"description": "Titre de la boîte de dialogue de filtrage des marqueurs"
@@ -1538,6 +1598,21 @@
"description": "Nom de la couche d'imagerie satellite ESRI"
},
"googleHybrid": "Google Hybride",
"@googleHybrid": {
"description": "Nom de la couche Google Hybride (satellite + étiquettes)"
},
"googleRoadmap": "Google Carte Routière",
"@googleRoadmap": {
"description": "Nom de la couche Google Carte Routière"
},
"googleTerrain": "Google Terrain",
"@googleTerrain": {
"description": "Nom de la couche Google Terrain (topographique)"
},
"downloadVisibleArea": "Télécharger la zone visible",
"@downloadVisibleArea": {
"description": "Info-bulle du bouton de téléchargement de la zone visible"
@@ -1673,6 +1748,11 @@
"description": "Message d'information lorsque le message est supprimé"
},
"copyText": "Copier le texte",
"textCopiedToClipboard": "Texte copié dans le presse-papiers",
"deleteMessage": "Supprimer le message",
"deleteMessageConfirmation": "Êtes-vous sûr de vouloir supprimer ce message?",
"refreshedContacts": "Contacts actualisés",
"@refreshedContacts": {
"description": "Message de succès lorsque les contacts sont actualisés"
@@ -1901,6 +1981,111 @@
"description": "Message d'erreur lorsque la suppression de MBTiles échoue"
},
"importExportCachedTiles": "Importer/Exporter les tuiles en cache",
"@importExportCachedTiles": {
"description": "Titre pour section d'importation/exportation"
},
"importExportDescription": "Sauvegarder, partager et restaurer les tuiles de carte téléchargées entre appareils",
"@importExportDescription": {
"description": "Description de la fonctionnalité d'importation/exportation"
},
"exportTilesToFile": "Exporter les tuiles vers fichier",
"@exportTilesToFile": {
"description": "Bouton pour exporter les tuiles"
},
"importTilesFromFile": "Importer les tuiles depuis fichier",
"@importTilesFromFile": {
"description": "Bouton pour importer les tuiles"
},
"selectExportLocation": "Sélectionner l'emplacement d'exportation",
"@selectExportLocation": {
"description": "Titre pour sélecteur de fichier d'exportation"
},
"selectImportFile": "Sélectionner l'archive de tuiles",
"@selectImportFile": {
"description": "Titre pour sélecteur de fichier d'importation"
},
"exportingTiles": "Exportation des tuiles...",
"@exportingTiles": {
"description": "Message de statut pendant l'exportation"
},
"importingTiles": "Importation des tuiles...",
"@importingTiles": {
"description": "Message de statut pendant l'importation"
},
"exportSuccess": "{count} tuiles exportées avec succès",
"@exportSuccess": {
"description": "Message de succès après exportation",
"placeholders": {
"count": {
"type": "int"
}
}
},
"importSuccess": "{count} magasins importés avec succès",
"@importSuccess": {
"description": "Message de succès après importation",
"placeholders": {
"count": {
"type": "int"
}
}
},
"exportFailed": "Échec de l'exportation: {error}",
"@exportFailed": {
"description": "Message d'erreur lors de l'échec de l'exportation",
"placeholders": {
"error": {
"type": "String"
}
}
},
"importFailed": "Échec de l'importation: {error}",
"@importFailed": {
"description": "Message d'erreur lors de l'échec de l'importation",
"placeholders": {
"error": {
"type": "String"
}
}
},
"exportNote": "Crée un fichier d'archive compressé (.fmtc) qui peut être partagé et importé sur d'autres appareils.",
"@exportNote": {
"description": "Note sur la fonctionnalité d'exportation"
},
"importNote": "Importe les tuiles de carte depuis un fichier d'archive précédemment exporté. Les tuiles seront fusionnées avec le cache existant.",
"@importNote": {
"description": "Note sur la fonctionnalité d'importation"
},
"noTilesToExport": "Aucune tuile à exporter",
"@noTilesToExport": {
"description": "Message quand le cache est vide"
},
"archiveContainsStores": "L'archive contient {count} magasins",
"@archiveContainsStores": {
"description": "Information sur le contenu de l'archive",
"placeholders": {
"count": {
"type": "int"
}
}
},
"vectorTiles": "Tuiles vectorielles",
"@vectorTiles": {
"description": "Libellé du type de tuile vectorielle"
@@ -2139,5 +2324,63 @@
"type": "String"
}
}
}
},
"sarTemplates": "SAR Templates",
"manageSarTemplates": "Manage cursor on target templates",
"addTemplate": "Add Template",
"editTemplate": "Edit Template",
"deleteTemplate": "Delete Template",
"templateName": "Template Name",
"templateNameHint": "e.g. Found Person",
"templateEmoji": "Emoji",
"emojiRequired": "Emoji est requis",
"nameRequired": "Nom est requis",
"templateDescription": "Description (Optional)",
"templateDescriptionHint": "Add additional context...",
"templateColor": "Color",
"previewFormat": "Preview (SAR Message Format)",
"importFromClipboard": "Import",
"exportToClipboard": "Export",
"deleteTemplateConfirmation": "Delete template '{name}'?",
"templateAdded": "Template added",
"templateUpdated": "Template updated",
"templateDeleted": "Template deleted",
"templatesImported": "{count, plural, =0{No templates imported} =1{Imported 1 template} other{Imported {count} templates}}",
"templatesExported": "{count, plural, =1{Exported 1 template to clipboard} other{Exported {count} templates to clipboard}}",
"importFailed": "Import failed: {error}",
"exportFailed": "Export failed: {error}",
"resetToDefaults": "Reset to Defaults",
"resetToDefaultsConfirmation": "This will delete all custom templates and restore the 4 default templates. Continue?",
"reset": "Reset",
"resetComplete": "Templates reset to defaults",
"noTemplates": "No templates available",
"tapAddToCreate": "Tap + to create your first template",
"ok": "OK",
"delete": "Supprimer",
"permissionsSection": "Autorisations",
"locationPermission": "Autorisation de localisation",
"checking": "Vérification...",
"locationPermissionGrantedAlways": "Accordée (Toujours)",
"locationPermissionGrantedWhileInUse": "Accordée (En cours d'utilisation)",
"locationPermissionDeniedTapToRequest": "Refusée - Appuyez pour demander",
"locationPermissionPermanentlyDeniedOpenSettings": "Refusée définitivement - Ouvrir les paramètres",
"locationPermissionDialogContent": "L'autorisation de localisation est définitivement refusée. Veuillez l'activer dans les paramètres de votre appareil pour utiliser le suivi GPS et le partage de localisation.",
"openSettings": "Ouvrir les paramètres",
"locationPermissionGranted": "Autorisation de localisation accordée !",
"locationPermissionRequiredForGps": "L'autorisation de localisation est nécessaire pour le suivi GPS et le partage de localisation.",
"locationPermissionAlreadyGranted": "L'autorisation de localisation est déjà accordée.",
"sarNavyBlue": "SAR Bleu Marine",
"sarNavyBlueDescription": "Mode Professionnel/Opérations",
"selectRecipient": "Sélectionner le destinataire",
"broadcastToAllNearby": "Diffuser à tous à proximité",
"searchRecipients": "Rechercher des destinataires...",
"noContactsFound": "Aucun contact trouvé",
"noRoomsFound": "Aucune salle trouvée",
"noContactsOrRoomsAvailable": "Aucun contact ou salle disponible",
"messagesWillBeSentToPublicChannel": "Les messages seront envoyés au canal public",
"newMessage": "Nouveau message",
"channel": "Canal"
}

View File

@@ -75,6 +75,10 @@
"displayPacketActivity": "Prikaži indikatore aktivnosti paketa u gornjoj traci",
"simpleMode": "Jednostavni način",
"simpleModeDescription": "Sakrij nevažne informacije u porukama i kontaktima",
"language": "Jezik",
"chooseLanguage": "Odaberite jezik",
@@ -181,7 +185,7 @@
"refresh": "Osvježi",
"sendDirectMessage": "Pošalji izravnu poruku",
"sendDirectMessage": "Pošalji",
"resetPath": "Resetiraj put (preusmjeri)",
@@ -221,6 +225,18 @@
"clearAllDrawings": "Očisti sve crteže",
"showReceivedDrawings": "Prikaži primljene crteže",
"showingAllDrawings": "Prikazujem sve crteže",
"showingOnlyYourDrawings": "Prikazujem samo vaše crteže",
"showSarMarkers": "Prikaži SAR oznake",
"showingSarMarkers": "Prikazujem SAR oznake",
"hidingSarMarkers": "Skrivam SAR oznake",
"clearAll": "Očisti sve",
"noLocalDrawings": "Nema lokalnih crteža za dijeljenje",
@@ -263,6 +279,8 @@
"location": "Lokacija",
"myLocation": "Moja lokacija",
"fromMap": "S karte",
"gettingLocation": "Dohvaćanje lokacije...",
@@ -379,6 +397,12 @@
"accuracy": "Točnost",
"distance": "Udaljenost",
"bearing": "Azimut",
"direction": "Smjer",
"filterMarkers": "Filtriraj markere",
"filterMarkersTooltip": "Filtriraj markere",
@@ -521,6 +545,12 @@
"esriSatellite": "ESRI satelit",
"googleHybrid": "Google hibridno",
"googleRoadmap": "Google cestovna karta",
"googleTerrain": "Google teren",
"downloadVisibleArea": "Preuzmi vidljivo područje",
"initializingMap": "Inicijalizacija karte...",
@@ -586,6 +616,10 @@
"cannotReplyContactNotFound": "Ne mogu odgovoriti: kontakt nije pronađen",
"messageDeleted": "Poruka izbrisana",
"copyText": "Kopiraj tekst",
"textCopiedToClipboard": "Tekst kopiran u međuspremnik",
"deleteMessage": "Izbriši poruku",
"deleteMessageConfirmation": "Jeste li sigurni da želite izbrisati ovu poruku?",
"refreshedContacts": "Kontakti osvježeni",
@@ -636,6 +670,38 @@
"failedToDeleteMbtiles": "Neuspjelo brisanje offline karte",
"importExportCachedTiles": "Uvoz/Izvoz predmemoriranih pločica",
"importExportDescription": "Sigurnosno kopirajte, dijelite i vraćajte preuzete pločice karte između uređaja",
"exportTilesToFile": "Izvezi pločice u datoteku",
"importTilesFromFile": "Uvezi pločice iz datoteke",
"selectExportLocation": "Odaberite lokaciju izvoza",
"selectImportFile": "Odaberite arhivu pločica",
"exportingTiles": "Izvažanje pločica...",
"importingTiles": "Uvažanje pločica...",
"exportSuccess": "Uspješno izvezeno {count} pločica",
"importSuccess": "Uspješno uvezeno {count} skladišta",
"exportFailed": "Izvoz nije uspio: {error}",
"importFailed": "Uvoz nije uspio: {error}",
"exportNote": "Stvara komprimiranu arhivsku datoteku (.fmtc) koju možete dijeliti i uvesti na drugim uređajima.",
"importNote": "Uvozi pločice karte iz prethodno izvezene arhivske datoteke. Pločice će biti spojene s postojećom predmemorijom.",
"noTilesToExport": "Nema pločica za izvoz",
"archiveContainsStores": "Arhiva sadrži {count} skladišta",
"vectorTiles": "Vektorske pločice",
"schema": "Shema",
@@ -722,5 +788,63 @@
"failedToSave": "Neuspjelo spremanje: {error}",
"failedToGetLocation": "Neuspjelo dohvaćanje lokacije: {error}"
"failedToGetLocation": "Neuspjelo dohvaćanje lokacije: {error}",
"sarTemplates": "SAR Templates",
"manageSarTemplates": "Manage cursor on target templates",
"addTemplate": "Add Template",
"editTemplate": "Edit Template",
"deleteTemplate": "Delete Template",
"templateName": "Template Name",
"templateNameHint": "e.g. Found Person",
"templateEmoji": "Emoji",
"emojiRequired": "Emoji je obavezan",
"nameRequired": "Ime je obavezno",
"templateDescription": "Description (Optional)",
"templateDescriptionHint": "Add additional context...",
"templateColor": "Color",
"previewFormat": "Preview (SAR Message Format)",
"importFromClipboard": "Import",
"exportToClipboard": "Export",
"deleteTemplateConfirmation": "Delete template '{name}'?",
"templateAdded": "Template added",
"templateUpdated": "Template updated",
"templateDeleted": "Template deleted",
"templatesImported": "{count, plural, =0{No templates imported} =1{Imported 1 template} other{Imported {count} templates}}",
"templatesExported": "{count, plural, =1{Exported 1 template to clipboard} other{Exported {count} templates to clipboard}}",
"importFailed": "Import failed: {error}",
"exportFailed": "Export failed: {error}",
"resetToDefaults": "Reset to Defaults",
"resetToDefaultsConfirmation": "This will delete all custom templates and restore the 4 default templates. Continue?",
"reset": "Reset",
"resetComplete": "Templates reset to defaults",
"noTemplates": "No templates available",
"tapAddToCreate": "Tap + to create your first template",
"ok": "OK",
"delete": "Obriši",
"permissionsSection": "Dozvole",
"locationPermission": "Dozvola za lokaciju",
"checking": "Provjera...",
"locationPermissionGrantedAlways": "Odobreno (Uvijek)",
"locationPermissionGrantedWhileInUse": "Odobreno (Tijekom uporabe)",
"locationPermissionDeniedTapToRequest": "Odbijeno - Dodirnite za zahtjev",
"locationPermissionPermanentlyDeniedOpenSettings": "Trajno odbijeno - Otvori postavke",
"locationPermissionDialogContent": "Dozvola za lokaciju je trajno odbijena. Omogućite je u postavkama uređaja kako biste koristili GPS praćenje i dijeljenje lokacije.",
"openSettings": "Otvori postavke",
"locationPermissionGranted": "Dozvola za lokaciju odobrena!",
"locationPermissionRequiredForGps": "Dozvola za lokaciju je potrebna za GPS praćenje i dijeljenje lokacije.",
"locationPermissionAlreadyGranted": "Dozvola za lokaciju je već odobrena.",
"sarNavyBlue": "SAR Mornarsko Plava",
"sarNavyBlueDescription": "Profesionalni/Operativni Način",
"selectRecipient": "Odaberi primatelja",
"broadcastToAllNearby": "Emituj svima u blizini",
"searchRecipients": "Pretraži primatelje...",
"noContactsFound": "Nema kontakata",
"noRoomsFound": "Nema soba",
"noContactsOrRoomsAvailable": "Nema dostupnih kontakata ili soba",
"messagesWillBeSentToPublicChannel": "Poruke će biti poslane na javni kanal",
"newMessage": "Nova poruka",
"channel": "Kanal"
}

View File

@@ -207,6 +207,16 @@
"description": "Descrizione per l'impostazione degli indicatori RX/TX"
},
"simpleMode": "Modalità Semplice",
"@simpleMode": {
"description": "Impostazione per abilitare la modalità semplice"
},
"simpleModeDescription": "Nascondi informazioni non essenziali nei messaggi e contatti",
"@simpleModeDescription": {
"description": "Descrizione per l'impostazione della modalità semplice"
},
"language": "Lingua",
"@language": {
"description": "Etichetta impostazione lingua"
@@ -514,7 +524,7 @@
"description": "Etichetta pulsante Aggiorna"
},
"sendDirectMessage": "Invia Messaggio Diretto",
"sendDirectMessage": "Invia",
"@sendDirectMessage": {
"description": "Azione per inviare messaggio diretto al contatto"
},
@@ -634,6 +644,36 @@
"description": "Azione per cancellare tutti i disegni locali"
},
"showReceivedDrawings": "Mostra Disegni Ricevuti",
"@showReceivedDrawings": {
"description": "Interruttore per mostrare/nascondere i disegni ricevuti da altri membri del team"
},
"showingAllDrawings": "Visualizzazione di tutti i disegni",
"@showingAllDrawings": {
"description": "Sottotitolo quando i disegni ricevuti sono visibili"
},
"showingOnlyYourDrawings": "Visualizzazione solo dei tuoi disegni",
"@showingOnlyYourDrawings": {
"description": "Sottotitolo quando i disegni ricevuti sono nascosti"
},
"showSarMarkers": "Mostra marcatori SAR",
"@showSarMarkers": {
"description": "Interruttore per mostrare/nascondere i marcatori SAR sulla mappa"
},
"showingSarMarkers": "Visualizzazione marcatori SAR",
"@showingSarMarkers": {
"description": "Sottotitolo quando i marcatori SAR sono visibili"
},
"hidingSarMarkers": "Nascondere marcatori SAR",
"@hidingSarMarkers": {
"description": "Sottotitolo quando i marcatori SAR sono nascosti"
},
"clearAll": "Cancella Tutto",
"@clearAll": {
"description": "Etichetta pulsante Cancella Tutto"
@@ -762,6 +802,11 @@
"description": "Etichetta per la sezione posizione"
},
"myLocation": "La mia posizione",
"@myLocation": {
"description": "Etichetta del pulsante per inserire la posizione GPS attuale"
},
"fromMap": "Dalla Mappa",
"@fromMap": {
"description": "Badge che mostra che la posizione proviene dal tocco sulla mappa"
@@ -1125,6 +1170,21 @@
"description": "Etichetta precisione GPS"
},
"distance": "Distanza",
"@distance": {
"description": "Etichetta distanza nella bussola"
},
"bearing": "Rilevamento",
"@bearing": {
"description": "Etichetta rilevamento nella bussola"
},
"direction": "Direzione",
"@direction": {
"description": "Etichetta direzione nella bussola"
},
"filterMarkers": "Filtra Marcatori",
"@filterMarkers": {
"description": "Titolo per la finestra filtra marcatori"
@@ -1538,6 +1598,21 @@
"description": "Nome livello immagini satellitari ESRI"
},
"googleHybrid": "Google Ibrido",
"@googleHybrid": {
"description": "Nome livello Google Ibrido (satellite + etichette)"
},
"googleRoadmap": "Google Mappa Stradale",
"@googleRoadmap": {
"description": "Nome livello Google Mappa Stradale"
},
"googleTerrain": "Google Terreno",
"@googleTerrain": {
"description": "Nome livello Google Terreno (topografico)"
},
"downloadVisibleArea": "Scarica area visibile",
"@downloadVisibleArea": {
"description": "Tooltip per il pulsante scarica area visibile"
@@ -1673,6 +1748,11 @@
"description": "Messaggio informativo quando un messaggio viene eliminato"
},
"copyText": "Copia testo",
"textCopiedToClipboard": "Testo copiato negli appunti",
"deleteMessage": "Elimina messaggio",
"deleteMessageConfirmation": "Sei sicuro di voler eliminare questo messaggio?",
"refreshedContacts": "Contatti aggiornati",
"@refreshedContacts": {
"description": "Messaggio di successo quando i contatti vengono aggiornati"
@@ -1901,6 +1981,111 @@
"description": "Messaggio di errore quando l'eliminazione MBTiles fallisce"
},
"importExportCachedTiles": "Importa/Esporta tile in cache",
"@importExportCachedTiles": {
"description": "Titolo per sezione di importazione/esportazione"
},
"importExportDescription": "Esegui backup, condividi e ripristina tile mappa scaricati tra dispositivi",
"@importExportDescription": {
"description": "Descrizione della funzionalità di importazione/esportazione"
},
"exportTilesToFile": "Esporta tile su file",
"@exportTilesToFile": {
"description": "Pulsante per esportare tile"
},
"importTilesFromFile": "Importa tile da file",
"@importTilesFromFile": {
"description": "Pulsante per importare tile"
},
"selectExportLocation": "Seleziona posizione esportazione",
"@selectExportLocation": {
"description": "Titolo per selettore file di esportazione"
},
"selectImportFile": "Seleziona archivio tile",
"@selectImportFile": {
"description": "Titolo per selettore file di importazione"
},
"exportingTiles": "Esportazione tile...",
"@exportingTiles": {
"description": "Messaggio di stato durante esportazione"
},
"importingTiles": "Importazione tile...",
"@importingTiles": {
"description": "Messaggio di stato durante importazione"
},
"exportSuccess": "{count} tile esportati con successo",
"@exportSuccess": {
"description": "Messaggio di successo dopo esportazione",
"placeholders": {
"count": {
"type": "int"
}
}
},
"importSuccess": "{count} archivi importati con successo",
"@importSuccess": {
"description": "Messaggio di successo dopo importazione",
"placeholders": {
"count": {
"type": "int"
}
}
},
"exportFailed": "Esportazione fallita: {error}",
"@exportFailed": {
"description": "Messaggio di errore quando esportazione fallisce",
"placeholders": {
"error": {
"type": "String"
}
}
},
"importFailed": "Importazione fallita: {error}",
"@importFailed": {
"description": "Messaggio di errore quando importazione fallisce",
"placeholders": {
"error": {
"type": "String"
}
}
},
"exportNote": "Crea un file archivio compresso (.fmtc) che può essere condiviso e importato su altri dispositivi.",
"@exportNote": {
"description": "Nota sulla funzionalità di esportazione"
},
"importNote": "Importa tile mappa da un file archivio precedentemente esportato. I tile verranno uniti con la cache esistente.",
"@importNote": {
"description": "Nota sulla funzionalità di importazione"
},
"noTilesToExport": "Nessun tile da esportare",
"@noTilesToExport": {
"description": "Messaggio quando cache è vuota"
},
"archiveContainsStores": "L'archivio contiene {count} archivi",
"@archiveContainsStores": {
"description": "Informazioni sul contenuto dell'archivio",
"placeholders": {
"count": {
"type": "int"
}
}
},
"vectorTiles": "Tile Vettoriali",
"@vectorTiles": {
"description": "Etichetta per il tipo tile vettoriale"
@@ -2139,5 +2324,63 @@
"type": "String"
}
}
}
},
"sarTemplates": "SAR Templates",
"manageSarTemplates": "Manage cursor on target templates",
"addTemplate": "Add Template",
"editTemplate": "Edit Template",
"deleteTemplate": "Delete Template",
"templateName": "Template Name",
"templateNameHint": "e.g. Found Person",
"templateEmoji": "Emoji",
"emojiRequired": "Emoji è obbligatorio",
"nameRequired": "Nome è obbligatorio",
"templateDescription": "Description (Optional)",
"templateDescriptionHint": "Add additional context...",
"templateColor": "Color",
"previewFormat": "Preview (SAR Message Format)",
"importFromClipboard": "Import",
"exportToClipboard": "Export",
"deleteTemplateConfirmation": "Delete template '{name}'?",
"templateAdded": "Template added",
"templateUpdated": "Template updated",
"templateDeleted": "Template deleted",
"templatesImported": "{count, plural, =0{No templates imported} =1{Imported 1 template} other{Imported {count} templates}}",
"templatesExported": "{count, plural, =1{Exported 1 template to clipboard} other{Exported {count} templates to clipboard}}",
"importFailed": "Import failed: {error}",
"exportFailed": "Export failed: {error}",
"resetToDefaults": "Reset to Defaults",
"resetToDefaultsConfirmation": "This will delete all custom templates and restore the 4 default templates. Continue?",
"reset": "Reset",
"resetComplete": "Templates reset to defaults",
"noTemplates": "No templates available",
"tapAddToCreate": "Tap + to create your first template",
"ok": "OK",
"delete": "Elimina",
"permissionsSection": "Permessi",
"locationPermission": "Permesso di posizione",
"checking": "Verifica in corso...",
"locationPermissionGrantedAlways": "Concesso (Sempre)",
"locationPermissionGrantedWhileInUse": "Concesso (Durante l'uso)",
"locationPermissionDeniedTapToRequest": "Negato - Tocca per richiedere",
"locationPermissionPermanentlyDeniedOpenSettings": "Negato permanentemente - Apri impostazioni",
"locationPermissionDialogContent": "Il permesso di posizione è permanentemente negato. Si prega di abilitarlo nelle impostazioni del dispositivo per utilizzare il tracciamento GPS e la condivisione della posizione.",
"openSettings": "Apri impostazioni",
"locationPermissionGranted": "Permesso di posizione concesso!",
"locationPermissionRequiredForGps": "Il permesso di posizione è necessario per il tracciamento GPS e la condivisione della posizione.",
"locationPermissionAlreadyGranted": "Il permesso di posizione è già concesso.",
"sarNavyBlue": "SAR Blu Navy",
"sarNavyBlueDescription": "Modalità Professionale/Operativa",
"selectRecipient": "Seleziona destinatario",
"broadcastToAllNearby": "Trasmetti a tutti nelle vicinanze",
"searchRecipients": "Cerca destinatari...",
"noContactsFound": "Nessun contatto trovato",
"noRoomsFound": "Nessuna stanza trovata",
"noContactsOrRoomsAvailable": "Nessun contatto o stanza disponibile",
"messagesWillBeSentToPublicChannel": "I messaggi saranno inviati al canale pubblico",
"newMessage": "Nuovo messaggio",
"channel": "Canale"
}

View File

@@ -330,6 +330,18 @@ abstract class AppLocalizations {
/// **'Display packet activity indicators in top bar'**
String get displayPacketActivity;
/// Setting to enable simple mode
///
/// In en, this message translates to:
/// **'Simple Mode'**
String get simpleMode;
/// Description for simple mode setting
///
/// In en, this message translates to:
/// **'Hide non-essential information in messages and contacts'**
String get simpleModeDescription;
/// Language setting label
///
/// In en, this message translates to:
@@ -656,7 +668,7 @@ abstract class AppLocalizations {
/// Action to send direct message to contact
///
/// In en, this message translates to:
/// **'Send Direct Message'**
/// **'Send'**
String get sendDirectMessage;
/// Action to reset contact path for re-routing
@@ -773,6 +785,42 @@ abstract class AppLocalizations {
/// **'Clear All Drawings'**
String get clearAllDrawings;
/// Toggle to show/hide received drawings from other team members
///
/// In en, this message translates to:
/// **'Show Received Drawings'**
String get showReceivedDrawings;
/// Subtitle when received drawings are visible
///
/// In en, this message translates to:
/// **'Showing all drawings'**
String get showingAllDrawings;
/// Subtitle when received drawings are hidden
///
/// In en, this message translates to:
/// **'Showing only your drawings'**
String get showingOnlyYourDrawings;
/// Toggle to show/hide SAR markers on map
///
/// In en, this message translates to:
/// **'Show SAR Markers'**
String get showSarMarkers;
/// Subtitle when SAR markers are visible
///
/// In en, this message translates to:
/// **'Showing SAR markers'**
String get showingSarMarkers;
/// Subtitle when SAR markers are hidden
///
/// In en, this message translates to:
/// **'Hiding SAR markers'**
String get hidingSarMarkers;
/// Clear all button label
///
/// In en, this message translates to:
@@ -899,6 +947,12 @@ abstract class AppLocalizations {
/// **'Location'**
String get location;
/// Button label to insert current GPS location
///
/// In en, this message translates to:
/// **'My Location'**
String get myLocation;
/// Badge showing location is from map tap
///
/// In en, this message translates to:
@@ -1247,6 +1301,18 @@ abstract class AppLocalizations {
/// **'Accuracy'**
String get accuracy;
/// Bearing label in compass
///
/// In en, this message translates to:
/// **'Bearing'**
String get bearing;
/// Direction label in compass
///
/// In en, this message translates to:
/// **'Direction'**
String get direction;
/// Title for filter markers dialog
///
/// In en, this message translates to:
@@ -1673,6 +1739,24 @@ abstract class AppLocalizations {
/// **'ESRI Satellite'**
String get esriSatellite;
/// Google Hybrid layer name (satellite + labels)
///
/// In en, this message translates to:
/// **'Google Hybrid'**
String get googleHybrid;
/// Google Roadmap layer name (street map)
///
/// In en, this message translates to:
/// **'Google Roadmap'**
String get googleRoadmap;
/// Google Terrain layer name (topographic)
///
/// In en, this message translates to:
/// **'Google Terrain'**
String get googleTerrain;
/// Tooltip for download visible area button
///
/// In en, this message translates to:
@@ -1805,6 +1889,24 @@ abstract class AppLocalizations {
/// **'Message deleted'**
String get messageDeleted;
/// Option to copy message text to clipboard
///
/// In en, this message translates to:
/// **'Copy text'**
String get copyText;
/// Dialog title for deleting a message
///
/// In en, this message translates to:
/// **'Delete message'**
String get deleteMessage;
/// Confirmation text for message deletion
///
/// In en, this message translates to:
/// **'Are you sure you want to delete this message?'**
String get deleteMessageConfirmation;
/// Success message when contacts are refreshed
///
/// In en, this message translates to:
@@ -2039,6 +2141,102 @@ abstract class AppLocalizations {
/// **'Failed to delete offline map'**
String get failedToDeleteMbtiles;
/// Title for import/export section
///
/// In en, this message translates to:
/// **'Import/Export Cached Tiles'**
String get importExportCachedTiles;
/// Description for import/export functionality
///
/// In en, this message translates to:
/// **'Backup, share, and restore downloaded map tiles between devices'**
String get importExportDescription;
/// Button to export tiles to archive file
///
/// In en, this message translates to:
/// **'Export Tiles to File'**
String get exportTilesToFile;
/// Button to import tiles from archive file
///
/// In en, this message translates to:
/// **'Import Tiles from File'**
String get importTilesFromFile;
/// Title for export file picker
///
/// In en, this message translates to:
/// **'Select Export Location'**
String get selectExportLocation;
/// Title for import file picker
///
/// In en, this message translates to:
/// **'Select Tile Archive'**
String get selectImportFile;
/// Status message during export
///
/// In en, this message translates to:
/// **'Exporting tiles...'**
String get exportingTiles;
/// Status message during import
///
/// In en, this message translates to:
/// **'Importing tiles...'**
String get importingTiles;
/// Success message after export
///
/// In en, this message translates to:
/// **'Exported {count} tiles successfully'**
String exportSuccess(int count);
/// Success message after import
///
/// In en, this message translates to:
/// **'Imported {count} stores successfully'**
String importSuccess(int count);
/// Error message when export fails
///
/// In en, this message translates to:
/// **'Export failed: {error}'**
String exportFailed(String error);
/// Error message when import fails
///
/// In en, this message translates to:
/// **'Import failed: {error}'**
String importFailed(String error);
/// Note about export functionality
///
/// In en, this message translates to:
/// **'Creates a compressed archive (.fmtc) file that can be shared and imported on other devices.'**
String get exportNote;
/// Note about import functionality
///
/// In en, this message translates to:
/// **'Imports map tiles from a previously exported archive file. Tiles will be merged with existing cache.'**
String get importNote;
/// Message when cache is empty
///
/// In en, this message translates to:
/// **'No tiles available to export'**
String get noTilesToExport;
/// Information about archive contents
///
/// In en, this message translates to:
/// **'Archive contains {count} stores'**
String archiveContainsStores(int count);
/// Label for vector tile type
///
/// In en, this message translates to:
@@ -2296,6 +2494,318 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Failed to get location: {error}'**
String failedToGetLocation(String error);
/// SAR templates menu title
///
/// In en, this message translates to:
/// **'SAR Templates'**
String get sarTemplates;
/// Subtitle for SAR templates settings
///
/// In en, this message translates to:
/// **'Manage cursor on target templates'**
String get manageSarTemplates;
/// Button to add new SAR template
///
/// In en, this message translates to:
/// **'Add Template'**
String get addTemplate;
/// Dialog title for editing template
///
/// In en, this message translates to:
/// **'Edit Template'**
String get editTemplate;
/// Action to delete template
///
/// In en, this message translates to:
/// **'Delete Template'**
String get deleteTemplate;
/// Label for template name field
///
/// In en, this message translates to:
/// **'Template Name'**
String get templateName;
/// Hint text for template name
///
/// In en, this message translates to:
/// **'e.g. Found Person'**
String get templateNameHint;
/// Label for template emoji field
///
/// In en, this message translates to:
/// **'Emoji'**
String get templateEmoji;
/// Validation error when emoji field is empty
///
/// In en, this message translates to:
/// **'Emoji is required'**
String get emojiRequired;
/// Validation error when name field is empty
///
/// In en, this message translates to:
/// **'Name is required'**
String get nameRequired;
/// Label for template description field
///
/// In en, this message translates to:
/// **'Description (Optional)'**
String get templateDescription;
/// Hint text for template description
///
/// In en, this message translates to:
/// **'Add additional context...'**
String get templateDescriptionHint;
/// Label for template color picker
///
/// In en, this message translates to:
/// **'Color'**
String get templateColor;
/// Label for format preview
///
/// In en, this message translates to:
/// **'Preview (SAR Message Format)'**
String get previewFormat;
/// Button to import templates from clipboard
///
/// In en, this message translates to:
/// **'Import'**
String get importFromClipboard;
/// Button to export templates to clipboard
///
/// In en, this message translates to:
/// **'Export'**
String get exportToClipboard;
/// Confirmation message for template deletion
///
/// In en, this message translates to:
/// **'Delete template \'{name}\'?'**
String deleteTemplateConfirmation(String name);
/// Success message when template is added
///
/// In en, this message translates to:
/// **'Template added'**
String get templateAdded;
/// Success message when template is updated
///
/// In en, this message translates to:
/// **'Template updated'**
String get templateUpdated;
/// Success message when template is deleted
///
/// In en, this message translates to:
/// **'Template deleted'**
String get templateDeleted;
/// Success message after importing templates
///
/// In en, this message translates to:
/// **'{count, plural, =0{No templates imported} =1{Imported 1 template} other{Imported {count} templates}}'**
String templatesImported(int count);
/// Success message after exporting templates
///
/// In en, this message translates to:
/// **'{count, plural, =1{Exported 1 template to clipboard} other{Exported {count} templates to clipboard}}'**
String templatesExported(int count);
/// Action to reset templates to defaults
///
/// In en, this message translates to:
/// **'Reset to Defaults'**
String get resetToDefaults;
/// Confirmation message for reset to defaults
///
/// In en, this message translates to:
/// **'This will delete all custom templates and restore the 4 default templates. Continue?'**
String get resetToDefaultsConfirmation;
/// Reset button label
///
/// In en, this message translates to:
/// **'Reset'**
String get reset;
/// Success message after reset
///
/// In en, this message translates to:
/// **'Templates reset to defaults'**
String get resetComplete;
/// Message when no templates exist
///
/// In en, this message translates to:
/// **'No templates available'**
String get noTemplates;
/// Helper text when no templates exist
///
/// In en, this message translates to:
/// **'Tap + to create your first template'**
String get tapAddToCreate;
/// OK button label
///
/// In en, this message translates to:
/// **'OK'**
String get ok;
/// Permissions section header
///
/// In en, this message translates to:
/// **'Permissions'**
String get permissionsSection;
/// Location permission label
///
/// In en, this message translates to:
/// **'Location Permission'**
String get locationPermission;
/// Loading state indicator
///
/// In en, this message translates to:
/// **'Checking...'**
String get checking;
/// Location permission status: granted always
///
/// In en, this message translates to:
/// **'Granted (Always)'**
String get locationPermissionGrantedAlways;
/// Location permission status: granted while in use
///
/// In en, this message translates to:
/// **'Granted (While In Use)'**
String get locationPermissionGrantedWhileInUse;
/// Location permission status: denied, user can request
///
/// In en, this message translates to:
/// **'Denied - Tap to request'**
String get locationPermissionDeniedTapToRequest;
/// Location permission status: permanently denied
///
/// In en, this message translates to:
/// **'Permanently Denied - Open Settings'**
String get locationPermissionPermanentlyDeniedOpenSettings;
/// Content for location permission dialog when permanently denied
///
/// In en, this message translates to:
/// **'Location permission is permanently denied. Please enable it in your device settings to use GPS tracking and location sharing features.'**
String get locationPermissionDialogContent;
/// Button to open device settings
///
/// In en, this message translates to:
/// **'Open Settings'**
String get openSettings;
/// Success message when location permission is granted
///
/// In en, this message translates to:
/// **'Location permission granted!'**
String get locationPermissionGranted;
/// Info message about location permission requirement
///
/// In en, this message translates to:
/// **'Location permission is required for GPS tracking and location sharing.'**
String get locationPermissionRequiredForGps;
/// Info message when permission is already granted
///
/// In en, this message translates to:
/// **'Location permission is already granted.'**
String get locationPermissionAlreadyGranted;
/// SAR Navy Blue theme name
///
/// In en, this message translates to:
/// **'SAR Navy Blue'**
String get sarNavyBlue;
/// Description for SAR Navy Blue theme
///
/// In en, this message translates to:
/// **'Professional/Operations Mode'**
String get sarNavyBlueDescription;
/// Title for recipient selector sheet
///
/// In en, this message translates to:
/// **'Select Recipient'**
String get selectRecipient;
/// Subtitle for public channel option
///
/// In en, this message translates to:
/// **'Broadcast to all nearby'**
String get broadcastToAllNearby;
/// Placeholder text for recipient search field
///
/// In en, this message translates to:
/// **'Search recipients...'**
String get searchRecipients;
/// Message when no contacts match search
///
/// In en, this message translates to:
/// **'No contacts found'**
String get noContactsFound;
/// Message when no rooms match search
///
/// In en, this message translates to:
/// **'No rooms found'**
String get noRoomsFound;
/// Message when no contacts or rooms exist
///
/// In en, this message translates to:
/// **'No contacts or rooms available'**
String get noContactsOrRoomsAvailable;
/// Info message when only public channel is available
///
/// In en, this message translates to:
/// **'Messages will be sent to public channel'**
String get messagesWillBeSentToPublicChannel;
/// Notification title for new message
///
/// In en, this message translates to:
/// **'New message'**
String get newMessage;
/// Channel label in notifications
///
/// In en, this message translates to:
/// **'Channel'**
String get channel;
}
class _AppLocalizationsDelegate

View File

@@ -130,6 +130,13 @@ class AppLocalizationsDe extends AppLocalizations {
String get displayPacketActivity =>
'Paketaktivitätsindikatoren in der oberen Leiste anzeigen';
@override
String get simpleMode => 'Einfacher Modus';
@override
String get simpleModeDescription =>
'Nicht wesentliche Informationen in Nachrichten und Kontakten ausblenden';
@override
String get language => 'Sprache';
@@ -316,7 +323,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get refresh => 'Aktualisieren';
@override
String get sendDirectMessage => 'Direktnachricht senden';
String get sendDirectMessage => 'Senden';
@override
String get resetPath => 'Pfad zurücksetzen (Umleitung)';
@@ -385,6 +392,24 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get clearAllDrawings => 'Alle Zeichnungen löschen';
@override
String get showReceivedDrawings => 'Empfangene Zeichnungen anzeigen';
@override
String get showingAllDrawings => 'Alle Zeichnungen werden angezeigt';
@override
String get showingOnlyYourDrawings => 'Nur Ihre Zeichnungen werden angezeigt';
@override
String get showSarMarkers => 'SAR-Markierungen anzeigen';
@override
String get showingSarMarkers => 'SAR-Markierungen werden angezeigt';
@override
String get hidingSarMarkers => 'SAR-Markierungen ausgeblendet';
@override
String get clearAll => 'Alle löschen';
@@ -460,6 +485,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get location => 'Standort';
@override
String get myLocation => 'Mein Standort';
@override
String get fromMap => 'Von Karte';
@@ -664,6 +692,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get accuracy => 'Genauigkeit';
@override
String get bearing => 'Peilung';
@override
String get direction => 'Richtung';
@override
String get filterMarkers => 'Markierungen filtern';
@@ -906,6 +940,15 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get esriSatellite => 'ESRI-Satellit';
@override
String get googleHybrid => 'Google Hybrid';
@override
String get googleRoadmap => 'Google Straßenkarte';
@override
String get googleTerrain => 'Google Gelände';
@override
String get downloadVisibleArea => 'Sichtbaren Bereich herunterladen';
@@ -988,6 +1031,16 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get messageDeleted => 'Nachricht gelöscht';
@override
String get copyText => 'Text kopieren';
@override
String get deleteMessage => 'Nachricht löschen';
@override
String get deleteMessageConfirmation =>
'Möchten Sie diese Nachricht wirklich löschen?';
@override
String get refreshedContacts => 'Kontakte aktualisiert';
@@ -1123,6 +1176,67 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get failedToDeleteMbtiles => 'Fehler beim Löschen der Offline-Karte';
@override
String get importExportCachedTiles => 'Import/Export gecachter Kacheln';
@override
String get importExportDescription =>
'Sichern, teilen und wiederherstellen Sie heruntergeladene Kartenkacheln zwischen Geräten';
@override
String get exportTilesToFile => 'Kacheln in Datei exportieren';
@override
String get importTilesFromFile => 'Kacheln aus Datei importieren';
@override
String get selectExportLocation => 'Exportspeicherort wählen';
@override
String get selectImportFile => 'Kachel-Archiv auswählen';
@override
String get exportingTiles => 'Exportiere Kacheln...';
@override
String get importingTiles => 'Importiere Kacheln...';
@override
String exportSuccess(int count) {
return '$count Kacheln erfolgreich exportiert';
}
@override
String importSuccess(int count) {
return '$count Speicher erfolgreich importiert';
}
@override
String exportFailed(String error) {
return 'Export failed: $error';
}
@override
String importFailed(String error) {
return 'Import failed: $error';
}
@override
String get exportNote =>
'Erstellt eine komprimierte Archivdatei (.fmtc), die auf anderen Geräten geteilt und importiert werden kann.';
@override
String get importNote =>
'Importiert Kartenkacheln aus einer zuvor exportierten Archivdatei. Kacheln werden mit dem vorhandenen Cache zusammengeführt.';
@override
String get noTilesToExport => 'Keine Kacheln zum Exportieren verfügbar';
@override
String archiveContainsStores(int count) {
return 'Archiv enthält $count Speicher';
}
@override
String get vectorTiles => 'Vektor-Tiles';
@@ -1263,4 +1377,188 @@ class AppLocalizationsDe extends AppLocalizations {
String failedToGetLocation(String error) {
return 'Fehler beim Abrufen des Standorts: $error';
}
@override
String get sarTemplates => 'SAR Templates';
@override
String get manageSarTemplates => 'Manage cursor on target templates';
@override
String get addTemplate => 'Add Template';
@override
String get editTemplate => 'Edit Template';
@override
String get deleteTemplate => 'Delete Template';
@override
String get templateName => 'Template Name';
@override
String get templateNameHint => 'e.g. Found Person';
@override
String get templateEmoji => 'Emoji';
@override
String get emojiRequired => 'Emoji ist erforderlich';
@override
String get nameRequired => 'Name ist erforderlich';
@override
String get templateDescription => 'Description (Optional)';
@override
String get templateDescriptionHint => 'Add additional context...';
@override
String get templateColor => 'Color';
@override
String get previewFormat => 'Preview (SAR Message Format)';
@override
String get importFromClipboard => 'Import';
@override
String get exportToClipboard => 'Export';
@override
String deleteTemplateConfirmation(String name) {
return 'Delete template \'$name\'?';
}
@override
String get templateAdded => 'Template added';
@override
String get templateUpdated => 'Template updated';
@override
String get templateDeleted => 'Template deleted';
@override
String templatesImported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Imported $count templates',
one: 'Imported 1 template',
zero: 'No templates imported',
);
return '$_temp0';
}
@override
String templatesExported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Exported $count templates to clipboard',
one: 'Exported 1 template to clipboard',
);
return '$_temp0';
}
@override
String get resetToDefaults => 'Reset to Defaults';
@override
String get resetToDefaultsConfirmation =>
'This will delete all custom templates and restore the 4 default templates. Continue?';
@override
String get reset => 'Reset';
@override
String get resetComplete => 'Templates reset to defaults';
@override
String get noTemplates => 'No templates available';
@override
String get tapAddToCreate => 'Tap + to create your first template';
@override
String get ok => 'OK';
@override
String get permissionsSection => 'Berechtigungen';
@override
String get locationPermission => 'Standortberechtigung';
@override
String get checking => 'Überprüfen...';
@override
String get locationPermissionGrantedAlways => 'Erteilt (Immer)';
@override
String get locationPermissionGrantedWhileInUse =>
'Erteilt (Während der Nutzung)';
@override
String get locationPermissionDeniedTapToRequest =>
'Verweigert - Tippen zum Anfragen';
@override
String get locationPermissionPermanentlyDeniedOpenSettings =>
'Dauerhaft verweigert - Einstellungen öffnen';
@override
String get locationPermissionDialogContent =>
'Die Standortberechtigung wurde dauerhaft verweigert. Bitte aktivieren Sie sie in Ihren Geräteeinstellungen, um GPS-Tracking und Standortfreigabe zu nutzen.';
@override
String get openSettings => 'Einstellungen öffnen';
@override
String get locationPermissionGranted => 'Standortberechtigung erteilt!';
@override
String get locationPermissionRequiredForGps =>
'Die Standortberechtigung ist erforderlich für GPS-Tracking und Standortfreigabe.';
@override
String get locationPermissionAlreadyGranted =>
'Die Standortberechtigung wurde bereits erteilt.';
@override
String get sarNavyBlue => 'SAR Navy Blau';
@override
String get sarNavyBlueDescription => 'Professionell/Einsatzmodus';
@override
String get selectRecipient => 'Empfänger auswählen';
@override
String get broadcastToAllNearby => 'An alle in der Nähe senden';
@override
String get searchRecipients => 'Empfänger suchen...';
@override
String get noContactsFound => 'Keine Kontakte gefunden';
@override
String get noRoomsFound => 'Keine Räume gefunden';
@override
String get noContactsOrRoomsAvailable =>
'Keine Kontakte oder Räume verfügbar';
@override
String get messagesWillBeSentToPublicChannel =>
'Nachrichten werden an öffentlichen Kanal gesendet';
@override
String get newMessage => 'Neue Nachricht';
@override
String get channel => 'Kanal';
}

View File

@@ -129,6 +129,13 @@ class AppLocalizationsEn extends AppLocalizations {
String get displayPacketActivity =>
'Display packet activity indicators in top bar';
@override
String get simpleMode => 'Simple Mode';
@override
String get simpleModeDescription =>
'Hide non-essential information in messages and contacts';
@override
String get language => 'Language';
@@ -314,7 +321,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get refresh => 'Refresh';
@override
String get sendDirectMessage => 'Send Direct Message';
String get sendDirectMessage => 'Send';
@override
String get resetPath => 'Reset Path (Re-route)';
@@ -382,6 +389,24 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get clearAllDrawings => 'Clear All Drawings';
@override
String get showReceivedDrawings => 'Show Received Drawings';
@override
String get showingAllDrawings => 'Showing all drawings';
@override
String get showingOnlyYourDrawings => 'Showing only your drawings';
@override
String get showSarMarkers => 'Show SAR Markers';
@override
String get showingSarMarkers => 'Showing SAR markers';
@override
String get hidingSarMarkers => 'Hiding SAR markers';
@override
String get clearAll => 'Clear All';
@@ -457,6 +482,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get location => 'Location';
@override
String get myLocation => 'My Location';
@override
String get fromMap => 'From Map';
@@ -660,6 +688,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get accuracy => 'Accuracy';
@override
String get bearing => 'Bearing';
@override
String get direction => 'Direction';
@override
String get filterMarkers => 'Filter Markers';
@@ -900,6 +934,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get esriSatellite => 'ESRI Satellite';
@override
String get googleHybrid => 'Google Hybrid';
@override
String get googleRoadmap => 'Google Roadmap';
@override
String get googleTerrain => 'Google Terrain';
@override
String get downloadVisibleArea => 'Download visible area';
@@ -978,6 +1021,16 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get messageDeleted => 'Message deleted';
@override
String get copyText => 'Copy text';
@override
String get deleteMessage => 'Delete message';
@override
String get deleteMessageConfirmation =>
'Are you sure you want to delete this message?';
@override
String get refreshedContacts => 'Refreshed contacts';
@@ -1112,6 +1165,67 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get failedToDeleteMbtiles => 'Failed to delete offline map';
@override
String get importExportCachedTiles => 'Import/Export Cached Tiles';
@override
String get importExportDescription =>
'Backup, share, and restore downloaded map tiles between devices';
@override
String get exportTilesToFile => 'Export Tiles to File';
@override
String get importTilesFromFile => 'Import Tiles from File';
@override
String get selectExportLocation => 'Select Export Location';
@override
String get selectImportFile => 'Select Tile Archive';
@override
String get exportingTiles => 'Exporting tiles...';
@override
String get importingTiles => 'Importing tiles...';
@override
String exportSuccess(int count) {
return 'Exported $count tiles successfully';
}
@override
String importSuccess(int count) {
return 'Imported $count stores successfully';
}
@override
String exportFailed(String error) {
return 'Export failed: $error';
}
@override
String importFailed(String error) {
return 'Import failed: $error';
}
@override
String get exportNote =>
'Creates a compressed archive (.fmtc) file that can be shared and imported on other devices.';
@override
String get importNote =>
'Imports map tiles from a previously exported archive file. Tiles will be merged with existing cache.';
@override
String get noTilesToExport => 'No tiles available to export';
@override
String archiveContainsStores(int count) {
return 'Archive contains $count stores';
}
@override
String get vectorTiles => 'Vector Tiles';
@@ -1250,4 +1364,185 @@ class AppLocalizationsEn extends AppLocalizations {
String failedToGetLocation(String error) {
return 'Failed to get location: $error';
}
@override
String get sarTemplates => 'SAR Templates';
@override
String get manageSarTemplates => 'Manage cursor on target templates';
@override
String get addTemplate => 'Add Template';
@override
String get editTemplate => 'Edit Template';
@override
String get deleteTemplate => 'Delete Template';
@override
String get templateName => 'Template Name';
@override
String get templateNameHint => 'e.g. Found Person';
@override
String get templateEmoji => 'Emoji';
@override
String get emojiRequired => 'Emoji is required';
@override
String get nameRequired => 'Name is required';
@override
String get templateDescription => 'Description (Optional)';
@override
String get templateDescriptionHint => 'Add additional context...';
@override
String get templateColor => 'Color';
@override
String get previewFormat => 'Preview (SAR Message Format)';
@override
String get importFromClipboard => 'Import';
@override
String get exportToClipboard => 'Export';
@override
String deleteTemplateConfirmation(String name) {
return 'Delete template \'$name\'?';
}
@override
String get templateAdded => 'Template added';
@override
String get templateUpdated => 'Template updated';
@override
String get templateDeleted => 'Template deleted';
@override
String templatesImported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Imported $count templates',
one: 'Imported 1 template',
zero: 'No templates imported',
);
return '$_temp0';
}
@override
String templatesExported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Exported $count templates to clipboard',
one: 'Exported 1 template to clipboard',
);
return '$_temp0';
}
@override
String get resetToDefaults => 'Reset to Defaults';
@override
String get resetToDefaultsConfirmation =>
'This will delete all custom templates and restore the 4 default templates. Continue?';
@override
String get reset => 'Reset';
@override
String get resetComplete => 'Templates reset to defaults';
@override
String get noTemplates => 'No templates available';
@override
String get tapAddToCreate => 'Tap + to create your first template';
@override
String get ok => 'OK';
@override
String get permissionsSection => 'Permissions';
@override
String get locationPermission => 'Location Permission';
@override
String get checking => 'Checking...';
@override
String get locationPermissionGrantedAlways => 'Granted (Always)';
@override
String get locationPermissionGrantedWhileInUse => 'Granted (While In Use)';
@override
String get locationPermissionDeniedTapToRequest => 'Denied - Tap to request';
@override
String get locationPermissionPermanentlyDeniedOpenSettings =>
'Permanently Denied - Open Settings';
@override
String get locationPermissionDialogContent =>
'Location permission is permanently denied. Please enable it in your device settings to use GPS tracking and location sharing features.';
@override
String get openSettings => 'Open Settings';
@override
String get locationPermissionGranted => 'Location permission granted!';
@override
String get locationPermissionRequiredForGps =>
'Location permission is required for GPS tracking and location sharing.';
@override
String get locationPermissionAlreadyGranted =>
'Location permission is already granted.';
@override
String get sarNavyBlue => 'SAR Navy Blue';
@override
String get sarNavyBlueDescription => 'Professional/Operations Mode';
@override
String get selectRecipient => 'Select Recipient';
@override
String get broadcastToAllNearby => 'Broadcast to all nearby';
@override
String get searchRecipients => 'Search recipients...';
@override
String get noContactsFound => 'No contacts found';
@override
String get noRoomsFound => 'No rooms found';
@override
String get noContactsOrRoomsAvailable => 'No contacts or rooms available';
@override
String get messagesWillBeSentToPublicChannel =>
'Messages will be sent to public channel';
@override
String get newMessage => 'New message';
@override
String get channel => 'Channel';
}

View File

@@ -129,6 +129,13 @@ class AppLocalizationsEs extends AppLocalizations {
String get displayPacketActivity =>
'Mostrar indicadores de actividad de paquetes en la barra superior';
@override
String get simpleMode => 'Modo Simple';
@override
String get simpleModeDescription =>
'Ocultar información no esencial en mensajes y contactos';
@override
String get language => 'Idioma';
@@ -315,7 +322,7 @@ class AppLocalizationsEs extends AppLocalizations {
String get refresh => 'Actualizar';
@override
String get sendDirectMessage => 'Enviar mensaje directo';
String get sendDirectMessage => 'Enviar';
@override
String get resetPath => 'Restablecer ruta (Re-enrutar)';
@@ -383,6 +390,24 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get clearAllDrawings => 'Borrar todos los dibujos';
@override
String get showReceivedDrawings => 'Mostrar dibujos recibidos';
@override
String get showingAllDrawings => 'Mostrando todos los dibujos';
@override
String get showingOnlyYourDrawings => 'Mostrando solo tus dibujos';
@override
String get showSarMarkers => 'Mostrar marcadores SAR';
@override
String get showingSarMarkers => 'Mostrando marcadores SAR';
@override
String get hidingSarMarkers => 'Ocultando marcadores SAR';
@override
String get clearAll => 'Borrar todo';
@@ -458,6 +483,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get location => 'Ubicación';
@override
String get myLocation => 'Mi ubicación';
@override
String get fromMap => 'Desde el mapa';
@@ -661,6 +689,12 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get accuracy => 'Precisión';
@override
String get bearing => 'Rumbo';
@override
String get direction => 'Dirección';
@override
String get filterMarkers => 'Filtrar marcadores';
@@ -904,6 +938,15 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get esriSatellite => 'ESRI Satélite';
@override
String get googleHybrid => 'Google Híbrido';
@override
String get googleRoadmap => 'Google Mapa de Carreteras';
@override
String get googleTerrain => 'Google Terreno';
@override
String get downloadVisibleArea => 'Descargar área visible';
@@ -984,6 +1027,16 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get messageDeleted => 'Mensaje eliminado';
@override
String get copyText => 'Copiar texto';
@override
String get deleteMessage => 'Eliminar mensaje';
@override
String get deleteMessageConfirmation =>
'¿Está seguro de que desea eliminar este mensaje?';
@override
String get refreshedContacts => 'Contactos actualizados';
@@ -1120,6 +1173,67 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get failedToDeleteMbtiles => 'Error al eliminar mapa sin conexión';
@override
String get importExportCachedTiles => 'Importar/Exportar teselas en caché';
@override
String get importExportDescription =>
'Realice copias de seguridad, comparta y restaure teselas de mapas descargadas entre dispositivos';
@override
String get exportTilesToFile => 'Exportar teselas a archivo';
@override
String get importTilesFromFile => 'Importar teselas desde archivo';
@override
String get selectExportLocation => 'Seleccionar ubicación de exportación';
@override
String get selectImportFile => 'Seleccionar archivo de teselas';
@override
String get exportingTiles => 'Exportando teselas...';
@override
String get importingTiles => 'Importando teselas...';
@override
String exportSuccess(int count) {
return '$count teselas exportadas exitosamente';
}
@override
String importSuccess(int count) {
return '$count almacenes importados exitosamente';
}
@override
String exportFailed(String error) {
return 'Export failed: $error';
}
@override
String importFailed(String error) {
return 'Import failed: $error';
}
@override
String get exportNote =>
'Crea un archivo comprimido (.fmtc) que se puede compartir e importar en otros dispositivos.';
@override
String get importNote =>
'Importa teselas de mapa desde un archivo previamente exportado. Las teselas se fusionarán con la caché existente.';
@override
String get noTilesToExport => 'No hay teselas para exportar';
@override
String archiveContainsStores(int count) {
return 'El archivo contiene $count almacenes';
}
@override
String get vectorTiles => 'Teselas vectoriales';
@@ -1258,4 +1372,188 @@ class AppLocalizationsEs extends AppLocalizations {
String failedToGetLocation(String error) {
return 'Error al obtener ubicación: $error';
}
@override
String get sarTemplates => 'SAR Templates';
@override
String get manageSarTemplates => 'Manage cursor on target templates';
@override
String get addTemplate => 'Add Template';
@override
String get editTemplate => 'Edit Template';
@override
String get deleteTemplate => 'Delete Template';
@override
String get templateName => 'Template Name';
@override
String get templateNameHint => 'e.g. Found Person';
@override
String get templateEmoji => 'Emoji';
@override
String get emojiRequired => 'Se requiere emoji';
@override
String get nameRequired => 'Se requiere nombre';
@override
String get templateDescription => 'Description (Optional)';
@override
String get templateDescriptionHint => 'Add additional context...';
@override
String get templateColor => 'Color';
@override
String get previewFormat => 'Preview (SAR Message Format)';
@override
String get importFromClipboard => 'Import';
@override
String get exportToClipboard => 'Export';
@override
String deleteTemplateConfirmation(String name) {
return 'Delete template \'$name\'?';
}
@override
String get templateAdded => 'Template added';
@override
String get templateUpdated => 'Template updated';
@override
String get templateDeleted => 'Template deleted';
@override
String templatesImported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Imported $count templates',
one: 'Imported 1 template',
zero: 'No templates imported',
);
return '$_temp0';
}
@override
String templatesExported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Exported $count templates to clipboard',
one: 'Exported 1 template to clipboard',
);
return '$_temp0';
}
@override
String get resetToDefaults => 'Reset to Defaults';
@override
String get resetToDefaultsConfirmation =>
'This will delete all custom templates and restore the 4 default templates. Continue?';
@override
String get reset => 'Reset';
@override
String get resetComplete => 'Templates reset to defaults';
@override
String get noTemplates => 'No templates available';
@override
String get tapAddToCreate => 'Tap + to create your first template';
@override
String get ok => 'OK';
@override
String get permissionsSection => 'Permisos';
@override
String get locationPermission => 'Permiso de ubicación';
@override
String get checking => 'Comprobando...';
@override
String get locationPermissionGrantedAlways => 'Concedido (Siempre)';
@override
String get locationPermissionGrantedWhileInUse =>
'Concedido (Durante el uso)';
@override
String get locationPermissionDeniedTapToRequest =>
'Denegado - Toca para solicitar';
@override
String get locationPermissionPermanentlyDeniedOpenSettings =>
'Denegado permanentemente - Abrir ajustes';
@override
String get locationPermissionDialogContent =>
'El permiso de ubicación está permanentemente denegado. Por favor, actívalo en la configuración de tu dispositivo para usar el rastreo GPS y compartir ubicación.';
@override
String get openSettings => 'Abrir ajustes';
@override
String get locationPermissionGranted => '¡Permiso de ubicación concedido!';
@override
String get locationPermissionRequiredForGps =>
'El permiso de ubicación es necesario para el rastreo GPS y compartir ubicación.';
@override
String get locationPermissionAlreadyGranted =>
'El permiso de ubicación ya está concedido.';
@override
String get sarNavyBlue => 'SAR Azul Marino';
@override
String get sarNavyBlueDescription => 'Modo Profesional/Operaciones';
@override
String get selectRecipient => 'Seleccionar destinatario';
@override
String get broadcastToAllNearby => 'Transmitir a todos cercanos';
@override
String get searchRecipients => 'Buscar destinatarios...';
@override
String get noContactsFound => 'No se encontraron contactos';
@override
String get noRoomsFound => 'No se encontraron salas';
@override
String get noContactsOrRoomsAvailable =>
'No hay contactos o salas disponibles';
@override
String get messagesWillBeSentToPublicChannel =>
'Los mensajes se enviarán al canal público';
@override
String get newMessage => 'Nuevo mensaje';
@override
String get channel => 'Canal';
}

View File

@@ -130,6 +130,13 @@ class AppLocalizationsFr extends AppLocalizations {
String get displayPacketActivity =>
'Afficher les indicateurs d\'activité des paquets dans la barre supérieure';
@override
String get simpleMode => 'Mode Simple';
@override
String get simpleModeDescription =>
'Masquer les informations non essentielles dans les messages et les contacts';
@override
String get language => 'Langue';
@@ -317,7 +324,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get refresh => 'Actualiser';
@override
String get sendDirectMessage => 'Envoyer un message direct';
String get sendDirectMessage => 'Envoyer';
@override
String get resetPath => 'Réinitialiser le chemin (Re-router)';
@@ -385,6 +392,24 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get clearAllDrawings => 'Effacer tous les dessins';
@override
String get showReceivedDrawings => 'Afficher les dessins reçus';
@override
String get showingAllDrawings => 'Affichage de tous les dessins';
@override
String get showingOnlyYourDrawings => 'Affichage uniquement de vos dessins';
@override
String get showSarMarkers => 'Afficher les marqueurs SAR';
@override
String get showingSarMarkers => 'Affichage des marqueurs SAR';
@override
String get hidingSarMarkers => 'Masquage des marqueurs SAR';
@override
String get clearAll => 'Tout effacer';
@@ -461,6 +486,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get location => 'Position';
@override
String get myLocation => 'Ma position';
@override
String get fromMap => 'Depuis la carte';
@@ -665,6 +693,12 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get accuracy => 'Précision';
@override
String get bearing => 'Relèvement';
@override
String get direction => 'Direction';
@override
String get filterMarkers => 'Filtrer les marqueurs';
@@ -909,6 +943,15 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get esriSatellite => 'Satellite ESRI';
@override
String get googleHybrid => 'Google Hybride';
@override
String get googleRoadmap => 'Google Carte Routière';
@override
String get googleTerrain => 'Google Terrain';
@override
String get downloadVisibleArea => 'Télécharger la zone visible';
@@ -989,6 +1032,16 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get messageDeleted => 'Message supprimé';
@override
String get copyText => 'Copier le texte';
@override
String get deleteMessage => 'Supprimer le message';
@override
String get deleteMessageConfirmation =>
'Êtes-vous sûr de vouloir supprimer ce message?';
@override
String get refreshedContacts => 'Contacts actualisés';
@@ -1126,6 +1179,68 @@ class AppLocalizationsFr extends AppLocalizations {
String get failedToDeleteMbtiles =>
'Échec de la suppression de la carte hors ligne';
@override
String get importExportCachedTiles => 'Importer/Exporter les tuiles en cache';
@override
String get importExportDescription =>
'Sauvegarder, partager et restaurer les tuiles de carte téléchargées entre appareils';
@override
String get exportTilesToFile => 'Exporter les tuiles vers fichier';
@override
String get importTilesFromFile => 'Importer les tuiles depuis fichier';
@override
String get selectExportLocation =>
'Sélectionner l\'emplacement d\'exportation';
@override
String get selectImportFile => 'Sélectionner l\'archive de tuiles';
@override
String get exportingTiles => 'Exportation des tuiles...';
@override
String get importingTiles => 'Importation des tuiles...';
@override
String exportSuccess(int count) {
return '$count tuiles exportées avec succès';
}
@override
String importSuccess(int count) {
return '$count magasins importés avec succès';
}
@override
String exportFailed(String error) {
return 'Export failed: $error';
}
@override
String importFailed(String error) {
return 'Import failed: $error';
}
@override
String get exportNote =>
'Crée un fichier d\'archive compressé (.fmtc) qui peut être partagé et importé sur d\'autres appareils.';
@override
String get importNote =>
'Importe les tuiles de carte depuis un fichier d\'archive précédemment exporté. Les tuiles seront fusionnées avec le cache existant.';
@override
String get noTilesToExport => 'Aucune tuile à exporter';
@override
String archiveContainsStores(int count) {
return 'L\'archive contient $count magasins';
}
@override
String get vectorTiles => 'Tuiles vectorielles';
@@ -1265,4 +1380,188 @@ class AppLocalizationsFr extends AppLocalizations {
String failedToGetLocation(String error) {
return 'Échec de l\'obtention de la position : $error';
}
@override
String get sarTemplates => 'SAR Templates';
@override
String get manageSarTemplates => 'Manage cursor on target templates';
@override
String get addTemplate => 'Add Template';
@override
String get editTemplate => 'Edit Template';
@override
String get deleteTemplate => 'Delete Template';
@override
String get templateName => 'Template Name';
@override
String get templateNameHint => 'e.g. Found Person';
@override
String get templateEmoji => 'Emoji';
@override
String get emojiRequired => 'Emoji est requis';
@override
String get nameRequired => 'Nom est requis';
@override
String get templateDescription => 'Description (Optional)';
@override
String get templateDescriptionHint => 'Add additional context...';
@override
String get templateColor => 'Color';
@override
String get previewFormat => 'Preview (SAR Message Format)';
@override
String get importFromClipboard => 'Import';
@override
String get exportToClipboard => 'Export';
@override
String deleteTemplateConfirmation(String name) {
return 'Delete template \'$name\'?';
}
@override
String get templateAdded => 'Template added';
@override
String get templateUpdated => 'Template updated';
@override
String get templateDeleted => 'Template deleted';
@override
String templatesImported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Imported $count templates',
one: 'Imported 1 template',
zero: 'No templates imported',
);
return '$_temp0';
}
@override
String templatesExported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Exported $count templates to clipboard',
one: 'Exported 1 template to clipboard',
);
return '$_temp0';
}
@override
String get resetToDefaults => 'Reset to Defaults';
@override
String get resetToDefaultsConfirmation =>
'This will delete all custom templates and restore the 4 default templates. Continue?';
@override
String get reset => 'Reset';
@override
String get resetComplete => 'Templates reset to defaults';
@override
String get noTemplates => 'No templates available';
@override
String get tapAddToCreate => 'Tap + to create your first template';
@override
String get ok => 'OK';
@override
String get permissionsSection => 'Autorisations';
@override
String get locationPermission => 'Autorisation de localisation';
@override
String get checking => 'Vérification...';
@override
String get locationPermissionGrantedAlways => 'Accordée (Toujours)';
@override
String get locationPermissionGrantedWhileInUse =>
'Accordée (En cours d\'utilisation)';
@override
String get locationPermissionDeniedTapToRequest =>
'Refusée - Appuyez pour demander';
@override
String get locationPermissionPermanentlyDeniedOpenSettings =>
'Refusée définitivement - Ouvrir les paramètres';
@override
String get locationPermissionDialogContent =>
'L\'autorisation de localisation est définitivement refusée. Veuillez l\'activer dans les paramètres de votre appareil pour utiliser le suivi GPS et le partage de localisation.';
@override
String get openSettings => 'Ouvrir les paramètres';
@override
String get locationPermissionGranted =>
'Autorisation de localisation accordée !';
@override
String get locationPermissionRequiredForGps =>
'L\'autorisation de localisation est nécessaire pour le suivi GPS et le partage de localisation.';
@override
String get locationPermissionAlreadyGranted =>
'L\'autorisation de localisation est déjà accordée.';
@override
String get sarNavyBlue => 'SAR Bleu Marine';
@override
String get sarNavyBlueDescription => 'Mode Professionnel/Opérations';
@override
String get selectRecipient => 'Sélectionner le destinataire';
@override
String get broadcastToAllNearby => 'Diffuser à tous à proximité';
@override
String get searchRecipients => 'Rechercher des destinataires...';
@override
String get noContactsFound => 'Aucun contact trouvé';
@override
String get noRoomsFound => 'Aucune salle trouvée';
@override
String get noContactsOrRoomsAvailable => 'Aucun contact ou salle disponible';
@override
String get messagesWillBeSentToPublicChannel =>
'Les messages seront envoyés au canal public';
@override
String get newMessage => 'Nouveau message';
@override
String get channel => 'Canal';
}

View File

@@ -129,6 +129,13 @@ class AppLocalizationsHr extends AppLocalizations {
String get displayPacketActivity =>
'Prikaži indikatore aktivnosti paketa u gornjoj traci';
@override
String get simpleMode => 'Jednostavni način';
@override
String get simpleModeDescription =>
'Sakrij nevažne informacije u porukama i kontaktima';
@override
String get language => 'Jezik';
@@ -305,7 +312,7 @@ class AppLocalizationsHr extends AppLocalizations {
String get deleteContact => 'Izbriši kontakt';
@override
String get delete => 'Izbriši';
String get delete => 'Obriši';
@override
String get viewOnMap => 'Prikaži na karti';
@@ -314,7 +321,7 @@ class AppLocalizationsHr extends AppLocalizations {
String get refresh => 'Osvježi';
@override
String get sendDirectMessage => 'Pošalji izravnu poruku';
String get sendDirectMessage => 'Pošalji';
@override
String get resetPath => 'Resetiraj put (preusmjeri)';
@@ -382,6 +389,24 @@ class AppLocalizationsHr extends AppLocalizations {
@override
String get clearAllDrawings => 'Očisti sve crteže';
@override
String get showReceivedDrawings => 'Prikaži primljene crteže';
@override
String get showingAllDrawings => 'Prikazujem sve crteže';
@override
String get showingOnlyYourDrawings => 'Prikazujem samo vaše crteže';
@override
String get showSarMarkers => 'Prikaži SAR oznake';
@override
String get showingSarMarkers => 'Prikazujem SAR oznake';
@override
String get hidingSarMarkers => 'Skrivam SAR oznake';
@override
String get clearAll => 'Očisti sve';
@@ -457,6 +482,9 @@ class AppLocalizationsHr extends AppLocalizations {
@override
String get location => 'Lokacija';
@override
String get myLocation => 'Moja lokacija';
@override
String get fromMap => 'S karte';
@@ -660,6 +688,12 @@ class AppLocalizationsHr extends AppLocalizations {
@override
String get accuracy => 'Točnost';
@override
String get bearing => 'Azimut';
@override
String get direction => 'Smjer';
@override
String get filterMarkers => 'Filtriraj markere';
@@ -900,6 +934,15 @@ class AppLocalizationsHr extends AppLocalizations {
@override
String get esriSatellite => 'ESRI satelit';
@override
String get googleHybrid => 'Google hibridno';
@override
String get googleRoadmap => 'Google cestovna karta';
@override
String get googleTerrain => 'Google teren';
@override
String get downloadVisibleArea => 'Preuzmi vidljivo područje';
@@ -980,6 +1023,16 @@ class AppLocalizationsHr extends AppLocalizations {
@override
String get messageDeleted => 'Poruka izbrisana';
@override
String get copyText => 'Kopiraj tekst';
@override
String get deleteMessage => 'Izbriši poruku';
@override
String get deleteMessageConfirmation =>
'Jeste li sigurni da želite izbrisati ovu poruku?';
@override
String get refreshedContacts => 'Kontakti osvježeni';
@@ -1113,6 +1166,67 @@ class AppLocalizationsHr extends AppLocalizations {
@override
String get failedToDeleteMbtiles => 'Neuspjelo brisanje offline karte';
@override
String get importExportCachedTiles => 'Uvoz/Izvoz predmemoriranih pločica';
@override
String get importExportDescription =>
'Sigurnosno kopirajte, dijelite i vraćajte preuzete pločice karte između uređaja';
@override
String get exportTilesToFile => 'Izvezi pločice u datoteku';
@override
String get importTilesFromFile => 'Uvezi pločice iz datoteke';
@override
String get selectExportLocation => 'Odaberite lokaciju izvoza';
@override
String get selectImportFile => 'Odaberite arhivu pločica';
@override
String get exportingTiles => 'Izvažanje pločica...';
@override
String get importingTiles => 'Uvažanje pločica...';
@override
String exportSuccess(int count) {
return 'Uspješno izvezeno $count pločica';
}
@override
String importSuccess(int count) {
return 'Uspješno uvezeno $count skladišta';
}
@override
String exportFailed(String error) {
return 'Export failed: $error';
}
@override
String importFailed(String error) {
return 'Import failed: $error';
}
@override
String get exportNote =>
'Stvara komprimiranu arhivsku datoteku (.fmtc) koju možete dijeliti i uvesti na drugim uređajima.';
@override
String get importNote =>
'Uvozi pločice karte iz prethodno izvezene arhivske datoteke. Pločice će biti spojene s postojećom predmemorijom.';
@override
String get noTilesToExport => 'Nema pločica za izvoz';
@override
String archiveContainsStores(int count) {
return 'Arhiva sadrži $count skladišta';
}
@override
String get vectorTiles => 'Vektorske pločice';
@@ -1251,4 +1365,187 @@ class AppLocalizationsHr extends AppLocalizations {
String failedToGetLocation(String error) {
return 'Neuspjelo dohvaćanje lokacije: $error';
}
@override
String get sarTemplates => 'SAR Templates';
@override
String get manageSarTemplates => 'Manage cursor on target templates';
@override
String get addTemplate => 'Add Template';
@override
String get editTemplate => 'Edit Template';
@override
String get deleteTemplate => 'Delete Template';
@override
String get templateName => 'Template Name';
@override
String get templateNameHint => 'e.g. Found Person';
@override
String get templateEmoji => 'Emoji';
@override
String get emojiRequired => 'Emoji je obavezan';
@override
String get nameRequired => 'Ime je obavezno';
@override
String get templateDescription => 'Description (Optional)';
@override
String get templateDescriptionHint => 'Add additional context...';
@override
String get templateColor => 'Color';
@override
String get previewFormat => 'Preview (SAR Message Format)';
@override
String get importFromClipboard => 'Import';
@override
String get exportToClipboard => 'Export';
@override
String deleteTemplateConfirmation(String name) {
return 'Delete template \'$name\'?';
}
@override
String get templateAdded => 'Template added';
@override
String get templateUpdated => 'Template updated';
@override
String get templateDeleted => 'Template deleted';
@override
String templatesImported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Imported $count templates',
one: 'Imported 1 template',
zero: 'No templates imported',
);
return '$_temp0';
}
@override
String templatesExported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Exported $count templates to clipboard',
one: 'Exported 1 template to clipboard',
);
return '$_temp0';
}
@override
String get resetToDefaults => 'Reset to Defaults';
@override
String get resetToDefaultsConfirmation =>
'This will delete all custom templates and restore the 4 default templates. Continue?';
@override
String get reset => 'Reset';
@override
String get resetComplete => 'Templates reset to defaults';
@override
String get noTemplates => 'No templates available';
@override
String get tapAddToCreate => 'Tap + to create your first template';
@override
String get ok => 'OK';
@override
String get permissionsSection => 'Dozvole';
@override
String get locationPermission => 'Dozvola za lokaciju';
@override
String get checking => 'Provjera...';
@override
String get locationPermissionGrantedAlways => 'Odobreno (Uvijek)';
@override
String get locationPermissionGrantedWhileInUse =>
'Odobreno (Tijekom uporabe)';
@override
String get locationPermissionDeniedTapToRequest =>
'Odbijeno - Dodirnite za zahtjev';
@override
String get locationPermissionPermanentlyDeniedOpenSettings =>
'Trajno odbijeno - Otvori postavke';
@override
String get locationPermissionDialogContent =>
'Dozvola za lokaciju je trajno odbijena. Omogućite je u postavkama uređaja kako biste koristili GPS praćenje i dijeljenje lokacije.';
@override
String get openSettings => 'Otvori postavke';
@override
String get locationPermissionGranted => 'Dozvola za lokaciju odobrena!';
@override
String get locationPermissionRequiredForGps =>
'Dozvola za lokaciju je potrebna za GPS praćenje i dijeljenje lokacije.';
@override
String get locationPermissionAlreadyGranted =>
'Dozvola za lokaciju je već odobrena.';
@override
String get sarNavyBlue => 'SAR Mornarsko Plava';
@override
String get sarNavyBlueDescription => 'Profesionalni/Operativni Način';
@override
String get selectRecipient => 'Odaberi primatelja';
@override
String get broadcastToAllNearby => 'Emituj svima u blizini';
@override
String get searchRecipients => 'Pretraži primatelje...';
@override
String get noContactsFound => 'Nema kontakata';
@override
String get noRoomsFound => 'Nema soba';
@override
String get noContactsOrRoomsAvailable => 'Nema dostupnih kontakata ili soba';
@override
String get messagesWillBeSentToPublicChannel =>
'Poruke će biti poslane na javni kanal';
@override
String get newMessage => 'Nova poruka';
@override
String get channel => 'Kanal';
}

View File

@@ -129,6 +129,13 @@ class AppLocalizationsIt extends AppLocalizations {
String get displayPacketActivity =>
'Mostra indicatori di attività pacchetti nella barra superiore';
@override
String get simpleMode => 'Modalità Semplice';
@override
String get simpleModeDescription =>
'Nascondi informazioni non essenziali nei messaggi e contatti';
@override
String get language => 'Lingua';
@@ -316,7 +323,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get refresh => 'Aggiorna';
@override
String get sendDirectMessage => 'Invia Messaggio Diretto';
String get sendDirectMessage => 'Invia';
@override
String get resetPath => 'Resetta Percorso (Ri-instrada)';
@@ -384,6 +391,24 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get clearAllDrawings => 'Cancella Tutti i Disegni';
@override
String get showReceivedDrawings => 'Mostra Disegni Ricevuti';
@override
String get showingAllDrawings => 'Visualizzazione di tutti i disegni';
@override
String get showingOnlyYourDrawings => 'Visualizzazione solo dei tuoi disegni';
@override
String get showSarMarkers => 'Mostra marcatori SAR';
@override
String get showingSarMarkers => 'Visualizzazione marcatori SAR';
@override
String get hidingSarMarkers => 'Nascondere marcatori SAR';
@override
String get clearAll => 'Cancella Tutto';
@@ -459,6 +484,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get location => 'Posizione';
@override
String get myLocation => 'La mia posizione';
@override
String get fromMap => 'Dalla Mappa';
@@ -662,6 +690,12 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get accuracy => 'Precisione';
@override
String get bearing => 'Rilevamento';
@override
String get direction => 'Direzione';
@override
String get filterMarkers => 'Filtra Marcatori';
@@ -905,6 +939,15 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get esriSatellite => 'ESRI Satellite';
@override
String get googleHybrid => 'Google Ibrido';
@override
String get googleRoadmap => 'Google Mappa Stradale';
@override
String get googleTerrain => 'Google Terreno';
@override
String get downloadVisibleArea => 'Scarica area visibile';
@@ -986,6 +1029,16 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get messageDeleted => 'Messaggio eliminato';
@override
String get copyText => 'Copia testo';
@override
String get deleteMessage => 'Elimina messaggio';
@override
String get deleteMessageConfirmation =>
'Sei sicuro di voler eliminare questo messaggio?';
@override
String get refreshedContacts => 'Contatti aggiornati';
@@ -1121,6 +1174,67 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get failedToDeleteMbtiles => 'Impossibile eliminare la mappa offline';
@override
String get importExportCachedTiles => 'Importa/Esporta tile in cache';
@override
String get importExportDescription =>
'Esegui backup, condividi e ripristina tile mappa scaricati tra dispositivi';
@override
String get exportTilesToFile => 'Esporta tile su file';
@override
String get importTilesFromFile => 'Importa tile da file';
@override
String get selectExportLocation => 'Seleziona posizione esportazione';
@override
String get selectImportFile => 'Seleziona archivio tile';
@override
String get exportingTiles => 'Esportazione tile...';
@override
String get importingTiles => 'Importazione tile...';
@override
String exportSuccess(int count) {
return '$count tile esportati con successo';
}
@override
String importSuccess(int count) {
return '$count archivi importati con successo';
}
@override
String exportFailed(String error) {
return 'Export failed: $error';
}
@override
String importFailed(String error) {
return 'Import failed: $error';
}
@override
String get exportNote =>
'Crea un file archivio compresso (.fmtc) che può essere condiviso e importato su altri dispositivi.';
@override
String get importNote =>
'Importa tile mappa da un file archivio precedentemente esportato. I tile verranno uniti con la cache esistente.';
@override
String get noTilesToExport => 'Nessun tile da esportare';
@override
String archiveContainsStores(int count) {
return 'L\'archivio contiene $count archivi';
}
@override
String get vectorTiles => 'Tile Vettoriali';
@@ -1261,4 +1375,187 @@ class AppLocalizationsIt extends AppLocalizations {
String failedToGetLocation(String error) {
return 'Impossibile ottenere la posizione: $error';
}
@override
String get sarTemplates => 'SAR Templates';
@override
String get manageSarTemplates => 'Manage cursor on target templates';
@override
String get addTemplate => 'Add Template';
@override
String get editTemplate => 'Edit Template';
@override
String get deleteTemplate => 'Delete Template';
@override
String get templateName => 'Template Name';
@override
String get templateNameHint => 'e.g. Found Person';
@override
String get templateEmoji => 'Emoji';
@override
String get emojiRequired => 'Emoji è obbligatorio';
@override
String get nameRequired => 'Nome è obbligatorio';
@override
String get templateDescription => 'Description (Optional)';
@override
String get templateDescriptionHint => 'Add additional context...';
@override
String get templateColor => 'Color';
@override
String get previewFormat => 'Preview (SAR Message Format)';
@override
String get importFromClipboard => 'Import';
@override
String get exportToClipboard => 'Export';
@override
String deleteTemplateConfirmation(String name) {
return 'Delete template \'$name\'?';
}
@override
String get templateAdded => 'Template added';
@override
String get templateUpdated => 'Template updated';
@override
String get templateDeleted => 'Template deleted';
@override
String templatesImported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Imported $count templates',
one: 'Imported 1 template',
zero: 'No templates imported',
);
return '$_temp0';
}
@override
String templatesExported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Exported $count templates to clipboard',
one: 'Exported 1 template to clipboard',
);
return '$_temp0';
}
@override
String get resetToDefaults => 'Reset to Defaults';
@override
String get resetToDefaultsConfirmation =>
'This will delete all custom templates and restore the 4 default templates. Continue?';
@override
String get reset => 'Reset';
@override
String get resetComplete => 'Templates reset to defaults';
@override
String get noTemplates => 'No templates available';
@override
String get tapAddToCreate => 'Tap + to create your first template';
@override
String get ok => 'OK';
@override
String get permissionsSection => 'Permessi';
@override
String get locationPermission => 'Permesso di posizione';
@override
String get checking => 'Verifica in corso...';
@override
String get locationPermissionGrantedAlways => 'Concesso (Sempre)';
@override
String get locationPermissionGrantedWhileInUse => 'Concesso (Durante l\'uso)';
@override
String get locationPermissionDeniedTapToRequest =>
'Negato - Tocca per richiedere';
@override
String get locationPermissionPermanentlyDeniedOpenSettings =>
'Negato permanentemente - Apri impostazioni';
@override
String get locationPermissionDialogContent =>
'Il permesso di posizione è permanentemente negato. Si prega di abilitarlo nelle impostazioni del dispositivo per utilizzare il tracciamento GPS e la condivisione della posizione.';
@override
String get openSettings => 'Apri impostazioni';
@override
String get locationPermissionGranted => 'Permesso di posizione concesso!';
@override
String get locationPermissionRequiredForGps =>
'Il permesso di posizione è necessario per il tracciamento GPS e la condivisione della posizione.';
@override
String get locationPermissionAlreadyGranted =>
'Il permesso di posizione è già concesso.';
@override
String get sarNavyBlue => 'SAR Blu Navy';
@override
String get sarNavyBlueDescription => 'Modalità Professionale/Operativa';
@override
String get selectRecipient => 'Seleziona destinatario';
@override
String get broadcastToAllNearby => 'Trasmetti a tutti nelle vicinanze';
@override
String get searchRecipients => 'Cerca destinatari...';
@override
String get noContactsFound => 'Nessun contatto trovato';
@override
String get noRoomsFound => 'Nessuna stanza trovata';
@override
String get noContactsOrRoomsAvailable =>
'Nessun contatto o stanza disponibile';
@override
String get messagesWillBeSentToPublicChannel =>
'I messaggi saranno inviati al canale pubblico';
@override
String get newMessage => 'Nuovo messaggio';
@override
String get channel => 'Canale';
}

View File

@@ -129,6 +129,13 @@ class AppLocalizationsSl extends AppLocalizations {
String get displayPacketActivity =>
'Prikaži kazalnike aktivnosti paketov v zgornji vrstici';
@override
String get simpleMode => 'Preprost način';
@override
String get simpleModeDescription =>
'Skrij nepomembne informacije v sporočilih in kontaktih';
@override
String get language => 'Jezik';
@@ -314,7 +321,7 @@ class AppLocalizationsSl extends AppLocalizations {
String get refresh => 'Osveži';
@override
String get sendDirectMessage => 'Pošlji neposredno sporočilo';
String get sendDirectMessage => 'Pošlji';
@override
String get resetPath => 'Ponastavi pot (preusmeri)';
@@ -382,6 +389,24 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get clearAllDrawings => 'Počisti vse risbe';
@override
String get showReceivedDrawings => 'Prikaži prejete risbe';
@override
String get showingAllDrawings => 'Prikazujem vse risbe';
@override
String get showingOnlyYourDrawings => 'Prikazujem samo vaše risbe';
@override
String get showSarMarkers => 'Prikaži SAR označevalce';
@override
String get showingSarMarkers => 'Prikazujem SAR označevalce';
@override
String get hidingSarMarkers => 'Skrivam SAR označevalce';
@override
String get clearAll => 'Počisti vse';
@@ -457,6 +482,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get location => 'Lokacija';
@override
String get myLocation => 'Moja lokacija';
@override
String get fromMap => 'Z zemljevida';
@@ -660,6 +688,12 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get accuracy => 'Natančnost';
@override
String get bearing => 'Azimut';
@override
String get direction => 'Smer';
@override
String get filterMarkers => 'Filtriraj označevalce';
@@ -900,6 +934,15 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get esriSatellite => 'ESRI satelit';
@override
String get googleHybrid => 'Google hibridno';
@override
String get googleRoadmap => 'Google cestni zemljevid';
@override
String get googleTerrain => 'Google teren';
@override
String get downloadVisibleArea => 'Prenesi vidno območje';
@@ -980,6 +1023,16 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get messageDeleted => 'Sporočilo izbrisano';
@override
String get copyText => 'Kopiraj besedilo';
@override
String get deleteMessage => 'Izbriši sporočilo';
@override
String get deleteMessageConfirmation =>
'Ali ste prepričani, da želite izbrisati to sporočilo?';
@override
String get refreshedContacts => 'Stiki osveženi';
@@ -1116,6 +1169,67 @@ class AppLocalizationsSl extends AppLocalizations {
String get failedToDeleteMbtiles =>
'Brisanje brezpoveznega zemljevida ni uspelo';
@override
String get importExportCachedTiles => 'Uvoz/Izvoz predpomnjenih ploščic';
@override
String get importExportDescription =>
'Varnostno kopirajte, delite in obnovite prenesene ploščice zemljevida med napravami';
@override
String get exportTilesToFile => 'Izvozi ploščice v datoteko';
@override
String get importTilesFromFile => 'Uvozi ploščice iz datoteke';
@override
String get selectExportLocation => 'Izberi lokacijo izvoza';
@override
String get selectImportFile => 'Izberi arhiv ploščic';
@override
String get exportingTiles => 'Izvažanje ploščic...';
@override
String get importingTiles => 'Uvažanje ploščic...';
@override
String exportSuccess(int count) {
return 'Uspešno izvoženih $count ploščic';
}
@override
String importSuccess(int count) {
return 'Uspešno uvoženih $count skladišč';
}
@override
String exportFailed(String error) {
return 'Export failed: $error';
}
@override
String importFailed(String error) {
return 'Import failed: $error';
}
@override
String get exportNote =>
'Ustvari stisnjeno arhivsko datoteko (.fmtc), ki jo lahko delite in uvozite na drugih napravah.';
@override
String get importNote =>
'Uvozi ploščice zemljevida iz predhodno izvožene arhivske datoteke. Ploščice bodo združene z obstoječim predpomnilnikom.';
@override
String get noTilesToExport => 'Ni ploščic za izvoz';
@override
String archiveContainsStores(int count) {
return 'Arhiv vsebuje $count skladišč';
}
@override
String get vectorTiles => 'Vektorske ploščice';
@@ -1254,4 +1368,186 @@ class AppLocalizationsSl extends AppLocalizations {
String failedToGetLocation(String error) {
return 'Pridobivanje lokacije ni uspelo: $error';
}
@override
String get sarTemplates => 'SAR Templates';
@override
String get manageSarTemplates => 'Manage cursor on target templates';
@override
String get addTemplate => 'Add Template';
@override
String get editTemplate => 'Edit Template';
@override
String get deleteTemplate => 'Delete Template';
@override
String get templateName => 'Template Name';
@override
String get templateNameHint => 'e.g. Found Person';
@override
String get templateEmoji => 'Emoji';
@override
String get emojiRequired => 'Emoji je obvezen';
@override
String get nameRequired => 'Ime je obvezno';
@override
String get templateDescription => 'Description (Optional)';
@override
String get templateDescriptionHint => 'Add additional context...';
@override
String get templateColor => 'Color';
@override
String get previewFormat => 'Preview (SAR Message Format)';
@override
String get importFromClipboard => 'Import';
@override
String get exportToClipboard => 'Export';
@override
String deleteTemplateConfirmation(String name) {
return 'Delete template \'$name\'?';
}
@override
String get templateAdded => 'Template added';
@override
String get templateUpdated => 'Template updated';
@override
String get templateDeleted => 'Template deleted';
@override
String templatesImported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Imported $count templates',
one: 'Imported 1 template',
zero: 'No templates imported',
);
return '$_temp0';
}
@override
String templatesExported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Exported $count templates to clipboard',
one: 'Exported 1 template to clipboard',
);
return '$_temp0';
}
@override
String get resetToDefaults => 'Reset to Defaults';
@override
String get resetToDefaultsConfirmation =>
'This will delete all custom templates and restore the 4 default templates. Continue?';
@override
String get reset => 'Reset';
@override
String get resetComplete => 'Templates reset to defaults';
@override
String get noTemplates => 'No templates available';
@override
String get tapAddToCreate => 'Tap + to create your first template';
@override
String get ok => 'OK';
@override
String get permissionsSection => 'Dovoljenja';
@override
String get locationPermission => 'Dovoljenje za lokacijo';
@override
String get checking => 'Preverjanje...';
@override
String get locationPermissionGrantedAlways => 'Odobreno (Vedno)';
@override
String get locationPermissionGrantedWhileInUse => 'Odobreno (Med uporabo)';
@override
String get locationPermissionDeniedTapToRequest =>
'Zavrnjeno - Tapnite za zahtevo';
@override
String get locationPermissionPermanentlyDeniedOpenSettings =>
'Trajno zavrnjeno - Odpri nastavitve';
@override
String get locationPermissionDialogContent =>
'Dovoljenje za lokacijo je trajno zavrnjeno. Omogočite ga v nastavitvah naprave za uporabo sledenja GPS in deljenja lokacije.';
@override
String get openSettings => 'Odpri nastavitve';
@override
String get locationPermissionGranted => 'Dovoljenje za lokacijo odobreno!';
@override
String get locationPermissionRequiredForGps =>
'Dovoljenje za lokacijo je potrebno za sledenje GPS in deljenje lokacije.';
@override
String get locationPermissionAlreadyGranted =>
'Dovoljenje za lokacijo je že odobreno.';
@override
String get sarNavyBlue => 'SAR Mornarska Modra';
@override
String get sarNavyBlueDescription => 'Profesionalni/Operativni Način';
@override
String get selectRecipient => 'Izberi prejemnika';
@override
String get broadcastToAllNearby => 'Oddajaj vsem v bližini';
@override
String get searchRecipients => 'Išči prejemnike...';
@override
String get noContactsFound => 'Ni kontaktov';
@override
String get noRoomsFound => 'Ni sob';
@override
String get noContactsOrRoomsAvailable => 'Ni na voljo kontaktov ali sob';
@override
String get messagesWillBeSentToPublicChannel =>
'Sporočila bodo poslana na javni kanal';
@override
String get newMessage => 'Novo sporočilo';
@override
String get channel => 'Kanal';
}

View File

@@ -75,6 +75,10 @@
"displayPacketActivity": "Prikaži kazalnike aktivnosti paketov v zgornji vrstici",
"simpleMode": "Preprost način",
"simpleModeDescription": "Skrij nepomembne informacije v sporočilih in kontaktih",
"language": "Jezik",
"chooseLanguage": "Izberite jezik",
@@ -181,7 +185,7 @@
"refresh": "Osveži",
"sendDirectMessage": "Pošlji neposredno sporočilo",
"sendDirectMessage": "Pošlji",
"resetPath": "Ponastavi pot (preusmeri)",
@@ -221,6 +225,18 @@
"clearAllDrawings": "Počisti vse risbe",
"showReceivedDrawings": "Prikaži prejete risbe",
"showingAllDrawings": "Prikazujem vse risbe",
"showingOnlyYourDrawings": "Prikazujem samo vaše risbe",
"showSarMarkers": "Prikaži SAR označevalce",
"showingSarMarkers": "Prikazujem SAR označevalce",
"hidingSarMarkers": "Skrivam SAR označevalce",
"clearAll": "Počisti vse",
"noLocalDrawings": "Ni lokalnih risb za deljenje",
@@ -263,6 +279,8 @@
"location": "Lokacija",
"myLocation": "Moja lokacija",
"fromMap": "Z zemljevida",
"gettingLocation": "Pridobivanje lokacije...",
@@ -379,6 +397,12 @@
"accuracy": "Natančnost",
"distance": "Razdalja",
"bearing": "Azimut",
"direction": "Smer",
"filterMarkers": "Filtriraj označevalce",
"filterMarkersTooltip": "Filtriraj označevalce",
@@ -521,6 +545,12 @@
"esriSatellite": "ESRI satelit",
"googleHybrid": "Google hibridno",
"googleRoadmap": "Google cestni zemljevid",
"googleTerrain": "Google teren",
"downloadVisibleArea": "Prenesi vidno območje",
"initializingMap": "Inicializacija zemljevida...",
@@ -586,6 +616,10 @@
"cannotReplyContactNotFound": "Ni mogoče odgovoriti: stik ni najden",
"messageDeleted": "Sporočilo izbrisano",
"copyText": "Kopiraj besedilo",
"textCopiedToClipboard": "Besedilo kopirano v odložišče",
"deleteMessage": "Izbriši sporočilo",
"deleteMessageConfirmation": "Ali ste prepričani, da želite izbrisati to sporočilo?",
"refreshedContacts": "Stiki osveženi",
@@ -636,6 +670,38 @@
"failedToDeleteMbtiles": "Brisanje brezpoveznega zemljevida ni uspelo",
"importExportCachedTiles": "Uvoz/Izvoz predpomnjenih ploščic",
"importExportDescription": "Varnostno kopirajte, delite in obnovite prenesene ploščice zemljevida med napravami",
"exportTilesToFile": "Izvozi ploščice v datoteko",
"importTilesFromFile": "Uvozi ploščice iz datoteke",
"selectExportLocation": "Izberi lokacijo izvoza",
"selectImportFile": "Izberi arhiv ploščic",
"exportingTiles": "Izvažanje ploščic...",
"importingTiles": "Uvažanje ploščic...",
"exportSuccess": "Uspešno izvoženih {count} ploščic",
"importSuccess": "Uspešno uvoženih {count} skladišč",
"exportFailed": "Izvoz ni uspel: {error}",
"importFailed": "Uvoz ni uspel: {error}",
"exportNote": "Ustvari stisnjeno arhivsko datoteko (.fmtc), ki jo lahko delite in uvozite na drugih napravah.",
"importNote": "Uvozi ploščice zemljevida iz predhodno izvožene arhivske datoteke. Ploščice bodo združene z obstoječim predpomnilnikom.",
"noTilesToExport": "Ni ploščic za izvoz",
"archiveContainsStores": "Arhiv vsebuje {count} skladišč",
"vectorTiles": "Vektorske ploščice",
"schema": "Shema",
@@ -722,5 +788,63 @@
"failedToSave": "Shranjevanje ni uspelo: {error}",
"failedToGetLocation": "Pridobivanje lokacije ni uspelo: {error}"
"failedToGetLocation": "Pridobivanje lokacije ni uspelo: {error}",
"sarTemplates": "SAR Templates",
"manageSarTemplates": "Manage cursor on target templates",
"addTemplate": "Add Template",
"editTemplate": "Edit Template",
"deleteTemplate": "Delete Template",
"templateName": "Template Name",
"templateNameHint": "e.g. Found Person",
"templateEmoji": "Emoji",
"emojiRequired": "Emoji je obvezen",
"nameRequired": "Ime je obvezno",
"templateDescription": "Description (Optional)",
"templateDescriptionHint": "Add additional context...",
"templateColor": "Color",
"previewFormat": "Preview (SAR Message Format)",
"importFromClipboard": "Import",
"exportToClipboard": "Export",
"deleteTemplateConfirmation": "Delete template '{name}'?",
"templateAdded": "Template added",
"templateUpdated": "Template updated",
"templateDeleted": "Template deleted",
"templatesImported": "{count, plural, =0{No templates imported} =1{Imported 1 template} other{Imported {count} templates}}",
"templatesExported": "{count, plural, =1{Exported 1 template to clipboard} other{Exported {count} templates to clipboard}}",
"importFailed": "Import failed: {error}",
"exportFailed": "Export failed: {error}",
"resetToDefaults": "Reset to Defaults",
"resetToDefaultsConfirmation": "This will delete all custom templates and restore the 4 default templates. Continue?",
"reset": "Reset",
"resetComplete": "Templates reset to defaults",
"noTemplates": "No templates available",
"tapAddToCreate": "Tap + to create your first template",
"ok": "OK",
"delete": "Izbriši",
"permissionsSection": "Dovoljenja",
"locationPermission": "Dovoljenje za lokacijo",
"checking": "Preverjanje...",
"locationPermissionGrantedAlways": "Odobreno (Vedno)",
"locationPermissionGrantedWhileInUse": "Odobreno (Med uporabo)",
"locationPermissionDeniedTapToRequest": "Zavrnjeno - Tapnite za zahtevo",
"locationPermissionPermanentlyDeniedOpenSettings": "Trajno zavrnjeno - Odpri nastavitve",
"locationPermissionDialogContent": "Dovoljenje za lokacijo je trajno zavrnjeno. Omogočite ga v nastavitvah naprave za uporabo sledenja GPS in deljenja lokacije.",
"openSettings": "Odpri nastavitve",
"locationPermissionGranted": "Dovoljenje za lokacijo odobreno!",
"locationPermissionRequiredForGps": "Dovoljenje za lokacijo je potrebno za sledenje GPS in deljenje lokacije.",
"locationPermissionAlreadyGranted": "Dovoljenje za lokacijo je že odobreno.",
"sarNavyBlue": "SAR Mornarska Modra",
"sarNavyBlueDescription": "Profesionalni/Operativni Način",
"selectRecipient": "Izberi prejemnika",
"broadcastToAllNearby": "Oddajaj vsem v bližini",
"searchRecipients": "Išči prejemnike...",
"noContactsFound": "Ni kontaktov",
"noRoomsFound": "Ni sob",
"noContactsOrRoomsAvailable": "Ni na voljo kontaktov ali sob",
"messagesWillBeSentToPublicChannel": "Sporočila bodo poslana na javni kanal",
"newMessage": "Novo sporočilo",
"channel": "Kanal"
}

View File

@@ -113,9 +113,11 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
ChangeNotifierProvider(create: (_) => ConnectionProvider()),
ChangeNotifierProvider(
create: (_) {
// Don't initialize here - it will be initialized in AppProvider.initialize()
// after connection is established and device info is available
return ContactsProvider();
// Initialize early to load persisted contacts for offline viewing
// Self-contact filtering will happen later when BLE connects
final provider = ContactsProvider();
provider.initializeEarly();
return provider;
},
),
ChangeNotifierProvider(

View File

@@ -71,6 +71,7 @@ abstract class MapDrawing {
final DateTime createdAt;
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)
MapDrawing({
required this.id,
@@ -79,6 +80,7 @@ abstract class MapDrawing {
required this.createdAt,
this.senderName,
this.isReceived = false,
this.messageId,
});
/// Convert to JSON for persistence
@@ -90,8 +92,12 @@ abstract class MapDrawing {
Map<String, dynamic> toNetworkJson();
/// Parse network JSON (compact format)
/// senderName will be populated from packet metadata
static MapDrawing? fromNetworkJson(Map<String, dynamic> json, {String? senderName}) {
/// senderName and messageId will be populated from packet metadata
static MapDrawing? fromNetworkJson(
Map<String, dynamic> json, {
String? senderName,
String? messageId,
}) {
final typeNum = json['t'] as int?;
if (typeNum == null || typeNum < 0 || typeNum >= DrawingShapeType.values.length) {
return null;
@@ -102,9 +108,17 @@ abstract class MapDrawing {
switch (type) {
case DrawingShapeType.line:
return LineDrawing.fromNetworkJson(json, senderName: senderName);
return LineDrawing.fromNetworkJson(
json,
senderName: senderName,
messageId: messageId,
);
case DrawingShapeType.rectangle:
return RectangleDrawing.fromNetworkJson(json, senderName: senderName);
return RectangleDrawing.fromNetworkJson(
json,
senderName: senderName,
messageId: messageId,
);
}
} catch (e) {
return null;
@@ -144,6 +158,7 @@ class LineDrawing extends MapDrawing {
required this.points,
super.senderName,
super.isReceived,
super.messageId,
}) : super(type: DrawingShapeType.line);
@override
@@ -184,7 +199,11 @@ class LineDrawing extends MapDrawing {
);
}
static LineDrawing fromNetworkJson(Map<String, dynamic> json, {String? senderName}) {
static LineDrawing fromNetworkJson(
Map<String, dynamic> json, {
String? senderName,
String? messageId,
}) {
// Parse ultra-compact format
final pointsFlat = (json['p'] as List<dynamic>).cast<double>();
final points = <LatLng>[];
@@ -199,6 +218,7 @@ class LineDrawing extends MapDrawing {
points: points,
senderName: senderName,
isReceived: true,
messageId: messageId, // Link to source message
);
}
@@ -226,6 +246,7 @@ class RectangleDrawing extends MapDrawing {
required this.bottomRight,
super.senderName,
super.isReceived,
super.messageId,
}) : super(type: DrawingShapeType.rectangle);
/// Get all corner points for rendering
@@ -276,7 +297,11 @@ class RectangleDrawing extends MapDrawing {
);
}
static RectangleDrawing fromNetworkJson(Map<String, dynamic> json, {String? senderName}) {
static RectangleDrawing fromNetworkJson(
Map<String, dynamic> json, {
String? senderName,
String? messageId,
}) {
// Parse ultra-compact format
final bounds = (json['b'] as List<dynamic>).cast<double>();
@@ -288,6 +313,7 @@ class RectangleDrawing extends MapDrawing {
bottomRight: LatLng(bounds[2], bounds[3]),
senderName: senderName,
isReceived: true,
messageId: messageId, // Link to source message
);
}

View File

@@ -6,6 +6,9 @@ enum MapLayerType {
openStreetMap,
openTopoMap,
esriWorldImagery,
googleHybrid,
googleRoadmap,
googleTerrain,
vectorMbtiles,
}
@@ -46,6 +49,12 @@ class MapLayer {
return localizations.openTopoMap;
case MapLayerType.esriWorldImagery:
return localizations.esriSatellite;
case MapLayerType.googleHybrid:
return localizations.googleHybrid;
case MapLayerType.googleRoadmap:
return localizations.googleRoadmap;
case MapLayerType.googleTerrain:
return localizations.googleTerrain;
case MapLayerType.vectorMbtiles:
// For vector tiles, use the name from metadata
return name;
@@ -77,10 +86,37 @@ class MapLayer {
maxZoom: 19, // ESRI World Imagery maximum
);
static const googleHybrid = MapLayer(
type: MapLayerType.googleHybrid,
name: 'Google Hybrid',
urlTemplate: 'http://mt0.google.com/vt/lyrs=y&hl=en&x={x}&y={y}&z={z}',
attribution: '© Google',
maxZoom: 20, // Google Maps maximum
);
static const googleRoadmap = MapLayer(
type: MapLayerType.googleRoadmap,
name: 'Google Roadmap',
urlTemplate: 'http://mt0.google.com/vt/lyrs=m&hl=en&x={x}&y={y}&z={z}',
attribution: '© Google',
maxZoom: 20, // Google Maps maximum
);
static const googleTerrain = MapLayer(
type: MapLayerType.googleTerrain,
name: 'Google Terrain',
urlTemplate: 'http://mt0.google.com/vt/lyrs=p&hl=en&x={x}&y={y}&z={z}',
attribution: '© Google',
maxZoom: 20, // Google Maps maximum
);
static const List<MapLayer> allLayers = [
openStreetMap,
openTopoMap,
esriWorldImagery,
googleHybrid,
googleRoadmap,
googleTerrain,
];
static MapLayer fromType(MapLayerType type) {

View File

@@ -1,4 +1,5 @@
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:latlong2/latlong.dart';
import 'sar_marker.dart';
@@ -51,6 +52,7 @@ class Message {
final SarMarkerType? sarMarkerType;
final LatLng? sarGpsCoordinates;
final String? sarNotes; // Optional message/notes for SAR marker
final String? sarCustomEmoji; // Custom emoji for unknown SAR marker types
// Display metadata
final DateTime receivedAt;
@@ -89,6 +91,7 @@ class Message {
this.sarMarkerType,
this.sarGpsCoordinates,
this.sarNotes,
this.sarCustomEmoji,
required this.receivedAt,
this.senderName,
this.deliveryStatus = MessageDeliveryStatus.received,
@@ -171,6 +174,13 @@ class Message {
return null;
}
// Debug: Check what's in sarNotes
debugPrint('📍 [Message.toSarMarker] Converting to marker:');
debugPrint(' message.text: "${text}"');
debugPrint(' message.sarNotes: "${sarNotes}"');
debugPrint(' message.sarMarkerType: ${sarMarkerType}');
debugPrint(' message.sarCustomEmoji: "${sarCustomEmoji}"');
return SarMarker(
id: id,
type: sarMarkerType!,
@@ -179,6 +189,7 @@ class Message {
senderPublicKey: senderPublicKeyPrefix,
senderName: senderName,
notes: sarNotes, // Use dedicated notes field instead of full text
customEmoji: sarCustomEmoji, // Preserve custom emoji for unknown types
);
}
@@ -275,6 +286,7 @@ class Message {
SarMarkerType? sarMarkerType,
LatLng? sarGpsCoordinates,
String? sarNotes,
String? sarCustomEmoji,
DateTime? receivedAt,
String? senderName,
MessageDeliveryStatus? deliveryStatus,
@@ -303,6 +315,7 @@ class Message {
sarMarkerType: sarMarkerType ?? this.sarMarkerType,
sarGpsCoordinates: sarGpsCoordinates ?? this.sarGpsCoordinates,
sarNotes: sarNotes ?? this.sarNotes,
sarCustomEmoji: sarCustomEmoji ?? this.sarCustomEmoji,
receivedAt: receivedAt ?? this.receivedAt,
senderName: senderName ?? this.senderName,
deliveryStatus: deliveryStatus ?? this.deliveryStatus,

View File

@@ -2,6 +2,7 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
import '../l10n/app_localizations.dart';
import '../services/sar_template_service.dart';
/// SAR (Search & Rescue) marker types
enum SarMarkerType {
@@ -75,6 +76,7 @@ class SarMarker {
final Uint8List? senderPublicKey;
final String? senderName;
final String? notes;
final String? customEmoji; // For custom SAR markers not in predefined types
SarMarker({
required this.id,
@@ -84,6 +86,7 @@ class SarMarker {
this.senderPublicKey,
this.senderName,
this.notes,
this.customEmoji,
});
/// Get sender public key as hex string (short)
@@ -109,9 +112,47 @@ class SarMarker {
return DateTime.now().difference(timestamp).inHours < 1;
}
/// Get display name
/// Get the emoji to display (custom emoji if available, otherwise type emoji)
String get emoji {
return customEmoji ?? type.emoji;
}
/// Get display name - uses notes if available, otherwise looks up template by emoji, otherwise type name
String get displayName {
return '${type.emoji} ${type.displayName}';
if (notes != null && notes!.isNotEmpty) {
return notes!;
}
// If no notes and we have a custom emoji, try to look up the template
if (customEmoji != null) {
// Import the service here to avoid circular dependencies
// We'll use a static lookup method
return _lookupTemplateNameByEmoji(customEmoji!) ?? type.displayName;
}
return type.displayName;
}
/// Look up template name by emoji from SarTemplateService
static String? _lookupTemplateNameByEmoji(String emoji) {
try {
// Use the singleton instance
final service = SarTemplateService();
if (!service.isInitialized) {
return null;
}
// Find template with matching emoji
final template = service.templates.firstWhere(
(t) => t.emoji == emoji,
orElse: () => throw StateError('No template found'),
);
return template.name;
} catch (e) {
// Template not found or service not initialized
return null;
}
}
SarMarker copyWith({
@@ -122,6 +163,7 @@ class SarMarker {
Uint8List? senderPublicKey,
String? senderName,
String? notes,
String? customEmoji,
}) {
return SarMarker(
id: id ?? this.id,
@@ -131,6 +173,7 @@ class SarMarker {
senderPublicKey: senderPublicKey ?? this.senderPublicKey,
senderName: senderName ?? this.senderName,
notes: notes ?? this.notes,
customEmoji: customEmoji ?? this.customEmoji,
);
}

View File

@@ -0,0 +1,196 @@
import 'package:flutter/material.dart';
/// SAR Template - Customizable template for SAR (Cursor on Target) messages
class SarTemplate {
final String id;
final String emoji;
final String name;
final String description;
final String colorHex;
final bool isDefault;
SarTemplate({
required this.id,
required this.emoji,
required this.name,
required this.description,
required this.colorHex,
this.isDefault = false,
});
/// Get color from hex string
Color get color {
final hexCode = colorHex.replaceAll('#', '');
return Color(int.parse('FF$hexCode', radix: 16));
}
/// Create from JSON
factory SarTemplate.fromJson(Map<String, dynamic> json) {
return SarTemplate(
id: json['id'] as String,
emoji: json['emoji'] as String,
name: json['name'] as String,
description: json['description'] as String? ?? '',
colorHex: json['colorHex'] as String,
isDefault: json['isDefault'] as bool? ?? false,
);
}
/// Convert to JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'emoji': emoji,
'name': name,
'description': description,
'colorHex': colorHex,
'isDefault': isDefault,
};
}
/// Create from SAR message format (S:emoji:0,0:description)
/// Example: S:🧑:0,0:Person found
factory SarTemplate.fromSarMessage(String message) {
final trimmed = message.trim();
if (!trimmed.startsWith('S:')) {
throw FormatException('SAR message must start with "S:"');
}
// Parse format: S:emoji:lat,lon:description
final parts = trimmed.split(':');
if (parts.length < 3) {
throw FormatException('Invalid SAR message format');
}
final emoji = parts[1].trim();
if (emoji.isEmpty) {
throw FormatException('Emoji cannot be empty');
}
// Extract description (everything after the third colon)
String description = '';
if (parts.length > 3) {
description = parts.sublist(3).join(':').trim();
}
// Generate ID from emoji + description
final id = '${emoji}_${DateTime.now().millisecondsSinceEpoch}';
// Auto-assign color based on emoji
String colorHex = _getColorForEmoji(emoji);
return SarTemplate(
id: id,
emoji: emoji,
name: description.isNotEmpty ? description : emoji,
description: description,
colorHex: colorHex,
isDefault: false,
);
}
/// Convert to SAR message format with placeholder coordinates
/// Example: S:🧑:0,0:Person found
String toSarMessage() {
if (description.isNotEmpty) {
return 'S:$emoji:0,0:$description';
}
return 'S:$emoji:0,0';
}
/// Auto-assign color based on emoji
static String _getColorForEmoji(String emoji) {
// Default emoji to color mapping
final colorMap = {
'🧑': '#4CAF50', // Green - Person
'👤': '#4CAF50', // Green - Person
'🔥': '#F44336', // Red - Fire
'🏕️': '#FF9800', // Orange - Staging
'': '#FF9800', // Orange - Staging
'📦': '#9C27B0', // Purple - Object
'🚁': '#2196F3', // Blue - Helicopter
'🚒': '#F44336', // Red - Fire truck
'🚑': '#F44336', // Red - Ambulance
'⚠️': '#FFC107', // Yellow - Warning
'': '#F44336', // Red - Hazard
'': '#4CAF50', // Green - Safe
'🏥': '#F44336', // Red - Medical
'💧': '#2196F3', // Blue - Water
'🌲': '#4CAF50', // Green - Forest
'⛰️': '#795548', // Brown - Mountain
};
return colorMap[emoji] ?? '#9E9E9E'; // Default gray
}
/// Copy with modifications
SarTemplate copyWith({
String? id,
String? emoji,
String? name,
String? description,
String? colorHex,
bool? isDefault,
}) {
return SarTemplate(
id: id ?? this.id,
emoji: emoji ?? this.emoji,
name: name ?? this.name,
description: description ?? this.description,
colorHex: colorHex ?? this.colorHex,
isDefault: isDefault ?? this.isDefault,
);
}
@override
String toString() {
return 'SarTemplate(id: $id, emoji: $emoji, name: $name, description: $description)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is SarTemplate && id == other.id;
}
@override
int get hashCode => id.hashCode;
/// Default templates (matches existing SarMarkerType)
static List<SarTemplate> get defaults {
return [
SarTemplate(
id: 'default_found_person',
emoji: '🧑',
name: 'Found Person',
description: '',
colorHex: '#4CAF50',
isDefault: true,
),
SarTemplate(
id: 'default_fire',
emoji: '🔥',
name: 'Fire',
description: '',
colorHex: '#F44336',
isDefault: true,
),
SarTemplate(
id: 'default_staging_area',
emoji: '🏕️',
name: 'Staging Area',
description: '',
colorHex: '#FF9800',
isDefault: true,
),
SarTemplate(
id: 'default_object',
emoji: '📦',
name: 'Object',
description: '',
colorHex: '#9C27B0',
isDefault: true,
),
];
}
}

View File

@@ -24,6 +24,9 @@ class AppProvider with ChangeNotifier {
bool _isInitialized = false;
bool get isInitialized => _isInitialized;
bool _isSimpleMode = false;
bool get isSimpleMode => _isSimpleMode;
AppProvider({
required this.connectionProvider,
required this.contactsProvider,
@@ -35,9 +38,33 @@ class AppProvider with ChangeNotifier {
_setupCallbacks();
_initializeTileCache();
_initializeLocationTracking();
_loadSimpleMode();
_isInitialized = true;
}
/// Load simple mode setting from shared preferences
Future<void> _loadSimpleMode() async {
try {
final prefs = await SharedPreferences.getInstance();
_isSimpleMode = prefs.getBool('simple_mode') ?? false;
notifyListeners();
} catch (e) {
debugPrint('Error loading simple mode setting: $e');
}
}
/// Toggle simple mode on/off
Future<void> toggleSimpleMode(bool enabled) async {
try {
_isSimpleMode = enabled;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('simple_mode', enabled);
notifyListeners();
} catch (e) {
debugPrint('Error saving simple mode setting: $e');
}
}
/// Initialize tile cache service
Future<void> _initializeTileCache() async {
try {
@@ -116,19 +143,17 @@ class AppProvider with ChangeNotifier {
final drawing = DrawingMessageParser.parseDrawingMessage(
message.text,
senderName: senderName,
messageId: message.id, // Pass message ID for navigation linking
);
if (drawing != null) {
debugPrint('🎨 [AppProvider] Drawing parsed successfully: ${drawing.type.name} from ${drawing.senderName ?? "unknown"}');
debugPrint(' Drawing linked to message ID: ${message.id}');
drawingProvider.addReceivedDrawing(drawing);
// Add informational message to chat
final drawingTypeStr = drawing.type.name.substring(0, 1).toUpperCase() +
drawing.type.name.substring(1);
final infoMessage = message.copyWith(
text: '📍 Received map drawing ($drawingTypeStr) from ${drawing.senderName ?? "unknown"}',
);
// Add the original drawing message to chat (not a modified info message)
// This allows users to click on the drawing message to navigate to it
messagesProvider.addMessage(
infoMessage,
message,
contactLookup: (name) => '',
);
} else {
@@ -238,12 +263,11 @@ class AppProvider with ChangeNotifier {
try {
// Initialize contacts provider with device public key to exclude self
// If already initialized (from early load), this will just filter out self-contact
// This must happen before getContacts to ensure proper filtering
if (!contactsProvider.isInitialized) {
await contactsProvider.initialize(
devicePublicKey: connectionProvider.deviceInfo.publicKey,
);
}
await contactsProvider.initialize(
devicePublicKey: connectionProvider.deviceInfo.publicKey,
);
// Note: Device clock is automatically synced during connection in MeshCoreBleService
// No need to sync it again here
@@ -257,9 +281,12 @@ class AppProvider with ChangeNotifier {
// Small delay to ensure contacts are fully loaded
await Future.delayed(const Duration(milliseconds: 500));
// Sync all channels to get channel names
debugPrint('📻 [AppProvider] Syncing channels...');
await connectionProvider.syncChannels();
// Sync channels to get channel names
// In simple mode: only sync first 5 channels for faster startup
// In normal mode: sync all channels (up to device max)
final channelsToSync = _isSimpleMode ? 5 : null;
debugPrint('📻 [AppProvider] Syncing channels${_isSimpleMode ? ' (simple mode: max 5)' : ''}...');
await connectionProvider.syncChannels(maxChannels: channelsToSync);
debugPrint('✅ [AppProvider] Channel sync complete');
// Configure the default public channel (channel 0)

View File

@@ -17,10 +17,49 @@ class ContactsProvider with ChangeNotifier {
bool get isInitialized => _isInitialized;
/// Initialize and load persisted contacts at app startup
/// This loads contacts without filtering, allowing offline viewing
Future<void> initializeEarly() async {
if (_isInitialized) return;
try {
debugPrint('📦 [ContactsProvider] Early loading persisted contacts (no filtering)...');
final storedContacts = await _storageService.loadContacts();
// Add stored contacts (excluding any with all-zeros public key)
const publicChannelKey = '0000000000000000000000000000000000000000000000000000000000000000';
for (final contact in storedContacts) {
// Skip any contacts with all-zeros public key (shouldn't happen, but safety check)
if (contact.publicKeyHex == publicChannelKey) {
continue;
}
_contacts[contact.publicKeyHex] = contact;
}
_isInitialized = true;
debugPrint('✅ [ContactsProvider] Early loaded ${storedContacts.length} persisted contacts');
// Ensure public channel exists after loading
_ensurePublicChannelExists();
notifyListeners();
} catch (e) {
debugPrint('❌ [ContactsProvider] Error in early initialization: $e');
_isInitialized = true; // Mark as initialized even on error
_ensurePublicChannelExists();
}
}
/// Initialize and load persisted contacts
/// [devicePublicKey] - device's own public key to exclude from loaded contacts
Future<void> initialize({Uint8List? devicePublicKey}) async {
if (_isInitialized) return;
if (_isInitialized) {
// If already initialized (from early load), just filter out self-contact
if (devicePublicKey != null) {
_removeSelfContact(devicePublicKey);
}
return;
}
try {
debugPrint('📦 [ContactsProvider] Loading persisted contacts...');
@@ -52,6 +91,18 @@ class ContactsProvider with ChangeNotifier {
}
}
/// Remove self-contact from loaded contacts (called after BLE connection established)
void _removeSelfContact(Uint8List devicePublicKey) {
final selfKeyHex = devicePublicKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
if (_contacts.containsKey(selfKeyHex)) {
final selfContact = _contacts[selfKeyHex]!;
debugPrint('🗑️ [ContactsProvider] Removing self-contact: ${selfContact.advName}');
_contacts.remove(selfKeyHex);
_persistContacts();
notifyListeners();
}
}
/// Ensure public channel always exists in the list
void _ensurePublicChannelExists() {
// Public channel has all-zeros public key (32 bytes = 64 hex chars)

View File

@@ -20,6 +20,8 @@ class DrawingProvider with ChangeNotifier {
// Drawing state
DrawingMode _drawingMode = DrawingMode.none;
Color _selectedColor = DrawingColors.palette[0];
bool _showReceivedDrawings = true;
bool _showSarMarkers = true;
// Completed drawings
final List<MapDrawing> _drawings = [];
@@ -32,7 +34,11 @@ class DrawingProvider with ChangeNotifier {
// Getters
DrawingMode get drawingMode => _drawingMode;
Color get selectedColor => _selectedColor;
List<MapDrawing> get drawings => List.unmodifiable(_drawings);
bool get showReceivedDrawings => _showReceivedDrawings;
bool get showSarMarkers => _showSarMarkers;
List<MapDrawing> get drawings => _showReceivedDrawings
? List.unmodifiable(_drawings)
: List.unmodifiable(_drawings.where((d) => !d.isReceived).toList());
MapDrawing? get currentDrawing => _currentDrawing;
List<LatLng> get currentLinePoints => List.unmodifiable(_currentLinePoints);
LatLng? get rectangleStartPoint => _rectangleStartPoint;
@@ -59,6 +65,18 @@ class DrawingProvider with ChangeNotifier {
notifyListeners();
}
/// Toggle visibility of received drawings
void toggleReceivedDrawings() {
_showReceivedDrawings = !_showReceivedDrawings;
notifyListeners();
}
/// Toggle visibility of SAR markers
void toggleSarMarkers() {
_showSarMarkers = !_showSarMarkers;
notifyListeners();
}
/// Start drawing a line
void startLine(LatLng point) {
if (_drawingMode != DrawingMode.line) return;

View File

@@ -31,6 +31,9 @@ class MessagesProvider with ChangeNotifier {
// Track which contact each sent message was sent to (for retry logic)
final Map<String, Contact> _messageContactMap = {};
// Navigation state for message highlighting/scrolling
String? _targetMessageId;
// Callback to connection provider for sending messages (set by AppProvider)
Future<bool> Function({
required Uint8List contactPublicKey,
@@ -70,11 +73,24 @@ class MessagesProvider with ChangeNotifier {
bool get isInitialized => _isInitialized;
String? get targetMessageId => _targetMessageId;
/// Set localizations for notifications
void setLocalizations(AppLocalizations localizations) {
_localizations = localizations;
}
/// Navigate to a specific message (scroll and highlight)
void navigateToMessage(String messageId) {
_targetMessageId = messageId;
notifyListeners();
}
/// Clear message navigation state
void clearMessageNavigation() {
_targetMessageId = null;
}
/// Get count of unread messages (excluding sent messages and system messages)
int get unreadCount => _messages
.where((m) =>
@@ -178,6 +194,9 @@ class MessagesProvider with ChangeNotifier {
_triggerSarNotification(finalMessage, marker);
}
}
} else if (!finalMessage.isSentMessage && !finalMessage.isSystemMessage) {
// Trigger notification for regular messages (not SAR, not sent by user, not system)
_triggerMessageNotification(finalMessage);
}
// Persist to storage asynchronously
@@ -296,6 +315,40 @@ class MessagesProvider with ChangeNotifier {
}
}
/// Trigger notification for regular message
Future<void> _triggerMessageNotification(Message message) async {
try {
// Get sender name from message
final senderName = message.senderName ?? message.senderKeyShort ?? 'Unknown';
// Determine if it's a channel message
final isChannelMessage = message.isChannelMessage;
// Get channel name if available
String? channelName;
if (isChannelMessage) {
// You could map channelIdx to channel name here if needed
// For now, use "Public" for channel 0
channelName = message.channelIdx == 0 ? 'Public' : 'Channel ${message.channelIdx}';
}
debugPrint('🔔 [MessagesProvider] Triggering message notification');
debugPrint(' Sender: $senderName');
debugPrint(' Type: ${isChannelMessage ? "Channel" : "Direct"}');
debugPrint(' Message: ${message.text.substring(0, message.text.length > 50 ? 50 : message.text.length)}...');
await _notificationService.showMessageNotification(
senderName: senderName,
messageText: message.text,
isChannelMessage: isChannelMessage,
channelName: channelName,
localizations: _localizations,
);
} catch (e) {
debugPrint('❌ [MessagesProvider] Error triggering message notification: $e');
}
}
/// Persist messages to storage (async, non-blocking)
Future<void> _persistMessages() async {
try {
@@ -529,6 +582,11 @@ class MessagesProvider with ChangeNotifier {
if (sendingMessage.isSarMarker) {
final marker = sendingMessage.toSarMarker();
if (marker != null) {
debugPrint(' ✅ SAR Marker created:');
debugPrint(' marker.id: ${marker.id}');
debugPrint(' marker.notes: "${marker.notes}"');
debugPrint(' marker.type: ${marker.type}');
debugPrint(' marker.displayName: ${marker.displayName}');
_sarMarkers[marker.id] = marker;
}
}

View File

@@ -8,7 +8,9 @@ import '../providers/app_provider.dart';
import '../widgets/contacts/contact_tile.dart';
class ContactsTab extends StatefulWidget {
const ContactsTab({super.key});
final VoidCallback? onNavigateToMap;
const ContactsTab({super.key, this.onNavigateToMap});
@override
State<ContactsTab> createState() => _ContactsTabState();
@@ -87,6 +89,9 @@ class _ContactsTabState extends State<ContactsTab> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final appProvider = context.watch<AppProvider>();
final isSimpleMode = appProvider.isSimpleMode;
return Consumer<ContactsProvider>(
builder: (context, contactsProvider, child) {
final chatContacts = contactsProvider.chatContacts;
@@ -143,6 +148,7 @@ class _ContactsTabState extends State<ContactsTab> {
currentPosition: _currentPosition,
calculateDistance: _calculateDistanceInMeters,
formatDistance: _formatDistance,
onNavigateToMap: widget.onNavigateToMap,
),
),
const Divider(height: 32),
@@ -161,6 +167,7 @@ class _ContactsTabState extends State<ContactsTab> {
currentPosition: _currentPosition,
calculateDistance: _calculateDistanceInMeters,
formatDistance: _formatDistance,
onNavigateToMap: widget.onNavigateToMap,
),
),
const Divider(height: 32),
@@ -179,13 +186,14 @@ class _ContactsTabState extends State<ContactsTab> {
currentPosition: _currentPosition,
calculateDistance: _calculateDistanceInMeters,
formatDistance: _formatDistance,
onNavigateToMap: widget.onNavigateToMap,
),
),
const Divider(height: 32),
],
// Channels
if (channels.isNotEmpty) ...[
// Channels (hidden in simple mode)
if (!isSimpleMode && channels.isNotEmpty) ...[
_SectionHeader(
title: l10n.channels,
count: channels.length,
@@ -197,6 +205,7 @@ class _ContactsTabState extends State<ContactsTab> {
currentPosition: _currentPosition,
calculateDistance: _calculateDistanceInMeters,
formatDistance: _formatDistance,
onNavigateToMap: widget.onNavigateToMap,
),
),
],

View File

@@ -581,13 +581,14 @@ class _HomeScreenState extends State<HomeScreen>
controller: _tabController,
children: [
MessagesTab(onNavigateToMap: () => _tabController.animateTo(2)),
const ContactsTab(),
ContactsTab(onNavigateToMap: () => _tabController.animateTo(2)),
MapTab(
onFullscreenChanged: (isFullscreen) {
setState(() {
_isMapFullscreen = isFullscreen;
});
},
onNavigateToMessages: () => _tabController.animateTo(0),
),
],
),
@@ -772,32 +773,35 @@ class _HomeScreenState extends State<HomeScreen>
],
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DeviceConfigScreen(),
),
);
},
onLongPress: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
PacketLogScreen(bleService: provider.bleService),
),
);
},
child: Container(
width: 32,
height: 32,
alignment: Alignment.center,
child: const Icon(Icons.settings, size: 18),
// Settings cog - hidden in simple mode
if (!context.watch<AppProvider>().isSimpleMode) ...[
const SizedBox(width: 8),
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DeviceConfigScreen(),
),
);
},
onLongPress: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
PacketLogScreen(bleService: provider.bleService),
),
);
},
child: Container(
width: 32,
height: 32,
alignment: Alignment.center,
child: const Icon(Icons.settings, size: 18),
),
),
),
],
],
),
),

View File

@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:file_picker/file_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import '../services/tile_cache_service.dart';
import '../services/validation_service.dart';
import '../services/mbtiles_service.dart';
@@ -391,6 +393,133 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
}
}
Future<void> _exportTiles() async {
try {
// Check if there are tiles to export
final tileCount = await widget.tileCacheService.getCachedTileCount();
if (tileCount == 0) {
_showError(AppLocalizations.of(context)!.noTilesToExport);
return;
}
if (!mounted) return;
setState(() {
_isLoading = true;
_statusMessage = AppLocalizations.of(context)!.exportingTiles;
});
// Export to temporary directory first (works on all platforms)
final tempDir = await getTemporaryDirectory();
final fileName = 'meshcore_tiles_${DateTime.now().millisecondsSinceEpoch}.fmtc';
final tempFilePath = '${tempDir.path}/$fileName';
final exportedCount = await widget.tileCacheService.exportStore(tempFilePath);
if (!mounted) return;
setState(() {
_isLoading = false;
_statusMessage = null;
});
// Share the file using share_plus (works on all platforms)
final file = File(tempFilePath);
if (await file.exists()) {
// Get the button position for iPad popover
final box = context.findRenderObject() as RenderBox?;
final sharePositionOrigin = box != null
? box.localToGlobal(Offset.zero) & box.size
: null;
final result = await SharePlus.instance.share(
ShareParams(
files: [XFile(tempFilePath)],
subject: 'MeshCore Map Tiles Export',
text: 'Exported $exportedCount map tiles',
sharePositionOrigin: sharePositionOrigin,
),
);
if (mounted) {
if (result.status == ShareResultStatus.success) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.exportSuccess(exportedCount)),
backgroundColor: Colors.green,
),
);
}
}
} else {
_showError('Export file not found');
}
} catch (e) {
if (!mounted) return;
setState(() {
_isLoading = false;
_statusMessage = null;
});
_showError(AppLocalizations.of(context)!.exportFailed(e.toString()));
}
}
Future<void> _importTiles() async {
try {
// Use file picker to select import file
final result = await FilePicker.platform.pickFiles(
dialogTitle: AppLocalizations.of(context)!.selectImportFile,
type: FileType.custom,
allowedExtensions: ['fmtc'],
);
if (result == null || result.files.isEmpty) return;
final filePath = result.files.first.path;
if (filePath == null) return;
if (!mounted) return;
setState(() {
_isLoading = true;
_statusMessage = AppLocalizations.of(context)!.importingTiles;
});
// Optional: Preview stores in archive before importing
try {
final stores = await widget.tileCacheService.listArchiveStores(filePath);
debugPrint('Archive contains stores: $stores');
} catch (e) {
debugPrint('Could not list stores: $e');
}
final importResult = await widget.tileCacheService.importStore(filePath);
if (!mounted) return;
setState(() {
_isLoading = false;
_statusMessage = null;
});
await _loadCacheStats(); // Refresh stats after import
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context)!.importSuccess(importResult['successfulStores'] as int),
),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (!mounted) return;
setState(() {
_isLoading = false;
_statusMessage = null;
});
_showError(AppLocalizations.of(context)!.importFailed(e.toString()));
}
}
void _showError(String message) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -424,6 +553,10 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
_buildMbtilesCard(),
const SizedBox(height: 16),
// Import/Export Cached Tiles
_buildImportExportCard(),
const SizedBox(height: 16),
// Download Region
_buildDownloadCard(),
const SizedBox(height: 16),
@@ -636,6 +769,60 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
);
}
Widget _buildImportExportCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context)!.importExportCachedTiles,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
AppLocalizations.of(context)!.importExportDescription,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
const SizedBox(height: 16),
// Export Section
ElevatedButton.icon(
onPressed: _isDownloading || _isLoading ? null : _exportTiles,
icon: const Icon(Icons.file_upload),
label: Text(AppLocalizations.of(context)!.exportTilesToFile),
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(48),
),
),
const SizedBox(height: 8),
Text(
AppLocalizations.of(context)!.exportNote,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
const SizedBox(height: 16),
// Import Section
ElevatedButton.icon(
onPressed: _isDownloading || _isLoading ? null : _importTiles,
icon: const Icon(Icons.file_download),
label: Text(AppLocalizations.of(context)!.importTilesFromFile),
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(48),
),
),
const SizedBox(height: 8),
Text(
AppLocalizations.of(context)!.importNote,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
),
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),

View File

@@ -41,10 +41,12 @@ import 'map_management_screen.dart';
class MapTab extends StatefulWidget {
final Function(bool)? onFullscreenChanged;
final VoidCallback? onNavigateToMessages;
const MapTab({
super.key,
this.onFullscreenChanged,
this.onNavigateToMessages,
});
@override
@@ -96,6 +98,15 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
// Access the singleton LocationTrackingService from AppProvider
LocationTrackingService get _locationService => LocationTrackingService();
/// Helper to compare public keys byte-by-byte
bool _publicKeysMatch(Uint8List key1, Uint8List key2) {
if (key1.length != key2.length) return false;
for (int i = 0; i < key1.length; i++) {
if (key1[i] != key2[i]) return false;
}
return true;
}
@override
void initState() {
super.initState();
@@ -867,17 +878,17 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
builder: (context) => SarUpdateSheet(
prePopulatedPosition: position,
allowLocationUpdate: false, // Don't allow changing to current location
onSend: (sarType, position, notes, roomPublicKey, sendToChannel) async {
await _sendSarMessage(sarType, position, notes, roomPublicKey, sendToChannel);
onSend: (emoji, name, position, roomPublicKey, sendToChannel) async {
await _sendSarMessage(emoji, name, position, roomPublicKey, sendToChannel);
},
),
);
}
Future<void> _sendSarMessage(
SarMarkerType sarType,
String emoji,
String name,
Position position,
String? notes,
Uint8List? roomPublicKey,
bool sendToChannel,
) async {
@@ -907,27 +918,50 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
}
try {
// Format: S:<emoji>:<latitude>,<longitude>
final sarMessage = 'S:${sarType.emoji}:${position.latitude},${position.longitude}';
// Add notes if provided
final fullMessage = notes != null && notes.isNotEmpty
? '$sarMessage $notes'
: sarMessage;
// Format: S:<emoji>:<latitude>,<longitude>:<name>
// Round coordinates to 5 decimal places (~1m accuracy) since most GPS is only that accurate
final sarMessage = 'S:$emoji:${position.latitude.toStringAsFixed(5)},${position.longitude.toStringAsFixed(5)}:$name';
if (sendToChannel) {
// Create message ID
final messageId = '${DateTime.now().millisecondsSinceEpoch}_channel_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: sarMessage,
receivedAt: DateTime.now(),
deliveryStatus: MessageDeliveryStatus.sending,
channelIdx: 0,
// SAR marker data is automatically added by SarMessageParser.enhanceMessage in MessagesProvider
);
// Add to messages list with "sending" status
messagesProvider.addSentMessage(sentMessage);
// Send to public channel (ephemeral, over-the-air only)
await connectionProvider.sendChannelMessage(
channelIdx: 0,
text: fullMessage,
text: sarMessage,
messageId: messageId,
);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${sarType.displayName} marker broadcast to public channel'),
const SnackBar(
content: Text('SAR marker broadcast to public channel'),
backgroundColor: Colors.orange,
duration: const Duration(seconds: 2),
duration: Duration(seconds: 2),
),
);
} else {
@@ -939,7 +973,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
final devicePublicKey = connectionProvider.deviceInfo.publicKey;
final senderPublicKeyPrefix = devicePublicKey?.sublist(0, 6);
// Create sent message object
// Create sent message object with recipient public key for retry support
final sentMessage = Message(
id: messageId,
messageType: MessageType.contact,
@@ -947,20 +981,29 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
pathLen: 0,
textType: MessageTextType.plain,
senderTimestamp: timestamp,
text: fullMessage,
text: sarMessage,
receivedAt: DateTime.now(),
deliveryStatus: MessageDeliveryStatus.sending,
recipientPublicKey: roomPublicKey, // Store recipient for retry
// SAR marker data is automatically added by SarMessageParser.enhanceMessage in MessagesProvider
);
// Add to messages list with "sending" status
messagesProvider.addSentMessage(sentMessage);
// Look up the room contact for path logging
final contactsProvider = context.read<ContactsProvider>();
final roomContact = contactsProvider.contacts.where((c) {
return c.publicKey.length >= roomPublicKey!.length &&
_publicKeysMatch(c.publicKey, roomPublicKey);
}).firstOrNull;
// Send SAR message to selected room (persisted and immutable)
final sentSuccessfully = await connectionProvider.sendTextMessage(
contactPublicKey: roomPublicKey!,
text: fullMessage,
text: sarMessage,
messageId: messageId, // Pass message ID so it can be tracked
contact: roomContact, // Include contact for path status logging
);
if (!sentSuccessfully) {
@@ -970,10 +1013,10 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${sarType.displayName} marker sent to room'),
const SnackBar(
content: Text('SAR marker sent to room'),
backgroundColor: Colors.green,
duration: const Duration(seconds: 2),
duration: Duration(seconds: 2),
),
);
}
@@ -991,10 +1034,17 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context); // Required for AutomaticKeepAliveClientMixin
final appProvider = context.watch<AppProvider>();
final isSimpleMode = appProvider.isSimpleMode;
return Consumer3<ContactsProvider, MessagesProvider, DrawingProvider>(
builder: (context, contactsProvider, messagesProvider, drawingProvider, child) {
final contactsWithLocation = contactsProvider.contactsWithLocation;
final sarMarkers = messagesProvider.sarMarkers;
// Filter SAR markers based on visibility toggle
final allSarMarkers = messagesProvider.sarMarkers;
final sarMarkers = drawingProvider.showSarMarkers
? allSarMarkers
: <SarMarker>[];
final center = _calculateCenter(contactsWithLocation, sarMarkers);
return Stack(
@@ -1167,6 +1217,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
DrawingLayer(
drawings: drawingProvider.drawings,
previewDrawing: drawingProvider.getPreviewDrawing(),
isSimpleMode: isSimpleMode,
),
MarkerLayer(
markers: [
@@ -1191,12 +1242,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
context: context,
mapRotation: _getMapRotation(),
onTap: (marker) {
_showDetailedCompassWithSarMarker(
context,
contactsProvider.contactsWithLocation,
messagesProvider.sarMarkers,
marker,
);
// Navigate to the corresponding message in Messages tab
messagesProvider.navigateToMessage(marker.id);
widget.onNavigateToMessages?.call();
},
),
// User location marker
@@ -1284,9 +1332,17 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
DrawingMarkersLayer(
drawings: drawingProvider.drawings,
showDeleteButtons: drawingProvider.isDrawing,
isSimpleMode: isSimpleMode,
onDeleteDrawing: (drawingId) {
drawingProvider.removeDrawing(drawingId);
},
onTapDrawing: (drawing) {
// Navigate to the corresponding message in Messages tab
if (drawing.messageId != null) {
messagesProvider.navigateToMessage(drawing.messageId!);
widget.onNavigateToMessages?.call();
}
},
),
],
),
@@ -1406,6 +1462,21 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
child: const Icon(Icons.layers),
),
const SizedBox(height: 8),
// In simple mode: show fullscreen button directly
// In normal mode: show options menu (which includes fullscreen)
if (context.watch<AppProvider>().isSimpleMode)
FloatingActionButton.small(
heroTag: 'fullscreen_toggle',
onPressed: () {
setState(() {
_isFullscreen = !_isFullscreen;
});
_saveSettings();
widget.onFullscreenChanged?.call(_isFullscreen);
},
child: Icon(_isFullscreen ? Icons.fullscreen_exit : Icons.fullscreen),
)
else
FloatingActionButton.small(
heroTag: 'options_menu',
onPressed: () => _showOptionsMenu(context),

View File

@@ -1,4 +1,3 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
@@ -8,12 +7,14 @@ import '../providers/contacts_provider.dart';
import '../providers/map_provider.dart';
import '../providers/connection_provider.dart';
import '../models/message.dart';
import '../models/contact.dart';
import '../models/sar_marker.dart';
import '../widgets/messages/sar_update_sheet.dart';
import '../widgets/messages/recipient_selector_sheet.dart';
import '../widgets/contacts/direct_message_sheet.dart';
import '../services/message_destination_preferences.dart';
import '../utils/toast_logger.dart';
import '../l10n/app_localizations.dart';
import '../utils/sar_marker_extensions.dart';
import '../utils/message_extensions.dart';
class MessagesTab extends StatefulWidget {
@@ -28,8 +29,14 @@ class MessagesTab extends StatefulWidget {
class _MessagesTabState extends State<MessagesTab> {
final TextEditingController _textController = TextEditingController();
final FocusNode _focusNode = FocusNode();
final ScrollController _scrollController = ScrollController();
int _characterCount = 0;
static const int _maxCharacters = 160;
String? _highlightedMessageId;
// Message destination state
String _destinationType = MessageDestinationPreferences.destinationTypeChannel;
Contact? _selectedRecipient;
/// Helper method to compare two public keys for equality
bool _publicKeysMatch(Uint8List key1, Uint8List key2) {
@@ -44,9 +51,21 @@ class _MessagesTabState extends State<MessagesTab> {
void initState() {
super.initState();
_textController.addListener(_updateCharacterCount);
// Load saved message destination
_loadSavedDestination();
// Mark all messages as read when tab is opened
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<MessagesProvider>().markAllAsRead();
_checkForNavigationRequest();
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Check for navigation request whenever dependencies change
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkForNavigationRequest();
});
}
@@ -54,21 +73,183 @@ class _MessagesTabState extends State<MessagesTab> {
void dispose() {
_textController.dispose();
_focusNode.dispose();
_scrollController.dispose();
super.dispose();
}
void _checkForNavigationRequest() {
final messagesProvider = context.read<MessagesProvider>();
final targetMessageId = messagesProvider.targetMessageId;
if (targetMessageId != null) {
_scrollToMessage(targetMessageId);
messagesProvider.clearMessageNavigation();
}
}
void _scrollToMessage(String messageId) {
final messagesProvider = context.read<MessagesProvider>();
final messages = _getFilteredMessages(messagesProvider);
final messageIndex = messages.indexWhere((m) => m.id == messageId);
if (messageIndex != -1 && _scrollController.hasClients) {
// Calculate position - accounting for reverse list
final itemHeight = 80.0; // Approximate height of a message bubble
final targetOffset = messageIndex * itemHeight;
// Scroll to the message
_scrollController.animateTo(
targetOffset,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
// Highlight the message briefly
setState(() {
_highlightedMessageId = messageId;
});
// Clear highlight after 2 seconds
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
setState(() {
_highlightedMessageId = null;
});
}
});
}
}
void _updateCharacterCount() {
setState(() {
_characterCount = _textController.text.length;
});
}
/// Load saved message destination from preferences
Future<void> _loadSavedDestination() async {
final savedDestination = await MessageDestinationPreferences.getDestination();
if (savedDestination == null || !mounted) {
// Default to public channel
return;
}
final type = savedDestination['type']!;
final publicKey = savedDestination['publicKey'];
setState(() {
_destinationType = type;
});
// If it's a contact or room, try to find it in the contacts list
if (publicKey != null && mounted) {
final contactsProvider = context.read<ContactsProvider>();
final contact = contactsProvider.contacts.where((c) {
return c.publicKeyHex == publicKey;
}).firstOrNull;
if (contact != null) {
setState(() {
_selectedRecipient = contact;
});
} else {
// Contact/room not found, fallback to public channel
debugPrint(
'⚠️ [MessagesTab] Saved recipient not found, falling back to public channel',
);
setState(() {
_destinationType = MessageDestinationPreferences.destinationTypeChannel;
_selectedRecipient = null;
});
await MessageDestinationPreferences.clearDestination();
}
}
}
/// Show recipient selector bottom sheet
void _showRecipientSelector() {
final contactsProvider = context.read<ContactsProvider>();
// Filter contacts by type
final contacts = contactsProvider.contacts
.where((c) => c.type == ContactType.chat)
.toList();
final rooms = contactsProvider.contacts
.where((c) => c.type == ContactType.room)
.toList();
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => RecipientSelectorSheet(
contacts: contacts,
rooms: rooms,
currentDestinationType: _destinationType,
currentRecipientPublicKey: _selectedRecipient?.publicKeyHex,
onSelect: _onRecipientSelected,
),
);
}
/// Handle recipient selection
Future<void> _onRecipientSelected(String type, Contact? recipient) async {
// Get display name before async gap
final recipientName = type == MessageDestinationPreferences.destinationTypeChannel
? AppLocalizations.of(context)!.publicChannel
: (recipient?.displayName ?? recipient?.advName ?? 'Unknown');
setState(() {
_destinationType = type;
_selectedRecipient = recipient;
});
// Save to preferences
await MessageDestinationPreferences.setDestination(
type,
recipientPublicKey: recipient?.publicKeyHex,
);
// Show confirmation toast
if (!mounted) return;
ToastLogger.success(
context,
'Messages will be sent to: $recipientName',
);
}
/// Get icon for current destination type
IconData _getDestinationIcon() {
if (_destinationType == MessageDestinationPreferences.destinationTypeChannel) {
return Icons.public;
} else if (_destinationType == MessageDestinationPreferences.destinationTypeRoom) {
return Icons.meeting_room;
} else {
return Icons.person;
}
}
/// Get tooltip for destination button
String _getDestinationTooltip() {
final l10n = AppLocalizations.of(context)!;
if (_destinationType == MessageDestinationPreferences.destinationTypeChannel) {
return '${l10n.publicChannel} (tap to change)';
} else if (_selectedRecipient != null) {
final recipientName = _selectedRecipient!.displayName ?? _selectedRecipient!.advName;
return '$recipientName (tap to change)';
}
return 'Select recipient';
}
Future<void> _sendMessage() async {
final text = _textController.text.trim();
if (text.isEmpty) return;
final connectionProvider = context.read<ConnectionProvider>();
final messagesProvider = context.read<MessagesProvider>();
final contactsProvider = context.read<ContactsProvider>();
if (!connectionProvider.deviceInfo.isConnected) {
if (!mounted) return;
@@ -77,37 +258,23 @@ class _MessagesTabState extends State<MessagesTab> {
}
try {
// Create message ID
final messageId = '${DateTime.now().millisecondsSinceEpoch}_channel_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: text,
receivedAt: DateTime.now(),
deliveryStatus: MessageDeliveryStatus.sending,
channelIdx: 0,
);
// Add to messages list with "sending" status
messagesProvider.addSentMessage(sentMessage);
// Send to public channel (channel 0)
await connectionProvider.sendChannelMessage(
channelIdx: 0,
text: text,
messageId: messageId,
);
// Check destination type and send accordingly
if (_destinationType == MessageDestinationPreferences.destinationTypeChannel) {
// Send to public channel
await _sendToChannel(text, connectionProvider, messagesProvider);
} else if (_selectedRecipient != null) {
// Send to contact or room
await _sendToRecipient(
text,
connectionProvider,
messagesProvider,
contactsProvider,
);
} else {
// Fallback to public channel if no recipient selected
debugPrint('⚠️ [MessagesTab] No recipient selected, falling back to channel');
await _sendToChannel(text, connectionProvider, messagesProvider);
}
_textController.clear();
_focusNode.unfocus();
@@ -119,17 +286,104 @@ class _MessagesTabState extends State<MessagesTab> {
}
}
/// Send message to public channel
Future<void> _sendToChannel(
String text,
ConnectionProvider connectionProvider,
MessagesProvider messagesProvider,
) async {
// Create message ID
final messageId = '${DateTime.now().millisecondsSinceEpoch}_channel_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: text,
receivedAt: DateTime.now(),
deliveryStatus: MessageDeliveryStatus.sending,
channelIdx: 0,
);
// Add to messages list with "sending" status
messagesProvider.addSentMessage(sentMessage);
// Send to public channel (channel 0)
await connectionProvider.sendChannelMessage(
channelIdx: 0,
text: text,
messageId: messageId,
);
}
/// Send message to contact or room
Future<void> _sendToRecipient(
String text,
ConnectionProvider connectionProvider,
MessagesProvider messagesProvider,
ContactsProvider contactsProvider,
) async {
if (_selectedRecipient == null) return;
// Create message ID
final messageId = '${DateTime.now().millisecondsSinceEpoch}_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 with recipient public key for retry support
final sentMessage = Message(
id: messageId,
messageType: MessageType.contact,
senderPublicKeyPrefix: senderPublicKeyPrefix,
pathLen: 0,
textType: MessageTextType.plain,
senderTimestamp: timestamp,
text: text,
receivedAt: DateTime.now(),
deliveryStatus: MessageDeliveryStatus.sending,
recipientPublicKey: _selectedRecipient!.publicKey,
);
// Add to messages list with "sending" status
messagesProvider.addSentMessage(sentMessage);
// Send message to selected recipient
final sentSuccessfully = await connectionProvider.sendTextMessage(
contactPublicKey: _selectedRecipient!.publicKey,
text: text,
messageId: messageId,
contact: _selectedRecipient,
);
if (!sentSuccessfully) {
// Mark message as failed if sending failed
messagesProvider.markMessageFailed(messageId);
}
}
void _showSarDialog() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => SarUpdateSheet(
onSend: (sarType, position, notes, roomPublicKey, sendToChannel) async {
onSend: (emoji, name, position, roomPublicKey, sendToChannel) async {
await _sendSarMessage(
sarType,
emoji,
name,
position,
notes,
roomPublicKey,
sendToChannel,
);
@@ -139,9 +393,9 @@ class _MessagesTabState extends State<MessagesTab> {
}
Future<void> _sendSarMessage(
SarMarkerType sarType,
String emoji,
String name,
Position position,
String? notes,
Uint8List? roomPublicKey,
bool sendToChannel,
) async {
@@ -161,14 +415,10 @@ class _MessagesTabState extends State<MessagesTab> {
}
try {
// Format: S:<emoji>:<latitude>,<longitude>
// Format: S:<emoji>:<latitude>,<longitude>:<name>
// Round coordinates to 5 decimal places (~1m accuracy) since most GPS is only that accurate
final sarMessage =
'S:${sarType.emoji}:${position.latitude},${position.longitude}';
// Add notes if provided
final fullMessage = notes != null && notes.isNotEmpty
? '$sarMessage $notes'
: sarMessage;
'S:$emoji:${position.latitude.toStringAsFixed(5)},${position.longitude.toStringAsFixed(5)}:$name';
if (sendToChannel) {
// Create message ID
@@ -187,7 +437,7 @@ class _MessagesTabState extends State<MessagesTab> {
pathLen: 0,
textType: MessageTextType.plain,
senderTimestamp: timestamp,
text: fullMessage,
text: sarMessage,
receivedAt: DateTime.now(),
deliveryStatus: MessageDeliveryStatus.sending,
channelIdx: 0,
@@ -200,14 +450,14 @@ class _MessagesTabState extends State<MessagesTab> {
// Send to public channel (ephemeral, over-the-air only)
await connectionProvider.sendChannelMessage(
channelIdx: 0,
text: fullMessage,
text: sarMessage,
messageId: messageId,
);
if (!mounted) return;
ToastLogger.success(
context,
'${sarType.getLocalizedName(context)} marker broadcast to public channel',
'SAR marker broadcast to public channel',
);
} else {
// Create message ID
@@ -226,7 +476,7 @@ class _MessagesTabState extends State<MessagesTab> {
pathLen: 0,
textType: MessageTextType.plain,
senderTimestamp: timestamp,
text: fullMessage,
text: sarMessage,
receivedAt: DateTime.now(),
deliveryStatus: MessageDeliveryStatus.sending,
recipientPublicKey: roomPublicKey, // Store recipient for retry
@@ -246,7 +496,7 @@ class _MessagesTabState extends State<MessagesTab> {
// Send SAR message to selected room (persisted and immutable)
final sentSuccessfully = await connectionProvider.sendTextMessage(
contactPublicKey: roomPublicKey!,
text: fullMessage,
text: sarMessage,
messageId: messageId, // Pass message ID so it can be tracked
contact: roomContact, // Include contact for path status logging
);
@@ -259,7 +509,7 @@ class _MessagesTabState extends State<MessagesTab> {
if (!mounted) return;
ToastLogger.success(
context,
'${sarType.getLocalizedName(context)} marker sent to room',
'SAR marker sent to room',
);
}
} catch (e) {
@@ -359,11 +609,13 @@ class _MessagesTabState extends State<MessagesTab> {
),
)
: ListView.builder(
controller: _scrollController,
reverse: true,
padding: const EdgeInsets.all(8),
itemCount: messages.length,
itemBuilder: (context, index) {
final message = messages[index];
final isHighlighted = message.id == _highlightedMessageId;
// Display system messages with minimal styling
if (message.isSystemMessage) {
@@ -372,6 +624,7 @@ class _MessagesTabState extends State<MessagesTab> {
return _MessageBubble(
message: message,
isHighlighted: isHighlighted,
onTap:
message.isSarMarker &&
message.sarGpsCoordinates != null
@@ -420,7 +673,22 @@ class _MessagesTabState extends State<MessagesTab> {
).colorScheme.onPrimaryContainer,
),
),
const SizedBox(width: 8),
const SizedBox(width: 4),
// Destination switcher button
IconButton(
icon: Icon(_getDestinationIcon()),
tooltip: _getDestinationTooltip(),
onPressed: _showRecipientSelector,
style: IconButton.styleFrom(
backgroundColor: _destinationType == MessageDestinationPreferences.destinationTypeChannel
? Theme.of(context).colorScheme.surfaceContainerHighest
: Theme.of(context).colorScheme.secondaryContainer,
foregroundColor: _destinationType == MessageDestinationPreferences.destinationTypeChannel
? Theme.of(context).colorScheme.onSurface
: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
const SizedBox(width: 4),
// Text field with embedded send button
Expanded(
child: TextField(
@@ -431,7 +699,9 @@ class _MessagesTabState extends State<MessagesTab> {
maxLengthEnforcement: MaxLengthEnforcement.enforced,
style: const TextStyle(fontSize: 14),
decoration: InputDecoration(
hintText: AppLocalizations.of(context)!.typeYourMessage,
hintText: AppLocalizations.of(
context,
)!.typeYourMessage,
hintStyle: const TextStyle(fontSize: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
@@ -448,7 +718,9 @@ class _MessagesTabState extends State<MessagesTab> {
fontSize: 10,
color: _characterCount > _maxCharacters * 0.9
? Colors.orange
: Theme.of(context).textTheme.bodySmall?.color,
: Theme.of(
context,
).textTheme.bodySmall?.color,
),
suffixIcon: IconButton(
icon: Icon(
@@ -481,8 +753,13 @@ class _MessagesTabState extends State<MessagesTab> {
class _MessageBubble extends StatelessWidget {
final Message message;
final VoidCallback? onTap;
final bool isHighlighted;
const _MessageBubble({required this.message, this.onTap});
const _MessageBubble({
required this.message,
this.onTap,
this.isHighlighted = false,
});
/// Helper method to compare two public keys for equality
bool _publicKeysMatch(Uint8List key1, Uint8List key2) {
@@ -604,11 +881,11 @@ class _MessageBubble extends StatelessWidget {
// Copy text option
ListTile(
leading: const Icon(Icons.copy),
title: const Text('Copy text'),
title: Text(AppLocalizations.of(context)!.copyText),
onTap: () {
Clipboard.setData(ClipboardData(text: message.text));
Navigator.pop(context);
ToastLogger.success(context, 'Text copied to clipboard');
ToastLogger.success(context, AppLocalizations.of(context)!.textCopiedToClipboard);
},
),
// Delete message option
@@ -672,8 +949,8 @@ class _MessageBubble extends StatelessWidget {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete message'),
content: const Text('Are you sure you want to delete this message?'),
title: Text(l10n.deleteMessage),
content: Text(l10n.deleteMessageConfirmation),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
@@ -684,10 +961,10 @@ class _MessageBubble extends StatelessWidget {
final messagesProvider = context.read<MessagesProvider>();
messagesProvider.deleteMessage(message.id);
Navigator.pop(context);
ToastLogger.info(context, 'Message deleted');
ToastLogger.info(context, l10n.messageDeleted);
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Delete'),
child: Text(l10n.delete),
),
],
),
@@ -820,39 +1097,55 @@ class _MessageBubble extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: isSarMarker
? _getSarMarkerColor(context, isDarkMode)
: _getMessageBubbleColor(context, isOwnMessage, isDarkMode),
color: isHighlighted
? Theme.of(context).colorScheme.primaryContainer
: isSarMarker
? _getSarMarkerColor(context, isDarkMode)
: _getMessageBubbleColor(context, isOwnMessage, isDarkMode),
borderRadius: BorderRadius.circular(12),
border: isSarMarker
border: isHighlighted
? Border.all(
color: _getSarMarkerBorderColor(context, isDarkMode),
width: 2,
color: Theme.of(context).colorScheme.primary,
width: 3,
)
: isOwnMessage
? Border.all(
color: Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.3),
width: 1.5,
)
: !message.isRead &&
!message.isSentMessage &&
!message.isSystemMessage
? Border.all(color: Colors.blue, width: 1.5)
: null,
boxShadow: isSarMarker
: isSarMarker
? Border.all(
color: _getSarMarkerBorderColor(context, isDarkMode),
width: 2,
)
: isOwnMessage
? Border.all(
color: Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.3),
width: 1.5,
)
: !message.isRead &&
!message.isSentMessage &&
!message.isSystemMessage
? Border.all(color: Colors.blue, width: 1.5)
: null,
boxShadow: isHighlighted
? [
BoxShadow(
color: _getSarMarkerBorderColor(
context,
isDarkMode,
).withValues(alpha: 0.3),
blurRadius: 8,
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5),
blurRadius: 12,
spreadRadius: 2,
offset: const Offset(0, 2),
),
]
: null,
: isSarMarker
? [
BoxShadow(
color: _getSarMarkerBorderColor(
context,
isDarkMode,
).withValues(alpha: 0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: null,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -968,7 +1261,8 @@ class _MessageBubble extends StatelessWidget {
Row(
children: [
Text(
message.sarMarkerType!.emoji,
// Use custom emoji if available (for unknown types), otherwise use type emoji
message.sarCustomEmoji ?? message.sarMarkerType!.emoji,
style: const TextStyle(fontSize: 28),
),
const SizedBox(width: 10),
@@ -977,7 +1271,10 @@ class _MessageBubble extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
message.sarMarkerType!.getLocalizedName(context),
// Show template name (sarNotes) if available, otherwise show localized type name
message.sarNotes != null && message.sarNotes!.isNotEmpty
? message.sarNotes!
: message.sarMarkerType!.getLocalizedName(context),
style: Theme.of(context).textTheme.titleSmall
?.copyWith(fontWeight: FontWeight.bold),
),
@@ -997,23 +1294,6 @@ class _MessageBubble extends StatelessWidget {
),
],
),
// Display SAR notes/message if present
if (message.sarNotes != null && message.sarNotes!.isNotEmpty) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.surfaceVariant.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
),
child: Text(
message.sarNotes!,
style: Theme.of(context).textTheme.bodySmall,
),
),
],
]
// Regular message content
else

View File

@@ -0,0 +1,450 @@
import 'package:flutter/material.dart';
import '../models/sar_template.dart';
import '../services/sar_template_service.dart';
import '../widgets/sar/sar_template_edit_dialog.dart';
import '../l10n/app_localizations.dart';
/// Screen for managing SAR templates
class SarTemplateManagementScreen extends StatefulWidget {
const SarTemplateManagementScreen({super.key});
@override
State<SarTemplateManagementScreen> createState() => _SarTemplateManagementScreenState();
}
class _SarTemplateManagementScreenState extends State<SarTemplateManagementScreen> {
final SarTemplateService _templateService = SarTemplateService();
bool _isLoading = false;
@override
void initState() {
super.initState();
_initializeService();
}
Future<void> _initializeService() async {
if (!_templateService.isInitialized) {
setState(() => _isLoading = true);
await _templateService.initialize();
if (mounted) {
setState(() => _isLoading = false);
}
}
}
Future<void> _addTemplate() async {
await showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => SarTemplateEditDialog(
onSave: (template) async {
await _templateService.addTemplate(template);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.templateAdded),
backgroundColor: Colors.green,
),
);
}
},
),
);
}
Future<void> _editTemplate(SarTemplate template) async {
await showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => SarTemplateEditDialog(
template: template,
onSave: (updatedTemplate) async {
await _templateService.updateTemplate(template.id, updatedTemplate);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.templateUpdated),
backgroundColor: Colors.green,
),
);
}
},
),
);
}
Future<void> _deleteTemplate(SarTemplate template) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context)!.deleteTemplate),
content: Text(
AppLocalizations.of(context)!.deleteTemplateConfirmation(template.name),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(AppLocalizations.of(context)!.cancel),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: Text(AppLocalizations.of(context)!.delete),
),
],
),
);
if (confirmed == true) {
await _templateService.deleteTemplate(template.id);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.templateDeleted),
backgroundColor: Colors.orange,
),
);
}
}
}
Future<void> _importFromClipboard() async {
setState(() => _isLoading = true);
try {
final importedCount = await _templateService.importFromClipboard();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.templatesImported(importedCount)),
backgroundColor: importedCount > 0 ? Colors.green : Colors.orange,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.importFailed(e.toString())),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
Future<void> _exportToClipboard() async {
try {
await _templateService.exportToClipboard();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context)!.templatesExported(_templateService.templateCount),
),
backgroundColor: Colors.green,
action: SnackBarAction(
label: AppLocalizations.of(context)!.ok,
textColor: Colors.white,
onPressed: () {},
),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.exportFailed(e.toString())),
backgroundColor: Colors.red,
),
);
}
}
}
Future<void> _resetToDefaults() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context)!.resetToDefaults),
content: Text(AppLocalizations.of(context)!.resetToDefaultsConfirmation),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(AppLocalizations.of(context)!.cancel),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: Text(AppLocalizations.of(context)!.reset),
),
],
),
);
if (confirmed == true) {
await _templateService.resetToDefaults();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.resetComplete),
backgroundColor: Colors.green,
),
);
}
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.sarTemplates),
actions: [
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
tooltip: 'More options',
onSelected: (value) {
switch (value) {
case 'import':
_importFromClipboard();
break;
case 'export':
_exportToClipboard();
break;
case 'reset':
_resetToDefaults();
break;
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'import',
child: ListTile(
leading: const Icon(Icons.download),
title: Text(l10n.importFromClipboard),
contentPadding: EdgeInsets.zero,
),
),
PopupMenuItem(
value: 'export',
child: ListTile(
leading: const Icon(Icons.upload),
title: Text(l10n.exportToClipboard),
contentPadding: EdgeInsets.zero,
),
),
const PopupMenuDivider(),
PopupMenuItem(
value: 'reset',
child: ListTile(
leading: const Icon(Icons.restart_alt),
title: Text(l10n.resetToDefaults),
contentPadding: EdgeInsets.zero,
),
),
],
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: ListenableBuilder(
listenable: _templateService,
builder: (context, child) {
final templates = _templateService.templates;
if (templates.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.location_searching,
size: 64,
color: colorScheme.onSurface.withValues(alpha: 0.3),
),
const SizedBox(height: 16),
Text(
l10n.noTemplates,
style: theme.textTheme.titleMedium?.copyWith(
color: colorScheme.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(height: 8),
Text(
l10n.tapAddToCreate,
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurface.withValues(alpha: 0.5),
),
),
],
),
);
}
return ListView.builder(
itemCount: templates.length,
itemBuilder: (context, index) {
final template = templates[index];
return _TemplateListItem(
template: template,
onTap: () => _editTemplate(template),
onDelete: () => _deleteTemplate(template),
);
},
);
},
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _addTemplate,
icon: const Icon(Icons.add),
label: Text(l10n.addTemplate),
),
);
}
}
/// Template list item widget
class _TemplateListItem extends StatelessWidget {
final SarTemplate template;
final VoidCallback onTap;
final VoidCallback onDelete;
const _TemplateListItem({
required this.template,
required this.onTap,
required this.onDelete,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Dismissible(
key: Key(template.id),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(Icons.delete, color: Colors.white),
),
confirmDismiss: (direction) async {
// Show confirmation dialog
return await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context)!.deleteTemplate),
content: Text(
AppLocalizations.of(context)!.deleteTemplateConfirmation(template.name),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(AppLocalizations.of(context)!.cancel),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: Text(AppLocalizations.of(context)!.delete),
),
],
),
);
},
onDismissed: (direction) => onDelete(),
child: ListTile(
onTap: onTap,
leading: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: template.color,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: template.color.withValues(alpha: 0.3),
blurRadius: 4,
spreadRadius: 1,
),
],
),
child: Center(
child: Text(
template.emoji,
style: const TextStyle(fontSize: 24),
),
),
),
title: Row(
children: [
Text(
template.name,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
if (template.isDefault) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: Colors.blue.withValues(alpha: 0.5),
),
),
child: Text(
'DEFAULT',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
),
),
),
],
],
),
subtitle: template.description.isNotEmpty
? Text(
template.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurface.withValues(alpha: 0.6),
),
)
: Text(
template.toSarMessage(),
style: TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: colorScheme.onSurface.withValues(alpha: 0.5),
),
),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
color: Colors.red,
onPressed: onDelete,
),
),
);
}
}

View File

@@ -12,6 +12,7 @@ import '../services/locale_preferences.dart';
import '../utils/sample_data_generator.dart';
import '../theme/app_theme.dart';
import '../l10n/app_localizations.dart';
import 'sar_template_management_screen.dart';
class SettingsScreen extends StatefulWidget {
final Function(AppThemeMode) onThemeChanged;
@@ -259,27 +260,27 @@ class _SettingsScreenState extends State<SettingsScreen> {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Row(
title: Row(
children: [
Icon(Icons.settings, size: 24),
SizedBox(width: 12),
Text('Location Permission'),
const Icon(Icons.settings, size: 24),
const SizedBox(width: 12),
Text(AppLocalizations.of(context)!.locationPermission),
],
),
content: const Text(
'Location permission is permanently denied. Please enable it in your device settings to use GPS tracking and location sharing features.',
content: Text(
AppLocalizations.of(context)!.locationPermissionDialogContent,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
child: Text(AppLocalizations.of(context)!.cancel),
),
ElevatedButton(
onPressed: () async {
Navigator.pop(context);
await Geolocator.openAppSettings();
},
child: const Text('Open Settings'),
child: Text(AppLocalizations.of(context)!.openSettings),
),
],
),
@@ -294,8 +295,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
newPermission == LocationPermission.always) {
// Permission granted
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Location permission granted!'),
SnackBar(
content: Text(AppLocalizations.of(context)!.locationPermissionGranted),
backgroundColor: Colors.green,
),
);
@@ -303,10 +304,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
} else {
// Permission denied
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Location permission is required for GPS tracking and location sharing.'),
SnackBar(
content: Text(AppLocalizations.of(context)!.locationPermissionRequiredForGps),
backgroundColor: Colors.orange,
duration: Duration(seconds: 4),
duration: const Duration(seconds: 4),
),
);
}
@@ -314,8 +315,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
// Already granted - show info
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Location permission is already granted.'),
SnackBar(
content: Text(AppLocalizations.of(context)!.locationPermissionAlreadyGranted),
backgroundColor: Colors.blue,
),
);
@@ -433,6 +434,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
await _saveRxTxPreference(value);
},
),
Consumer<AppProvider>(
builder: (context, appProvider, child) => SwitchListTile(
secondary: const Icon(Icons.visibility_off),
title: Text(AppLocalizations.of(context)!.simpleMode),
subtitle: Text(AppLocalizations.of(context)!.simpleModeDescription),
value: appProvider.isSimpleMode,
onChanged: (value) async {
await appProvider.toggleSimpleMode(value);
},
),
),
ListTile(
leading: const Icon(Icons.language),
title: Text(AppLocalizations.of(context)!.language),
@@ -440,18 +452,32 @@ class _SettingsScreenState extends State<SettingsScreen> {
trailing: const Icon(Icons.chevron_right),
onTap: () => _showLanguageDialog(),
),
ListTile(
leading: const Icon(Icons.location_searching),
title: Text(AppLocalizations.of(context)!.sarTemplates),
subtitle: Text(AppLocalizations.of(context)!.manageSarTemplates),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SarTemplateManagementScreen(),
),
);
},
),
const Divider(),
// Permissions Section
_buildSectionHeader('Permissions'),
_buildSectionHeader(AppLocalizations.of(context)!.permissionsSection),
ListTile(
leading: const Icon(Icons.location_on),
title: const Text('Location Permission'),
title: Text(AppLocalizations.of(context)!.locationPermission),
subtitle: FutureBuilder<LocationPermission>(
future: Geolocator.checkPermission(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Text('Checking...');
return Text(AppLocalizations.of(context)!.checking);
}
final permission = snapshot.data!;
String statusText;
@@ -459,23 +485,23 @@ class _SettingsScreenState extends State<SettingsScreen> {
switch (permission) {
case LocationPermission.always:
statusText = 'Granted (Always)';
statusText = AppLocalizations.of(context)!.locationPermissionGrantedAlways;
statusColor = Colors.green;
break;
case LocationPermission.whileInUse:
statusText = 'Granted (While In Use)';
statusText = AppLocalizations.of(context)!.locationPermissionGrantedWhileInUse;
statusColor = Colors.green;
break;
case LocationPermission.denied:
statusText = 'Denied - Tap to request';
statusText = AppLocalizations.of(context)!.locationPermissionDeniedTapToRequest;
statusColor = Colors.orange;
break;
case LocationPermission.deniedForever:
statusText = 'Permanently Denied - Open Settings';
statusText = AppLocalizations.of(context)!.locationPermissionPermanentlyDeniedOpenSettings;
statusColor = Colors.red;
break;
default:
statusText = 'Unknown';
statusText = AppLocalizations.of(context)!.unknown;
statusColor = Colors.grey;
}
@@ -906,7 +932,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
RadioListTile<AppThemeMode>(
title: Row(
children: [
const Text('SAR Navy Blue'),
Text(AppLocalizations.of(context)!.sarNavyBlue),
const SizedBox(width: 8),
Container(
width: 16,
@@ -919,7 +945,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
],
),
subtitle: const Text('Professional/Operations Mode'),
subtitle: Text(AppLocalizations.of(context)!.sarNavyBlueDescription),
value: AppThemeMode.sarNavyBlue,
groupValue: _selectedTheme,
onChanged: (value) {

View File

@@ -184,7 +184,7 @@ class MapMarkerService {
),
padding: const EdgeInsets.all(6),
child: Text(
marker.type.emoji,
marker.emoji, // Use custom emoji if available
style: const TextStyle(fontSize: 18),
),
),
@@ -198,7 +198,7 @@ class MapMarkerService {
borderRadius: BorderRadius.circular(3),
),
child: Text(
marker.type.displayName,
marker.displayName, // Uses notes if available, otherwise type.displayName
style: const TextStyle(
color: Colors.white,
fontSize: 9,

View File

@@ -401,8 +401,8 @@ class MeshCoreBleService {
required double latitude,
required double longitude,
}) async {
// Note: This command does not return an ACK, so we use writeData (fire-and-forget)
await _commandSender.writeData(FrameBuilder.buildSetAdvertLatLon(
// This command returns OK (0x00) response, so wait for acknowledgment
await _commandSender.writeDataAndWaitForAck(FrameBuilder.buildSetAdvertLatLon(
latitude: latitude,
longitude: longitude,
));

View File

@@ -0,0 +1,71 @@
import 'package:shared_preferences/shared_preferences.dart';
/// Service for managing message destination preferences
/// Stores the last selected recipient (channel, contact, or room) for sending messages
class MessageDestinationPreferences {
static const String _destinationTypeKey = 'message_destination_type';
static const String _recipientPublicKeyKey = 'message_recipient_public_key';
/// Destination types
static const String destinationTypeChannel = 'channel';
static const String destinationTypeContact = 'contact';
static const String destinationTypeRoom = 'room';
/// Get the saved destination configuration
/// Returns a map with 'type' and optional 'publicKey'
/// Returns null if no preference is saved (defaults to public channel)
static Future<Map<String, String>?> getDestination() async {
final prefs = await SharedPreferences.getInstance();
final type = prefs.getString(_destinationTypeKey);
if (type == null) {
return null; // Use default (public channel)
}
final publicKey = prefs.getString(_recipientPublicKeyKey);
return {
'type': type,
if (publicKey != null) 'publicKey': publicKey,
};
}
/// Save the selected destination
/// [type] - one of: destinationTypeChannel, destinationTypeContact, destinationTypeRoom
/// [recipientPublicKey] - hex string of recipient's public key (required for contact/room)
static Future<void> setDestination(
String type, {
String? recipientPublicKey,
}) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_destinationTypeKey, type);
if (recipientPublicKey != null) {
await prefs.setString(_recipientPublicKeyKey, recipientPublicKey);
} else {
await prefs.remove(_recipientPublicKeyKey);
}
}
/// Clear the saved destination (resets to default public channel)
static Future<void> clearDestination() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_destinationTypeKey);
await prefs.remove(_recipientPublicKeyKey);
}
/// Get display name for destination type
static String getDestinationTypeName(String type) {
switch (type) {
case destinationTypeChannel:
return 'Channel';
case destinationTypeContact:
return 'Contact';
case destinationTypeRoom:
return 'Room';
default:
return 'Unknown';
}
}
}

View File

@@ -20,6 +20,7 @@ class NotificationService {
// Notification IDs
static const int _sarNotificationId = 1000;
static const int _messageNotificationId = 2000;
// Notification channels
static const String _urgentChannelId = 'sar_urgent';
@@ -27,6 +28,11 @@ class NotificationService {
static const String _urgentChannelDescription =
'Critical alerts for SAR markers (found persons, fires, staging areas)';
static const String _messagesChannelId = 'messages';
static const String _messagesChannelName = 'Messages';
static const String _messagesChannelDescription =
'Notifications for incoming messages from contacts and channels';
/// Initialize notification service
Future<void> initialize() async {
if (_isInitialized) return;
@@ -125,8 +131,20 @@ class NotificationService {
sound: RawResourceAndroidNotificationSound('notification'),
);
// Messages channel with high priority
const messagesChannel = AndroidNotificationChannel(
_messagesChannelId,
_messagesChannelName,
description: _messagesChannelDescription,
importance: Importance.high,
playSound: true,
enableVibration: true,
showBadge: true,
);
await androidPlugin.createNotificationChannel(urgentChannel);
debugPrint('✅ [NotificationService] Created urgent notification channel');
await androidPlugin.createNotificationChannel(messagesChannel);
debugPrint('✅ [NotificationService] Created notification channels');
} catch (e) {
debugPrint('⚠️ [NotificationService] Error creating channels: $e');
}
@@ -303,6 +321,93 @@ class NotificationService {
}
}
/// Show notification for regular message (contact or channel)
Future<void> showMessageNotification({
required String senderName,
required String messageText,
required bool isChannelMessage,
String? channelName,
AppLocalizations? localizations,
}) async {
if (!_isInitialized) {
debugPrint('⚠️ [NotificationService] Not initialized, skipping notification');
return;
}
if (!_permissionGranted) {
debugPrint('⚠️ [NotificationService] Permission not granted, skipping notification');
return;
}
try {
// Generate unique notification ID based on timestamp
final notificationId = _messageNotificationId + (DateTime.now().millisecondsSinceEpoch % 1000);
// Build notification title and body
final title = isChannelMessage
? (localizations != null
? '${localizations.channel}: ${channelName ?? "Public"}'
: 'Channel: ${channelName ?? "Public"}')
: (localizations != null
? '${localizations.newMessage} ${localizations.from} $senderName'
: 'New message from $senderName');
final body = messageText.length > 200
? '${messageText.substring(0, 200)}...'
: messageText;
// Android notification details
final androidDetails = AndroidNotificationDetails(
_messagesChannelId,
_messagesChannelName,
channelDescription: _messagesChannelDescription,
importance: Importance.high,
priority: Priority.high,
ticker: title,
playSound: true,
enableVibration: true,
showWhen: true,
when: DateTime.now().millisecondsSinceEpoch,
styleInformation: BigTextStyleInformation(
body,
contentTitle: title,
summaryText: senderName,
),
);
// iOS notification details
final darwinDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
sound: 'default',
threadIdentifier: isChannelMessage ? 'channel_messages' : 'direct_messages',
subtitle: senderName,
);
// Combined notification details
final notificationDetails = NotificationDetails(
android: androidDetails,
iOS: darwinDetails,
);
// Show notification
await _notificationsPlugin.show(
notificationId,
title,
body,
notificationDetails,
payload: 'message:${isChannelMessage ? "channel" : "contact"}',
);
debugPrint('✅ [NotificationService] Showed message notification');
debugPrint(' Sender: $senderName');
debugPrint(' Type: ${isChannelMessage ? "Channel" : "Direct"}');
} catch (e) {
debugPrint('❌ [NotificationService] Error showing message notification: $e');
}
}
/// Cancel all notifications
Future<void> cancelAll() async {
try {

View File

@@ -0,0 +1,244 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/sar_template.dart';
import '../utils/sar_message_parser.dart';
/// SAR Template Service - Manages SAR templates with persistence
class SarTemplateService extends ChangeNotifier {
static final SarTemplateService _instance = SarTemplateService._internal();
factory SarTemplateService() => _instance;
SarTemplateService._internal();
static const String _storageKey = 'sar_templates';
List<SarTemplate> _templates = [];
bool _initialized = false;
/// Get all templates
List<SarTemplate> get templates => List.unmodifiable(_templates);
/// Get default templates
List<SarTemplate> get defaultTemplates =>
_templates.where((t) => t.isDefault).toList();
/// Get custom templates
List<SarTemplate> get customTemplates =>
_templates.where((t) => !t.isDefault).toList();
/// Check if initialized
bool get isInitialized => _initialized;
/// Initialize service and load templates
Future<void> initialize() async {
if (_initialized) return;
try {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_storageKey);
if (jsonString != null && jsonString.isNotEmpty) {
// Load saved templates
final List<dynamic> jsonList = json.decode(jsonString);
_templates = jsonList.map((json) => SarTemplate.fromJson(json)).toList();
// Ensure defaults exist (in case user deleted them or version upgrade)
_ensureDefaultTemplates();
} else {
// First time - initialize with defaults
_templates = SarTemplate.defaults;
await _saveToStorage();
}
_initialized = true;
notifyListeners();
debugPrint('SarTemplateService initialized with ${_templates.length} templates');
} catch (e) {
debugPrint('Error initializing SAR templates: $e');
// Fallback to defaults on error
_templates = SarTemplate.defaults;
_initialized = true;
notifyListeners();
}
}
/// Ensure default templates exist
void _ensureDefaultTemplates() {
final defaults = SarTemplate.defaults;
final existingDefaultIds = _templates.where((t) => t.isDefault).map((t) => t.id).toSet();
// Add missing defaults
for (final defaultTemplate in defaults) {
if (!existingDefaultIds.contains(defaultTemplate.id)) {
_templates.insert(0, defaultTemplate);
}
}
}
/// Save templates to storage
Future<void> _saveToStorage() async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonList = _templates.map((t) => t.toJson()).toList();
final jsonString = json.encode(jsonList);
await prefs.setString(_storageKey, jsonString);
debugPrint('Saved ${_templates.length} SAR templates to storage');
} catch (e) {
debugPrint('Error saving SAR templates: $e');
rethrow;
}
}
/// Add new template
Future<void> addTemplate(SarTemplate template) async {
_templates.add(template);
await _saveToStorage();
notifyListeners();
debugPrint('Added SAR template: ${template.name}');
}
/// Update existing template
Future<void> updateTemplate(String id, SarTemplate updatedTemplate) async {
final index = _templates.indexWhere((t) => t.id == id);
if (index != -1) {
_templates[index] = updatedTemplate;
await _saveToStorage();
notifyListeners();
debugPrint('Updated SAR template: ${updatedTemplate.name}');
} else {
throw Exception('Template with id $id not found');
}
}
/// Delete template
Future<void> deleteTemplate(String id) async {
final template = _templates.firstWhere((t) => t.id == id);
_templates.removeWhere((t) => t.id == id);
await _saveToStorage();
notifyListeners();
debugPrint('Deleted SAR template: ${template.name}');
}
/// Get template by ID
SarTemplate? getTemplateById(String id) {
try {
return _templates.firstWhere((t) => t.id == id);
} catch (e) {
return null;
}
}
/// Import templates from clipboard
/// Expects SAR message format (one per line):
/// S:🧑:0,0:Person found
/// S:🔥:0,0:Active fire
Future<int> importFromClipboard() async {
try {
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
if (clipboardData == null || clipboardData.text == null || clipboardData.text!.trim().isEmpty) {
throw Exception('Clipboard is empty');
}
return importFromText(clipboardData.text!);
} catch (e) {
debugPrint('Error importing from clipboard: $e');
rethrow;
}
}
/// Import templates from text (SAR message format)
Future<int> importFromText(String text) async {
try {
final lines = text.split('\n').where((line) => line.trim().isNotEmpty).toList();
int importedCount = 0;
final List<String> errors = [];
for (final line in lines) {
final trimmed = line.trim();
if (!trimmed.startsWith('S:')) {
errors.add('Invalid format: $trimmed');
continue;
}
// Validate with parser
if (!SarMessageParser.isValidFormat(trimmed)) {
final error = SarMessageParser.getFormatError(trimmed);
errors.add(error ?? 'Invalid SAR message format');
continue;
}
try {
final template = SarTemplate.fromSarMessage(trimmed);
// Check for duplicates (same emoji + name)
final isDuplicate = _templates.any((t) =>
t.emoji == template.emoji && t.name == template.name
);
if (!isDuplicate) {
_templates.add(template);
importedCount++;
}
} catch (e) {
errors.add('Error parsing line: $trimmed - $e');
}
}
if (importedCount > 0) {
await _saveToStorage();
notifyListeners();
}
if (errors.isNotEmpty) {
debugPrint('Import errors: ${errors.join(', ')}');
}
debugPrint('Imported $importedCount SAR templates');
return importedCount;
} catch (e) {
debugPrint('Error importing templates: $e');
rethrow;
}
}
/// Export all templates to clipboard (SAR message format)
Future<void> exportToClipboard() async {
try {
final sarMessages = _templates.map((t) => t.toSarMessage()).join('\n');
await Clipboard.setData(ClipboardData(text: sarMessages));
debugPrint('Exported ${_templates.length} templates to clipboard');
} catch (e) {
debugPrint('Error exporting to clipboard: $e');
rethrow;
}
}
/// Export templates to text (SAR message format)
String exportToText() {
return _templates.map((t) => t.toSarMessage()).join('\n');
}
/// Reset to default templates
Future<void> resetToDefaults() async {
_templates = SarTemplate.defaults;
await _saveToStorage();
notifyListeners();
debugPrint('Reset to default SAR templates');
}
/// Clear all templates (including defaults)
Future<void> clearAll() async {
_templates.clear();
await _saveToStorage();
notifyListeners();
debugPrint('Cleared all SAR templates');
}
/// Get count of templates
int get templateCount => _templates.length;
/// Check if template exists
bool hasTemplate(String id) {
return _templates.any((t) => t.id == id);
}
}

View File

@@ -1,7 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart';
import 'package:flutter_map_tile_caching/custom_backend_api.dart';
import 'package:vector_map_tiles_mbtiles/vector_map_tiles_mbtiles.dart';
import 'package:mbtiles/mbtiles.dart';
import '../models/map_layer.dart';
@@ -173,6 +172,91 @@ class TileCacheService {
}
}
/// Export the current tile cache store to an archive file
///
/// [outputPath] - Full path where the archive should be saved (e.g., '/path/to/export.fmtc')
///
/// Returns the number of tiles exported
Future<int> exportStore(String outputPath) async {
if (!_isInitialized) {
throw StateError(
'TileCacheService not initialized. Call initialize() first.',
);
}
try {
final external = FMTCRoot.external(pathToArchive: outputPath);
final result = await external.export(storeNames: [_storeName]);
debugPrint('Export completed: $result tiles exported to $outputPath');
return result;
} catch (e) {
debugPrint('Error exporting store: $e');
rethrow;
}
}
/// Import a tile cache store from an archive file
///
/// [filePath] - Path to the .fmtc archive file to import
/// [storeNames] - Optional list of store names to import (null = import all)
/// [strategy] - Conflict resolution strategy (default: merge)
///
/// Returns a map with import statistics (e.g., tile count, stores imported)
Future<Map<String, dynamic>> importStore(
String filePath, {
List<String>? storeNames,
ImportConflictStrategy strategy = ImportConflictStrategy.merge,
}) async {
if (!_isInitialized) {
throw StateError(
'TileCacheService not initialized. Call initialize() first.',
);
}
try {
final external = FMTCRoot.external(pathToArchive: filePath);
final result = external.import(storeNames: storeNames, strategy: strategy);
// Wait for the import to complete and get tile count
final tileCount = await result.complete;
// Wait for store states
final storesToStates = await result.storesToStates;
debugPrint('Import completed: $tileCount tiles imported, ${storesToStates.length} stores');
// Count successful stores (those that weren't skipped)
final successfulCount = storesToStates.values.where((state) => state.name != null).length;
return {
'successfulStores': successfulCount,
'tileCount': tileCount,
'storesToStates': storesToStates,
};
} catch (e) {
debugPrint('Error importing store: $e');
rethrow;
}
}
/// List all stores available in an archive file without importing
///
/// [filePath] - Path to the .fmtc archive file to inspect
///
/// Returns a list of store names contained in the archive
Future<List<String>> listArchiveStores(String filePath) async {
try {
final external = FMTCRoot.external(pathToArchive: filePath);
final stores = await external.listStores;
debugPrint('Archive contains ${stores.length} stores: $stores');
return stores;
} catch (e) {
debugPrint('Error listing archive stores: $e');
rethrow;
}
}
void dispose() {
_isInitialized = false;
}

View File

@@ -12,9 +12,13 @@ class DrawingMessageParser {
}
/// Parse drawing message text into MapDrawing object
/// senderName should be extracted from packet metadata
/// senderName and messageId should be extracted from packet metadata
/// Returns null if parsing fails
static MapDrawing? parseDrawingMessage(String text, {String? senderName}) {
static MapDrawing? parseDrawingMessage(
String text, {
String? senderName,
String? messageId,
}) {
if (!isDrawingMessage(text)) {
return null;
}
@@ -27,8 +31,12 @@ class DrawingMessageParser {
final json = jsonDecode(jsonStr) as Map<String, dynamic>;
// Use ultra-compact network format parser
// Sender name comes from packet metadata, not JSON
return MapDrawing.fromNetworkJson(json, senderName: senderName);
// Sender name and message ID come from packet metadata, not JSON
return MapDrawing.fromNetworkJson(
json,
senderName: senderName,
messageId: messageId,
);
} catch (e) {
return null;
}

View File

@@ -142,7 +142,7 @@ class SampleDataGenerator {
pathLen: 1,
textType: MessageTextType.plain,
senderTimestamp: timestamp.millisecondsSinceEpoch ~/ 1000,
text: 'S:🧑:${lat.toStringAsFixed(4)},${lon.toStringAsFixed(4)}',
text: 'S:🧑:${lat.toStringAsFixed(5)},${lon.toStringAsFixed(5)}',
receivedAt: timestamp,
isSarMarker: true,
sarMarkerType: SarMarkerType.foundPerson,
@@ -171,7 +171,7 @@ class SampleDataGenerator {
pathLen: 1,
textType: MessageTextType.plain,
senderTimestamp: timestamp.millisecondsSinceEpoch ~/ 1000,
text: 'S:🔥:${lat.toStringAsFixed(4)},${lon.toStringAsFixed(4)}',
text: 'S:🔥:${lat.toStringAsFixed(5)},${lon.toStringAsFixed(5)}',
receivedAt: timestamp,
isSarMarker: true,
sarMarkerType: SarMarkerType.fire,
@@ -200,7 +200,7 @@ class SampleDataGenerator {
pathLen: 1,
textType: MessageTextType.plain,
senderTimestamp: timestamp.millisecondsSinceEpoch ~/ 1000,
text: 'S:🏕️:${lat.toStringAsFixed(4)},${lon.toStringAsFixed(4)}',
text: 'S:🏕️:${lat.toStringAsFixed(5)},${lon.toStringAsFixed(5)}',
receivedAt: timestamp,
isSarMarker: true,
sarMarkerType: SarMarkerType.stagingArea,
@@ -236,7 +236,7 @@ class SampleDataGenerator {
pathLen: 1,
textType: MessageTextType.plain,
senderTimestamp: timestamp.millisecondsSinceEpoch ~/ 1000,
text: 'S:📦:${lat.toStringAsFixed(4)},${lon.toStringAsFixed(4)}${notes[i % notes.length]}',
text: 'S:📦:${lat.toStringAsFixed(5)},${lon.toStringAsFixed(5)}${notes[i % notes.length]}',
receivedAt: timestamp,
isSarMarker: true,
sarMarkerType: SarMarkerType.object,
@@ -281,14 +281,14 @@ class SampleDataGenerator {
// Mix regular messages and SAR markers
final emergencyMessages = [
'URGENT: Medical assistance needed at sector 4',
'S:🧑:${center.latitude.toStringAsFixed(4)},${(center.longitude + 0.005).toStringAsFixed(4)} Adult male, conscious',
'S:🧑:${center.latitude.toStringAsFixed(5)},${(center.longitude + 0.005).toStringAsFixed(5)} Adult male, conscious',
'Fire spotted - coordinates incoming',
'S:🔥:${(center.latitude + 0.008).toStringAsFixed(4)},${(center.longitude + 0.003).toStringAsFixed(4)} Spreading rapidly!',
'S:🔥:${(center.latitude + 0.008).toStringAsFixed(5)},${(center.longitude + 0.003).toStringAsFixed(5)} Spreading rapidly!',
'PRIORITY: Need helicopter support',
'Medical team en route to your location',
'Evac helicopter ETA 10 minutes',
'Emergency resolved - all clear',
'S:🏕️:${(center.latitude - 0.002).toStringAsFixed(4)},${(center.longitude - 0.004).toStringAsFixed(4)} Emergency staging area',
'S:🏕️:${(center.latitude - 0.002).toStringAsFixed(5)},${(center.longitude - 0.004).toStringAsFixed(5)} Emergency staging area',
'Emergency services notified and responding',
];

View File

@@ -81,6 +81,9 @@ class SarMessageParser {
sarMarkerType: sarInfo.type,
sarGpsCoordinates: sarInfo.location,
sarNotes: sarInfo.notes, // Extract and store notes
sarCustomEmoji: sarInfo.type == SarMarkerType.unknown
? sarInfo.emoji // Preserve custom emoji for unknown types
: null,
);
}

View File

@@ -8,6 +8,7 @@ import '../../models/room_login_state.dart';
import '../../providers/connection_provider.dart';
import '../../providers/contacts_provider.dart';
import '../../providers/map_provider.dart';
import '../../providers/app_provider.dart';
import 'direct_message_sheet.dart';
import 'room_login_sheet.dart';
import '../../utils/toast_logger.dart';
@@ -18,6 +19,7 @@ class ContactTile extends StatelessWidget {
final Position? currentPosition;
final double Function(double, double, double, double)? calculateDistance;
final String Function(double)? formatDistance;
final VoidCallback? onNavigateToMap;
const ContactTile({
super.key,
@@ -25,10 +27,14 @@ class ContactTile extends StatelessWidget {
this.currentPosition,
this.calculateDistance,
this.formatDistance,
this.onNavigateToMap,
});
@override
Widget build(BuildContext context) {
final appProvider = context.watch<AppProvider>();
final isSimpleMode = appProvider.isSimpleMode;
final hasTelemetry = contact.telemetry != null && contact.telemetry!.isRecent;
final battery = contact.displayBattery;
final location = contact.displayLocation;
@@ -113,8 +119,8 @@ class ContactTile extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
),
// Battery indicator
if (battery != null) ...[
// Battery indicator - hidden in simple mode
if (!isSimpleMode && battery != null) ...[
const SizedBox(width: 4),
Icon(
_getBatteryIcon(battery),
@@ -130,8 +136,8 @@ class ContactTile extends StatelessWidget {
),
),
],
// Connection type indicator (direct/flood) - shown for all contact types
if (contact.type != ContactType.channel) ...[
// Connection type indicator (direct/flood) - hidden in simple mode
if (!isSimpleMode && contact.type != ContactType.channel) ...[
const SizedBox(width: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
@@ -168,129 +174,186 @@ class ContactTile extends StatelessWidget {
],
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
// Room login status badges
if (roomLoginState != null && roomLoginState.isLoggedIn) ...[
Row(
subtitle: isSimpleMode
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (roomLoginState.isAdmin)
Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.red, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.admin_panel_settings, size: 10, color: Colors.red),
const SizedBox(width: 2),
Text(
AppLocalizations.of(context)!.admin,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.red,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
],
),
),
if (roomLoginState.isAdmin) const SizedBox(width: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.green, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
const SizedBox(height: 4),
// Simple mode: Only show location and distance
if (location != null) ...[
Row(
children: [
const Icon(Icons.check_circle, size: 10, color: Colors.green),
const SizedBox(width: 2),
Text(
AppLocalizations.of(context)!.loggedIn,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.green,
fontWeight: FontWeight.bold,
fontSize: 10,
const Icon(Icons.location_on, size: 12, color: Colors.blue),
const SizedBox(width: 4),
Expanded(
child: Text(
'GPS: ${location.latitude.toStringAsFixed(5)}, ${location.longitude.toStringAsFixed(5)}',
style: Theme.of(context).textTheme.labelSmall,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
),
const SizedBox(height: 4),
],
// Last seen + GPS info combined
Row(
children: [
Icon(
Icons.access_time,
size: 12,
color: contact.isRecentlySeen ? Colors.green : Colors.grey,
),
const SizedBox(width: 4),
Text(
contact.timeSinceLastSeen,
style: Theme.of(context).textTheme.labelSmall,
),
if (location != null) ...[
const SizedBox(width: 8),
const Text('', style: TextStyle(color: Colors.grey)),
const SizedBox(width: 8),
if (hasTelemetry)
const Icon(Icons.sensors, size: 12, color: Colors.green)
else
const Icon(Icons.sensors_off, size: 12, color: Colors.grey),
const SizedBox(width: 4),
Expanded(
child: Text(
'GPS: ${location.latitude.toStringAsFixed(4)}, ${location.longitude.toStringAsFixed(4)}',
style: Theme.of(context).textTheme.labelSmall,
overflow: TextOverflow.ellipsis,
if (distanceText != null) ...[
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.straighten, size: 12, color: Colors.blue),
const SizedBox(width: 4),
Text(
'${AppLocalizations.of(context)!.distance}: $distanceText',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.blue,
fontWeight: FontWeight.w500,
),
),
],
),
],
] else
Text(
AppLocalizations.of(context)!.noGpsData,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.grey,
),
),
),
] else ...[
const SizedBox(width: 8),
const Text('', style: TextStyle(color: Colors.grey)),
const SizedBox(width: 8),
const Icon(Icons.sensors_off, size: 12, color: Colors.grey),
const SizedBox(width: 4),
Text(
AppLocalizations.of(context)!.noGpsData,
style: Theme.of(context).textTheme.labelSmall,
),
],
],
),
// Distance info (new row)
if (distanceText != null) ...[
const SizedBox(height: 4),
Row(
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.straighten, size: 12, color: Colors.blue),
const SizedBox(width: 4),
Text(
'${AppLocalizations.of(context)!.distance}: $distanceText',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.blue,
fontWeight: FontWeight.w500,
const SizedBox(height: 4),
// Room login status badges
if (roomLoginState != null && roomLoginState.isLoggedIn) ...[
Row(
children: [
if (roomLoginState.isAdmin)
Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.red, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.admin_panel_settings, size: 10, color: Colors.red),
const SizedBox(width: 2),
Text(
AppLocalizations.of(context)!.admin,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.red,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
],
),
),
if (roomLoginState.isAdmin) const SizedBox(width: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.green, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.check_circle, size: 10, color: Colors.green),
const SizedBox(width: 2),
Text(
AppLocalizations.of(context)!.loggedIn,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.green,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
],
),
),
],
),
const SizedBox(height: 4),
],
// Last seen + GPS info combined
Row(
children: [
Icon(
Icons.access_time,
size: 12,
color: contact.isRecentlySeen ? Colors.green : Colors.grey,
),
const SizedBox(width: 4),
Text(
contact.timeSinceLastSeen,
style: Theme.of(context).textTheme.labelSmall,
),
if (location != null) ...[
const SizedBox(width: 8),
const Text('', style: TextStyle(color: Colors.grey)),
const SizedBox(width: 8),
if (hasTelemetry)
const Icon(Icons.sensors, size: 12, color: Colors.green)
else
const Icon(Icons.sensors_off, size: 12, color: Colors.grey),
const SizedBox(width: 4),
Expanded(
child: Text(
'GPS: ${location.latitude.toStringAsFixed(5)}, ${location.longitude.toStringAsFixed(5)}',
style: Theme.of(context).textTheme.labelSmall,
overflow: TextOverflow.ellipsis,
),
),
] else ...[
const SizedBox(width: 8),
const Text('', style: TextStyle(color: Colors.grey)),
const SizedBox(width: 8),
const Icon(Icons.sensors_off, size: 12, color: Colors.grey),
const SizedBox(width: 4),
Text(
AppLocalizations.of(context)!.noGpsData,
style: Theme.of(context).textTheme.labelSmall,
),
],
],
),
// Distance info (new row)
if (distanceText != null) ...[
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.straighten, size: 12, color: Colors.blue),
const SizedBox(width: 4),
Text(
'${AppLocalizations.of(context)!.distance}: $distanceText',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.blue,
fontWeight: FontWeight.w500,
),
),
],
),
],
],
),
],
],
),
trailing: null,
onTap: () => _showContactDetails(context, contact),
onTap: () {
// In simple mode, tap directly opens message sheet for chat contacts
if (isSimpleMode && contact.type == ContactType.chat) {
_showDirectMessageDialog(context, contact);
} else if (isSimpleMode && contact.type == ContactType.repeater) {
// In simple mode, tapping a repeater jumps to the map
_jumpToMapForRepeater(context, contact);
} else if (isSimpleMode && contact.type == ContactType.room && !contact.isPublicChannel) {
_showRoomLoginDialog(context, contact);
} else {
_showContactDetails(context, contact);
}
},
onLongPress: () async {
final connectionProvider = context.read<ConnectionProvider>();
@@ -360,6 +423,29 @@ class ContactTile extends StatelessWidget {
);
}
void _jumpToMapForRepeater(BuildContext context, Contact contact) {
final location = contact.displayLocation;
if (location != null) {
final mapProvider = context.read<MapProvider>();
// Navigate to map location
mapProvider.navigateToLocation(
location: LatLng(location.latitude, location.longitude),
zoom: 15.0,
animate: true,
);
// Switch to map tab using callback
onNavigateToMap?.call();
} else {
// No location available, just show toast
ToastLogger.info(
context,
'Repeater ${contact.displayName} has no location data',
);
}
}
void _showDeleteConfirmation(BuildContext context, Contact contact) {
showDialog(
context: context,
@@ -587,8 +673,8 @@ class ContactTile extends StatelessWidget {
);
Navigator.pop(context);
// Switch to map tab (assuming it's index 2)
DefaultTabController.of(context).animateTo(2);
// Switch to map tab using callback
onNavigateToMap?.call();
},
icon: const Icon(Icons.map, size: 18),
label: Text(AppLocalizations.of(context)!.viewOnMap),

View File

@@ -1,12 +1,14 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:geolocator/geolocator.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import '../../models/contact.dart';
import '../../models/message.dart';
import '../../providers/connection_provider.dart';
import '../../providers/messages_provider.dart';
import '../../providers/app_provider.dart';
import '../../utils/toast_logger.dart';
import '../../l10n/app_localizations.dart';
@@ -174,6 +176,9 @@ class _DirectMessageSheetState extends State<DirectMessageSheet> {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final appProvider = context.watch<AppProvider>();
final isSimpleMode = appProvider.isSimpleMode;
final contactLocation = widget.contact.displayLocation;
return Container(
height: MediaQuery.of(context).size.height * 0.9,
@@ -222,35 +227,90 @@ class _DirectMessageSheetState extends State<DirectMessageSheet> {
),
),
// Info banner
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(Icons.info_outline, color: Theme.of(context).colorScheme.onPrimaryContainer),
const SizedBox(width: 12),
Expanded(
child: Text(
AppLocalizations.of(context)!.directMessageInfo(widget.contact.displayName),
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
fontSize: 13,
// Mini map in simple mode (scrollable content)
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 16),
if (isSimpleMode && contactLocation != null) ...[
GestureDetector(
onTap: () {
// Hide keyboard when tapping on map
_focusNode.unfocus();
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: colorScheme.outline),
),
clipBehavior: Clip.antiAlias,
child: FlutterMap(
options: MapOptions(
initialCenter: LatLng(
contactLocation.latitude,
contactLocation.longitude,
),
initialZoom: 13.0,
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.pinchZoom | InteractiveFlag.drag,
),
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.meshcore.sar',
),
MarkerLayer(
markers: [
Marker(
point: LatLng(
contactLocation.latitude,
contactLocation.longitude,
),
width: 40,
height: 40,
child: Icon(
Icons.location_on,
color: colorScheme.primary,
size: 40,
),
),
],
),
],
),
),
),
),
),
],
const SizedBox(height: 8),
// Location coordinates
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.gps_fixed, size: 14, color: colorScheme.onSurfaceVariant),
const SizedBox(width: 4),
Text(
'${contactLocation.latitude.toStringAsFixed(5)}, ${contactLocation.longitude.toStringAsFixed(5)}',
style: TextStyle(
color: colorScheme.onSurfaceVariant,
fontSize: 12,
fontFamily: 'monospace',
),
),
],
),
),
const SizedBox(height: 16),
],
],
),
),
),
const SizedBox(height: 16),
const Spacer(),
// Message input
Container(
padding: EdgeInsets.only(
@@ -321,7 +381,7 @@ class _DirectMessageSheetState extends State<DirectMessageSheet> {
OutlinedButton.icon(
onPressed: _insertCurrentLocation,
icon: const Icon(Icons.my_location, size: 18),
label: const Text('Location'),
label: Text(AppLocalizations.of(context)!.myLocation),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
side: BorderSide(color: colorScheme.outline),

View File

@@ -124,7 +124,7 @@ class CompassSarList extends StatelessWidget {
color: markerColor,
size: 24,
),
title: Text(marker.type.displayName),
title: Text(marker.displayName),
subtitle: Text(
'${_bearingToCardinal(bearing)}${_formatDistance(distance)}${marker.timeAgo}',
style: Theme.of(context).textTheme.bodySmall,

View File

@@ -534,7 +534,7 @@ class _DetailedCompassDialogState extends State<DetailedCompassDialog> {
additionalInfo = 'Battery: ${_selectedContact!.telemetry!.batteryPercentage!.round()}%';
}
} else if (_selectedSarMarker != null) {
title = _selectedSarMarker!.type.displayName;
title = _selectedSarMarker!.displayName;
targetLocation = _selectedSarMarker!.location;
additionalInfo = _selectedSarMarker!.timeAgo;
@@ -664,21 +664,21 @@ class _DetailedCompassDialogState extends State<DetailedCompassDialog> {
children: [
_buildLargeInfoCard(
context,
'Distance',
AppLocalizations.of(context)!.distance,
_formatDistance(distance),
Icons.straighten,
color,
),
_buildLargeInfoCard(
context,
'Bearing',
AppLocalizations.of(context)!.bearing,
'${bearing.round()}°',
Icons.navigation,
color,
),
_buildLargeInfoCard(
context,
'Direction',
AppLocalizations.of(context)!.direction,
_bearingToCardinal(bearing),
Icons.explore,
color,

View File

@@ -8,11 +8,13 @@ import '../../l10n/app_localizations.dart';
class DrawingLayer extends StatelessWidget {
final List<MapDrawing> drawings;
final MapDrawing? previewDrawing;
final bool isSimpleMode;
const DrawingLayer({
super.key,
required this.drawings,
this.previewDrawing,
this.isSimpleMode = false,
});
@override
@@ -45,8 +47,9 @@ class DrawingLayer extends StatelessWidget {
opacity = 0.6;
strokeWidth = 4.0;
} else if (drawing.isReceived) {
// Received drawing from another node (thinner, more transparent)
opacity = 0.7;
// Received drawing from another node
// In simple mode: solid (opacity 1.0), in normal mode: translucent (0.7)
opacity = isSimpleMode ? 1.0 : 0.7;
strokeWidth = 3.0;
} else {
// Local drawing (solid line, normal thickness)
@@ -82,13 +85,17 @@ class DrawingLayer extends StatelessWidget {
class DrawingMarkersLayer extends StatelessWidget {
final List<MapDrawing> drawings;
final Function(String drawingId)? onDeleteDrawing;
final Function(MapDrawing drawing)? onTapDrawing;
final bool showDeleteButtons;
final bool isSimpleMode;
const DrawingMarkersLayer({
super.key,
required this.drawings,
this.onDeleteDrawing,
this.onTapDrawing,
this.showDeleteButtons = false,
this.isSimpleMode = false,
});
@override
@@ -134,49 +141,64 @@ class DrawingMarkersLayer extends StatelessWidget {
),
),
);
} else if (drawing.isReceived && drawing.senderName != null) {
// Show sender badge for received drawings (when not in drawing mode)
} else if (drawing.isReceived && drawing.senderName != null && !isSimpleMode) {
// Show sender badge for received drawings (when not in drawing mode and not in simple mode)
// Make it tappable if message ID is available
markers.add(
Marker(
point: centerPoint,
width: 120,
height: 30,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: drawing.color.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white, width: 1.5),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.person,
color: Colors.white,
size: 14,
),
const SizedBox(width: 4),
Flexible(
child: Text(
drawing.senderName!,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
child: GestureDetector(
onTap: drawing.messageId != null && onTapDrawing != null
? () => onTapDrawing!(drawing)
: null,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: drawing.color.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white, width: 1.5),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
),
],
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.person,
color: Colors.white,
size: 14,
),
const SizedBox(width: 4),
Flexible(
child: Text(
drawing.senderName!,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
// Add indicator that this is tappable
if (drawing.messageId != null && onTapDrawing != null) ...[
const SizedBox(width: 4),
const Icon(
Icons.arrow_forward_ios,
color: Colors.white,
size: 10,
),
],
],
),
),
),
),

View File

@@ -201,6 +201,43 @@ class DrawingToolbar extends StatelessWidget {
drawingProvider.setDrawingMode(DrawingMode.rectangle);
},
),
const Divider(),
// Toggle received drawings visibility
SwitchListTile(
secondary: Icon(
drawingProvider.showReceivedDrawings
? Icons.visibility
: Icons.visibility_off,
),
title: Text(AppLocalizations.of(context)!.showReceivedDrawings),
subtitle: Text(
drawingProvider.showReceivedDrawings
? AppLocalizations.of(context)!.showingAllDrawings
: AppLocalizations.of(context)!.showingOnlyYourDrawings,
),
value: drawingProvider.showReceivedDrawings,
onChanged: (value) {
drawingProvider.toggleReceivedDrawings();
},
),
// Toggle SAR markers visibility
SwitchListTile(
secondary: Icon(
drawingProvider.showSarMarkers
? Icons.pin_drop
: Icons.pin_drop_outlined,
),
title: Text(AppLocalizations.of(context)!.showSarMarkers),
subtitle: Text(
drawingProvider.showSarMarkers
? AppLocalizations.of(context)!.showingSarMarkers
: AppLocalizations.of(context)!.hidingSarMarkers,
),
value: drawingProvider.showSarMarkers,
onChanged: (value) {
drawingProvider.toggleSarMarkers();
},
),
if (drawingProvider.drawings.isNotEmpty) ...[
const Divider(),
ListTile(

View File

@@ -158,7 +158,7 @@ class MapMarkers {
),
padding: const EdgeInsets.all(6),
child: Text(
marker.type.emoji,
marker.emoji, // Use custom emoji if available
style: const TextStyle(fontSize: 18),
),
),
@@ -171,16 +171,27 @@ class MapMarkers {
color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.circular(3),
),
child: Text(
marker.type.getLocalizedName(context),
style: const TextStyle(
color: Colors.white,
fontSize: 9,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
textAlign: TextAlign.center,
child: Builder(
builder: (context) {
// Debug: Print what we're actually displaying
debugPrint('🗺️ [MapMarker] Displaying SAR marker:');
debugPrint(' marker.notes: "${marker.notes}"');
debugPrint(' marker.type: ${marker.type}');
debugPrint(' marker.type.displayName: ${marker.type.displayName}');
debugPrint(' marker.displayName: ${marker.displayName}');
return Text(
marker.displayName,
style: const TextStyle(
color: Colors.white,
fontSize: 9,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
textAlign: TextAlign.center,
);
},
),
),
],
@@ -252,9 +263,9 @@ class MapMarkers {
builder: (context) => AlertDialog(
title: Row(
children: [
Text(marker.type.emoji, style: const TextStyle(fontSize: 24)),
Text(marker.emoji, style: const TextStyle(fontSize: 24)), // Use custom emoji if available
const SizedBox(width: 8),
Expanded(child: Text(marker.type.getLocalizedName(context))),
Expanded(child: Text(marker.displayName)),
],
),
content: Column(

View File

@@ -0,0 +1,344 @@
import 'package:flutter/material.dart';
import '../../models/contact.dart';
import '../../l10n/app_localizations.dart';
/// Bottom sheet for selecting message recipient (channel, contact, or room)
class RecipientSelectorSheet extends StatefulWidget {
final List<Contact> contacts;
final List<Contact> rooms;
final String? currentDestinationType;
final String? currentRecipientPublicKey;
final Function(String type, Contact? recipient) onSelect;
const RecipientSelectorSheet({
super.key,
required this.contacts,
required this.rooms,
this.currentDestinationType,
this.currentRecipientPublicKey,
required this.onSelect,
});
@override
State<RecipientSelectorSheet> createState() => _RecipientSelectorSheetState();
}
class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
final TextEditingController _searchController = TextEditingController();
String _searchQuery = '';
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
List<Contact> _filterContacts(List<Contact> contacts) {
if (_searchQuery.isEmpty) return contacts;
final query = _searchQuery.toLowerCase();
return contacts.where((contact) {
final name = contact.displayName?.toLowerCase() ?? contact.advName.toLowerCase();
return name.contains(query);
}).toList();
}
bool _isSelected(String type, Contact? contact) {
if (widget.currentDestinationType != type) return false;
if (type == 'channel') return true;
if (contact == null) return false;
return contact.publicKeyHex == widget.currentRecipientPublicKey;
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final filteredContacts = _filterContacts(widget.contacts);
final filteredRooms = _filterContacts(widget.rooms);
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
),
),
child: Row(
children: [
Text(
l10n.selectRecipient,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const Spacer(),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
tooltip: l10n.close,
),
],
),
),
// Search field
Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: l10n.searchRecipients,
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
setState(() {
_searchQuery = '';
});
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onChanged: (value) {
setState(() {
_searchQuery = value;
});
},
),
),
// Recipients list
Flexible(
child: ListView(
shrinkWrap: true,
children: [
// Public Channel option
_buildRecipientTile(
context: context,
icon: Icons.public,
title: l10n.publicChannel,
subtitle: l10n.broadcastToAllNearby,
isSelected: _isSelected('channel', null),
onTap: () {
widget.onSelect('channel', null);
Navigator.pop(context);
},
),
const Divider(),
// Contacts section
if (widget.contacts.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Text(
l10n.contacts,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
if (filteredContacts.isEmpty)
Padding(
padding: const EdgeInsets.all(16),
child: Text(
l10n.noContactsFound,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).disabledColor,
fontStyle: FontStyle.italic,
),
textAlign: TextAlign.center,
),
)
else
...filteredContacts.map((contact) {
return _buildRecipientTile(
context: context,
icon: Icons.person,
title: contact.displayName ?? contact.advName,
subtitle: contact.publicKeyShort,
emoji: contact.roleEmoji,
isSelected: _isSelected('contact', contact),
onTap: () {
widget.onSelect('contact', contact);
Navigator.pop(context);
},
);
}),
],
const Divider(),
// Rooms section
if (widget.rooms.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Text(
l10n.rooms,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
if (filteredRooms.isEmpty)
Padding(
padding: const EdgeInsets.all(16),
child: Text(
l10n.noRoomsFound,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).disabledColor,
fontStyle: FontStyle.italic,
),
textAlign: TextAlign.center,
),
)
else
...filteredRooms.map((room) {
return _buildRecipientTile(
context: context,
icon: Icons.meeting_room,
title: room.displayName ?? room.advName,
subtitle: room.publicKeyShort,
emoji: room.roleEmoji,
isSelected: _isSelected('room', room),
onTap: () {
widget.onSelect('room', room);
Navigator.pop(context);
},
);
}),
],
// Empty state
if (widget.contacts.isEmpty && widget.rooms.isEmpty) ...[
Padding(
padding: const EdgeInsets.all(32),
child: Column(
children: [
Icon(
Icons.people_outline,
size: 64,
color: Theme.of(context).disabledColor,
),
const SizedBox(height: 16),
Text(
l10n.noContactsOrRoomsAvailable,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).disabledColor,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
l10n.messagesWillBeSentToPublicChannel,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).disabledColor,
),
textAlign: TextAlign.center,
),
],
),
),
],
const SizedBox(height: 16),
],
),
),
],
),
);
}
Widget _buildRecipientTile({
required BuildContext context,
required IconData icon,
required String title,
required String subtitle,
String? emoji,
required bool isSelected,
required VoidCallback onTap,
}) {
return ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(20),
),
child: Icon(
icon,
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
title: Row(
children: [
if (emoji != null && emoji.isNotEmpty) ...[
Text(emoji, style: const TextStyle(fontSize: 16)),
const SizedBox(width: 8),
],
Expanded(
child: Text(
title,
style: TextStyle(
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
),
],
),
subtitle: Text(
subtitle,
style: const TextStyle(
fontSize: 12,
fontFamily: 'monospace',
).copyWith(
color: Theme.of(context).textTheme.bodySmall?.color?.withValues(alpha: 0.6),
),
),
trailing: isSelected
? Icon(
Icons.check_circle,
color: Theme.of(context).colorScheme.primary,
)
: null,
onTap: onTap,
);
}
}

View File

@@ -4,14 +4,15 @@ import 'package:provider/provider.dart';
import 'package:geolocator/geolocator.dart';
import '../../providers/contacts_provider.dart';
import '../../models/contact.dart';
import '../../models/sar_marker.dart';
import '../../models/sar_template.dart';
import '../../services/validation_service.dart';
import '../../services/sar_template_service.dart';
import '../../l10n/app_localizations.dart';
/// SAR Update Sheet - Modal bottom sheet for creating and sending SAR markers
/// This widget is public so it can be used from both messages_tab.dart and map_tab.dart
class SarUpdateSheet extends StatefulWidget {
final Future<void> Function(SarMarkerType, Position, String?, Uint8List?, bool) onSend;
final Future<void> Function(String emoji, String name, Position, Uint8List?, bool) onSend;
final Position? prePopulatedPosition;
final bool allowLocationUpdate;
@@ -27,7 +28,9 @@ class SarUpdateSheet extends StatefulWidget {
}
class _SarUpdateSheetState extends State<SarUpdateSheet> {
SarMarkerType _selectedType = SarMarkerType.foundPerson;
SarTemplate? _selectedTemplate;
List<SarTemplate> _templates = [];
final SarTemplateService _templateService = SarTemplateService();
Position? _currentPosition;
bool _loadingLocation = false;
String? _locationError;
@@ -37,6 +40,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
@override
void initState() {
super.initState();
_initializeTemplates();
// Use pre-populated position if provided, otherwise get current location
if (widget.prePopulatedPosition != null) {
_currentPosition = widget.prePopulatedPosition;
@@ -46,6 +50,21 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
_setDefaultDestination();
}
Future<void> _initializeTemplates() async {
if (!_templateService.isInitialized) {
await _templateService.initialize();
}
if (mounted) {
setState(() {
_templates = _templateService.templates;
// Select first template by default
if (_templates.isNotEmpty) {
_selectedTemplate = _templates.first;
}
});
}
}
void _setDefaultDestination() {
// Set default to first room, or first channel if no rooms exist
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -140,19 +159,23 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
Widget build(BuildContext context) {
// Get keyboard height to adjust padding
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
final bottomSafeArea = MediaQuery.of(context).padding.bottom;
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Container(
height: MediaQuery.of(context).size.height * 0.9,
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
// Header
Container(
return AnimatedPadding(
padding: EdgeInsets.only(bottom: keyboardHeight),
duration: const Duration(milliseconds: 100),
child: Container(
height: MediaQuery.of(context).size.height * 0.9,
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
// Header
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
@@ -196,7 +219,9 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
left: 16,
right: 16,
top: 16,
bottom: keyboardHeight > 0 ? keyboardHeight + 16 : 16,
// Add bottom padding for button area (button + padding + safe area)
// Button height ~48px + container padding 32px + safe area
bottom: 80 + bottomSafeArea,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -211,30 +236,17 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
),
),
const SizedBox(height: 12),
MarkerTypeChip(
type: SarMarkerType.foundPerson,
isSelected: _selectedType == SarMarkerType.foundPerson,
onTap: () => setState(() => _selectedType = SarMarkerType.foundPerson),
),
const SizedBox(height: 8),
MarkerTypeChip(
type: SarMarkerType.fire,
isSelected: _selectedType == SarMarkerType.fire,
onTap: () => setState(() => _selectedType = SarMarkerType.fire),
),
const SizedBox(height: 8),
MarkerTypeChip(
type: SarMarkerType.stagingArea,
isSelected: _selectedType == SarMarkerType.stagingArea,
onTap: () => setState(() => _selectedType = SarMarkerType.stagingArea),
),
const SizedBox(height: 8),
MarkerTypeChip(
type: SarMarkerType.object,
isSelected: _selectedType == SarMarkerType.object,
onTap: () => setState(() => _selectedType = SarMarkerType.object),
),
const SizedBox(height: 24),
..._templates.map((template) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: TemplateChip(
template: template,
isSelected: _selectedTemplate?.id == template.id,
onTap: () => setState(() => _selectedTemplate = template),
),
);
}).toList(),
const SizedBox(height: 16),
// Destination selection (compact dropdown with rooms and channel)
Text(
@@ -595,35 +607,28 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
// Bottom action button
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: SafeArea(
top: false,
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _currentPosition == null || _selectedContact == null
onPressed: _currentPosition == null || _selectedContact == null || _selectedTemplate == null
? null
: () async {
final validator = ValidationService();
// Validate coordinates
final coordResult = validator.validateCoordinates(
_currentPosition!.latitude,
_currentPosition!.longitude,
);
if (!coordResult.isValid) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(coordResult.errorMessage!),
backgroundColor: Colors.red,
),
);
}
return;
}
final notes = _notesController.text.trim();
// Validate notes length if provided
final notes = _notesController.text.trim();
if (notes.isNotEmpty) {
final notesResult = validator.validateName(
notes,
@@ -642,6 +647,23 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
}
}
// Validate coordinates
final coordResult = validator.validateCoordinates(
_currentPosition!.latitude,
_currentPosition!.longitude,
);
if (!coordResult.isValid) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(coordResult.errorMessage!),
backgroundColor: Colors.red,
),
);
}
return;
}
// Validate location accuracy (warn if >50m)
if (_currentPosition!.accuracy != null &&
_currentPosition!.accuracy! > 50.0) {
@@ -669,10 +691,21 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
if (shouldContinue != true) return;
}
// Combine template name with optional notes
String displayText;
if (notes.isNotEmpty) {
// Include both template name and custom notes
displayText = '${_selectedTemplate!.name} - $notes';
} else {
// Just the template name
displayText = _selectedTemplate!.name;
}
// Send SAR marker with emoji and display text
await widget.onSend(
_selectedType,
_selectedTemplate!.emoji,
displayText,
_currentPosition!,
notes.isEmpty ? null : notes,
_selectedContact!.isChannel
? null
: _selectedContact!.publicKey,
@@ -701,43 +734,29 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
),
],
),
),
);
}
}
/// Marker Type Chip widget - Displays a selectable SAR marker type
class MarkerTypeChip extends StatelessWidget {
final SarMarkerType type;
/// Template Chip widget - Displays a selectable SAR template
class TemplateChip extends StatelessWidget {
final SarTemplate template;
final bool isSelected;
final VoidCallback onTap;
const MarkerTypeChip({
const TemplateChip({
super.key,
required this.type,
required this.template,
required this.isSelected,
required this.onTap,
});
Color _getMarkerColor() {
switch (type) {
case SarMarkerType.foundPerson:
return Colors.green;
case SarMarkerType.fire:
return Colors.red;
case SarMarkerType.stagingArea:
return Colors.orange;
case SarMarkerType.object:
return Colors.purple;
case SarMarkerType.unknown:
return Colors.grey;
}
}
@override
Widget build(BuildContext context) {
final color = _getMarkerColor();
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final color = template.color;
return InkWell(
onTap: onTap,
@@ -758,18 +777,35 @@ class MarkerTypeChip extends StatelessWidget {
child: Row(
children: [
Text(
type.emoji,
template.emoji,
style: const TextStyle(fontSize: 32),
),
const SizedBox(width: 16),
Expanded(
child: Text(
type.getLocalizedName(context),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: isSelected ? color : colorScheme.onSurface,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
template.name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: isSelected ? color : colorScheme.onSurface,
),
),
if (template.description.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
template.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurface.withValues(alpha: 0.6),
),
),
],
],
),
),
if (isSelected)

View File

@@ -0,0 +1,321 @@
import 'package:flutter/material.dart';
import '../../models/sar_template.dart';
import '../../l10n/app_localizations.dart';
/// Dialog for adding or editing SAR templates
class SarTemplateEditDialog extends StatefulWidget {
final SarTemplate? template; // Null for new template
final Function(SarTemplate) onSave;
const SarTemplateEditDialog({
super.key,
this.template,
required this.onSave,
});
@override
State<SarTemplateEditDialog> createState() => _SarTemplateEditDialogState();
}
class _SarTemplateEditDialogState extends State<SarTemplateEditDialog> {
late TextEditingController _emojiController;
late TextEditingController _nameController;
late TextEditingController _descriptionController;
late String _selectedColor;
final List<Map<String, dynamic>> _colorOptions = [
{'name': 'Green', 'hex': '#4CAF50'},
{'name': 'Red', 'hex': '#F44336'},
{'name': 'Orange', 'hex': '#FF9800'},
{'name': 'Purple', 'hex': '#9C27B0'},
{'name': 'Blue', 'hex': '#2196F3'},
{'name': 'Yellow', 'hex': '#FFC107'},
{'name': 'Brown', 'hex': '#795548'},
{'name': 'Gray', 'hex': '#9E9E9E'},
];
String? _emojiError;
String? _nameError;
@override
void initState() {
super.initState();
_emojiController = TextEditingController(text: widget.template?.emoji ?? '');
_nameController = TextEditingController(text: widget.template?.name ?? '');
_descriptionController = TextEditingController(text: widget.template?.description ?? '');
_selectedColor = widget.template?.colorHex ?? '#4CAF50';
}
@override
void dispose() {
_emojiController.dispose();
_nameController.dispose();
_descriptionController.dispose();
super.dispose();
}
bool _validate() {
final l10n = AppLocalizations.of(context)!;
setState(() {
_emojiError = null;
_nameError = null;
});
bool isValid = true;
if (_emojiController.text.trim().isEmpty) {
setState(() {
_emojiError = l10n.emojiRequired;
});
isValid = false;
}
if (_nameController.text.trim().isEmpty) {
setState(() {
_nameError = l10n.nameRequired;
});
isValid = false;
}
return isValid;
}
void _save() {
if (!_validate()) return;
final template = SarTemplate(
id: widget.template?.id ?? 'custom_${DateTime.now().millisecondsSinceEpoch}',
emoji: _emojiController.text.trim(),
name: _nameController.text.trim(),
description: _descriptionController.text.trim(),
colorHex: _selectedColor,
isDefault: widget.template?.isDefault ?? false,
);
widget.onSave(template);
Navigator.of(context).pop();
}
String _getPreview() {
final emoji = _emojiController.text.trim();
final description = _descriptionController.text.trim();
if (emoji.isEmpty) return 'S::0,0';
if (description.isEmpty) return 'S:$emoji:0,0';
return 'S:$emoji:0,0:$description';
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final l10n = AppLocalizations.of(context)!;
final bottomPadding = MediaQuery.of(context).viewInsets.bottom;
return Container(
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: DraggableScrollableSheet(
initialChildSize: 0.9,
minChildSize: 0.5,
maxChildSize: 0.95,
expand: false,
builder: (context, scrollController) {
return SingleChildScrollView(
controller: scrollController,
child: Padding(
padding: EdgeInsets.fromLTRB(24, 24, 24, 24 + bottomPadding),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Drag handle
Center(
child: Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(2),
),
),
),
// Header
Text(
widget.template == null ? l10n.addTemplate : l10n.editTemplate,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 24),
// Emoji field
TextField(
controller: _emojiController,
decoration: InputDecoration(
labelText: l10n.templateEmoji,
hintText: '🧑',
errorText: _emojiError,
filled: true,
fillColor: colorScheme.surfaceContainerHighest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
prefixIcon: const Icon(Icons.emoji_emotions),
),
maxLength: 4,
style: const TextStyle(fontSize: 24),
textAlign: TextAlign.center,
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 16),
// Name field
TextField(
controller: _nameController,
decoration: InputDecoration(
labelText: l10n.templateName,
hintText: l10n.templateNameHint,
errorText: _nameError,
filled: true,
fillColor: colorScheme.surfaceContainerHighest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
prefixIcon: const Icon(Icons.label),
),
maxLength: 30,
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 16),
// Description field
TextField(
controller: _descriptionController,
decoration: InputDecoration(
labelText: l10n.templateDescription,
hintText: l10n.templateDescriptionHint,
filled: true,
fillColor: colorScheme.surfaceContainerHighest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
prefixIcon: const Icon(Icons.description),
),
maxLength: 100,
maxLines: 2,
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 16),
// Color picker
Text(
l10n.templateColor,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
children: _colorOptions.map((colorOption) {
final hex = colorOption['hex'] as String;
final color = Color(int.parse('FF${hex.replaceAll('#', '')}', radix: 16));
final isSelected = _selectedColor == hex;
return GestureDetector(
onTap: () => setState(() => _selectedColor = hex),
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: isSelected ? colorScheme.primary : Colors.transparent,
width: 3,
),
boxShadow: [
if (isSelected)
BoxShadow(
color: colorScheme.primary.withValues(alpha: 0.3),
blurRadius: 8,
spreadRadius: 2,
),
],
),
child: isSelected
? const Icon(Icons.check, color: Colors.white)
: null,
),
);
}).toList(),
),
const SizedBox(height: 24),
// Preview
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: colorScheme.outline.withValues(alpha: 0.3),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.previewFormat,
style: theme.textTheme.labelSmall?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
_getPreview(),
style: TextStyle(
fontFamily: 'monospace',
fontSize: 14,
color: colorScheme.onSurface,
),
),
],
),
),
const SizedBox(height: 24),
// Actions
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(l10n.cancel),
),
const SizedBox(width: 12),
ElevatedButton.icon(
onPressed: _save,
icon: const Icon(Icons.save),
label: Text(l10n.save),
),
],
),
],
),
),
);
},
),
);
}
}