mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
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:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
196
lib/models/sar_template.dart
Normal file
196
lib/models/sar_template.dart
Normal 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,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
450
lib/screens/sar_template_management_screen.dart
Normal file
450
lib/screens/sar_template_management_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
));
|
||||
|
||||
71
lib/services/message_destination_preferences.dart
Normal file
71
lib/services/message_destination_preferences.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
244
lib/services/sar_template_service.dart
Normal file
244
lib/services/sar_template_service.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
344
lib/widgets/messages/recipient_selector_sheet.dart
Normal file
344
lib/widgets/messages/recipient_selector_sheet.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
321
lib/widgets/sar/sar_template_edit_dialog.dart
Normal file
321
lib/widgets/sar/sar_template_edit_dialog.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user