From da3302b0306f6f7555b27be1ea6980867aa46d8e Mon Sep 17 00:00:00 2001 From: Janez T Date: Tue, 28 Oct 2025 19:09:11 +0100 Subject: [PATCH] feat: Enhance map functionality with download area selection and user location pointer - Added download area selection feature in MapProvider with methods to enter, exit, and update bounds. - Implemented DownloadAreaOverlay widget for user interface during download area selection. - Integrated download area preview in MapManagementScreen with visual feedback. - Introduced LocationPointer widget to indicate user location and heading on the map. - Updated localization strings in app_sl.arb for improved clarity and accuracy. - Refactored map marker generation to utilize the new LocationPointer for user location representation. - Improved error handling in LocationTrackingService for initial position setting. --- l10n.yaml | 1 + lib/l10n/app_hr.arb | 109 ++++++------ lib/l10n/app_localizations_hr.dart | 112 ++++++------- lib/l10n/app_localizations_sl.dart | 109 ++++++------ lib/l10n/app_sl.arb | 113 ++++++------- lib/l10n/untranslated.json | 1 + lib/providers/map_provider.dart | 29 ++++ lib/screens/map_management_screen.dart | 137 ++++++++++++++- lib/screens/map_tab.dart | 98 +++++++++-- lib/services/location_tracking_service.dart | 5 +- lib/services/map_marker_service.dart | 39 ++--- lib/services/meshcore_ble_service.dart | 5 +- lib/widgets/map/download_area_overlay.dart | 108 ++++++++++++ lib/widgets/map/location_pointer.dart | 175 ++++++++++++++++++++ 14 files changed, 769 insertions(+), 272 deletions(-) create mode 100644 lib/l10n/untranslated.json create mode 100644 lib/widgets/map/download_area_overlay.dart create mode 100644 lib/widgets/map/location_pointer.dart diff --git a/l10n.yaml b/l10n.yaml index 15338f2..1c7e2fa 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -1,3 +1,4 @@ arb-dir: lib/l10n template-arb-file: app_en.arb output-localization-file: app_localizations.dart +untranslated-messages-file: lib/l10n/untranslated.json diff --git a/lib/l10n/app_hr.arb b/lib/l10n/app_hr.arb index 863a4bc..b5d27b6 100644 --- a/lib/l10n/app_hr.arb +++ b/lib/l10n/app_hr.arb @@ -69,7 +69,7 @@ "autoSystem": "Automatski (Sustav)", - "followSystemTheme": "Slijedi sistemsku temu", + "followSystemTheme": "Slijedi temu sustava", "showRxTxIndicators": "Prikaži RX/TX indikatore", @@ -141,11 +141,11 @@ "technologiesUsed": "Korištene tehnologije:", - "technologiesList": "• Flutter za višeplatformski razvoj\n• BLE (Bluetooth Low Energy) za mesh mrežu\n• OpenStreetMap za kartografiju\n• Provider za upravljanje stanjem\n• SharedPreferences za lokalno pohranu", + "technologiesList": "• Flutter za višeplatformski razvoj\n• BLE (Bluetooth Low Energy) za mesh mrežu\n• OpenStreetMap za kartografiju\n• Provider za upravljanje stanjem\n• SharedPreferences za lokalnu pohranu", "moreInfo": "Više informacija", - "learnMoreAbout": "Saznajte više o MeshCore SAR", + "learnMoreAbout": "Saznajte više o MeshCore SAR-u", "developer": "Programer", @@ -175,7 +175,7 @@ "locationBroadcast": "Emitiranje lokacije: {latitude}, {longitude}", - "defaultPinInfo": "Zadani PIN za uređaje bez zaslona je 123456. Problemi s uparivanjem? Zaboravite bluetooth uređaj u postavkama sustava.", + "defaultPinInfo": "Zadani PIN za uređaje bez zaslona je 123456. Problemi s uparivanjem? Zaboravite Bluetooth uređaj u postavkama sustava.", "noMessagesYet": "Još nema poruka", @@ -217,7 +217,7 @@ "deleteDrawing": "Izbriši crtež", - "drawingTools": "Alati za crtanje", + "drawLine": "Nacrtaj liniju", @@ -433,7 +433,7 @@ "accuracy": "Točnost", - "distance": "Udaljenost", + "bearing": "Azimut", @@ -467,7 +467,7 @@ "degreesLeft": "{degrees}° lijevo", - "latLonFormat": "Ššir: {latitude} Dužina: {longitude}", + "latLonFormat": "Šir: {latitude} Duž: {longitude}", "noContactsYet": "Još nema kontakata", @@ -487,7 +487,7 @@ "cacheSize": "Veličina predmemorije", - "storeName": "Ime pohrane", + "storeName": "Naziv spremišta", "noCacheStatistics": "Statistika predmemorije nije dostupna", @@ -571,7 +571,7 @@ "resetMapRotationTooltip": "Vrati kartu na sjever", - "showMapDebugInfo": "Prikaži debug informacije karte", + "showMapDebugInfo": "Prikaži informacije za otklanjanje pogrešaka karte", "displayZoomLevelBounds": "Prikaži razinu zumiranja i granice", @@ -585,7 +585,7 @@ "esriSatellite": "ESRI satelit", - "googleHybrid": "Google hibridno", + "googleHybrid": "Google hibridna karta", "googleRoadmap": "Google cestovna karta", @@ -769,8 +769,6 @@ "trailHiddenRecording": "Putanja je skrivena (još se snima)", - "distance": "Udaljenost", - "duration": "Trajanje", "points": "Točke", @@ -849,7 +847,7 @@ "lat": "Šir", - "lon": "Dug", + "lon": "Duž", "useCurrentLocation": "Koristi trenutnu lokaciju", @@ -879,28 +877,27 @@ "templateEmoji": "Emoji", "emojiRequired": "Emoji je obavezan", "nameRequired": "Ime je obavezno", - "templateDescription": "Description (Optional)", - "templateDescriptionHint": "Add additional context...", - "templateColor": "Color", - "previewFormat": "Preview (SAR Message Format)", + "templateDescription": "Opis (neobavezno)", + "templateDescriptionHint": "Dodajte dodatni kontekst...", + "templateColor": "Boja", + "previewFormat": "Pregled (format SAR poruke)", "importFromClipboard": "Uvezi", "exportToClipboard": "Izvezi", - "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}", + "deleteTemplateConfirmation": "Izbrisati predložak '{name}'?", + "templateAdded": "Predložak dodan", + "templateUpdated": "Predložak ažuriran", + "templateDeleted": "Predložak izbrisan", + "templatesImported": "{count, plural, =0{Nema uvezenih predložaka} =1{Uvezen 1 predložak} other{Uvezeno {count} predložaka}}", + "templatesExported": "{count, plural, =1{Izvezen 1 predložak u međuspremnik} other{Izvezeno {count} predložaka u međuspremnik}}", + "importFailed": "Uvoz nije uspio: {error}", + "exportFailed": "Izvoz nije uspio: {error}", "resetToDefaults": "Vrati na zadane", "resetToDefaultsConfirmation": "Ovo će izbrisati sve prilagođene predloške i vratiti 4 zadana predloška. Nastaviti?", "reset": "Vrati", "resetComplete": "Predlošci vraćeni na zadane", - "noTemplates": "No templates available", - "tapAddToCreate": "Tap + to create your first template", + "noTemplates": "Nema dostupnih predložaka", + "tapAddToCreate": "Dodirnite + za izradu prvog predloška", "ok": "OK", - "delete": "Obriši", "permissionsSection": "Dozvole", "locationPermission": "Dozvola za lokaciju", @@ -918,7 +915,7 @@ "sarNavyBlueDescription": "Profesionalni/Operativni Način", "selectRecipient": "Odaberi primatelja", - "broadcastToAllNearby": "Emituj svima u blizini", + "broadcastToAllNearby": "Emitiraj svima u blizini", "searchRecipients": "Pretraži primatelje...", "noContactsFound": "Nema kontakata", "noRoomsFound": "Nema soba", @@ -946,9 +943,9 @@ "sampleObjectVehicle": " Napušteno vozilo - provjeriti vlasnika", "sampleObjectCamping": " Otkrivena oprema za kampiranje", "sampleObjectTrailMarker": " Oznaka staze pronađena izvan puta", - "sampleMsgAllTeamsCheckIn": "Svi timovi se jave", + "sampleMsgAllTeamsCheckIn": "Svi timovi, javite se", "sampleMsgWeatherUpdate": "Ažuriranje vremena: Vedro nebo, temp 18°C", - "sampleMsgBaseCamp": "Bazni kamp uspostavljen na okupljalište", + "sampleMsgBaseCamp": "Bazni kamp uspostavljen na okupljalištu", "sampleMsgTeamAlpha": "Tim se kreće prema sektoru 2", "sampleMsgRadioCheck": "Provjera radija - sve stanice odgovorite", "sampleMsgWaterSupply": "Opskrba vodom dostupna na kontrolnoj točki 3", @@ -985,33 +982,33 @@ "rectangleDrawing": "Pravokutni crtež", "coordinatesCopiedToClipboard": "Koordinate kopirane u međuspremnik", - "drawingShared": "Crtež karte", + "drawingShared": "Crtež podijeljen", "drawingHidden": "Crtež sakriven s karte", "alreadyShared": "{count, plural, =1{1 već podijeljeno} other{{count} već podijeljeno}}", "newDrawingsShared": "Podijeljeno {count} novi{plural} crtež{plural}", "drawingTools": "Alati za crtanje", - "shareDrawing": "Podijeli Crtež", + "shareDrawing": "Podijeli crtež", "shareWithAllNearbyDevices": "Podijeli sa svim obližnjim uređajima", "shareToRoom": "Podijeli u Sobu", "sendToPersistentStorage": "Pošalji u trajnu pohranu sobe", "deleteDrawingConfirm": "Jeste li sigurni da želite izbrisati ovaj crtež?", "drawingDeleted": "Crtež izbrisan", - "yourDrawingsCount": "Vaši Crteži ({count})", + "yourDrawingsCount": "Vaši crteži ({count})", "shared": "Podijeljeno", "line": "Linija", "rectangle": "Pravokutnik", - "updateAvailable": "Dostupno Ažuriranje", - "currentVersion": "Trenutna", - "latestVersion": "Najnovija", - "downloadUpdate": "Preuzmi", + "updateAvailable": "Dostupno ažuriranje", + "currentVersion": "Trenutna verzija", + "latestVersion": "Najnovija verzija", + "downloadUpdate": "Preuzmi ažuriranje", "updateLater": "Kasnije", "cadastralParcels": "Katastarske čestice", "forestRoads": "Šumske ceste", "showCadastralParcels": "Prikaži katastarske čestice", "showForestRoads": "Prikaži šumske ceste", - "wmsOverlays": "WMS Prekrivanja", + "wmsOverlays": "WMS prekrivanja", "recentMessages": "Nedavne poruke", @@ -1036,31 +1033,31 @@ "wizardNext": "Dalje", "wizardGetStarted": "Započni", "wizardWelcomeTitle": "Dobrodošli u MeshCore SAR", - "wizardWelcomeDescription": "Moćan alat za komunikaciju bez mreže za spasilačke operacije. Povežite se sa svojim timom koristeći mesh radio tehnologiju kada tradicionalne mreže nisu dostupne.", - "wizardConnectingTitle": "Povezivanje s Radiom", - "wizardConnectingDescription": "Povežite svoj pametni telefon s MeshCore radio uređajem putem Bluetootha kako biste započeli komunikaciju bez mreže.", - "wizardConnectingFeature1": "Skeniraj obližnje MeshCore uređaje", - "wizardConnectingFeature2": "Uparite se s vašim radiom putem Bluetootha", - "wizardConnectingFeature3": "Radi potpuno offline - internet nije potreban", - "wizardSimpleModeTitle": "Jednostavan Način", - "wizardSimpleModeDescription": "Novi u mesh umrežavanju? Omogućite jednostavan način za pojednostavljeno sučelje samo s bitnim funkcijama.", - "wizardSimpleModeFeature1": "Sučelje prijateljsko početnicima s osnovnim funkcijama", - "wizardSimpleModeFeature2": "Prebacite na Napredni način bilo kada u Postavkama", + "wizardWelcomeDescription": "Moćan alat za komunikaciju izvan mreže za spasilačke operacije. Povežite se s timom uz mesh radijsku tehnologiju kada tradicionalne mreže nisu dostupne.", + "wizardConnectingTitle": "Povezivanje s radiom", + "wizardConnectingDescription": "Povežite telefon s MeshCore radijskim uređajem putem Bluetootha i započnite komunikaciju izvan mreže.", + "wizardConnectingFeature1": "Skenira obližnje MeshCore uređaje", + "wizardConnectingFeature2": "Uparivanje s radijem putem Bluetootha", + "wizardConnectingFeature3": "Radi potpuno izvan mreže — internet nije potreban", + "wizardSimpleModeTitle": "Jednostavan način", + "wizardSimpleModeDescription": "Prvi put koristite mesh mrežu? Uključite jednostavan način za pojednostavljeno sučelje s osnovnim funkcijama.", + "wizardSimpleModeFeature1": "Sučelje prilagođeno početnicima s osnovnim funkcijama", + "wizardSimpleModeFeature2": "U svakom trenutku prebacite na napredni način u Postavkama", "wizardChannelTitle": "Kanali", - "wizardChannelDescription": "Emitirajte poruke svima na kanalu, savršeno za objave i koordinaciju cijele ekipe.", + "wizardChannelDescription": "Šaljite poruke svima na kanalu — idealno za obavijesti i koordinaciju tima.", "wizardChannelFeature1": "Javni kanal za opću komunikaciju ekipe", "wizardChannelFeature2": "Stvorite prilagođene kanale za specifične grupe", "wizardChannelFeature3": "Poruke se automatski prosljeđuju putem mreže", "wizardContactsTitle": "Kontakti", - "wizardContactsDescription": "Vaši članovi tima se automatski pojavljuju kada se pridruže mesh mreži. Pošaljite im direktne poruke ili pogledajte njihovu lokaciju.", + "wizardContactsDescription": "Članovi tima se prikazuju automatski kada se pridruže mesh mreži. Šaljite im izravne poruke ili pogledajte njihovu lokaciju.", "wizardContactsFeature1": "Kontakti se automatski otkrivaju", "wizardContactsFeature2": "Šaljite privatne direktne poruke", - "wizardContactsFeature3": "Pogledajte razinu baterije i vrijeme zadnjeg viđenja", - "wizardMapTitle": "Karta & Lokacija", - "wizardMapDescription": "Pratite svoj tim u stvarnom vremenu i označite važne lokacije za spasilačke operacije.", - "wizardMapFeature1": "SAR oznake za pronađene osobe, požare i područja postavljanja", + "wizardContactsFeature3": "Prikažite stanje baterije i vrijeme zadnje aktivnosti", + "wizardMapTitle": "Karta i lokacija", + "wizardMapDescription": "Pratite tim u stvarnom vremenu i označavajte ključne lokacije za spasilačke operacije.", + "wizardMapFeature1": "SAR oznake za pronađene osobe, požare i točke okupljanja", "wizardMapFeature2": "GPS praćenje članova tima u stvarnom vremenu", - "wizardMapFeature3": "Preuzmite offline karte za udaljena područja", + "wizardMapFeature3": "Preuzmite karte za rad izvan mreže", "wizardMapFeature4": "Crtajte oblike i dijelite taktičke informacije", "viewWelcomeTutorial": "Pogledaj uputu dobrodošlice" } diff --git a/lib/l10n/app_localizations_hr.dart b/lib/l10n/app_localizations_hr.dart index baa63c1..e87f121 100644 --- a/lib/l10n/app_localizations_hr.dart +++ b/lib/l10n/app_localizations_hr.dart @@ -120,7 +120,7 @@ class AppLocalizationsHr extends AppLocalizations { String get autoSystem => 'Automatski (Sustav)'; @override - String get followSystemTheme => 'Slijedi sistemsku temu'; + String get followSystemTheme => 'Slijedi temu sustava'; @override String get showRxTxIndicators => 'Prikaži RX/TX indikatore'; @@ -240,13 +240,13 @@ class AppLocalizationsHr extends AppLocalizations { @override String get technologiesList => - '• Flutter za višeplatformski razvoj\n• BLE (Bluetooth Low Energy) za mesh mrežu\n• OpenStreetMap za kartografiju\n• Provider za upravljanje stanjem\n• SharedPreferences za lokalno pohranu'; + '• Flutter za višeplatformski razvoj\n• BLE (Bluetooth Low Energy) za mesh mrežu\n• OpenStreetMap za kartografiju\n• Provider za upravljanje stanjem\n• SharedPreferences za lokalnu pohranu'; @override String get moreInfo => 'Više informacija'; @override - String get learnMoreAbout => 'Saznajte više o MeshCore SAR'; + String get learnMoreAbout => 'Saznajte više o MeshCore SAR-u'; @override String get developer => 'Programer'; @@ -306,7 +306,7 @@ class AppLocalizationsHr extends AppLocalizations { @override String get defaultPinInfo => - 'Zadani PIN za uređaje bez zaslona je 123456. Problemi s uparivanjem? Zaboravite bluetooth uređaj u postavkama sustava.'; + 'Zadani PIN za uređaje bez zaslona je 123456. Problemi s uparivanjem? Zaboravite Bluetooth uređaj u postavkama sustava.'; @override String get noMessagesYet => 'Još nema poruka'; @@ -318,7 +318,7 @@ class AppLocalizationsHr extends AppLocalizations { String get deleteContact => 'Izbriši kontakt'; @override - String get delete => 'Obriši'; + String get delete => 'Izbriši'; @override String get viewOnMap => 'Prikaži na karti'; @@ -815,7 +815,7 @@ class AppLocalizationsHr extends AppLocalizations { @override String latLonFormat(String latitude, String longitude) { - return 'Ššir: $latitude Dužina: $longitude'; + return 'Šir: $latitude Duž: $longitude'; } @override @@ -847,7 +847,7 @@ class AppLocalizationsHr extends AppLocalizations { String get cacheSize => 'Veličina predmemorije'; @override - String get storeName => 'Ime pohrane'; + String get storeName => 'Naziv spremišta'; @override String get noCacheStatistics => 'Statistika predmemorije nije dostupna'; @@ -992,7 +992,8 @@ class AppLocalizationsHr extends AppLocalizations { String get resetMapRotationTooltip => 'Vrati kartu na sjever'; @override - String get showMapDebugInfo => 'Prikaži debug informacije karte'; + String get showMapDebugInfo => + 'Prikaži informacije za otklanjanje pogrešaka karte'; @override String get displayZoomLevelBounds => 'Prikaži razinu zumiranja i granice'; @@ -1014,7 +1015,7 @@ class AppLocalizationsHr extends AppLocalizations { String get esriSatellite => 'ESRI satelit'; @override - String get googleHybrid => 'Google hibridno'; + String get googleHybrid => 'Google hibridna karta'; @override String get googleRoadmap => 'Google cestovna karta'; @@ -1310,12 +1311,12 @@ class AppLocalizationsHr extends AppLocalizations { @override String exportFailed(String error) { - return 'Export failed: $error'; + return 'Izvoz nije uspio: $error'; } @override String importFailed(String error) { - return 'Import failed: $error'; + return 'Uvoz nije uspio: $error'; } @override @@ -1495,7 +1496,7 @@ class AppLocalizationsHr extends AppLocalizations { String get lat => 'Šir'; @override - String get lon => 'Dug'; + String get lon => 'Duž'; @override String get useCurrentLocation => 'Koristi trenutnu lokaciju'; @@ -1563,16 +1564,16 @@ class AppLocalizationsHr extends AppLocalizations { String get nameRequired => 'Ime je obavezno'; @override - String get templateDescription => 'Description (Optional)'; + String get templateDescription => 'Opis (neobavezno)'; @override - String get templateDescriptionHint => 'Add additional context...'; + String get templateDescriptionHint => 'Dodajte dodatni kontekst...'; @override - String get templateColor => 'Color'; + String get templateColor => 'Boja'; @override - String get previewFormat => 'Preview (SAR Message Format)'; + String get previewFormat => 'Pregled (format SAR poruke)'; @override String get importFromClipboard => 'Uvezi'; @@ -1582,26 +1583,26 @@ class AppLocalizationsHr extends AppLocalizations { @override String deleteTemplateConfirmation(String name) { - return 'Delete template \'$name\'?'; + return 'Izbrisati predložak \'$name\'?'; } @override - String get templateAdded => 'Template added'; + String get templateAdded => 'Predložak dodan'; @override - String get templateUpdated => 'Template updated'; + String get templateUpdated => 'Predložak ažuriran'; @override - String get templateDeleted => 'Template deleted'; + String get templateDeleted => 'Predložak izbrisan'; @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', + other: 'Uvezeno $count predložaka', + one: 'Uvezen 1 predložak', + zero: 'Nema uvezenih predložaka', ); return '$_temp0'; } @@ -1611,8 +1612,8 @@ class AppLocalizationsHr extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Exported $count templates to clipboard', - one: 'Exported 1 template to clipboard', + other: 'Izvezeno $count predložaka u međuspremnik', + one: 'Izvezen 1 predložak u međuspremnik', ); return '$_temp0'; } @@ -1631,10 +1632,10 @@ class AppLocalizationsHr extends AppLocalizations { String get resetComplete => 'Predlošci vraćeni na zadane'; @override - String get noTemplates => 'No templates available'; + String get noTemplates => 'Nema dostupnih predložaka'; @override - String get tapAddToCreate => 'Tap + to create your first template'; + String get tapAddToCreate => 'Dodirnite + za izradu prvog predloška'; @override String get ok => 'OK'; @@ -1691,7 +1692,7 @@ class AppLocalizationsHr extends AppLocalizations { String get selectRecipient => 'Odaberi primatelja'; @override - String get broadcastToAllNearby => 'Emituj svima u blizini'; + String get broadcastToAllNearby => 'Emitiraj svima u blizini'; @override String get searchRecipients => 'Pretraži primatelje...'; @@ -1773,14 +1774,14 @@ class AppLocalizationsHr extends AppLocalizations { String get sampleObjectTrailMarker => ' Oznaka staze pronađena izvan puta'; @override - String get sampleMsgAllTeamsCheckIn => 'Svi timovi se jave'; + String get sampleMsgAllTeamsCheckIn => 'Svi timovi, javite se'; @override String get sampleMsgWeatherUpdate => 'Ažuriranje vremena: Vedro nebo, temp 18°C'; @override - String get sampleMsgBaseCamp => 'Bazni kamp uspostavljen na okupljalište'; + String get sampleMsgBaseCamp => 'Bazni kamp uspostavljen na okupljalištu'; @override String get sampleMsgTeamAlpha => 'Tim se kreće prema sektoru 2'; @@ -1893,7 +1894,7 @@ class AppLocalizationsHr extends AppLocalizations { 'Koordinate kopirane u međuspremnik'; @override - String get drawingShared => 'Crtež karte'; + String get drawingShared => 'Crtež podijeljen'; @override String get drawingHidden => 'Crtež sakriven s karte'; @@ -1915,7 +1916,7 @@ class AppLocalizationsHr extends AppLocalizations { } @override - String get shareDrawing => 'Podijeli Crtež'; + String get shareDrawing => 'Podijeli crtež'; @override String get shareWithAllNearbyDevices => @@ -1936,7 +1937,7 @@ class AppLocalizationsHr extends AppLocalizations { @override String yourDrawingsCount(int count) { - return 'Vaši Crteži ($count)'; + return 'Vaši crteži ($count)'; } @override @@ -1949,16 +1950,16 @@ class AppLocalizationsHr extends AppLocalizations { String get rectangle => 'Pravokutnik'; @override - String get updateAvailable => 'Dostupno Ažuriranje'; + String get updateAvailable => 'Dostupno ažuriranje'; @override - String get currentVersion => 'Trenutna'; + String get currentVersion => 'Trenutna verzija'; @override - String get latestVersion => 'Najnovija'; + String get latestVersion => 'Najnovija verzija'; @override - String get downloadUpdate => 'Preuzmi'; + String get downloadUpdate => 'Preuzmi ažuriranje'; @override String get updateLater => 'Kasnije'; @@ -1976,7 +1977,7 @@ class AppLocalizationsHr extends AppLocalizations { String get showForestRoads => 'Prikaži šumske ceste'; @override - String get wmsOverlays => 'WMS Prekrivanja'; + String get wmsOverlays => 'WMS prekrivanja'; @override String get recentMessages => 'Nedavne poruke'; @@ -2048,47 +2049,47 @@ class AppLocalizationsHr extends AppLocalizations { @override String get wizardWelcomeDescription => - 'Moćan alat za komunikaciju bez mreže za spasilačke operacije. Povežite se sa svojim timom koristeći mesh radio tehnologiju kada tradicionalne mreže nisu dostupne.'; + 'Moćan alat za komunikaciju izvan mreže za spasilačke operacije. Povežite se s timom uz mesh radijsku tehnologiju kada tradicionalne mreže nisu dostupne.'; @override - String get wizardConnectingTitle => 'Povezivanje s Radiom'; + String get wizardConnectingTitle => 'Povezivanje s radiom'; @override String get wizardConnectingDescription => - 'Povežite svoj pametni telefon s MeshCore radio uređajem putem Bluetootha kako biste započeli komunikaciju bez mreže.'; + 'Povežite telefon s MeshCore radijskim uređajem putem Bluetootha i započnite komunikaciju izvan mreže.'; @override - String get wizardConnectingFeature1 => 'Skeniraj obližnje MeshCore uređaje'; + String get wizardConnectingFeature1 => 'Skenira obližnje MeshCore uređaje'; @override String get wizardConnectingFeature2 => - 'Uparite se s vašim radiom putem Bluetootha'; + 'Uparivanje s radijem putem Bluetootha'; @override String get wizardConnectingFeature3 => - 'Radi potpuno offline - internet nije potreban'; + 'Radi potpuno izvan mreže — internet nije potreban'; @override - String get wizardSimpleModeTitle => 'Jednostavan Način'; + String get wizardSimpleModeTitle => 'Jednostavan način'; @override String get wizardSimpleModeDescription => - 'Novi u mesh umrežavanju? Omogućite jednostavan način za pojednostavljeno sučelje samo s bitnim funkcijama.'; + 'Prvi put koristite mesh mrežu? Uključite jednostavan način za pojednostavljeno sučelje s osnovnim funkcijama.'; @override String get wizardSimpleModeFeature1 => - 'Sučelje prijateljsko početnicima s osnovnim funkcijama'; + 'Sučelje prilagođeno početnicima s osnovnim funkcijama'; @override String get wizardSimpleModeFeature2 => - 'Prebacite na Napredni način bilo kada u Postavkama'; + 'U svakom trenutku prebacite na napredni način u Postavkama'; @override String get wizardChannelTitle => 'Kanali'; @override String get wizardChannelDescription => - 'Emitirajte poruke svima na kanalu, savršeno za objave i koordinaciju cijele ekipe.'; + 'Šaljite poruke svima na kanalu — idealno za obavijesti i koordinaciju tima.'; @override String get wizardChannelFeature1 => 'Javni kanal za opću komunikaciju ekipe'; @@ -2106,7 +2107,7 @@ class AppLocalizationsHr extends AppLocalizations { @override String get wizardContactsDescription => - 'Vaši članovi tima se automatski pojavljuju kada se pridruže mesh mreži. Pošaljite im direktne poruke ili pogledajte njihovu lokaciju.'; + 'Članovi tima se prikazuju automatski kada se pridruže mesh mreži. Šaljite im izravne poruke ili pogledajte njihovu lokaciju.'; @override String get wizardContactsFeature1 => 'Kontakti se automatski otkrivaju'; @@ -2116,26 +2117,25 @@ class AppLocalizationsHr extends AppLocalizations { @override String get wizardContactsFeature3 => - 'Pogledajte razinu baterije i vrijeme zadnjeg viđenja'; + 'Prikažite stanje baterije i vrijeme zadnje aktivnosti'; @override - String get wizardMapTitle => 'Karta & Lokacija'; + String get wizardMapTitle => 'Karta i lokacija'; @override String get wizardMapDescription => - 'Pratite svoj tim u stvarnom vremenu i označite važne lokacije za spasilačke operacije.'; + 'Pratite tim u stvarnom vremenu i označavajte ključne lokacije za spasilačke operacije.'; @override String get wizardMapFeature1 => - 'SAR oznake za pronađene osobe, požare i područja postavljanja'; + 'SAR oznake za pronađene osobe, požare i točke okupljanja'; @override String get wizardMapFeature2 => 'GPS praćenje članova tima u stvarnom vremenu'; @override - String get wizardMapFeature3 => - 'Preuzmite offline karte za udaljena područja'; + String get wizardMapFeature3 => 'Preuzmite karte za rad izvan mreže'; @override String get wizardMapFeature4 => diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 29b98e4..e91919e 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -306,7 +306,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get defaultPinInfo => - 'Privzeta PIN koda za naprave brez zaslona je 123456. Težave s seznanitvijo? Pozabite napravo bluetooth v sistemskih nastavitvah.'; + 'Privzeta PIN koda za naprave brez zaslona je 123456. Težave s seznanitvijo? Pozabite napravo Bluetooth v sistemskih nastavitvah.'; @override String get noMessagesYet => 'Še ni sporočil'; @@ -829,7 +829,7 @@ class AppLocalizationsSl extends AppLocalizations { String get teamMembers => 'Člani ekipe'; @override - String get repeaters => 'Repetitorji'; + String get repeaters => 'Ponavljalniki'; @override String get rooms => 'Sobe'; @@ -992,7 +992,8 @@ class AppLocalizationsSl extends AppLocalizations { String get resetMapRotationTooltip => 'Ponastavi zemljevid na sever'; @override - String get showMapDebugInfo => 'Prikaži debug informacije zemljevida'; + String get showMapDebugInfo => + 'Prikaži informacije za razhroščevanje zemljevida'; @override String get displayZoomLevelBounds => 'Prikaži nivo povečave in meje'; @@ -1014,7 +1015,7 @@ class AppLocalizationsSl extends AppLocalizations { String get esriSatellite => 'ESRI satelit'; @override - String get googleHybrid => 'Google hibridno'; + String get googleHybrid => 'Google hibridni zemljevid'; @override String get googleRoadmap => 'Google cestni zemljevid'; @@ -1313,12 +1314,12 @@ class AppLocalizationsSl extends AppLocalizations { @override String exportFailed(String error) { - return 'Export failed: $error'; + return 'Izvoz ni uspel: $error'; } @override String importFailed(String error) { - return 'Import failed: $error'; + return 'Uvoz ni uspel: $error'; } @override @@ -1497,7 +1498,7 @@ class AppLocalizationsSl extends AppLocalizations { String get lat => 'Šir'; @override - String get lon => 'Dol'; + String get lon => 'Dolž'; @override String get useCurrentLocation => 'Uporabi trenutno lokacijo'; @@ -1509,7 +1510,7 @@ class AppLocalizationsSl extends AppLocalizations { String get chatNode => 'Vozlišče za klepet'; @override - String get repeater => 'Ojačevalnik'; + String get repeater => 'Ponavljalnik'; @override String get roomChannel => 'Soba/Kanal'; @@ -1565,16 +1566,16 @@ class AppLocalizationsSl extends AppLocalizations { String get nameRequired => 'Ime je obvezno'; @override - String get templateDescription => 'Description (Optional)'; + String get templateDescription => 'Opis (neobvezno)'; @override - String get templateDescriptionHint => 'Add additional context...'; + String get templateDescriptionHint => 'Dodajte dodatni kontekst...'; @override - String get templateColor => 'Color'; + String get templateColor => 'Barva'; @override - String get previewFormat => 'Preview (SAR Message Format)'; + String get previewFormat => 'Predogled (oblika SAR sporočila)'; @override String get importFromClipboard => 'Uvozi'; @@ -1584,26 +1585,26 @@ class AppLocalizationsSl extends AppLocalizations { @override String deleteTemplateConfirmation(String name) { - return 'Delete template \'$name\'?'; + return 'Izbrišem predlogo \'$name\'?'; } @override - String get templateAdded => 'Template added'; + String get templateAdded => 'Predloga dodana'; @override - String get templateUpdated => 'Template updated'; + String get templateUpdated => 'Predloga posodobljena'; @override - String get templateDeleted => 'Template deleted'; + String get templateDeleted => 'Predloga izbrisana'; @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', + other: 'Uvoženih $count predlog', + one: 'Uvožena 1 predloga', + zero: 'Ni uvoženih predlog', ); return '$_temp0'; } @@ -1613,8 +1614,8 @@ class AppLocalizationsSl extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Exported $count templates to clipboard', - one: 'Exported 1 template to clipboard', + other: 'Izvoženih $count predlog v odložišče', + one: 'Izvožena 1 predloga v odložišče', ); return '$_temp0'; } @@ -1633,10 +1634,10 @@ class AppLocalizationsSl extends AppLocalizations { String get resetComplete => 'Predloge ponastavljene na privzeto'; @override - String get noTemplates => 'No templates available'; + String get noTemplates => 'Ni razpoložljivih predlog'; @override - String get tapAddToCreate => 'Tap + to create your first template'; + String get tapAddToCreate => 'Tapnite + za ustvarjanje prve predloge'; @override String get ok => 'OK'; @@ -1839,7 +1840,7 @@ class AppLocalizationsSl extends AppLocalizations { 'Helikopter za evakuacijo prihod 10 minut'; @override - String get sampleMsgEmergencyResolved => 'Nujnost rešena - vse čisto'; + String get sampleMsgEmergencyResolved => 'Nujna situacija rešena – vse čisto'; @override String get sampleMsgEmergencyStagingArea => ' Nujno zbirališče'; @@ -1894,7 +1895,7 @@ class AppLocalizationsSl extends AppLocalizations { String get coordinatesCopiedToClipboard => 'Koordinate kopirane v odložišče'; @override - String get drawingShared => 'Risba zemljevida'; + String get drawingShared => 'Risba deljena'; @override String get drawingHidden => 'Risba skrita z zemljevida'; @@ -1916,7 +1917,7 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get shareDrawing => 'Deli Risbo'; + String get shareDrawing => 'Deli risbo'; @override String get shareWithAllNearbyDevices => 'Deli z vsemi bližnjimi napravami'; @@ -1936,7 +1937,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String yourDrawingsCount(int count) { - return 'Vaše Risbe ($count)'; + return 'Vaše risbe ($count)'; } @override @@ -1949,16 +1950,16 @@ class AppLocalizationsSl extends AppLocalizations { String get rectangle => 'Pravokotnik'; @override - String get updateAvailable => 'Na Voljo Posodobitev'; + String get updateAvailable => 'Na voljo je posodobitev'; @override - String get currentVersion => 'Trenutna'; + String get currentVersion => 'Trenutna različica'; @override - String get latestVersion => 'Najnovejša'; + String get latestVersion => 'Najnovejša različica'; @override - String get downloadUpdate => 'Prenesi'; + String get downloadUpdate => 'Prenesi posodobitev'; @override String get updateLater => 'Kasneje'; @@ -1976,7 +1977,7 @@ class AppLocalizationsSl extends AppLocalizations { String get showForestRoads => 'Prikaži gozdne ceste'; @override - String get wmsOverlays => 'WMS Prekrivanja'; + String get wmsOverlays => 'WMS prekrivanja'; @override String get recentMessages => 'Nedavna sporočila'; @@ -2047,47 +2048,47 @@ class AppLocalizationsSl extends AppLocalizations { @override String get wizardWelcomeDescription => - 'Močno orodje za komunikacijo brez omrežja za reševalne operacije. Povežite se s svojo ekipo z uporabo mrežne radijske tehnologije, ko tradicionalna omrežja niso na voljo.'; + 'Zmogljivo orodje za komunikacijo brez omrežja za reševalne operacije. S svojo ekipo se povežite z mesh radijsko tehnologijo, ko tradicionalna omrežja niso na voljo.'; @override - String get wizardConnectingTitle => 'Povezava z Radijem'; + String get wizardConnectingTitle => 'Povezava z radijem'; @override String get wizardConnectingDescription => - 'Povežite svoj pametni telefon z napravo MeshCore radio preko Bluetootha za začetek komunikacije brez omrežja.'; + 'Svoj telefon povežite z radijsko napravo MeshCore prek Bluetootha in začnite komunicirati brez omrežja.'; @override - String get wizardConnectingFeature1 => 'Iskanje bližnjih naprav MeshCore'; + String get wizardConnectingFeature1 => 'Poišče bližnje naprave MeshCore'; @override String get wizardConnectingFeature2 => - 'Združite s svojim radijem preko Bluetootha'; + 'Seznanitev z radijsko napravo prek Bluetootha'; @override String get wizardConnectingFeature3 => - 'Deluje popolnoma brez povezave - internet ni potreben'; + 'Deluje povsem brez povezave — internet ni potreben'; @override - String get wizardSimpleModeTitle => 'Preprost Način'; + String get wizardSimpleModeTitle => 'Preprost način'; @override String get wizardSimpleModeDescription => - 'Novi v mrežnem omrežju? Omogočite preprost način za poenostavljen vmesnik z le bistvenimi funkcijami.'; + 'Ste novi v mesh omrežju? Vključite preprost način za poenostavljen vmesnik z osnovnimi funkcijami.'; @override String get wizardSimpleModeFeature1 => - 'Vmesnik prijazen za začetnike z osnovnimi funkcijami'; + 'Vmesnik, prilagojen začetnikom, z osnovnimi funkcijami'; @override String get wizardSimpleModeFeature2 => - 'Kadarkoli preklopite na napredni način v nastavitvah'; + 'Na napredni način lahko kadarkoli preklopite v nastavitvah'; @override String get wizardChannelTitle => 'Kanali'; @override String get wizardChannelDescription => - 'Oddajajte sporočila vsem na kanalu, popolno za objave in usklajevanje cele ekipe.'; + 'Pošiljajte sporočila vsem na kanalu — idealno za obvestila in koordinacijo ekipe.'; @override String get wizardChannelFeature1 => @@ -2099,47 +2100,47 @@ class AppLocalizationsSl extends AppLocalizations { @override String get wizardChannelFeature3 => - 'Sporočila so samodejno posredovana preko mreže'; + 'Sporočila se samodejno posredujejo prek mreže'; @override String get wizardContactsTitle => 'Stiki'; @override String get wizardContactsDescription => - 'Vaši člani ekipe se samodejno prikažejo, ko se pridružijo mrežnemu omrežju. Pošljite jim neposredna sporočila ali si oglejte njihovo lokacijo.'; + 'Člani ekipe se prikažejo samodejno, ko se pridružijo mesh omrežju. Pošiljajte jim neposredna sporočila ali si oglejte njihovo lokacijo.'; @override - String get wizardContactsFeature1 => 'Stiki se odkrijejo samodejno'; + String get wizardContactsFeature1 => 'Samodejno odkrivanje stikov'; @override String get wizardContactsFeature2 => - 'Pošiljajte zasebna neposredna sporočila'; + 'Pošiljanje zasebnih neposrednih sporočil'; @override String get wizardContactsFeature3 => - 'Oglejte si raven baterije in čas zadnjega ogleda'; + 'Prikaz stanja baterije in časa zadnje aktivnosti'; @override - String get wizardMapTitle => 'Zemljevid & Lokacija'; + String get wizardMapTitle => 'Zemljevid in lokacija'; @override String get wizardMapDescription => - 'Sledite svoji ekipi v realnem času in označite pomembne lokacije za reševalne operacije.'; + 'Spremljajte svojo ekipo v realnem času in označujte ključne lokacije za reševalne operacije.'; @override String get wizardMapFeature1 => - 'SAR označevalci za najdene osebe, požare in območja postavitve'; + 'SAR označevalci za najdene osebe, požare in zbirna mesta'; @override - String get wizardMapFeature2 => 'Sledenje GPS članov ekipe v realnem času'; + String get wizardMapFeature2 => 'Sledenje članom ekipe z GPS v realnem času'; @override String get wizardMapFeature3 => - 'Prenesite zemljevide brez povezave za oddaljene kraje'; + 'Prenesite zemljevide za uporabo brez povezave'; @override String get wizardMapFeature4 => - 'Narišite oblike in delite taktične informacije'; + 'Rišite oblike in delite taktične informacije'; @override String get viewWelcomeTutorial => 'Ogled vadnice dobrodošlice'; diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 95cec08..eab77dd 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -175,7 +175,7 @@ "locationBroadcast": "Oddajanje lokacije: {latitude}, {longitude}", - "defaultPinInfo": "Privzeta PIN koda za naprave brez zaslona je 123456. Težave s seznanitvijo? Pozabite napravo bluetooth v sistemskih nastavitvah.", + "defaultPinInfo": "Privzeta PIN koda za naprave brez zaslona je 123456. Težave s seznanitvijo? Pozabite napravo Bluetooth v sistemskih nastavitvah.", "noMessagesYet": "Še ni sporočil", @@ -185,6 +185,8 @@ "delete": "Izbriši", + + "viewOnMap": "Poglej na zemljevidu", "refresh": "Osveži", @@ -359,7 +361,7 @@ "noGpsData": "Ni GPS podatkov", - "distance": "Razdalja", + "pingingDirect": "Pinganje {name} (neposredno preko poti)...", @@ -435,6 +437,8 @@ "distance": "Razdalja", + + "bearing": "Azimut", "direction": "Smer", @@ -475,7 +479,7 @@ "teamMembers": "Člani ekipe", - "repeaters": "Repetitorji", + "repeaters": "Ponavljalniki", "rooms": "Sobe", @@ -571,7 +575,7 @@ "resetMapRotationTooltip": "Ponastavi zemljevid na sever", - "showMapDebugInfo": "Prikaži debug informacije zemljevida", + "showMapDebugInfo": "Prikaži informacije za razhroščevanje zemljevida", "displayZoomLevelBounds": "Prikaži nivo povečave in meje", @@ -585,7 +589,7 @@ "esriSatellite": "ESRI satelit", - "googleHybrid": "Google hibridno", + "googleHybrid": "Google hibridni zemljevid", "googleRoadmap": "Google cestni zemljevid", @@ -769,8 +773,6 @@ "trailHiddenRecording": "Pot je skrita (še se snema)", - "distance": "Razdalja", - "duration": "Trajanje", "points": "Točke", @@ -849,7 +851,7 @@ "lat": "Šir", - "lon": "Dol", + "lon": "Dolž", "useCurrentLocation": "Uporabi trenutno lokacijo", @@ -857,7 +859,7 @@ "chatNode": "Vozlišče za klepet", - "repeater": "Ojačevalnik", + "repeater": "Ponavljalnik", "roomChannel": "Soba/Kanal", @@ -879,28 +881,27 @@ "templateEmoji": "Emoji", "emojiRequired": "Emoji je obvezen", "nameRequired": "Ime je obvezno", - "templateDescription": "Description (Optional)", - "templateDescriptionHint": "Add additional context...", - "templateColor": "Color", - "previewFormat": "Preview (SAR Message Format)", + "templateDescription": "Opis (neobvezno)", + "templateDescriptionHint": "Dodajte dodatni kontekst...", + "templateColor": "Barva", + "previewFormat": "Predogled (oblika SAR sporočila)", "importFromClipboard": "Uvozi", "exportToClipboard": "Izvozi", - "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}", + "deleteTemplateConfirmation": "Izbrišem predlogo '{name}'?", + "templateAdded": "Predloga dodana", + "templateUpdated": "Predloga posodobljena", + "templateDeleted": "Predloga izbrisana", + "templatesImported": "{count, plural, =0{Ni uvoženih predlog} =1{Uvožena 1 predloga} other{Uvoženih {count} predlog}}", + "templatesExported": "{count, plural, =1{Izvožena 1 predloga v odložišče} other{Izvoženih {count} predlog v odložišče}}", + "importFailed": "Uvoz ni uspel: {error}", + "exportFailed": "Izvoz ni uspel: {error}", "resetToDefaults": "Ponastavi na privzeto", "resetToDefaultsConfirmation": "To bo izbrisalo vse prilagojene predloge in obnovilo 4 privzete predloge. Nadaljevati?", "reset": "Ponastavi", "resetComplete": "Predloge ponastavljene na privzeto", - "noTemplates": "No templates available", - "tapAddToCreate": "Tap + to create your first template", + "noTemplates": "Ni razpoložljivih predlog", + "tapAddToCreate": "Tapnite + za ustvarjanje prve predloge", "ok": "OK", - "delete": "Izbriši", "permissionsSection": "Dovoljenja", "locationPermission": "Dovoljenje za lokacijo", @@ -965,7 +966,7 @@ "sampleMsgPriorityHelicopter": "PRIORITETA: Potrebna podpora helikopterja", "sampleMsgMedicalTeamEnRoute": "Medicinska ekipa na poti do vaše lokacije", "sampleMsgEvacHelicopter": "Helikopter za evakuacijo prihod 10 minut", - "sampleMsgEmergencyResolved": "Nujnost rešena - vse čisto", + "sampleMsgEmergencyResolved": "Nujna situacija rešena – vse čisto", "sampleMsgEmergencyStagingArea": " Nujno zbirališče", "sampleMsgEmergencyServices": "Nujne službe obveščene in se odzivajo", "sampleAlphaTeamLead": "Vodja Ekipe", @@ -985,33 +986,33 @@ "rectangleDrawing": "Pravokotna risba", "coordinatesCopiedToClipboard": "Koordinate kopirane v odložišče", - "drawingShared": "Risba zemljevida", + "drawingShared": "Risba deljena", "drawingHidden": "Risba skrita z zemljevida", "alreadyShared": "{count, plural, =1{1 že deljeno} other{{count} že deljeno}}", "newDrawingsShared": "Deljeno {count} nov{plural} risb{plural}", - "drawingTools": "Orodja za risanje", - "shareDrawing": "Deli Risbo", + + "shareDrawing": "Deli risbo", "shareWithAllNearbyDevices": "Deli z vsemi bližnjimi napravami", "shareToRoom": "Deli v Sobo", "sendToPersistentStorage": "Pošlji v trajno shrambo sobe", "deleteDrawingConfirm": "Ali ste prepričani, da želite izbrisati to risbo?", "drawingDeleted": "Risba izbrisana", - "yourDrawingsCount": "Vaše Risbe ({count})", + "yourDrawingsCount": "Vaše risbe ({count})", "shared": "Deljeno", "line": "Črta", "rectangle": "Pravokotnik", - "updateAvailable": "Na Voljo Posodobitev", - "currentVersion": "Trenutna", - "latestVersion": "Najnovejša", - "downloadUpdate": "Prenesi", + "updateAvailable": "Na voljo je posodobitev", + "currentVersion": "Trenutna različica", + "latestVersion": "Najnovejša različica", + "downloadUpdate": "Prenesi posodobitev", "updateLater": "Kasneje", "cadastralParcels": "Katastrske parcele", "forestRoads": "Gozdne ceste", "showCadastralParcels": "Prikaži katastrske parcele", "showForestRoads": "Prikaži gozdne ceste", - "wmsOverlays": "WMS Prekrivanja", + "wmsOverlays": "WMS prekrivanja", "recentMessages": "Nedavna sporočila", @@ -1036,31 +1037,31 @@ "wizardNext": "Naprej", "wizardGetStarted": "Začni", "wizardWelcomeTitle": "Dobrodošli v MeshCore SAR", - "wizardWelcomeDescription": "Močno orodje za komunikacijo brez omrežja za reševalne operacije. Povežite se s svojo ekipo z uporabo mrežne radijske tehnologije, ko tradicionalna omrežja niso na voljo.", - "wizardConnectingTitle": "Povezava z Radijem", - "wizardConnectingDescription": "Povežite svoj pametni telefon z napravo MeshCore radio preko Bluetootha za začetek komunikacije brez omrežja.", - "wizardConnectingFeature1": "Iskanje bližnjih naprav MeshCore", - "wizardConnectingFeature2": "Združite s svojim radijem preko Bluetootha", - "wizardConnectingFeature3": "Deluje popolnoma brez povezave - internet ni potreben", - "wizardSimpleModeTitle": "Preprost Način", - "wizardSimpleModeDescription": "Novi v mrežnem omrežju? Omogočite preprost način za poenostavljen vmesnik z le bistvenimi funkcijami.", - "wizardSimpleModeFeature1": "Vmesnik prijazen za začetnike z osnovnimi funkcijami", - "wizardSimpleModeFeature2": "Kadarkoli preklopite na napredni način v nastavitvah", + "wizardWelcomeDescription": "Zmogljivo orodje za komunikacijo brez omrežja za reševalne operacije. S svojo ekipo se povežite z mesh radijsko tehnologijo, ko tradicionalna omrežja niso na voljo.", + "wizardConnectingTitle": "Povezava z radijem", + "wizardConnectingDescription": "Svoj telefon povežite z radijsko napravo MeshCore prek Bluetootha in začnite komunicirati brez omrežja.", + "wizardConnectingFeature1": "Poišče bližnje naprave MeshCore", + "wizardConnectingFeature2": "Seznanitev z radijsko napravo prek Bluetootha", + "wizardConnectingFeature3": "Deluje povsem brez povezave — internet ni potreben", + "wizardSimpleModeTitle": "Preprost način", + "wizardSimpleModeDescription": "Ste novi v mesh omrežju? Vključite preprost način za poenostavljen vmesnik z osnovnimi funkcijami.", + "wizardSimpleModeFeature1": "Vmesnik, prilagojen začetnikom, z osnovnimi funkcijami", + "wizardSimpleModeFeature2": "Na napredni način lahko kadarkoli preklopite v nastavitvah", "wizardChannelTitle": "Kanali", - "wizardChannelDescription": "Oddajajte sporočila vsem na kanalu, popolno za objave in usklajevanje cele ekipe.", + "wizardChannelDescription": "Pošiljajte sporočila vsem na kanalu — idealno za obvestila in koordinacijo ekipe.", "wizardChannelFeature1": "Javni kanal za splošno komunikacijo ekipe", "wizardChannelFeature2": "Ustvarite kanale po meri za določene skupine", - "wizardChannelFeature3": "Sporočila so samodejno posredovana preko mreže", + "wizardChannelFeature3": "Sporočila se samodejno posredujejo prek mreže", "wizardContactsTitle": "Stiki", - "wizardContactsDescription": "Vaši člani ekipe se samodejno prikažejo, ko se pridružijo mrežnemu omrežju. Pošljite jim neposredna sporočila ali si oglejte njihovo lokacijo.", - "wizardContactsFeature1": "Stiki se odkrijejo samodejno", - "wizardContactsFeature2": "Pošiljajte zasebna neposredna sporočila", - "wizardContactsFeature3": "Oglejte si raven baterije in čas zadnjega ogleda", - "wizardMapTitle": "Zemljevid & Lokacija", - "wizardMapDescription": "Sledite svoji ekipi v realnem času in označite pomembne lokacije za reševalne operacije.", - "wizardMapFeature1": "SAR označevalci za najdene osebe, požare in območja postavitve", - "wizardMapFeature2": "Sledenje GPS članov ekipe v realnem času", - "wizardMapFeature3": "Prenesite zemljevide brez povezave za oddaljene kraje", - "wizardMapFeature4": "Narišite oblike in delite taktične informacije", + "wizardContactsDescription": "Člani ekipe se prikažejo samodejno, ko se pridružijo mesh omrežju. Pošiljajte jim neposredna sporočila ali si oglejte njihovo lokacijo.", + "wizardContactsFeature1": "Samodejno odkrivanje stikov", + "wizardContactsFeature2": "Pošiljanje zasebnih neposrednih sporočil", + "wizardContactsFeature3": "Prikaz stanja baterije in časa zadnje aktivnosti", + "wizardMapTitle": "Zemljevid in lokacija", + "wizardMapDescription": "Spremljajte svojo ekipo v realnem času in označujte ključne lokacije za reševalne operacije.", + "wizardMapFeature1": "SAR označevalci za najdene osebe, požare in zbirna mesta", + "wizardMapFeature2": "Sledenje članom ekipe z GPS v realnem času", + "wizardMapFeature3": "Prenesite zemljevide za uporabo brez povezave", + "wizardMapFeature4": "Rišite oblike in delite taktične informacije", "viewWelcomeTutorial": "Ogled vadnice dobrodošlice" } diff --git a/lib/l10n/untranslated.json b/lib/l10n/untranslated.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/lib/l10n/untranslated.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/lib/providers/map_provider.dart b/lib/providers/map_provider.dart index 2430cd0..9c86a42 100644 --- a/lib/providers/map_provider.dart +++ b/lib/providers/map_provider.dart @@ -1,4 +1,5 @@ import 'package:flutter/foundation.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../models/location_trail.dart'; @@ -27,6 +28,10 @@ class MapProvider with ChangeNotifier { // Imported trail (from GPX) LocationTrail? _importedTrail; + // Download area selection + bool _isSelectingDownloadArea = false; + LatLngBounds? _downloadAreaBounds; + LatLng? get targetLocation => _targetLocation; double? get targetZoom => _targetZoom; bool get shouldAnimate => _shouldAnimate; @@ -48,6 +53,10 @@ class MapProvider with ChangeNotifier { // Imported trail getters LocationTrail? get importedTrail => _importedTrail; + // Download area getters + bool get isSelectingDownloadArea => _isSelectingDownloadArea; + LatLngBounds? get downloadAreaBounds => _downloadAreaBounds; + void navigateToLocation({ required LatLng location, double zoom = 15.0, @@ -302,4 +311,24 @@ class MapProvider with ChangeNotifier { _isTrailVisible = true; notifyListeners(); } + + /// Enter download area selection mode with initial bounds + void enterDownloadAreaMode(LatLngBounds initialBounds) { + _isSelectingDownloadArea = true; + _downloadAreaBounds = initialBounds; + notifyListeners(); + } + + /// Exit download area selection mode + void exitDownloadAreaMode() { + _isSelectingDownloadArea = false; + _downloadAreaBounds = null; + notifyListeners(); + } + + /// Update the download area bounds (while dragging/resizing) + void updateDownloadAreaBounds(LatLngBounds bounds) { + _downloadAreaBounds = bounds; + notifyListeners(); + } } diff --git a/lib/screens/map_management_screen.dart b/lib/screens/map_management_screen.dart index 80387c5..c1d3fc6 100644 --- a/lib/screens/map_management_screen.dart +++ b/lib/screens/map_management_screen.dart @@ -887,7 +887,44 @@ class _MapManagementScreenState extends State { ); } + /// Convert zoom level to user-friendly description + String _getZoomDescription(int zoom) { + if (zoom <= 5) { + return 'Continental view (very low detail)'; + } else if (zoom <= 8) { + return 'Country view (low detail)'; + } else if (zoom <= 10) { + return 'Regional view (basic detail)'; + } else if (zoom <= 12) { + return 'City view (moderate detail)'; + } else if (zoom <= 15) { + return 'Neighborhood view (good detail)'; + } else if (zoom <= 17) { + return 'Street view (high detail)'; + } else { + return 'Building view (very high detail)'; + } + } + Widget _buildDownloadCard() { + // Calculate current bounds for preview + LatLngBounds? previewBounds; + try { + final north = double.tryParse(_northController.text); + final south = double.tryParse(_southController.text); + final east = double.tryParse(_eastController.text); + final west = double.tryParse(_westController.text); + + if (north != null && south != null && east != null && west != null) { + previewBounds = LatLngBounds( + LatLng(south, west), + LatLng(north, east), + ); + } + } catch (e) { + // Invalid bounds, preview will be null + } + return Card( child: Padding( padding: const EdgeInsets.all(16), @@ -900,6 +937,80 @@ class _MapManagementScreenState extends State { ), const SizedBox(height: 16), + // Preview map (if bounds are valid) + if (previewBounds != null) ...[ + Container( + height: 200, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey[300]!), + borderRadius: BorderRadius.circular(8), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Stack( + children: [ + FlutterMap( + options: MapOptions( + initialCenter: previewBounds.center, + initialZoom: 12.0, + minZoom: 1, + maxZoom: 19, + interactionOptions: const InteractionOptions( + flags: InteractiveFlag.none, // Static preview + ), + onMapReady: () { + // Fit bounds after map is ready would require MapController + // For now, center on bounds center + }, + ), + children: [ + TileLayer( + urlTemplate: _selectedLayer.urlTemplate, + userAgentPackageName: 'com.meshcore.sar', + ), + // Blue rectangle showing download area + PolygonLayer( + polygons: [ + Polygon( + points: [ + LatLng(previewBounds.north, previewBounds.west), + LatLng(previewBounds.north, previewBounds.east), + LatLng(previewBounds.south, previewBounds.east), + LatLng(previewBounds.south, previewBounds.west), + ], + color: Colors.blue.withValues(alpha: 0.2), + borderColor: Colors.blue, + borderStrokeWidth: 3.0, + ), + ], + ), + ], + ), + // Label overlay + Positioned( + top: 8, + left: 8, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.6), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + 'Download Area Preview', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.white, + ), + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + ], + // Map Layer Selection DropdownButtonFormField( initialValue: _selectedLayer, @@ -1010,13 +1121,22 @@ class _MapManagementScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(AppLocalizations.of(context)!.minZoom(_minZoom)), + Text( + AppLocalizations.of(context)!.minZoom(_minZoom), + style: const TextStyle(fontWeight: FontWeight.w500), + ), + Text( + _getZoomDescription(_minZoom), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.grey[600], + ), + ), Slider( value: _minZoom.toDouble(), min: 1, max: 19, divisions: 18, - label: '$_minZoom', + label: '$_minZoom - ${_getZoomDescription(_minZoom)}', onChanged: _isDownloading ? null : (value) { @@ -1036,13 +1156,22 @@ class _MapManagementScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(AppLocalizations.of(context)!.maxZoom(_maxZoom)), + Text( + AppLocalizations.of(context)!.maxZoom(_maxZoom), + style: const TextStyle(fontWeight: FontWeight.w500), + ), + Text( + _getZoomDescription(_maxZoom), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.grey[600], + ), + ), Slider( value: _maxZoom.toDouble(), min: 1, max: 19, divisions: 18, - label: '$_maxZoom', + label: '$_maxZoom - ${_getZoomDescription(_maxZoom)}', onChanged: _isDownloading ? null : (value) { diff --git a/lib/screens/map_tab.dart b/lib/screens/map_tab.dart index ccfb9ea..afa00e3 100644 --- a/lib/screens/map_tab.dart +++ b/lib/screens/map_tab.dart @@ -38,6 +38,7 @@ import '../widgets/map/drawing_toolbar.dart'; import '../widgets/map/location_trail_layer.dart'; import '../widgets/map/trail_controls.dart'; import '../widgets/map/map_message_overlay.dart'; +import '../widgets/map/download_area_overlay.dart'; import '../widgets/messages/sar_update_sheet.dart'; import '../l10n/app_localizations.dart'; import 'map_management_screen.dart'; @@ -708,21 +709,10 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { try { // Get current map bounds final bounds = _mapController.camera.visibleBounds; - final currentZoom = _mapController.camera.zoom.round(); - // Navigate to Map Management screen with pre-populated data - final appProvider = context.read(); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => MapManagementScreen( - tileCacheService: appProvider.tileCacheService, - initialLayer: _currentLayer, - initialBounds: bounds, - initialZoom: currentZoom, - ), - ), - ); + // Enter download area selection mode (show preview overlay) + final mapProvider = context.read(); + mapProvider.enterDownloadAreaMode(bounds); } catch (e) { debugPrint('Error accessing map camera: $e'); } @@ -1550,13 +1540,15 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { widget.onNavigateToMessages?.call(); }, ), - // User location marker + // User location marker with directional pointer if (_markerService.generateUserLocationMarker( position: _locationService.currentPosition, + heading: _currentHeading, context: context, ) != null) _markerService.generateUserLocationMarker( position: _locationService.currentPosition, + heading: _currentHeading, context: context, )!, // Measurement point 1 marker @@ -1745,6 +1737,36 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { } }, ), + // Download area selection polygon (rendered on top when in selection mode) + Consumer( + builder: (context, mapProvider, _) { + if (!mapProvider.isSelectingDownloadArea || mapProvider.downloadAreaBounds == null) { + return const SizedBox.shrink(); + } + final bounds = mapProvider.downloadAreaBounds!; + + // Add padding to the bounds so the rectangle is visible within the screen + // Calculate 5% padding on each side + final latPadding = (bounds.north - bounds.south) * 0.05; + final lonPadding = (bounds.east - bounds.west) * 0.05; + + return PolygonLayer( + polygons: [ + Polygon( + points: [ + LatLng(bounds.north - latPadding, bounds.west + lonPadding), // Top-left + LatLng(bounds.north - latPadding, bounds.east - lonPadding), // Top-right + LatLng(bounds.south + latPadding, bounds.east - lonPadding), // Bottom-right + LatLng(bounds.south + latPadding, bounds.west + lonPadding), // Bottom-left + ], + color: Colors.blue.withValues(alpha: 0.2), + borderColor: Colors.blue, + borderStrokeWidth: 3.0, + ), + ], + ); + }, + ), ], ), ) @@ -1761,6 +1783,52 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { ], ), ), + // Download area overlay (shown when in download area selection mode) + Consumer( + builder: (context, mapProvider, _) { + if (!mapProvider.isSelectingDownloadArea || mapProvider.downloadAreaBounds == null) { + return const SizedBox.shrink(); + } + return DownloadAreaOverlay( + bounds: mapProvider.downloadAreaBounds!, + onConfirm: () { + // Navigate to map management screen with selected bounds + final bounds = mapProvider.downloadAreaBounds!; + final zoom = _mapController.camera.zoom.round(); + + // Find matching layer from MapLayer.allLayers to avoid instance mismatch + // Only pass initialLayer if it's in allLayers (standard layers only) + MapLayer? initialLayer; + try { + initialLayer = MapLayer.allLayers.firstWhere( + (layer) => layer.type == _currentLayer.type, + ); + } catch (e) { + // Current layer not in allLayers (WMS/vector), don't pass it + initialLayer = null; + } + + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => MapManagementScreen( + tileCacheService: _tileCache, + initialBounds: bounds, + initialLayer: initialLayer, + initialZoom: zoom, + ), + ), + ); + + // Exit download area selection mode + mapProvider.exitDownloadAreaMode(); + }, + onCancel: () { + // Exit download area selection mode + mapProvider.exitDownloadAreaMode(); + }, + ); + }, + ), // Exit fullscreen button - top left (only shown in fullscreen mode) if (_isFullscreen) Positioned( diff --git a/lib/services/location_tracking_service.dart b/lib/services/location_tracking_service.dart index e6accef..5450c6e 100644 --- a/lib/services/location_tracking_service.dart +++ b/lib/services/location_tracking_service.dart @@ -412,9 +412,10 @@ class LocationTrackingService { debugPrint('✅ [LocationTracking] Initial position set without broadcast'); debugPrint(' Next broadcast allowed in ${minTimeIntervalSeconds}s'); } catch (e) { - debugPrint('❌ [LocationTracking] Failed to set initial position: $e'); - onError?.call('Failed to set initial position: $e'); + debugPrint('⚠️ [LocationTracking] Failed to set initial position: $e'); + debugPrint(' Will retry on next GPS update'); // Don't mark as set on failure, so it will retry on next update + // Don't call onError - this is not critical since it will retry automatically } } diff --git a/lib/services/map_marker_service.dart b/lib/services/map_marker_service.dart index bcc0307..88839b6 100644 --- a/lib/services/map_marker_service.dart +++ b/lib/services/map_marker_service.dart @@ -5,6 +5,7 @@ import 'package:latlong2/latlong.dart'; import 'package:geolocator/geolocator.dart'; import '../models/contact.dart'; import '../models/sar_marker.dart'; +import '../widgets/map/location_pointer.dart'; /// Centralized service for map marker management. /// @@ -222,47 +223,31 @@ class MapMarkerService { }).toList(); } - /// Generate user location marker. + /// Generate user location marker with directional pointer. /// /// Parameters: /// - [position]: Current GPS position + /// - [heading]: Current heading in degrees (0-360, where 0 = North) + /// Pass null or -1 if heading unavailable /// - [context]: Build context for theme access /// /// Returns null if position is unavailable. Marker? generateUserLocationMarker({ required Position? position, + double? heading, required BuildContext context, }) { if (position == null) return null; return Marker( point: LatLng(position.latitude, position.longitude), - width: 40, - height: 40, - rotate: false, // Don't rotate with map - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.3), - shape: BoxShape.circle, - ), - child: Container( - margin: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - shape: BoxShape.circle, - boxShadow: const [ - BoxShadow( - color: Colors.black26, - blurRadius: 4, - ), - ], - ), - child: const Icon( - Icons.my_location, - color: Colors.white, - size: 16, - ), - ), + width: 60, + height: 60, + rotate: false, // Don't rotate with map - we handle rotation internally + child: LocationPointer( + heading: heading, + color: Theme.of(context).colorScheme.primary, + size: 60, ), ); } diff --git a/lib/services/meshcore_ble_service.dart b/lib/services/meshcore_ble_service.dart index 038d98b..f273703 100644 --- a/lib/services/meshcore_ble_service.dart +++ b/lib/services/meshcore_ble_service.dart @@ -468,8 +468,9 @@ class MeshCoreBleService { required double latitude, required double longitude, }) async { - // This command returns OK (0x00) response, so wait for acknowledgment - await _commandSender.writeDataAndWaitForAck( + // This command updates device's advertised location + // Fire-and-forget - no ACK needed since actual broadcast happens via sendSelfAdvert + await _commandSender.writeData( FrameBuilder.buildSetAdvertLatLon( latitude: latitude, longitude: longitude, diff --git a/lib/widgets/map/download_area_overlay.dart b/lib/widgets/map/download_area_overlay.dart new file mode 100644 index 0000000..425c34b --- /dev/null +++ b/lib/widgets/map/download_area_overlay.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; + +/// Overlay widget that displays controls for download area selection. +/// The actual polygon should be rendered inside FlutterMap's children. +class DownloadAreaOverlay extends StatelessWidget { + final LatLngBounds bounds; + final VoidCallback onConfirm; + final VoidCallback onCancel; + + const DownloadAreaOverlay({ + super.key, + required this.bounds, + required this.onConfirm, + required this.onCancel, + }); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + // Control buttons at the top + Positioned( + top: 16, + left: 16, + right: 16, + child: Card( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Download Area Selection', + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'The blue rectangle shows the area to be downloaded. ' + 'To change the area, tap Cancel and select download again.', + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: onCancel, + icon: const Icon(Icons.close), + label: const Text('Cancel'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: FilledButton.icon( + onPressed: onConfirm, + icon: const Icon(Icons.check), + label: const Text('Confirm'), + ), + ), + ], + ), + ], + ), + ), + ), + ), + + // Area info at the bottom + Positioned( + bottom: 16, + left: 16, + right: 16, + child: Card( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Area Bounds', + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(height: 4), + Text( + 'N: ${bounds.north.toStringAsFixed(4)}° ' + 'S: ${bounds.south.toStringAsFixed(4)}°', + style: Theme.of(context).textTheme.bodySmall, + ), + Text( + 'E: ${bounds.east.toStringAsFixed(4)}° ' + 'W: ${bounds.west.toStringAsFixed(4)}°', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/map/location_pointer.dart b/lib/widgets/map/location_pointer.dart new file mode 100644 index 0000000..cdf388e --- /dev/null +++ b/lib/widgets/map/location_pointer.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; + +/// A navigation arrow pointer that indicates the user's location and direction of travel. +/// +/// The pointer consists of: +/// - An outer semi-transparent circle representing GPS accuracy +/// - An inner triangular arrow pointing in the direction of travel/heading +/// - Optional rotation based on compass or GPS heading +class LocationPointer extends StatelessWidget { + /// The heading in degrees (0-360, where 0 = North, 90 = East) + /// If null or -1, the pointer will not rotate + final double? heading; + + /// The primary color for the pointer + final Color color; + + /// The size of the entire widget + final double size; + + const LocationPointer({ + super.key, + this.heading, + required this.color, + this.size = 40.0, + }); + + @override + Widget build(BuildContext context) { + // Determine if we have valid heading data + final hasValidHeading = heading != null && heading! >= 0; + + // Calculate rotation angle (convert heading to radians) + final rotationAngle = hasValidHeading ? (heading! * 3.14159 / 180.0) : 0.0; + + return SizedBox( + width: size, + height: size, + child: Stack( + alignment: Alignment.center, + children: [ + // Outer accuracy circle (very subtle, light blue) + Container( + width: size * 0.6, + height: size * 0.6, + decoration: BoxDecoration( + color: const Color(0xFF4285F4).withValues(alpha: 0.2), + shape: BoxShape.circle, + ), + ), + // Inner rotatable arrow pointer (much larger - 90% of size) + Transform.rotate( + angle: rotationAngle, + child: CustomPaint( + size: Size(size * 0.9, size * 0.9), + painter: _NavigationPointerPainter( + color: color, + hasValidHeading: hasValidHeading, + ), + ), + ), + ], + ), + ); + } +} + +/// Custom painter that draws a navigation arrow pointer +class _NavigationPointerPainter extends CustomPainter { + final Color color; + final bool hasValidHeading; + + _NavigationPointerPainter({ + required this.color, + required this.hasValidHeading, + }); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + final width = size.width; + final height = size.height; + + if (hasValidHeading) { + // Create navigation arrow with V-shaped cutout at bottom + final arrowPath = Path(); + + // Top point (sharp tip) + arrowPath.moveTo(center.dx, height * 0.08); + + // Right side down to bottom right + arrowPath.lineTo(center.dx + width * 0.42, height * 0.92); + + // V-cutout at bottom - right side to center + arrowPath.lineTo(center.dx, height * 0.70); + + // V-cutout - center to left side + arrowPath.lineTo(center.dx - width * 0.42, height * 0.92); + + // Left side back up to top + arrowPath.lineTo(center.dx, height * 0.08); + + arrowPath.close(); + + // Draw shadow for depth + final shadowPaint = Paint() + ..color = Colors.black.withValues(alpha: 0.25) + ..style = PaintingStyle.fill + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4); + + canvas.save(); + canvas.translate(2, 2); + canvas.drawPath(arrowPath, shadowPaint); + canvas.restore(); + + // Left side (lighter cyan) - #75D5F0 + final leftSidePath = Path(); + leftSidePath.moveTo(center.dx, height * 0.08); + leftSidePath.lineTo(center.dx - width * 0.42, height * 0.92); + leftSidePath.lineTo(center.dx, height * 0.70); + leftSidePath.close(); + + final leftPaint = Paint() + ..color = const Color(0xFF75D5F0) + ..style = PaintingStyle.fill; + canvas.drawPath(leftSidePath, leftPaint); + + // Right side (darker cyan) - #00B8E6 + final rightSidePath = Path(); + rightSidePath.moveTo(center.dx, height * 0.08); + rightSidePath.lineTo(center.dx, height * 0.70); + rightSidePath.lineTo(center.dx + width * 0.42, height * 0.92); + rightSidePath.close(); + + final rightPaint = Paint() + ..color = const Color(0xFF00B8E6) + ..style = PaintingStyle.fill; + canvas.drawPath(rightSidePath, rightPaint); + + // Optional: Draw white border for contrast + final borderPaint = Paint() + ..color = Colors.white + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0 + ..strokeJoin = StrokeJoin.round; + canvas.drawPath(arrowPath, borderPaint); + + } else { + // No heading available - draw a blue circle with white border + final circlePaint = Paint() + ..color = const Color(0xFF4285F4) + ..style = PaintingStyle.fill; + + canvas.drawCircle(center, width * 0.4, circlePaint); + + // White border + final borderPaint = Paint() + ..color = Colors.white + ..style = PaintingStyle.stroke + ..strokeWidth = 2.5; + canvas.drawCircle(center, width * 0.4, borderPaint); + + // Center white dot + final centerDot = Paint() + ..color = Colors.white + ..style = PaintingStyle.fill; + canvas.drawCircle(center, width * 0.15, centerDot); + } + } + + @override + bool shouldRepaint(_NavigationPointerPainter oldDelegate) { + return oldDelegate.color != color || + oldDelegate.hasValidHeading != hasValidHeading; + } +}