mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Add GPX import/export functionality and enhance trail management
- Implemented GPX export and import features in GpxService for location trails. - Added UI controls for exporting and importing trails in the TrailControls widget. - Introduced localization strings for GPX operations in Italian and Slovenian. - Enhanced MapProvider to manage imported trails and toggle visibility for contact trails. - Updated trail color management with TrailColorService for consistent color assignment based on contact roles. - Improved UI for displaying contact trails with toggle options for individual and all contact trails. - Refactored detailed compass dialog to remove unnecessary path toggle button for contacts.
This commit is contained in:
@@ -2315,6 +2315,111 @@
|
|||||||
"description": "Tooltip für Verlaufssteuerung"
|
"description": "Tooltip für Verlaufssteuerung"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"exportTrailToGpx": "Verlauf als GPX exportieren",
|
||||||
|
"@exportTrailToGpx": {
|
||||||
|
"description": "Beschriftung der Schaltfläche zum Exportieren des Verlaufs in eine GPX-Datei"
|
||||||
|
},
|
||||||
|
|
||||||
|
"importTrailFromGpx": "Verlauf aus GPX importieren",
|
||||||
|
"@importTrailFromGpx": {
|
||||||
|
"description": "Beschriftung der Schaltfläche zum Importieren des Verlaufs aus einer GPX-Datei"
|
||||||
|
},
|
||||||
|
|
||||||
|
"trailExportedSuccessfully": "Verlauf erfolgreich exportiert!",
|
||||||
|
"@trailExportedSuccessfully": {
|
||||||
|
"description": "Erfolgsmeldung beim Exportieren des Verlaufs"
|
||||||
|
},
|
||||||
|
|
||||||
|
"failedToExportTrail": "Exportieren des Verlaufs fehlgeschlagen",
|
||||||
|
"@failedToExportTrail": {
|
||||||
|
"description": "Fehlermeldung beim Exportieren des Verlaufs"
|
||||||
|
},
|
||||||
|
|
||||||
|
"failedToImportTrail": "Importieren des Verlaufs fehlgeschlagen: {error}",
|
||||||
|
"@failedToImportTrail": {
|
||||||
|
"description": "Fehlermeldung beim Importieren des Verlaufs",
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"importTrail": "Verlauf importieren",
|
||||||
|
"@importTrail": {
|
||||||
|
"description": "Titel des Dialogs zum Importieren des Verlaufs"
|
||||||
|
},
|
||||||
|
|
||||||
|
"importTrailQuestion": "Verlauf mit {pointCount} Punkten importieren?\n\nSie können Ihren aktuellen Verlauf ersetzen oder daneben anzeigen.",
|
||||||
|
"@importTrailQuestion": {
|
||||||
|
"description": "Inhalt des Bestätigungsdialogs zum Importieren des Verlaufs",
|
||||||
|
"placeholders": {
|
||||||
|
"pointCount": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"viewAlongside": "Daneben anzeigen",
|
||||||
|
"@viewAlongside": {
|
||||||
|
"description": "Schaltfläche zum Importieren des Verlaufs neben dem aktuellen Verlauf"
|
||||||
|
},
|
||||||
|
|
||||||
|
"replaceCurrent": "Aktuellen ersetzen",
|
||||||
|
"@replaceCurrent": {
|
||||||
|
"description": "Schaltfläche zum Ersetzen des aktuellen Verlaufs durch importierten Verlauf"
|
||||||
|
},
|
||||||
|
|
||||||
|
"trailImported": "Verlauf importiert! ({pointCount} Punkte)",
|
||||||
|
"@trailImported": {
|
||||||
|
"description": "Erfolgsmeldung beim Importieren des Verlaufs",
|
||||||
|
"placeholders": {
|
||||||
|
"pointCount": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"trailReplaced": "Verlauf ersetzt! ({pointCount} Punkte)",
|
||||||
|
"@trailReplaced": {
|
||||||
|
"description": "Erfolgsmeldung beim Ersetzen des Verlaufs",
|
||||||
|
"placeholders": {
|
||||||
|
"pointCount": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"contactTrails": "Kontaktverläufe",
|
||||||
|
"@contactTrails": {
|
||||||
|
"description": "Überschrift des Bereichs für Kontaktverläufe"
|
||||||
|
},
|
||||||
|
|
||||||
|
"showAllContactTrails": "Alle Kontaktverläufe anzeigen",
|
||||||
|
"@showAllContactTrails": {
|
||||||
|
"description": "Beschriftung zum Umschalten der Anzeige aller Kontaktverläufe"
|
||||||
|
},
|
||||||
|
|
||||||
|
"noContactsWithLocationHistory": "Keine Kontakte mit Standortverlauf",
|
||||||
|
"@noContactsWithLocationHistory": {
|
||||||
|
"description": "Untertitel, wenn keine Kontakte mit Verläufen vorhanden sind"
|
||||||
|
},
|
||||||
|
|
||||||
|
"showingTrailsForContacts": "Verläufe für {count} Kontakte anzeigen",
|
||||||
|
"@showingTrailsForContacts": {
|
||||||
|
"description": "Untertitel mit Anzahl der Kontakte mit Verläufen",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"individualContactTrails": "Einzelne Kontaktverläufe",
|
||||||
|
"@individualContactTrails": {
|
||||||
|
"description": "Titel des Erweiterungselements für einzelne Kontaktverläufe"
|
||||||
|
},
|
||||||
|
|
||||||
"deviceInformation": "Geräteinformationen",
|
"deviceInformation": "Geräteinformationen",
|
||||||
"@deviceInformation": {
|
"@deviceInformation": {
|
||||||
"description": "Überschrift des Geräteinformationen-Bereichs"
|
"description": "Überschrift des Geräteinformationen-Bereichs"
|
||||||
|
|||||||
@@ -2375,6 +2375,111 @@
|
|||||||
"description": "Trail controls tooltip"
|
"description": "Trail controls tooltip"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"exportTrailToGpx": "Export Trail to GPX",
|
||||||
|
"@exportTrailToGpx": {
|
||||||
|
"description": "Button label to export trail to GPX file"
|
||||||
|
},
|
||||||
|
|
||||||
|
"importTrailFromGpx": "Import Trail from GPX",
|
||||||
|
"@importTrailFromGpx": {
|
||||||
|
"description": "Button label to import trail from GPX file"
|
||||||
|
},
|
||||||
|
|
||||||
|
"trailExportedSuccessfully": "Trail exported successfully!",
|
||||||
|
"@trailExportedSuccessfully": {
|
||||||
|
"description": "Success message when trail is exported"
|
||||||
|
},
|
||||||
|
|
||||||
|
"failedToExportTrail": "Failed to export trail",
|
||||||
|
"@failedToExportTrail": {
|
||||||
|
"description": "Error message when trail export fails"
|
||||||
|
},
|
||||||
|
|
||||||
|
"failedToImportTrail": "Failed to import trail: {error}",
|
||||||
|
"@failedToImportTrail": {
|
||||||
|
"description": "Error message when trail import fails",
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"importTrail": "Import Trail",
|
||||||
|
"@importTrail": {
|
||||||
|
"description": "Import trail dialog title"
|
||||||
|
},
|
||||||
|
|
||||||
|
"importTrailQuestion": "Import trail with {pointCount} points?\n\nYou can replace your current trail or view it alongside.",
|
||||||
|
"@importTrailQuestion": {
|
||||||
|
"description": "Import trail confirmation dialog content",
|
||||||
|
"placeholders": {
|
||||||
|
"pointCount": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"viewAlongside": "View Alongside",
|
||||||
|
"@viewAlongside": {
|
||||||
|
"description": "Button to import trail alongside current trail"
|
||||||
|
},
|
||||||
|
|
||||||
|
"replaceCurrent": "Replace Current",
|
||||||
|
"@replaceCurrent": {
|
||||||
|
"description": "Button to replace current trail with imported trail"
|
||||||
|
},
|
||||||
|
|
||||||
|
"trailImported": "Trail imported! ({pointCount} points)",
|
||||||
|
"@trailImported": {
|
||||||
|
"description": "Success message when trail is imported",
|
||||||
|
"placeholders": {
|
||||||
|
"pointCount": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"trailReplaced": "Trail replaced! ({pointCount} points)",
|
||||||
|
"@trailReplaced": {
|
||||||
|
"description": "Success message when trail is replaced",
|
||||||
|
"placeholders": {
|
||||||
|
"pointCount": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"contactTrails": "Contact Trails",
|
||||||
|
"@contactTrails": {
|
||||||
|
"description": "Contact trails section header"
|
||||||
|
},
|
||||||
|
|
||||||
|
"showAllContactTrails": "Show All Contact Trails",
|
||||||
|
"@showAllContactTrails": {
|
||||||
|
"description": "Toggle label to show all contact trails"
|
||||||
|
},
|
||||||
|
|
||||||
|
"noContactsWithLocationHistory": "No contacts with location history",
|
||||||
|
"@noContactsWithLocationHistory": {
|
||||||
|
"description": "Subtitle when no contacts have trails"
|
||||||
|
},
|
||||||
|
|
||||||
|
"showingTrailsForContacts": "Showing trails for {count} contacts",
|
||||||
|
"@showingTrailsForContacts": {
|
||||||
|
"description": "Subtitle showing number of contacts with trails",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"individualContactTrails": "Individual Contact Trails",
|
||||||
|
"@individualContactTrails": {
|
||||||
|
"description": "Expansion tile title for individual contact trails"
|
||||||
|
},
|
||||||
|
|
||||||
"deviceInformation": "Device Information",
|
"deviceInformation": "Device Information",
|
||||||
"@deviceInformation": {
|
"@deviceInformation": {
|
||||||
"description": "Device information section header"
|
"description": "Device information section header"
|
||||||
|
|||||||
@@ -2310,6 +2310,111 @@
|
|||||||
"description": "Tooltip de controles del rastro"
|
"description": "Tooltip de controles del rastro"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"exportTrailToGpx": "Exportar rastro a GPX",
|
||||||
|
"@exportTrailToGpx": {
|
||||||
|
"description": "Etiqueta del botón para exportar el rastro a archivo GPX"
|
||||||
|
},
|
||||||
|
|
||||||
|
"importTrailFromGpx": "Importar rastro desde GPX",
|
||||||
|
"@importTrailFromGpx": {
|
||||||
|
"description": "Etiqueta del botón para importar el rastro desde archivo GPX"
|
||||||
|
},
|
||||||
|
|
||||||
|
"trailExportedSuccessfully": "¡Rastro exportado exitosamente!",
|
||||||
|
"@trailExportedSuccessfully": {
|
||||||
|
"description": "Mensaje de éxito cuando se exporta el rastro"
|
||||||
|
},
|
||||||
|
|
||||||
|
"failedToExportTrail": "Error al exportar el rastro",
|
||||||
|
"@failedToExportTrail": {
|
||||||
|
"description": "Mensaje de error cuando falla la exportación del rastro"
|
||||||
|
},
|
||||||
|
|
||||||
|
"failedToImportTrail": "Error al importar el rastro: {error}",
|
||||||
|
"@failedToImportTrail": {
|
||||||
|
"description": "Mensaje de error cuando falla la importación del rastro",
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"importTrail": "Importar rastro",
|
||||||
|
"@importTrail": {
|
||||||
|
"description": "Título del diálogo de importación de rastro"
|
||||||
|
},
|
||||||
|
|
||||||
|
"importTrailQuestion": "¿Importar rastro con {pointCount} puntos?\n\nPuede reemplazar su rastro actual o verlo junto a él.",
|
||||||
|
"@importTrailQuestion": {
|
||||||
|
"description": "Contenido del diálogo de confirmación de importación de rastro",
|
||||||
|
"placeholders": {
|
||||||
|
"pointCount": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"viewAlongside": "Ver junto",
|
||||||
|
"@viewAlongside": {
|
||||||
|
"description": "Botón para importar el rastro junto al rastro actual"
|
||||||
|
},
|
||||||
|
|
||||||
|
"replaceCurrent": "Reemplazar actual",
|
||||||
|
"@replaceCurrent": {
|
||||||
|
"description": "Botón para reemplazar el rastro actual con el rastro importado"
|
||||||
|
},
|
||||||
|
|
||||||
|
"trailImported": "¡Rastro importado! ({pointCount} puntos)",
|
||||||
|
"@trailImported": {
|
||||||
|
"description": "Mensaje de éxito cuando se importa el rastro",
|
||||||
|
"placeholders": {
|
||||||
|
"pointCount": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"trailReplaced": "¡Rastro reemplazado! ({pointCount} puntos)",
|
||||||
|
"@trailReplaced": {
|
||||||
|
"description": "Mensaje de éxito cuando se reemplaza el rastro",
|
||||||
|
"placeholders": {
|
||||||
|
"pointCount": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"contactTrails": "Rastros de contactos",
|
||||||
|
"@contactTrails": {
|
||||||
|
"description": "Encabezado de la sección de rastros de contactos"
|
||||||
|
},
|
||||||
|
|
||||||
|
"showAllContactTrails": "Mostrar todos los rastros de contactos",
|
||||||
|
"@showAllContactTrails": {
|
||||||
|
"description": "Etiqueta del interruptor para mostrar todos los rastros de contactos"
|
||||||
|
},
|
||||||
|
|
||||||
|
"noContactsWithLocationHistory": "No hay contactos con historial de ubicación",
|
||||||
|
"@noContactsWithLocationHistory": {
|
||||||
|
"description": "Subtítulo cuando no hay contactos con rastros"
|
||||||
|
},
|
||||||
|
|
||||||
|
"showingTrailsForContacts": "Mostrando rastros para {count} contactos",
|
||||||
|
"@showingTrailsForContacts": {
|
||||||
|
"description": "Subtítulo que muestra el número de contactos con rastros",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"individualContactTrails": "Rastros individuales de contactos",
|
||||||
|
"@individualContactTrails": {
|
||||||
|
"description": "Título del elemento expandible para rastros individuales de contactos"
|
||||||
|
},
|
||||||
|
|
||||||
"deviceInformation": "Información del dispositivo",
|
"deviceInformation": "Información del dispositivo",
|
||||||
"@deviceInformation": {
|
"@deviceInformation": {
|
||||||
"description": "Encabezado de la sección de información del dispositivo"
|
"description": "Encabezado de la sección de información del dispositivo"
|
||||||
|
|||||||
@@ -2315,6 +2315,111 @@
|
|||||||
"description": "Info-bulle des contrôles de la trace"
|
"description": "Info-bulle des contrôles de la trace"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"exportTrailToGpx": "Exporter la trace vers GPX",
|
||||||
|
"@exportTrailToGpx": {
|
||||||
|
"description": "Étiquette du bouton pour exporter la trace vers un fichier GPX"
|
||||||
|
},
|
||||||
|
|
||||||
|
"importTrailFromGpx": "Importer la trace depuis GPX",
|
||||||
|
"@importTrailFromGpx": {
|
||||||
|
"description": "Étiquette du bouton pour importer la trace depuis un fichier GPX"
|
||||||
|
},
|
||||||
|
|
||||||
|
"trailExportedSuccessfully": "Trace exportée avec succès!",
|
||||||
|
"@trailExportedSuccessfully": {
|
||||||
|
"description": "Message de réussite lors de l'exportation de la trace"
|
||||||
|
},
|
||||||
|
|
||||||
|
"failedToExportTrail": "Échec de l'exportation de la trace",
|
||||||
|
"@failedToExportTrail": {
|
||||||
|
"description": "Message d'erreur lors de l'échec de l'exportation de la trace"
|
||||||
|
},
|
||||||
|
|
||||||
|
"failedToImportTrail": "Échec de l'importation de la trace: {error}",
|
||||||
|
"@failedToImportTrail": {
|
||||||
|
"description": "Message d'erreur lors de l'échec de l'importation de la trace",
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"importTrail": "Importer la trace",
|
||||||
|
"@importTrail": {
|
||||||
|
"description": "Titre du dialogue d'importation de trace"
|
||||||
|
},
|
||||||
|
|
||||||
|
"importTrailQuestion": "Importer la trace avec {pointCount} points?\n\nVous pouvez remplacer votre trace actuelle ou l'afficher à côté.",
|
||||||
|
"@importTrailQuestion": {
|
||||||
|
"description": "Contenu du dialogue de confirmation d'importation de trace",
|
||||||
|
"placeholders": {
|
||||||
|
"pointCount": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"viewAlongside": "Afficher à côté",
|
||||||
|
"@viewAlongside": {
|
||||||
|
"description": "Bouton pour importer la trace à côté de la trace actuelle"
|
||||||
|
},
|
||||||
|
|
||||||
|
"replaceCurrent": "Remplacer l'actuel",
|
||||||
|
"@replaceCurrent": {
|
||||||
|
"description": "Bouton pour remplacer la trace actuelle par la trace importée"
|
||||||
|
},
|
||||||
|
|
||||||
|
"trailImported": "Trace importée! ({pointCount} points)",
|
||||||
|
"@trailImported": {
|
||||||
|
"description": "Message de réussite lors de l'importation de la trace",
|
||||||
|
"placeholders": {
|
||||||
|
"pointCount": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"trailReplaced": "Trace remplacée! ({pointCount} points)",
|
||||||
|
"@trailReplaced": {
|
||||||
|
"description": "Message de réussite lors du remplacement de la trace",
|
||||||
|
"placeholders": {
|
||||||
|
"pointCount": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"contactTrails": "Traces des contacts",
|
||||||
|
"@contactTrails": {
|
||||||
|
"description": "En-tête de section des traces des contacts"
|
||||||
|
},
|
||||||
|
|
||||||
|
"showAllContactTrails": "Afficher toutes les traces des contacts",
|
||||||
|
"@showAllContactTrails": {
|
||||||
|
"description": "Étiquette du commutateur pour afficher toutes les traces des contacts"
|
||||||
|
},
|
||||||
|
|
||||||
|
"noContactsWithLocationHistory": "Aucun contact avec historique de localisation",
|
||||||
|
"@noContactsWithLocationHistory": {
|
||||||
|
"description": "Sous-titre lorsqu'il n'y a pas de contacts avec des traces"
|
||||||
|
},
|
||||||
|
|
||||||
|
"showingTrailsForContacts": "Affichage des traces pour {count} contacts",
|
||||||
|
"@showingTrailsForContacts": {
|
||||||
|
"description": "Sous-titre montrant le nombre de contacts avec des traces",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"individualContactTrails": "Traces individuelles des contacts",
|
||||||
|
"@individualContactTrails": {
|
||||||
|
"description": "Titre de l'élément extensible pour les traces individuelles des contacts"
|
||||||
|
},
|
||||||
|
|
||||||
"deviceInformation": "Informations sur l'appareil",
|
"deviceInformation": "Informations sur l'appareil",
|
||||||
"@deviceInformation": {
|
"@deviceInformation": {
|
||||||
"description": "En-tête de section des informations sur l'appareil"
|
"description": "En-tête de section des informations sur l'appareil"
|
||||||
|
|||||||
@@ -787,6 +787,38 @@
|
|||||||
|
|
||||||
"trailControls": "Upravljanje putanjom",
|
"trailControls": "Upravljanje putanjom",
|
||||||
|
|
||||||
|
"exportTrailToGpx": "Izvezi putanju u GPX",
|
||||||
|
|
||||||
|
"importTrailFromGpx": "Uvezi putanju iz GPX",
|
||||||
|
|
||||||
|
"trailExportedSuccessfully": "Putanja uspješno izvezena!",
|
||||||
|
|
||||||
|
"failedToExportTrail": "Izvoz putanje nije uspio",
|
||||||
|
|
||||||
|
"failedToImportTrail": "Uvoz putanje nije uspio: {error}",
|
||||||
|
|
||||||
|
"importTrail": "Uvezi putanju",
|
||||||
|
|
||||||
|
"importTrailQuestion": "Uvezi putanju s {pointCount} točaka?\n\nMožete zamijeniti trenutnu putanju ili je prikazati zajedno.",
|
||||||
|
|
||||||
|
"viewAlongside": "Prikaži zajedno",
|
||||||
|
|
||||||
|
"replaceCurrent": "Zamijeni trenutnu",
|
||||||
|
|
||||||
|
"trailImported": "Putanja uvezena! ({pointCount} točaka)",
|
||||||
|
|
||||||
|
"trailReplaced": "Putanja zamijenjena! ({pointCount} točaka)",
|
||||||
|
|
||||||
|
"contactTrails": "Putanje kontakata",
|
||||||
|
|
||||||
|
"showAllContactTrails": "Prikaži sve putanje kontakata",
|
||||||
|
|
||||||
|
"noContactsWithLocationHistory": "Nema kontakata s poviješću lokacije",
|
||||||
|
|
||||||
|
"showingTrailsForContacts": "Prikazujem putanje za {count} kontakata",
|
||||||
|
|
||||||
|
"individualContactTrails": "Pojedinačne putanje kontakata",
|
||||||
|
|
||||||
"deviceInformation": "Informacije o uređaju",
|
"deviceInformation": "Informacije o uređaju",
|
||||||
|
|
||||||
"bleName": "BLE naziv",
|
"bleName": "BLE naziv",
|
||||||
|
|||||||
@@ -2315,6 +2315,111 @@
|
|||||||
"description": "Tooltip controlli traccia"
|
"description": "Tooltip controlli traccia"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"exportTrailToGpx": "Esporta traccia in GPX",
|
||||||
|
"@exportTrailToGpx": {
|
||||||
|
"description": "Etichetta del pulsante per esportare la traccia in file GPX"
|
||||||
|
},
|
||||||
|
|
||||||
|
"importTrailFromGpx": "Importa traccia da GPX",
|
||||||
|
"@importTrailFromGpx": {
|
||||||
|
"description": "Etichetta del pulsante per importare la traccia da file GPX"
|
||||||
|
},
|
||||||
|
|
||||||
|
"trailExportedSuccessfully": "Traccia esportata con successo!",
|
||||||
|
"@trailExportedSuccessfully": {
|
||||||
|
"description": "Messaggio di successo quando la traccia viene esportata"
|
||||||
|
},
|
||||||
|
|
||||||
|
"failedToExportTrail": "Esportazione traccia fallita",
|
||||||
|
"@failedToExportTrail": {
|
||||||
|
"description": "Messaggio di errore quando l'esportazione della traccia fallisce"
|
||||||
|
},
|
||||||
|
|
||||||
|
"failedToImportTrail": "Importazione traccia fallita: {error}",
|
||||||
|
"@failedToImportTrail": {
|
||||||
|
"description": "Messaggio di errore quando l'importazione della traccia fallisce",
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"importTrail": "Importa traccia",
|
||||||
|
"@importTrail": {
|
||||||
|
"description": "Titolo del dialogo di importazione traccia"
|
||||||
|
},
|
||||||
|
|
||||||
|
"importTrailQuestion": "Importare traccia con {pointCount} punti?\n\nPuoi sostituire la tua traccia attuale o visualizzarla affiancata.",
|
||||||
|
"@importTrailQuestion": {
|
||||||
|
"description": "Contenuto del dialogo di conferma importazione traccia",
|
||||||
|
"placeholders": {
|
||||||
|
"pointCount": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"viewAlongside": "Visualizza affiancata",
|
||||||
|
"@viewAlongside": {
|
||||||
|
"description": "Pulsante per importare la traccia affiancata alla traccia attuale"
|
||||||
|
},
|
||||||
|
|
||||||
|
"replaceCurrent": "Sostituisci attuale",
|
||||||
|
"@replaceCurrent": {
|
||||||
|
"description": "Pulsante per sostituire la traccia attuale con la traccia importata"
|
||||||
|
},
|
||||||
|
|
||||||
|
"trailImported": "Traccia importata! ({pointCount} punti)",
|
||||||
|
"@trailImported": {
|
||||||
|
"description": "Messaggio di successo quando la traccia viene importata",
|
||||||
|
"placeholders": {
|
||||||
|
"pointCount": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"trailReplaced": "Traccia sostituita! ({pointCount} punti)",
|
||||||
|
"@trailReplaced": {
|
||||||
|
"description": "Messaggio di successo quando la traccia viene sostituita",
|
||||||
|
"placeholders": {
|
||||||
|
"pointCount": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"contactTrails": "Tracce contatti",
|
||||||
|
"@contactTrails": {
|
||||||
|
"description": "Intestazione sezione tracce contatti"
|
||||||
|
},
|
||||||
|
|
||||||
|
"showAllContactTrails": "Mostra tutte le tracce dei contatti",
|
||||||
|
"@showAllContactTrails": {
|
||||||
|
"description": "Etichetta dell'interruttore per mostrare tutte le tracce dei contatti"
|
||||||
|
},
|
||||||
|
|
||||||
|
"noContactsWithLocationHistory": "Nessun contatto con cronologia posizione",
|
||||||
|
"@noContactsWithLocationHistory": {
|
||||||
|
"description": "Sottotitolo quando non ci sono contatti con tracce"
|
||||||
|
},
|
||||||
|
|
||||||
|
"showingTrailsForContacts": "Visualizzazione tracce per {count} contatti",
|
||||||
|
"@showingTrailsForContacts": {
|
||||||
|
"description": "Sottotitolo che mostra il numero di contatti con tracce",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"individualContactTrails": "Tracce individuali dei contatti",
|
||||||
|
"@individualContactTrails": {
|
||||||
|
"description": "Titolo dell'elemento espandibile per le tracce individuali dei contatti"
|
||||||
|
},
|
||||||
|
|
||||||
"deviceInformation": "Informazioni Dispositivo",
|
"deviceInformation": "Informazioni Dispositivo",
|
||||||
"@deviceInformation": {
|
"@deviceInformation": {
|
||||||
"description": "Intestazione sezione informazioni dispositivo"
|
"description": "Intestazione sezione informazioni dispositivo"
|
||||||
|
|||||||
@@ -2517,6 +2517,102 @@ abstract class AppLocalizations {
|
|||||||
/// **'Trail Controls'**
|
/// **'Trail Controls'**
|
||||||
String get trailControls;
|
String get trailControls;
|
||||||
|
|
||||||
|
/// Button label to export trail to GPX file
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Export Trail to GPX'**
|
||||||
|
String get exportTrailToGpx;
|
||||||
|
|
||||||
|
/// Button label to import trail from GPX file
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Import Trail from GPX'**
|
||||||
|
String get importTrailFromGpx;
|
||||||
|
|
||||||
|
/// Success message when trail is exported
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Trail exported successfully!'**
|
||||||
|
String get trailExportedSuccessfully;
|
||||||
|
|
||||||
|
/// Error message when trail export fails
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Failed to export trail'**
|
||||||
|
String get failedToExportTrail;
|
||||||
|
|
||||||
|
/// Error message when trail import fails
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Failed to import trail: {error}'**
|
||||||
|
String failedToImportTrail(String error);
|
||||||
|
|
||||||
|
/// Import trail dialog title
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Import Trail'**
|
||||||
|
String get importTrail;
|
||||||
|
|
||||||
|
/// Import trail confirmation dialog content
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Import trail with {pointCount} points?\n\nYou can replace your current trail or view it alongside.'**
|
||||||
|
String importTrailQuestion(int pointCount);
|
||||||
|
|
||||||
|
/// Button to import trail alongside current trail
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'View Alongside'**
|
||||||
|
String get viewAlongside;
|
||||||
|
|
||||||
|
/// Button to replace current trail with imported trail
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Replace Current'**
|
||||||
|
String get replaceCurrent;
|
||||||
|
|
||||||
|
/// Success message when trail is imported
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Trail imported! ({pointCount} points)'**
|
||||||
|
String trailImported(int pointCount);
|
||||||
|
|
||||||
|
/// Success message when trail is replaced
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Trail replaced! ({pointCount} points)'**
|
||||||
|
String trailReplaced(int pointCount);
|
||||||
|
|
||||||
|
/// Contact trails section header
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Contact Trails'**
|
||||||
|
String get contactTrails;
|
||||||
|
|
||||||
|
/// Toggle label to show all contact trails
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Show All Contact Trails'**
|
||||||
|
String get showAllContactTrails;
|
||||||
|
|
||||||
|
/// Subtitle when no contacts have trails
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No contacts with location history'**
|
||||||
|
String get noContactsWithLocationHistory;
|
||||||
|
|
||||||
|
/// Subtitle showing number of contacts with trails
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Showing trails for {count} contacts'**
|
||||||
|
String showingTrailsForContacts(int count);
|
||||||
|
|
||||||
|
/// Expansion tile title for individual contact trails
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Individual Contact Trails'**
|
||||||
|
String get individualContactTrails;
|
||||||
|
|
||||||
/// Device information section header
|
/// Device information section header
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -1402,6 +1402,65 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get trailControls => 'Verlaufssteuerung';
|
String get trailControls => 'Verlaufssteuerung';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get exportTrailToGpx => 'Verlauf als GPX exportieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importTrailFromGpx => 'Verlauf aus GPX importieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get trailExportedSuccessfully => 'Verlauf erfolgreich exportiert!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get failedToExportTrail => 'Exportieren des Verlaufs fehlgeschlagen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String failedToImportTrail(String error) {
|
||||||
|
return 'Importieren des Verlaufs fehlgeschlagen: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importTrail => 'Verlauf importieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String importTrailQuestion(int pointCount) {
|
||||||
|
return 'Verlauf mit $pointCount Punkten importieren?\n\nSie können Ihren aktuellen Verlauf ersetzen oder daneben anzeigen.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewAlongside => 'Daneben anzeigen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get replaceCurrent => 'Aktuellen ersetzen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String trailImported(int pointCount) {
|
||||||
|
return 'Verlauf importiert! ($pointCount Punkte)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String trailReplaced(int pointCount) {
|
||||||
|
return 'Verlauf ersetzt! ($pointCount Punkte)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactTrails => 'Kontaktverläufe';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get showAllContactTrails => 'Alle Kontaktverläufe anzeigen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noContactsWithLocationHistory =>
|
||||||
|
'Keine Kontakte mit Standortverlauf';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String showingTrailsForContacts(int count) {
|
||||||
|
return 'Verläufe für $count Kontakte anzeigen';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get individualContactTrails => 'Einzelne Kontaktverläufe';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get deviceInformation => 'Geräteinformationen';
|
String get deviceInformation => 'Geräteinformationen';
|
||||||
|
|
||||||
|
|||||||
@@ -1390,6 +1390,65 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get trailControls => 'Trail Controls';
|
String get trailControls => 'Trail Controls';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get exportTrailToGpx => 'Export Trail to GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importTrailFromGpx => 'Import Trail from GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get trailExportedSuccessfully => 'Trail exported successfully!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get failedToExportTrail => 'Failed to export trail';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String failedToImportTrail(String error) {
|
||||||
|
return 'Failed to import trail: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importTrail => 'Import Trail';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String importTrailQuestion(int pointCount) {
|
||||||
|
return 'Import trail with $pointCount points?\n\nYou can replace your current trail or view it alongside.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewAlongside => 'View Alongside';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get replaceCurrent => 'Replace Current';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String trailImported(int pointCount) {
|
||||||
|
return 'Trail imported! ($pointCount points)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String trailReplaced(int pointCount) {
|
||||||
|
return 'Trail replaced! ($pointCount points)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactTrails => 'Contact Trails';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get showAllContactTrails => 'Show All Contact Trails';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noContactsWithLocationHistory =>
|
||||||
|
'No contacts with location history';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String showingTrailsForContacts(int count) {
|
||||||
|
return 'Showing trails for $count contacts';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get individualContactTrails => 'Individual Contact Trails';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get deviceInformation => 'Device Information';
|
String get deviceInformation => 'Device Information';
|
||||||
|
|
||||||
|
|||||||
@@ -1401,6 +1401,65 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get trailControls => 'Controles del rastro';
|
String get trailControls => 'Controles del rastro';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get exportTrailToGpx => 'Exportar rastro a GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importTrailFromGpx => 'Importar rastro desde GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get trailExportedSuccessfully => '¡Rastro exportado exitosamente!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get failedToExportTrail => 'Error al exportar el rastro';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String failedToImportTrail(String error) {
|
||||||
|
return 'Error al importar el rastro: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importTrail => 'Importar rastro';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String importTrailQuestion(int pointCount) {
|
||||||
|
return '¿Importar rastro con $pointCount puntos?\n\nPuede reemplazar su rastro actual o verlo junto a él.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewAlongside => 'Ver junto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get replaceCurrent => 'Reemplazar actual';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String trailImported(int pointCount) {
|
||||||
|
return '¡Rastro importado! ($pointCount puntos)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String trailReplaced(int pointCount) {
|
||||||
|
return '¡Rastro reemplazado! ($pointCount puntos)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactTrails => 'Rastros de contactos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get showAllContactTrails => 'Mostrar todos los rastros de contactos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noContactsWithLocationHistory =>
|
||||||
|
'No hay contactos con historial de ubicación';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String showingTrailsForContacts(int count) {
|
||||||
|
return 'Mostrando rastros para $count contactos';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get individualContactTrails => 'Rastros individuales de contactos';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get deviceInformation => 'Información del dispositivo';
|
String get deviceInformation => 'Información del dispositivo';
|
||||||
|
|
||||||
|
|||||||
@@ -1407,6 +1407,65 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get trailControls => 'Contrôles de la trace';
|
String get trailControls => 'Contrôles de la trace';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get exportTrailToGpx => 'Exporter la trace vers GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importTrailFromGpx => 'Importer la trace depuis GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get trailExportedSuccessfully => 'Trace exportée avec succès!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get failedToExportTrail => 'Échec de l\'exportation de la trace';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String failedToImportTrail(String error) {
|
||||||
|
return 'Échec de l\'importation de la trace: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importTrail => 'Importer la trace';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String importTrailQuestion(int pointCount) {
|
||||||
|
return 'Importer la trace avec $pointCount points?\n\nVous pouvez remplacer votre trace actuelle ou l\'afficher à côté.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewAlongside => 'Afficher à côté';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get replaceCurrent => 'Remplacer l\'actuel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String trailImported(int pointCount) {
|
||||||
|
return 'Trace importée! ($pointCount points)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String trailReplaced(int pointCount) {
|
||||||
|
return 'Trace remplacée! ($pointCount points)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactTrails => 'Traces des contacts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get showAllContactTrails => 'Afficher toutes les traces des contacts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noContactsWithLocationHistory =>
|
||||||
|
'Aucun contact avec historique de localisation';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String showingTrailsForContacts(int count) {
|
||||||
|
return 'Affichage des traces pour $count contacts';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get individualContactTrails => 'Traces individuelles des contacts';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get deviceInformation => 'Informations sur l\'appareil';
|
String get deviceInformation => 'Informations sur l\'appareil';
|
||||||
|
|
||||||
|
|||||||
@@ -1390,6 +1390,65 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get trailControls => 'Upravljanje putanjom';
|
String get trailControls => 'Upravljanje putanjom';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get exportTrailToGpx => 'Izvezi putanju u GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importTrailFromGpx => 'Uvezi putanju iz GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get trailExportedSuccessfully => 'Putanja uspješno izvezena!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get failedToExportTrail => 'Izvoz putanje nije uspio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String failedToImportTrail(String error) {
|
||||||
|
return 'Uvoz putanje nije uspio: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importTrail => 'Uvezi putanju';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String importTrailQuestion(int pointCount) {
|
||||||
|
return 'Uvezi putanju s $pointCount točaka?\n\nMožete zamijeniti trenutnu putanju ili je prikazati zajedno.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewAlongside => 'Prikaži zajedno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get replaceCurrent => 'Zamijeni trenutnu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String trailImported(int pointCount) {
|
||||||
|
return 'Putanja uvezena! ($pointCount točaka)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String trailReplaced(int pointCount) {
|
||||||
|
return 'Putanja zamijenjena! ($pointCount točaka)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactTrails => 'Putanje kontakata';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get showAllContactTrails => 'Prikaži sve putanje kontakata';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noContactsWithLocationHistory =>
|
||||||
|
'Nema kontakata s poviješću lokacije';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String showingTrailsForContacts(int count) {
|
||||||
|
return 'Prikazujem putanje za $count kontakata';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get individualContactTrails => 'Pojedinačne putanje kontakata';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get deviceInformation => 'Informacije o uređaju';
|
String get deviceInformation => 'Informacije o uređaju';
|
||||||
|
|
||||||
|
|||||||
@@ -1401,6 +1401,65 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get trailControls => 'Controlli Traccia';
|
String get trailControls => 'Controlli Traccia';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get exportTrailToGpx => 'Esporta traccia in GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importTrailFromGpx => 'Importa traccia da GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get trailExportedSuccessfully => 'Traccia esportata con successo!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get failedToExportTrail => 'Esportazione traccia fallita';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String failedToImportTrail(String error) {
|
||||||
|
return 'Importazione traccia fallita: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importTrail => 'Importa traccia';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String importTrailQuestion(int pointCount) {
|
||||||
|
return 'Importare traccia con $pointCount punti?\n\nPuoi sostituire la tua traccia attuale o visualizzarla affiancata.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewAlongside => 'Visualizza affiancata';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get replaceCurrent => 'Sostituisci attuale';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String trailImported(int pointCount) {
|
||||||
|
return 'Traccia importata! ($pointCount punti)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String trailReplaced(int pointCount) {
|
||||||
|
return 'Traccia sostituita! ($pointCount punti)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactTrails => 'Tracce contatti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get showAllContactTrails => 'Mostra tutte le tracce dei contatti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noContactsWithLocationHistory =>
|
||||||
|
'Nessun contatto con cronologia posizione';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String showingTrailsForContacts(int count) {
|
||||||
|
return 'Visualizzazione tracce per $count contatti';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get individualContactTrails => 'Tracce individuali dei contatti';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get deviceInformation => 'Informazioni Dispositivo';
|
String get deviceInformation => 'Informazioni Dispositivo';
|
||||||
|
|
||||||
|
|||||||
@@ -1393,6 +1393,64 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get trailControls => 'Nadzor poti';
|
String get trailControls => 'Nadzor poti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get exportTrailToGpx => 'Izvozi pot v GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importTrailFromGpx => 'Uvozi pot iz GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get trailExportedSuccessfully => 'Pot uspešno izvožena!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get failedToExportTrail => 'Izvoz poti ni uspel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String failedToImportTrail(String error) {
|
||||||
|
return 'Uvoz poti ni uspel: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importTrail => 'Uvozi pot';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String importTrailQuestion(int pointCount) {
|
||||||
|
return 'Uvozi pot s $pointCount točkami?\n\nLahko zamenjate trenutno pot ali jo prikažete skupaj.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewAlongside => 'Prikaži skupaj';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get replaceCurrent => 'Zamenjaj trenutno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String trailImported(int pointCount) {
|
||||||
|
return 'Pot uvožena! ($pointCount točk)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String trailReplaced(int pointCount) {
|
||||||
|
return 'Pot zamenjana! ($pointCount točk)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactTrails => 'Poti stikov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get showAllContactTrails => 'Prikaži vse poti stikov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noContactsWithLocationHistory => 'Ni stikov z zgodovino lokacije';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String showingTrailsForContacts(int count) {
|
||||||
|
return 'Prikazujem poti za $count stikov';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get individualContactTrails => 'Posamezne poti stikov';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get deviceInformation => 'Informacije o napravi';
|
String get deviceInformation => 'Informacije o napravi';
|
||||||
|
|
||||||
|
|||||||
@@ -787,6 +787,38 @@
|
|||||||
|
|
||||||
"trailControls": "Nadzor poti",
|
"trailControls": "Nadzor poti",
|
||||||
|
|
||||||
|
"exportTrailToGpx": "Izvozi pot v GPX",
|
||||||
|
|
||||||
|
"importTrailFromGpx": "Uvozi pot iz GPX",
|
||||||
|
|
||||||
|
"trailExportedSuccessfully": "Pot uspešno izvožena!",
|
||||||
|
|
||||||
|
"failedToExportTrail": "Izvoz poti ni uspel",
|
||||||
|
|
||||||
|
"failedToImportTrail": "Uvoz poti ni uspel: {error}",
|
||||||
|
|
||||||
|
"importTrail": "Uvozi pot",
|
||||||
|
|
||||||
|
"importTrailQuestion": "Uvozi pot s {pointCount} točkami?\n\nLahko zamenjate trenutno pot ali jo prikažete skupaj.",
|
||||||
|
|
||||||
|
"viewAlongside": "Prikaži skupaj",
|
||||||
|
|
||||||
|
"replaceCurrent": "Zamenjaj trenutno",
|
||||||
|
|
||||||
|
"trailImported": "Pot uvožena! ({pointCount} točk)",
|
||||||
|
|
||||||
|
"trailReplaced": "Pot zamenjana! ({pointCount} točk)",
|
||||||
|
|
||||||
|
"contactTrails": "Poti stikov",
|
||||||
|
|
||||||
|
"showAllContactTrails": "Prikaži vse poti stikov",
|
||||||
|
|
||||||
|
"noContactsWithLocationHistory": "Ni stikov z zgodovino lokacije",
|
||||||
|
|
||||||
|
"showingTrailsForContacts": "Prikazujem poti za {count} stikov",
|
||||||
|
|
||||||
|
"individualContactTrails": "Posamezne poti stikov",
|
||||||
|
|
||||||
"deviceInformation": "Informacije o napravi",
|
"deviceInformation": "Informacije o napravi",
|
||||||
|
|
||||||
"bleName": "BLE ime",
|
"bleName": "BLE ime",
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ class MapProvider with ChangeNotifier {
|
|||||||
bool _showCadastralOverlay = false;
|
bool _showCadastralOverlay = false;
|
||||||
bool _showForestRoadsOverlay = false;
|
bool _showForestRoadsOverlay = false;
|
||||||
|
|
||||||
|
// Contact trail toggles
|
||||||
|
bool _showAllContactTrails = false;
|
||||||
|
|
||||||
|
// Imported trail (from GPX)
|
||||||
|
LocationTrail? _importedTrail;
|
||||||
|
|
||||||
LatLng? get targetLocation => _targetLocation;
|
LatLng? get targetLocation => _targetLocation;
|
||||||
double? get targetZoom => _targetZoom;
|
double? get targetZoom => _targetZoom;
|
||||||
bool get shouldAnimate => _shouldAnimate;
|
bool get shouldAnimate => _shouldAnimate;
|
||||||
@@ -36,6 +42,12 @@ class MapProvider with ChangeNotifier {
|
|||||||
bool get showCadastralOverlay => _showCadastralOverlay;
|
bool get showCadastralOverlay => _showCadastralOverlay;
|
||||||
bool get showForestRoadsOverlay => _showForestRoadsOverlay;
|
bool get showForestRoadsOverlay => _showForestRoadsOverlay;
|
||||||
|
|
||||||
|
// Contact trail getters
|
||||||
|
bool get showAllContactTrails => _showAllContactTrails;
|
||||||
|
|
||||||
|
// Imported trail getters
|
||||||
|
LocationTrail? get importedTrail => _importedTrail;
|
||||||
|
|
||||||
void navigateToLocation({
|
void navigateToLocation({
|
||||||
required LatLng location,
|
required LatLng location,
|
||||||
double zoom = 15.0,
|
double zoom = 15.0,
|
||||||
@@ -245,4 +257,49 @@ class MapProvider with ChangeNotifier {
|
|||||||
await prefs.setBool('map_show_cadastral_overlay', _showCadastralOverlay);
|
await prefs.setBool('map_show_cadastral_overlay', _showCadastralOverlay);
|
||||||
await prefs.setBool('map_show_forest_roads_overlay', _showForestRoadsOverlay);
|
await prefs.setBool('map_show_forest_roads_overlay', _showForestRoadsOverlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toggle all contact trails on/off
|
||||||
|
Future<void> toggleAllContactTrails() async {
|
||||||
|
_showAllContactTrails = !_showAllContactTrails;
|
||||||
|
notifyListeners();
|
||||||
|
await _saveTrailSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load trail settings from SharedPreferences
|
||||||
|
Future<void> loadTrailSettings() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
_showAllContactTrails = prefs.getBool('map_show_all_contact_trails') ?? false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save trail settings to SharedPreferences
|
||||||
|
Future<void> _saveTrailSettings() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setBool('map_show_all_contact_trails', _showAllContactTrails);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set imported trail (from GPX import)
|
||||||
|
void setImportedTrail(LocationTrail trail) {
|
||||||
|
_importedTrail = trail;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear imported trail
|
||||||
|
void clearImportedTrail() {
|
||||||
|
_importedTrail = null;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace current trail with imported trail
|
||||||
|
void replaceCurrentTrailWithImport(LocationTrail importedTrail) {
|
||||||
|
// End current trail if active
|
||||||
|
if (_currentTrail != null && _currentTrail!.isActive) {
|
||||||
|
endTrail();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set imported trail as current trail
|
||||||
|
_currentTrail = importedTrail;
|
||||||
|
_isTrailVisible = true;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import '../services/background_location_service.dart';
|
|||||||
import '../services/location_tracking_service.dart';
|
import '../services/location_tracking_service.dart';
|
||||||
import '../services/map_marker_service.dart';
|
import '../services/map_marker_service.dart';
|
||||||
import '../services/mbtiles_service.dart';
|
import '../services/mbtiles_service.dart';
|
||||||
|
import '../services/trail_color_service.dart';
|
||||||
import '../widgets/map_debug_info.dart';
|
import '../widgets/map_debug_info.dart';
|
||||||
import '../widgets/map/map_legend.dart';
|
import '../widgets/map/map_legend.dart';
|
||||||
import '../widgets/map/compass_widget.dart';
|
import '../widgets/map/compass_widget.dart';
|
||||||
@@ -1441,22 +1442,55 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
// Advertisement path polylines (rendered before markers)
|
// Imported trail layer (rendered at bottom for reference)
|
||||||
Consumer<MapProvider>(
|
Consumer<MapProvider>(
|
||||||
builder: (context, mapProvider, _) {
|
builder: (context, mapProvider, _) {
|
||||||
|
if (mapProvider.importedTrail == null ||
|
||||||
|
mapProvider.importedTrail!.points.length < 2) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
return PolylineLayer(
|
return PolylineLayer(
|
||||||
polylines: contactsWithLocation
|
polylines: [
|
||||||
.where((contact) => mapProvider.isContactPathVisible(contact.publicKeyHex))
|
Polyline(
|
||||||
|
points: mapProvider.importedTrail!.latLngPoints,
|
||||||
|
color: Colors.green.withValues(alpha: 0.7),
|
||||||
|
strokeWidth: 3.0,
|
||||||
|
borderColor: Colors.white.withValues(alpha: 0.4),
|
||||||
|
borderStrokeWidth: 1.0,
|
||||||
|
// DOTTED pattern to distinguish from other trails
|
||||||
|
pattern: StrokePattern.dotted(spacingFactor: 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Contact trail polylines (rendered before user trail and markers)
|
||||||
|
Consumer<MapProvider>(
|
||||||
|
builder: (context, mapProvider, _) {
|
||||||
|
// Determine which contacts to show trails for
|
||||||
|
final contactsToShow = mapProvider.showAllContactTrails
|
||||||
|
? contactsWithLocation // Show all when master toggle is ON
|
||||||
|
: contactsWithLocation.where((contact) =>
|
||||||
|
mapProvider.isContactPathVisible(contact.publicKeyHex)); // Individual toggles
|
||||||
|
|
||||||
|
return PolylineLayer(
|
||||||
|
polylines: contactsToShow
|
||||||
.where((contact) => contact.advertHistory.length >= 2)
|
.where((contact) => contact.advertHistory.length >= 2)
|
||||||
.map((contact) {
|
.map((contact) {
|
||||||
|
// Use TrailColorService for consistent, emoji-based colors
|
||||||
|
final color = TrailColorService.getTrailColor(contact);
|
||||||
|
|
||||||
return Polyline(
|
return Polyline(
|
||||||
points: contact.advertHistory
|
points: contact.advertHistory
|
||||||
.map((advert) => advert.location)
|
.map((advert) => advert.location)
|
||||||
.toList(),
|
.toList(),
|
||||||
color: Colors.blue.withValues(alpha: 0.7),
|
color: color,
|
||||||
strokeWidth: 3.0,
|
strokeWidth: 2.5, // Thinner than user trail
|
||||||
borderColor: Colors.white.withValues(alpha: 0.5),
|
borderColor: Colors.white.withValues(alpha: 0.3),
|
||||||
borderStrokeWidth: 1.0,
|
borderStrokeWidth: 1.0,
|
||||||
|
// DASHED pattern to distinguish from solid user trail
|
||||||
|
pattern: StrokePattern.dashed(segments: [8, 4]),
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.toList(),
|
.toList(),
|
||||||
|
|||||||
339
lib/services/gpx_service.dart
Normal file
339
lib/services/gpx_service.dart
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
import 'package:xml/xml.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import '../models/location_trail.dart';
|
||||||
|
|
||||||
|
/// Service for importing and exporting location trails in GPX format
|
||||||
|
class GpxService {
|
||||||
|
/// Export a LocationTrail to GPX 1.1 format
|
||||||
|
/// Returns the GPX content as a string
|
||||||
|
static String exportToGpx(LocationTrail trail, {String? customName}) {
|
||||||
|
final builder = XmlBuilder();
|
||||||
|
|
||||||
|
builder.processing('xml', 'version="1.0" encoding="UTF-8"');
|
||||||
|
builder.element('gpx', nest: () {
|
||||||
|
// GPX attributes
|
||||||
|
builder.attribute('version', '1.1');
|
||||||
|
builder.attribute('creator', 'MeshCore SAR');
|
||||||
|
builder.attribute(
|
||||||
|
'xmlns',
|
||||||
|
'http://www.topografix.com/GPX/1/1',
|
||||||
|
);
|
||||||
|
builder.attribute(
|
||||||
|
'xmlns:xsi',
|
||||||
|
'http://www.w3.org/2001/XMLSchema-instance',
|
||||||
|
);
|
||||||
|
builder.attribute(
|
||||||
|
'xsi:schemaLocation',
|
||||||
|
'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Metadata section
|
||||||
|
builder.element('metadata', nest: () {
|
||||||
|
final name = customName ??
|
||||||
|
'MeshCore Trail - ${_formatDateTime(trail.startTime)}';
|
||||||
|
builder.element('name', nest: () => builder.text(name));
|
||||||
|
builder.element(
|
||||||
|
'time',
|
||||||
|
nest: () => builder.text(trail.startTime.toIso8601String()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add trail statistics in description
|
||||||
|
final distance = trail.totalDistance;
|
||||||
|
final duration = trail.duration;
|
||||||
|
final description =
|
||||||
|
'Distance: ${_formatDistance(distance)}, '
|
||||||
|
'Duration: ${_formatDuration(duration)}, '
|
||||||
|
'Points: ${trail.points.length}';
|
||||||
|
builder.element('desc', nest: () => builder.text(description));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track section
|
||||||
|
builder.element('trk', nest: () {
|
||||||
|
final trackName = customName ?? 'MeshCore Trail';
|
||||||
|
builder.element('name', nest: () => builder.text(trackName));
|
||||||
|
|
||||||
|
// Track segment with all points
|
||||||
|
builder.element('trkseg', nest: () {
|
||||||
|
for (final point in trail.points) {
|
||||||
|
builder.element('trkpt', nest: () {
|
||||||
|
builder.attribute('lat', point.position.latitude.toString());
|
||||||
|
builder.attribute('lon', point.position.longitude.toString());
|
||||||
|
|
||||||
|
// Timestamp (required for proper GPX)
|
||||||
|
builder.element(
|
||||||
|
'time',
|
||||||
|
nest: () => builder.text(point.timestamp.toIso8601String()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Elevation (optional, set to 0 if not available)
|
||||||
|
builder.element('ele', nest: () => builder.text('0'));
|
||||||
|
|
||||||
|
// Extensions for additional data (accuracy, speed)
|
||||||
|
if (point.accuracy != null || point.speed != null) {
|
||||||
|
builder.element('extensions', nest: () {
|
||||||
|
if (point.accuracy != null) {
|
||||||
|
builder.element(
|
||||||
|
'accuracy',
|
||||||
|
nest: () => builder.text(point.accuracy.toString()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (point.speed != null) {
|
||||||
|
builder.element(
|
||||||
|
'speed',
|
||||||
|
nest: () => builder.text(point.speed.toString()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
final document = builder.buildDocument();
|
||||||
|
return document.toXmlString(pretty: true, indent: ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse GPX content and return a LocationTrail
|
||||||
|
/// Throws FormatException if GPX is invalid
|
||||||
|
static LocationTrail importFromGpx(String gpxContent) {
|
||||||
|
try {
|
||||||
|
final document = XmlDocument.parse(gpxContent);
|
||||||
|
final gpxElement = document.findElements('gpx').firstOrNull;
|
||||||
|
|
||||||
|
if (gpxElement == null) {
|
||||||
|
throw const FormatException('Invalid GPX file: Missing <gpx> element');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract track name from metadata or track element (currently unused, kept for future use)
|
||||||
|
// String? trackName;
|
||||||
|
// final metadataName =
|
||||||
|
// gpxElement.findElements('metadata').firstOrNull?.findElements('name').firstOrNull?.innerText;
|
||||||
|
// final trackNameElement = gpxElement
|
||||||
|
// .findElements('trk')
|
||||||
|
// .firstOrNull
|
||||||
|
// ?.findElements('name')
|
||||||
|
// .firstOrNull
|
||||||
|
// ?.innerText;
|
||||||
|
// trackName = metadataName ?? trackNameElement ?? 'Imported Trail';
|
||||||
|
|
||||||
|
// Extract track points
|
||||||
|
final trackPoints = <TrailPoint>[];
|
||||||
|
final tracks = gpxElement.findElements('trk');
|
||||||
|
|
||||||
|
if (tracks.isEmpty) {
|
||||||
|
throw const FormatException(
|
||||||
|
'Invalid GPX file: No <trk> elements found',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process first track only
|
||||||
|
final track = tracks.first;
|
||||||
|
final segments = track.findElements('trkseg');
|
||||||
|
|
||||||
|
for (final segment in segments) {
|
||||||
|
final trkpts = segment.findElements('trkpt');
|
||||||
|
|
||||||
|
for (final trkpt in trkpts) {
|
||||||
|
try {
|
||||||
|
// Extract latitude and longitude (required)
|
||||||
|
final latStr = trkpt.getAttribute('lat');
|
||||||
|
final lonStr = trkpt.getAttribute('lon');
|
||||||
|
|
||||||
|
if (latStr == null || lonStr == null) {
|
||||||
|
debugPrint('⚠️ Skipping track point: Missing lat/lon attributes');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final lat = double.parse(latStr);
|
||||||
|
final lon = double.parse(lonStr);
|
||||||
|
|
||||||
|
// Extract timestamp (optional)
|
||||||
|
final timeStr =
|
||||||
|
trkpt.findElements('time').firstOrNull?.innerText;
|
||||||
|
final timestamp = timeStr != null
|
||||||
|
? DateTime.parse(timeStr)
|
||||||
|
: DateTime.now();
|
||||||
|
|
||||||
|
// Extract elevation (optional, currently unused but parsed for future use)
|
||||||
|
// final eleStr = trkpt.findElements('ele').firstOrNull?.innerText;
|
||||||
|
// final elevation = eleStr != null ? double.tryParse(eleStr) : null;
|
||||||
|
|
||||||
|
// Extract extensions (accuracy, speed)
|
||||||
|
double? accuracy;
|
||||||
|
double? speed;
|
||||||
|
final extensions =
|
||||||
|
trkpt.findElements('extensions').firstOrNull;
|
||||||
|
if (extensions != null) {
|
||||||
|
final accuracyStr =
|
||||||
|
extensions.findElements('accuracy').firstOrNull?.innerText;
|
||||||
|
final speedStr =
|
||||||
|
extensions.findElements('speed').firstOrNull?.innerText;
|
||||||
|
accuracy = accuracyStr != null ? double.tryParse(accuracyStr) : null;
|
||||||
|
speed = speedStr != null ? double.tryParse(speedStr) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create trail point
|
||||||
|
trackPoints.add(
|
||||||
|
TrailPoint(
|
||||||
|
position: LatLng(lat, lon),
|
||||||
|
timestamp: timestamp,
|
||||||
|
accuracy: accuracy,
|
||||||
|
speed: speed,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('⚠️ Error parsing track point: $e');
|
||||||
|
// Continue with next point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackPoints.isEmpty) {
|
||||||
|
throw const FormatException(
|
||||||
|
'Invalid GPX file: No valid track points found',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create LocationTrail from parsed points
|
||||||
|
final startTime = trackPoints.first.timestamp;
|
||||||
|
final endTime = trackPoints.last.timestamp;
|
||||||
|
|
||||||
|
return LocationTrail(
|
||||||
|
id: 'imported_${DateTime.now().millisecondsSinceEpoch}',
|
||||||
|
points: trackPoints,
|
||||||
|
startTime: startTime,
|
||||||
|
endTime: endTime,
|
||||||
|
isActive: false,
|
||||||
|
);
|
||||||
|
} on XmlException catch (e) {
|
||||||
|
throw FormatException('Invalid GPX XML: ${e.message}');
|
||||||
|
} catch (e) {
|
||||||
|
throw FormatException('Failed to parse GPX file: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Export trail to file and trigger system share sheet
|
||||||
|
/// Returns true if successful
|
||||||
|
static Future<bool> exportTrailToFile(
|
||||||
|
LocationTrail trail, {
|
||||||
|
String? customName,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// Generate GPX content
|
||||||
|
debugPrint('📤 Generating GPX content...');
|
||||||
|
final gpxContent = exportToGpx(trail, customName: customName);
|
||||||
|
|
||||||
|
// Create filename with timestamp
|
||||||
|
final timestamp = DateTime.now();
|
||||||
|
final filename =
|
||||||
|
'meshcore_trail_${timestamp.year}-${timestamp.month.toString().padLeft(2, '0')}-${timestamp.day.toString().padLeft(2, '0')}_${timestamp.hour.toString().padLeft(2, '0')}${timestamp.minute.toString().padLeft(2, '0')}${timestamp.second.toString().padLeft(2, '0')}.gpx';
|
||||||
|
|
||||||
|
// Save to temporary directory
|
||||||
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
final file = File('${tempDir.path}/$filename');
|
||||||
|
await file.writeAsString(gpxContent);
|
||||||
|
|
||||||
|
debugPrint('📤 GPX file saved: ${file.path}');
|
||||||
|
debugPrint('📤 File size: ${file.lengthSync()} bytes');
|
||||||
|
|
||||||
|
// Share the file using system share sheet
|
||||||
|
final result = await Share.shareXFiles(
|
||||||
|
[XFile(file.path, mimeType: 'application/gpx+xml')],
|
||||||
|
subject: 'MeshCore Trail Export',
|
||||||
|
text: 'MeshCore SAR location trail (${trail.points.length} points)',
|
||||||
|
);
|
||||||
|
|
||||||
|
debugPrint('📤 Share result: ${result.status}');
|
||||||
|
return result.status == ShareResultStatus.success ||
|
||||||
|
result.status == ShareResultStatus.unavailable; // unavailable = user dismissed, still OK
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Failed to export trail: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Import trail from GPX file using file picker
|
||||||
|
/// Returns LocationTrail if successful, null if cancelled or failed
|
||||||
|
static Future<LocationTrail?> importTrailFromFile() async {
|
||||||
|
try {
|
||||||
|
// Open file picker for GPX files
|
||||||
|
debugPrint('📥 Opening file picker for GPX import...');
|
||||||
|
final result = await FilePicker.platform.pickFiles(
|
||||||
|
type: FileType.custom,
|
||||||
|
allowedExtensions: ['gpx'],
|
||||||
|
allowMultiple: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == null || result.files.isEmpty) {
|
||||||
|
debugPrint('📥 Import cancelled by user');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final file = result.files.first;
|
||||||
|
debugPrint('📥 Selected file: ${file.name}');
|
||||||
|
debugPrint('📥 File size: ${file.size} bytes');
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
String gpxContent;
|
||||||
|
if (file.path != null) {
|
||||||
|
// File has path (mobile)
|
||||||
|
gpxContent = await File(file.path!).readAsString();
|
||||||
|
} else if (file.bytes != null) {
|
||||||
|
// File has bytes (web)
|
||||||
|
gpxContent = String.fromCharCodes(file.bytes!);
|
||||||
|
} else {
|
||||||
|
throw Exception('Unable to read file content');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse GPX content
|
||||||
|
debugPrint('📥 Parsing GPX content...');
|
||||||
|
final trail = importFromGpx(gpxContent);
|
||||||
|
debugPrint(
|
||||||
|
'✅ Successfully imported trail: ${trail.points.length} points',
|
||||||
|
);
|
||||||
|
|
||||||
|
return trail;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Failed to import trail: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Format distance for display
|
||||||
|
static String _formatDistance(double meters) {
|
||||||
|
if (meters < 1000) {
|
||||||
|
return '${meters.toStringAsFixed(0)} m';
|
||||||
|
} else {
|
||||||
|
return '${(meters / 1000).toStringAsFixed(2)} km';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Format duration for display
|
||||||
|
static String _formatDuration(Duration duration) {
|
||||||
|
final hours = duration.inHours;
|
||||||
|
final minutes = duration.inMinutes.remainder(60);
|
||||||
|
final seconds = duration.inSeconds.remainder(60);
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return '${hours}h ${minutes}m ${seconds}s';
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return '${minutes}m ${seconds}s';
|
||||||
|
} else {
|
||||||
|
return '${seconds}s';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Format DateTime for filename
|
||||||
|
static String _formatDateTime(DateTime dt) {
|
||||||
|
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} '
|
||||||
|
'${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
}
|
||||||
236
lib/services/trail_color_service.dart
Normal file
236
lib/services/trail_color_service.dart
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../models/contact.dart';
|
||||||
|
|
||||||
|
/// Service for assigning consistent colors to contact trails
|
||||||
|
/// Uses emoji-based semantic mapping with deterministic hash fallback
|
||||||
|
class TrailColorService {
|
||||||
|
// 64-color palette optimized for visibility on maps
|
||||||
|
// Organized by hue families for better distribution
|
||||||
|
// Avoids pure blue (#2196F3) which is reserved for user trail
|
||||||
|
static final List<Color> _colorPalette = [
|
||||||
|
// Reds (8)
|
||||||
|
const Color(0xFFE53935),
|
||||||
|
const Color(0xFFD32F2F),
|
||||||
|
const Color(0xFFC62828),
|
||||||
|
const Color(0xFFB71C1C),
|
||||||
|
const Color(0xFFFF5252),
|
||||||
|
const Color(0xFFFF1744),
|
||||||
|
const Color(0xFFD50000),
|
||||||
|
const Color(0xFFC51162),
|
||||||
|
|
||||||
|
// Pinks (4)
|
||||||
|
const Color(0xFFEC407A),
|
||||||
|
const Color(0xFFE91E63),
|
||||||
|
const Color(0xFFC2185B),
|
||||||
|
const Color(0xFFAD1457),
|
||||||
|
|
||||||
|
// Purples (8)
|
||||||
|
const Color(0xFF9C27B0),
|
||||||
|
const Color(0xFF8E24AA),
|
||||||
|
const Color(0xFF7B1FA2),
|
||||||
|
const Color(0xFF6A1B9A),
|
||||||
|
const Color(0xFFAB47BC),
|
||||||
|
const Color(0xFF9C27B0),
|
||||||
|
const Color(0xFF8E24AA),
|
||||||
|
const Color(0xFF7B1FA2),
|
||||||
|
|
||||||
|
// Deep Purples (4)
|
||||||
|
const Color(0xFF673AB7),
|
||||||
|
const Color(0xFF5E35B1),
|
||||||
|
const Color(0xFF512DA8),
|
||||||
|
const Color(0xFF4527A0),
|
||||||
|
|
||||||
|
// Indigos (4)
|
||||||
|
const Color(0xFF3F51B5),
|
||||||
|
const Color(0xFF3949AB),
|
||||||
|
const Color(0xFF303F9F),
|
||||||
|
const Color(0xFF283593),
|
||||||
|
|
||||||
|
// Blues (4) - Dark blues only, avoid user trail blue
|
||||||
|
const Color(0xFF1E88E5),
|
||||||
|
const Color(0xFF1976D2),
|
||||||
|
const Color(0xFF1565C0),
|
||||||
|
const Color(0xFF0D47A1),
|
||||||
|
|
||||||
|
// Cyans (4)
|
||||||
|
const Color(0xFF00ACC1),
|
||||||
|
const Color(0xFF0097A7),
|
||||||
|
const Color(0xFF00838F),
|
||||||
|
const Color(0xFF006064),
|
||||||
|
|
||||||
|
// Teals (4)
|
||||||
|
const Color(0xFF00897B),
|
||||||
|
const Color(0xFF00796B),
|
||||||
|
const Color(0xFF00695C),
|
||||||
|
const Color(0xFF004D40),
|
||||||
|
|
||||||
|
// Greens (8)
|
||||||
|
const Color(0xFF43A047),
|
||||||
|
const Color(0xFF388E3C),
|
||||||
|
const Color(0xFF2E7D32),
|
||||||
|
const Color(0xFF1B5E20),
|
||||||
|
const Color(0xFF66BB6A),
|
||||||
|
const Color(0xFF4CAF50),
|
||||||
|
const Color(0xFF388E3C),
|
||||||
|
const Color(0xFF2E7D32),
|
||||||
|
|
||||||
|
// Limes (4)
|
||||||
|
const Color(0xFF9E9D24),
|
||||||
|
const Color(0xFF827717),
|
||||||
|
const Color(0xFFC0CA33),
|
||||||
|
const Color(0xFFAFB42B),
|
||||||
|
|
||||||
|
// Ambers (4)
|
||||||
|
const Color(0xFFFFA726),
|
||||||
|
const Color(0xFFFF9800),
|
||||||
|
const Color(0xFFFB8C00),
|
||||||
|
const Color(0xFFF57C00),
|
||||||
|
|
||||||
|
// Oranges (4)
|
||||||
|
const Color(0xFFFF7043),
|
||||||
|
const Color(0xFFFF5722),
|
||||||
|
const Color(0xFFF4511E),
|
||||||
|
const Color(0xFFE64A19),
|
||||||
|
|
||||||
|
// Browns (4)
|
||||||
|
const Color(0xFF6D4C41),
|
||||||
|
const Color(0xFF5D4037),
|
||||||
|
const Color(0xFF4E342E),
|
||||||
|
const Color(0xFF3E2723),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Emoji to color mapping for SAR roles
|
||||||
|
// Uses semantic colors that match emergency service conventions
|
||||||
|
static final Map<String, Color> _emojiColorMap = {
|
||||||
|
// Emergency Services - Firefighters
|
||||||
|
'🚒': Color(0xFFD32F2F), // Fire engine → Red
|
||||||
|
'🧑🚒': Color(0xFFD32F2F), // Firefighter → Red
|
||||||
|
'👨🚒': Color(0xFFD32F2F), // Firefighter → Red
|
||||||
|
'👩🚒': Color(0xFFD32F2F), // Firefighter → Red
|
||||||
|
'🔥': Color(0xFFFF5722), // Fire → Orange-Red
|
||||||
|
|
||||||
|
// Emergency Services - Medical
|
||||||
|
'🚑': Color(0xFF43A047), // Ambulance → Green (medical cross)
|
||||||
|
'👨⚕️': Color(0xFF43A047), // Health worker → Green
|
||||||
|
'👩⚕️': Color(0xFF43A047), // Health worker → Green
|
||||||
|
'🧑⚕️': Color(0xFF43A047), // Health worker → Green
|
||||||
|
'⚕️': Color(0xFF43A047), // Medical symbol → Green
|
||||||
|
|
||||||
|
// Emergency Services - Police
|
||||||
|
'👮': Color(0xFF1976D2), // Police → Blue
|
||||||
|
'👮♂️': Color(0xFF1976D2), // Police → Blue
|
||||||
|
'👮♀️': Color(0xFF1976D2), // Police → Blue
|
||||||
|
'🚔': Color(0xFF1976D2), // Police car → Blue
|
||||||
|
|
||||||
|
// Emergency Services - Aviation
|
||||||
|
'🧑✈️': Color(0xFF1565C0), // Pilot → Dark Blue
|
||||||
|
'👨✈️': Color(0xFF1565C0), // Pilot → Dark Blue
|
||||||
|
'👩✈️': Color(0xFF1565C0), // Pilot → Dark Blue
|
||||||
|
'🚁': Color(0xFF8E24AA), // Helicopter → Purple
|
||||||
|
|
||||||
|
// SAR Roles - Mountain/Alpine
|
||||||
|
'🏔️': Color(0xFF6D4C41), // Mountain → Brown
|
||||||
|
'⛰️': Color(0xFF6D4C41), // Mountain → Brown
|
||||||
|
'🧗': Color(0xFF6D4C41), // Climber → Brown
|
||||||
|
'🧗♂️': Color(0xFF6D4C41), // Climber → Brown
|
||||||
|
'🧗♀️': Color(0xFF6D4C41), // Climber → Brown
|
||||||
|
'🥾': Color(0xFF5D4037), // Hiking boot → Dark Brown
|
||||||
|
|
||||||
|
// SAR Roles - K9 Unit
|
||||||
|
'🐕': Color(0xFFFFA726), // Dog → Orange
|
||||||
|
'🐶': Color(0xFFFFA726), // Dog → Orange
|
||||||
|
'🦮': Color(0xFFFFA726), // Service dog → Orange
|
||||||
|
|
||||||
|
// SAR Roles - Water Rescue
|
||||||
|
'🚤': Color(0xFF00ACC1), // Speedboat → Cyan
|
||||||
|
'⛵': Color(0xFF00ACC1), // Sailboat → Cyan
|
||||||
|
'🏊': Color(0xFF00897B), // Swimmer → Teal
|
||||||
|
'🏊♂️': Color(0xFF00897B), // Swimmer → Teal
|
||||||
|
'🏊♀️': Color(0xFF00897B), // Swimmer → Teal
|
||||||
|
|
||||||
|
// Team Roles - Leadership
|
||||||
|
'🎯': Color(0xFFE64A19), // Target → Deep Orange (team leader)
|
||||||
|
'⭐': Color(0xFFFDD835), // Star → Yellow (coordinator)
|
||||||
|
'👑': Color(0xFFFDD835), // Crown → Yellow (leader)
|
||||||
|
|
||||||
|
// Team Roles - Communication
|
||||||
|
'📡': Color(0xFF00897B), // Satellite → Teal (radio/comms)
|
||||||
|
'📻': Color(0xFF00897B), // Radio → Teal
|
||||||
|
'📞': Color(0xFF00897B), // Phone → Teal
|
||||||
|
|
||||||
|
// Team Roles - Navigation
|
||||||
|
'🗺️': Color(0xFF00ACC1), // Map → Cyan (navigator)
|
||||||
|
'🧭': Color(0xFF00ACC1), // Compass → Cyan
|
||||||
|
'📍': Color(0xFFE53935), // Pin → Red (location marker)
|
||||||
|
|
||||||
|
// Team Roles - Documentation
|
||||||
|
'📷': Color(0xFFAB47BC), // Camera → Light Purple
|
||||||
|
'📹': Color(0xFFAB47BC), // Video camera → Light Purple
|
||||||
|
'📝': Color(0xFF9E9D24), // Note → Lime (scribe)
|
||||||
|
|
||||||
|
// Equipment
|
||||||
|
'🔦': Color(0xFFFB8C00), // Flashlight → Amber
|
||||||
|
'⚡': Color(0xFFFDD835), // Lightning → Yellow (power/energy)
|
||||||
|
'🔋': Color(0xFF43A047), // Battery → Green
|
||||||
|
'🎒': Color(0xFF5D4037), // Backpack → Brown
|
||||||
|
|
||||||
|
// Generic Person Icons
|
||||||
|
'👤': Color(0xFF9E9E9E), // Silhouette → Gray
|
||||||
|
'🧑': Color(0xFF9E9E9E), // Person → Gray
|
||||||
|
'👨': Color(0xFF9E9E9E), // Man → Gray
|
||||||
|
'👩': Color(0xFF9E9E9E), // Woman → Gray
|
||||||
|
'👥': Color(0xFF757575), // People → Dark Gray
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Get trail color for a contact
|
||||||
|
/// Priority: Emoji mapping > Name hash > Default
|
||||||
|
static Color getTrailColor(Contact contact) {
|
||||||
|
// 1. Try emoji-based color mapping
|
||||||
|
if (contact.roleEmoji != null) {
|
||||||
|
final emojiColor = _emojiColorMap[contact.roleEmoji];
|
||||||
|
if (emojiColor != null) {
|
||||||
|
// Return with slight transparency for better map visibility
|
||||||
|
return emojiColor.withValues(alpha: 0.75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Deterministic color based on display name
|
||||||
|
// Use display name (without emoji) for consistent hashing
|
||||||
|
final name = contact.displayName.isNotEmpty
|
||||||
|
? contact.displayName
|
||||||
|
: contact.publicKeyHex;
|
||||||
|
|
||||||
|
final hash = _hashString(name);
|
||||||
|
final colorIndex = hash % _colorPalette.length; // 0-63
|
||||||
|
|
||||||
|
return _colorPalette[colorIndex].withValues(alpha: 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple string hash function (DJB2 algorithm)
|
||||||
|
/// Same algorithm used for echo detection in the app
|
||||||
|
static int _hashString(String str) {
|
||||||
|
int hash = 5381;
|
||||||
|
for (int i = 0; i < str.length; i++) {
|
||||||
|
hash = ((hash << 5) + hash) + str.codeUnitAt(i);
|
||||||
|
hash = hash & 0xFFFFFFFF; // Keep 32-bit
|
||||||
|
}
|
||||||
|
return hash.abs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all unique colors currently in use by contacts with trails
|
||||||
|
static List<Color> getActiveColors(List<Contact> contacts) {
|
||||||
|
final colors = <Color>{};
|
||||||
|
for (final contact in contacts) {
|
||||||
|
if (contact.advertHistory.length >= 2) {
|
||||||
|
colors.add(getTrailColor(contact));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return colors.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a color is from emoji mapping (semantic) vs hash-based
|
||||||
|
static bool isSemanticColor(Contact contact) {
|
||||||
|
if (contact.roleEmoji == null) return false;
|
||||||
|
return _emojiColorMap.containsKey(contact.roleEmoji);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -621,26 +621,7 @@ class _DetailedCompassDialogState extends State<DetailedCompassDialog> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Show path toggle button for contacts with history
|
// Close button to deselect contact
|
||||||
if (_selectedContact != null && _selectedContact!.advertHistory.length >= 2)
|
|
||||||
Consumer<MapProvider>(
|
|
||||||
builder: (context, mapProvider, _) {
|
|
||||||
final isPathVisible = mapProvider.isContactPathVisible(_selectedContact!.publicKeyHex);
|
|
||||||
return IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
isPathVisible ? Icons.route : Icons.route_outlined,
|
|
||||||
size: 20,
|
|
||||||
color: isPathVisible ? Theme.of(context).colorScheme.primary : null,
|
|
||||||
),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
tooltip: isPathVisible ? 'Hide path' : 'Show path (${_selectedContact!.advertHistory.length} points)',
|
|
||||||
onPressed: () {
|
|
||||||
mapProvider.toggleContactPath(_selectedContact!.publicKeyHex);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.close, size: 20),
|
icon: const Icon(Icons.close, size: 20),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../../providers/map_provider.dart';
|
import '../../providers/map_provider.dart';
|
||||||
|
import '../../providers/contacts_provider.dart';
|
||||||
|
import '../../providers/app_provider.dart';
|
||||||
|
import '../../services/gpx_service.dart';
|
||||||
|
import '../../services/trail_color_service.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
|
|
||||||
/// Trail management controls widget
|
/// Trail management controls widget
|
||||||
@@ -9,18 +13,27 @@ class TrailControls extends StatelessWidget {
|
|||||||
|
|
||||||
void _showTrailMenu(BuildContext context) {
|
void _showTrailMenu(BuildContext context) {
|
||||||
final mapProvider = Provider.of<MapProvider>(context, listen: false);
|
final mapProvider = Provider.of<MapProvider>(context, listen: false);
|
||||||
|
final contactsProvider = Provider.of<ContactsProvider>(context, listen: false);
|
||||||
|
final appProvider = Provider.of<AppProvider>(context, listen: false);
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final isSimpleMode = appProvider.isSimpleMode;
|
||||||
|
|
||||||
|
// Get contacts with trails (advertHistory >= 2 points)
|
||||||
|
final contactsWithTrails = contactsProvider.contactsWithLocation
|
||||||
|
.where((c) => c.advertHistory.length >= 2)
|
||||||
|
.toList();
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
builder: (context) => StatefulBuilder(
|
builder: (context) => StatefulBuilder(
|
||||||
builder: (context, setModalState) => Container(
|
builder: (context, setModalState) => SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16),
|
child: Container(
|
||||||
child: Column(
|
padding: const EdgeInsets.all(16),
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.timeline, size: 24),
|
const Icon(Icons.timeline, size: 24),
|
||||||
@@ -89,6 +102,60 @@ class TrailControls extends StatelessWidget {
|
|||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// GPX Export/Import buttons (hidden in simple mode)
|
||||||
|
if (!isSimpleMode) ...[
|
||||||
|
if (mapProvider.currentTrail != null && mapProvider.currentTrail!.points.isNotEmpty)
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () async {
|
||||||
|
final success = await GpxService.exportTrailToFile(mapProvider.currentTrail!);
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(success
|
||||||
|
? l10n.trailExportedSuccessfully
|
||||||
|
: l10n.failedToExportTrail),
|
||||||
|
backgroundColor: success ? Colors.green : Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.upload),
|
||||||
|
label: Text(l10n.exportTrailToGpx),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () async {
|
||||||
|
try {
|
||||||
|
final trail = await GpxService.importTrailFromFile();
|
||||||
|
if (trail != null && context.mounted) {
|
||||||
|
_showImportDialog(context, mapProvider, trail, l10n);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(l10n.failedToImportTrail(e.toString())),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.download),
|
||||||
|
label: Text(l10n.importTrailFromGpx),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
|
||||||
// Clear trail button
|
// Clear trail button
|
||||||
if (mapProvider.currentTrail != null && mapProvider.currentTrail!.points.isNotEmpty)
|
if (mapProvider.currentTrail != null && mapProvider.currentTrail!.points.isNotEmpty)
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
@@ -134,6 +201,81 @@ class TrailControls extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Contact Trails Section
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.people, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
l10n.contactTrails,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Show All Contact Trails toggle
|
||||||
|
SwitchListTile(
|
||||||
|
secondary: const Icon(Icons.route),
|
||||||
|
title: Text(l10n.showAllContactTrails),
|
||||||
|
subtitle: Text(contactsWithTrails.isEmpty
|
||||||
|
? l10n.noContactsWithLocationHistory
|
||||||
|
: l10n.showingTrailsForContacts(contactsWithTrails.length)),
|
||||||
|
value: mapProvider.showAllContactTrails,
|
||||||
|
onChanged: contactsWithTrails.isNotEmpty
|
||||||
|
? (value) {
|
||||||
|
mapProvider.toggleAllContactTrails();
|
||||||
|
setModalState(() {}); // Update modal UI
|
||||||
|
}
|
||||||
|
: null, // Disable if no contacts with trails
|
||||||
|
),
|
||||||
|
|
||||||
|
// Individual contact trails (when "show all" is OFF)
|
||||||
|
if (!mapProvider.showAllContactTrails && contactsWithTrails.isNotEmpty)
|
||||||
|
ExpansionTile(
|
||||||
|
title: Text(l10n.individualContactTrails),
|
||||||
|
initiallyExpanded: false,
|
||||||
|
children: contactsWithTrails.map((contact) {
|
||||||
|
final trailColor = TrailColorService.getTrailColor(contact);
|
||||||
|
final isVisible = mapProvider.isContactPathVisible(contact.publicKeyHex);
|
||||||
|
|
||||||
|
return SwitchListTile(
|
||||||
|
// Color indicator with emoji
|
||||||
|
secondary: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (contact.roleEmoji != null)
|
||||||
|
Text(contact.roleEmoji!, style: const TextStyle(fontSize: 18)),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Container(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: trailColor,
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
title: Text(contact.displayName),
|
||||||
|
subtitle: Text('${contact.advertHistory.length} points'),
|
||||||
|
value: isVisible,
|
||||||
|
onChanged: (value) {
|
||||||
|
mapProvider.toggleContactPath(contact.publicKeyHex);
|
||||||
|
setModalState(() {}); // Update modal UI
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
// Close button
|
// Close button
|
||||||
@@ -142,6 +284,7 @@ class TrailControls extends StatelessWidget {
|
|||||||
child: Text(l10n.close),
|
child: Text(l10n.close),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -173,6 +316,50 @@ class TrailControls extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showImportDialog(BuildContext context, MapProvider mapProvider, trail, AppLocalizations l10n) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(l10n.importTrail),
|
||||||
|
content: Text(l10n.importTrailQuestion(trail.points.length)),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(l10n.cancel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
mapProvider.setImportedTrail(trail);
|
||||||
|
Navigator.pop(context); // Close dialog
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(l10n.trailImported(trail.points.length)),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(l10n.viewAlongside),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
mapProvider.replaceCurrentTrailWithImport(trail);
|
||||||
|
Navigator.pop(context); // Close dialog
|
||||||
|
Navigator.pop(context); // Close bottom sheet
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(l10n.trailReplaced(trail.points.length)),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
style: TextButton.styleFrom(foregroundColor: Colors.blue),
|
||||||
|
child: Text(l10n.replaceCurrent),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildStatRow({
|
Widget _buildStatRow({
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required String label,
|
required String label,
|
||||||
|
|||||||
@@ -1180,7 +1180,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
xml:
|
xml:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: xml
|
name: xml
|
||||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||||
|
|||||||
@@ -99,6 +99,9 @@ dependencies:
|
|||||||
# Cryptographic functions (for channel secret hashing)
|
# Cryptographic functions (for channel secret hashing)
|
||||||
crypto: ^3.0.3
|
crypto: ^3.0.3
|
||||||
|
|
||||||
|
# XML parsing for GPX import/export
|
||||||
|
xml: ^6.5.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
Reference in New Issue
Block a user