mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Refactor localization in UI components and error messages
- Updated various UI components to utilize AppLocalizations for better internationalization support. - Replaced hardcoded strings with localized strings in home_screen.dart, ble_connection_manager.dart, contact_tile.dart, room_login_sheet.dart, compass widgets, and sar_update_sheet.dart. - Improved user feedback messages by integrating localization for actions like pinging contacts, displaying connection statuses, and error notifications. - Enhanced the clarity of UI elements by ensuring all text is translatable, improving the overall user experience for non-English speakers.
This commit is contained in:
@@ -677,5 +677,496 @@
|
|||||||
"typeYourMessage": "Type your message...",
|
"typeYourMessage": "Type your message...",
|
||||||
"@typeYourMessage": {
|
"@typeYourMessage": {
|
||||||
"description": "Placeholder text for message input field"
|
"description": "Placeholder text for message input field"
|
||||||
|
},
|
||||||
|
|
||||||
|
"quickLocationMarker": "Quick location marker",
|
||||||
|
"@quickLocationMarker": {
|
||||||
|
"description": "Subtitle for SAR marker sheet header"
|
||||||
|
},
|
||||||
|
|
||||||
|
"markerType": "Marker Type",
|
||||||
|
"@markerType": {
|
||||||
|
"description": "Label for marker type selection section"
|
||||||
|
},
|
||||||
|
|
||||||
|
"sendTo": "Send To",
|
||||||
|
"@sendTo": {
|
||||||
|
"description": "Label for destination selection section"
|
||||||
|
},
|
||||||
|
|
||||||
|
"noDestinationsAvailable": "No destinations available.",
|
||||||
|
"@noDestinationsAvailable": {
|
||||||
|
"description": "Warning when no rooms or channels exist"
|
||||||
|
},
|
||||||
|
|
||||||
|
"selectDestination": "Select destination...",
|
||||||
|
"@selectDestination": {
|
||||||
|
"description": "Placeholder for destination dropdown"
|
||||||
|
},
|
||||||
|
|
||||||
|
"ephemeralBroadcastInfo": "Ephemeral: Broadcast over-the-air only. Not stored - nodes must be online.",
|
||||||
|
"@ephemeralBroadcastInfo": {
|
||||||
|
"description": "Information about ephemeral channel broadcasts"
|
||||||
|
},
|
||||||
|
|
||||||
|
"persistentRoomInfo": "Persistent: Stored immutably in room. Synced automatically and preserved offline.",
|
||||||
|
"@persistentRoomInfo": {
|
||||||
|
"description": "Information about persistent room storage"
|
||||||
|
},
|
||||||
|
|
||||||
|
"location": "Location",
|
||||||
|
"@location": {
|
||||||
|
"description": "Label for location section"
|
||||||
|
},
|
||||||
|
|
||||||
|
"fromMap": "From Map",
|
||||||
|
"@fromMap": {
|
||||||
|
"description": "Badge showing location is from map tap"
|
||||||
|
},
|
||||||
|
|
||||||
|
"gettingLocation": "Getting location...",
|
||||||
|
"@gettingLocation": {
|
||||||
|
"description": "Loading message while fetching GPS location"
|
||||||
|
},
|
||||||
|
|
||||||
|
"locationError": "Location Error",
|
||||||
|
"@locationError": {
|
||||||
|
"description": "Title for location error messages"
|
||||||
|
},
|
||||||
|
|
||||||
|
"retry": "Retry",
|
||||||
|
"@retry": {
|
||||||
|
"description": "Retry button label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"refreshLocation": "Refresh location",
|
||||||
|
"@refreshLocation": {
|
||||||
|
"description": "Tooltip for refresh location button"
|
||||||
|
},
|
||||||
|
|
||||||
|
"accuracyMeters": "Accuracy: ±{accuracy}m",
|
||||||
|
"@accuracyMeters": {
|
||||||
|
"description": "Display of GPS accuracy in meters",
|
||||||
|
"placeholders": {
|
||||||
|
"accuracy": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"notesOptional": "Notes (optional)",
|
||||||
|
"@notesOptional": {
|
||||||
|
"description": "Label for optional notes field"
|
||||||
|
},
|
||||||
|
|
||||||
|
"addAdditionalInformation": "Add additional information...",
|
||||||
|
"@addAdditionalInformation": {
|
||||||
|
"description": "Placeholder for notes field"
|
||||||
|
},
|
||||||
|
|
||||||
|
"lowAccuracyWarning": "Location accuracy is ±{accuracy}m. This may not be accurate enough for SAR operations.\n\nContinue anyway?",
|
||||||
|
"@lowAccuracyWarning": {
|
||||||
|
"description": "Warning dialog content for low GPS accuracy",
|
||||||
|
"placeholders": {
|
||||||
|
"accuracy": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"loginToRoom": "Login to Room",
|
||||||
|
"@loginToRoom": {
|
||||||
|
"description": "Title for room login dialog"
|
||||||
|
},
|
||||||
|
|
||||||
|
"enterPasswordInfo": "Enter the password to access this room. The password will be saved for future use.",
|
||||||
|
"@enterPasswordInfo": {
|
||||||
|
"description": "Information about room password"
|
||||||
|
},
|
||||||
|
|
||||||
|
"password": "Password",
|
||||||
|
"@password": {
|
||||||
|
"description": "Password field label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"enterRoomPassword": "Enter room password",
|
||||||
|
"@enterRoomPassword": {
|
||||||
|
"description": "Password field hint"
|
||||||
|
},
|
||||||
|
|
||||||
|
"loggingInDots": "Logging in...",
|
||||||
|
"@loggingInDots": {
|
||||||
|
"description": "Button text while logging in"
|
||||||
|
},
|
||||||
|
|
||||||
|
"login": "Login",
|
||||||
|
"@login": {
|
||||||
|
"description": "Login button label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"failedToAddRoom": "Failed to add room to device: {error}\n\nThe room may not have advertised yet.\nTry waiting for the room to broadcast.",
|
||||||
|
"@failedToAddRoom": {
|
||||||
|
"description": "Error message when adding room fails",
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"direct": "Direct",
|
||||||
|
"@direct": {
|
||||||
|
"description": "Direct routing indicator"
|
||||||
|
},
|
||||||
|
|
||||||
|
"flood": "Flood",
|
||||||
|
"@flood": {
|
||||||
|
"description": "Flood routing indicator"
|
||||||
|
},
|
||||||
|
|
||||||
|
"admin": "Admin",
|
||||||
|
"@admin": {
|
||||||
|
"description": "Admin badge label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"loggedIn": "Logged In",
|
||||||
|
"@loggedIn": {
|
||||||
|
"description": "Logged in status badge"
|
||||||
|
},
|
||||||
|
|
||||||
|
"noGpsData": "No GPS data",
|
||||||
|
"@noGpsData": {
|
||||||
|
"description": "Message when GPS data is not available"
|
||||||
|
},
|
||||||
|
|
||||||
|
"distance": "Distance",
|
||||||
|
"@distance": {
|
||||||
|
"description": "Distance label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"pingingDirect": "Pinging {name} (direct via path)...",
|
||||||
|
"@pingingDirect": {
|
||||||
|
"description": "Status message for direct ping",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"pingingFlood": "Pinging {name} (flooding - no path)...",
|
||||||
|
"@pingingFlood": {
|
||||||
|
"description": "Status message for flood ping",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"directPingTimeout": "Direct ping timeout - retrying {name} with flooding...",
|
||||||
|
"@directPingTimeout": {
|
||||||
|
"description": "Warning when direct ping times out",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"pingSuccessful": "Ping successful to {name}{fallback}",
|
||||||
|
"@pingSuccessful": {
|
||||||
|
"description": "Success message for ping",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"fallback": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"viaFloodingFallback": " (via flooding fallback)",
|
||||||
|
"@viaFloodingFallback": {
|
||||||
|
"description": "Suffix for ping success with fallback"
|
||||||
|
},
|
||||||
|
|
||||||
|
"pingFailed": "Ping failed to {name} - no response received",
|
||||||
|
"@pingFailed": {
|
||||||
|
"description": "Error message when ping fails",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"deleteContactConfirmation": "Are you sure you want to delete \"{name}\"?\n\nThis will remove the contact from both the app and the companion radio device.",
|
||||||
|
"@deleteContactConfirmation": {
|
||||||
|
"description": "Confirmation message for deleting contact",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"removingContact": "Removing {name}...",
|
||||||
|
"@removingContact": {
|
||||||
|
"description": "Status message while removing contact",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"contactRemoved": "Contact \"{name}\" removed",
|
||||||
|
"@contactRemoved": {
|
||||||
|
"description": "Success message after removing contact",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"failedToRemoveContact": "Failed to remove contact: {error}",
|
||||||
|
"@failedToRemoveContact": {
|
||||||
|
"description": "Error message when contact removal fails",
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"type": "Type",
|
||||||
|
"@type": {
|
||||||
|
"description": "Contact type label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"publicKey": "Public Key",
|
||||||
|
"@publicKey": {
|
||||||
|
"description": "Public key label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"lastSeen": "Last Seen",
|
||||||
|
"@lastSeen": {
|
||||||
|
"description": "Last seen label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"roomStatus": "Room Status",
|
||||||
|
"@roomStatus": {
|
||||||
|
"description": "Room status section header"
|
||||||
|
},
|
||||||
|
|
||||||
|
"loginStatus": "Login Status",
|
||||||
|
"@loginStatus": {
|
||||||
|
"description": "Login status label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"notLoggedIn": "Not Logged In",
|
||||||
|
"@notLoggedIn": {
|
||||||
|
"description": "Not logged in status"
|
||||||
|
},
|
||||||
|
|
||||||
|
"adminAccess": "Admin Access",
|
||||||
|
"@adminAccess": {
|
||||||
|
"description": "Admin access label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"yes": "Yes",
|
||||||
|
"@yes": {
|
||||||
|
"description": "Yes answer"
|
||||||
|
},
|
||||||
|
|
||||||
|
"no": "No",
|
||||||
|
"@no": {
|
||||||
|
"description": "No answer"
|
||||||
|
},
|
||||||
|
|
||||||
|
"permissions": "Permissions",
|
||||||
|
"@permissions": {
|
||||||
|
"description": "Permissions label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"passwordSaved": "Password Saved",
|
||||||
|
"@passwordSaved": {
|
||||||
|
"description": "Password saved label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"locationColon": "Location:",
|
||||||
|
"@locationColon": {
|
||||||
|
"description": "Location section header"
|
||||||
|
},
|
||||||
|
|
||||||
|
"telemetry": "Telemetry",
|
||||||
|
"@telemetry": {
|
||||||
|
"description": "Telemetry section header"
|
||||||
|
},
|
||||||
|
|
||||||
|
"requestingTelemetry": "Requesting telemetry from {name}...",
|
||||||
|
"@requestingTelemetry": {
|
||||||
|
"description": "Status message while requesting telemetry",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"voltage": "Voltage",
|
||||||
|
"@voltage": {
|
||||||
|
"description": "Voltage label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"battery": "Battery",
|
||||||
|
"@battery": {
|
||||||
|
"description": "Battery label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"temperature": "Temperature",
|
||||||
|
"@temperature": {
|
||||||
|
"description": "Temperature label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"humidity": "Humidity",
|
||||||
|
"@humidity": {
|
||||||
|
"description": "Humidity label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"pressure": "Pressure",
|
||||||
|
"@pressure": {
|
||||||
|
"description": "Pressure label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"gpsTelemetry": "GPS (Telemetry)",
|
||||||
|
"@gpsTelemetry": {
|
||||||
|
"description": "GPS from telemetry label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"updated": "Updated",
|
||||||
|
"@updated": {
|
||||||
|
"description": "Updated timestamp label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"pathResetInfo": "Path reset for {name}. Next message will find a new route.",
|
||||||
|
"@pathResetInfo": {
|
||||||
|
"description": "Info message after path reset",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"reLoginToRoom": "Re-Login to Room",
|
||||||
|
"@reLoginToRoom": {
|
||||||
|
"description": "Button to re-login to room"
|
||||||
|
},
|
||||||
|
|
||||||
|
"heading": "Heading",
|
||||||
|
"@heading": {
|
||||||
|
"description": "Compass heading label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"elevation": "Elevation",
|
||||||
|
"@elevation": {
|
||||||
|
"description": "Elevation/altitude label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"accuracy": "Accuracy",
|
||||||
|
"@accuracy": {
|
||||||
|
"description": "GPS accuracy label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"filterMarkers": "Filter Markers",
|
||||||
|
"@filterMarkers": {
|
||||||
|
"description": "Title for filter markers dialog"
|
||||||
|
},
|
||||||
|
|
||||||
|
"filterMarkersTooltip": "Filter markers",
|
||||||
|
"@filterMarkersTooltip": {
|
||||||
|
"description": "Tooltip for filter button"
|
||||||
|
},
|
||||||
|
|
||||||
|
"contactsFilter": "Contacts",
|
||||||
|
"@contactsFilter": {
|
||||||
|
"description": "Filter option for contacts"
|
||||||
|
},
|
||||||
|
|
||||||
|
"sarMarkers": "SAR Markers",
|
||||||
|
"@sarMarkers": {
|
||||||
|
"description": "SAR markers section header"
|
||||||
|
},
|
||||||
|
|
||||||
|
"foundPerson": "Found Person",
|
||||||
|
"@foundPerson": {
|
||||||
|
"description": "Found person SAR marker type"
|
||||||
|
},
|
||||||
|
|
||||||
|
"fire": "Fire",
|
||||||
|
"@fire": {
|
||||||
|
"description": "Fire SAR marker type"
|
||||||
|
},
|
||||||
|
|
||||||
|
"stagingArea": "Staging Area",
|
||||||
|
"@stagingArea": {
|
||||||
|
"description": "Staging area SAR marker type"
|
||||||
|
},
|
||||||
|
|
||||||
|
"showAll": "Show All",
|
||||||
|
"@showAll": {
|
||||||
|
"description": "Button to show all filters"
|
||||||
|
},
|
||||||
|
|
||||||
|
"nearbyContacts": "Nearby Contacts",
|
||||||
|
"@nearbyContacts": {
|
||||||
|
"description": "Title for nearby contacts list in compass"
|
||||||
|
},
|
||||||
|
|
||||||
|
"locationUnavailable": "Location unavailable",
|
||||||
|
"@locationUnavailable": {
|
||||||
|
"description": "Message when GPS location is unavailable"
|
||||||
|
},
|
||||||
|
|
||||||
|
"ahead": "ahead",
|
||||||
|
"@ahead": {
|
||||||
|
"description": "Relative bearing direction - ahead"
|
||||||
|
},
|
||||||
|
|
||||||
|
"degreesRight": "{degrees}° right",
|
||||||
|
"@degreesRight": {
|
||||||
|
"description": "Relative bearing direction - right",
|
||||||
|
"placeholders": {
|
||||||
|
"degrees": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"degreesLeft": "{degrees}° left",
|
||||||
|
"@degreesLeft": {
|
||||||
|
"description": "Relative bearing direction - left",
|
||||||
|
"placeholders": {
|
||||||
|
"degrees": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"latLonFormat": "Lat: {latitude} Lon: {longitude}",
|
||||||
|
"@latLonFormat": {
|
||||||
|
"description": "Latitude and longitude display format",
|
||||||
|
"placeholders": {
|
||||||
|
"latitude": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"longitude": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -231,5 +231,165 @@
|
|||||||
|
|
||||||
"directMessageInfo": "Ova poruka će biti poslana izravno {contactName}. Također će se prikazati u glavnom feedu poruka.",
|
"directMessageInfo": "Ova poruka će biti poslana izravno {contactName}. Također će se prikazati u glavnom feedu poruka.",
|
||||||
|
|
||||||
"typeYourMessage": "Upišite svoju poruku..."
|
"typeYourMessage": "Upišite svoju poruku...",
|
||||||
|
|
||||||
|
"quickLocationMarker": "Brzi označitelj lokacije",
|
||||||
|
|
||||||
|
"markerType": "Vrsta markera",
|
||||||
|
|
||||||
|
"sendTo": "Pošalji na",
|
||||||
|
|
||||||
|
"noDestinationsAvailable": "Nema dostupnih odredišta.",
|
||||||
|
|
||||||
|
"selectDestination": "Odaberite odredište...",
|
||||||
|
|
||||||
|
"ephemeralBroadcastInfo": "Privremeno: Samo emitiranje. Nije pohranjeno - čvorovi moraju biti online.",
|
||||||
|
|
||||||
|
"persistentRoomInfo": "Trajno: Nepromjenjivo pohranjeno u sobi. Automatski sinkronizirano i očuvano offline.",
|
||||||
|
|
||||||
|
"location": "Lokacija",
|
||||||
|
|
||||||
|
"fromMap": "S karte",
|
||||||
|
|
||||||
|
"gettingLocation": "Dohvaćanje lokacije...",
|
||||||
|
|
||||||
|
"locationError": "Greška lokacije",
|
||||||
|
|
||||||
|
"retry": "Pokušaj ponovno",
|
||||||
|
|
||||||
|
"refreshLocation": "Osvježi lokaciju",
|
||||||
|
|
||||||
|
"accuracyMeters": "Točnost: ±{accuracy}m",
|
||||||
|
|
||||||
|
"notesOptional": "Napomene (opcionalno)",
|
||||||
|
|
||||||
|
"addAdditionalInformation": "Dodajte dodatne informacije...",
|
||||||
|
|
||||||
|
"lowAccuracyWarning": "Točnost lokacije je ±{accuracy}m. Ovo možda nije dovoljno precizno za SAR operacije.\n\nNastaviti svejedno?",
|
||||||
|
|
||||||
|
"loginToRoom": "Prijava u sobu",
|
||||||
|
|
||||||
|
"enterPasswordInfo": "Unesite lozinku za pristup ovoj sobi. Lozinka će biti spremljena za buduću upotrebu.",
|
||||||
|
|
||||||
|
"password": "Lozinka",
|
||||||
|
|
||||||
|
"enterRoomPassword": "Unesite lozinku sobe",
|
||||||
|
|
||||||
|
"loggingInDots": "Prijavljivanje...",
|
||||||
|
|
||||||
|
"login": "Prijava",
|
||||||
|
|
||||||
|
"failedToAddRoom": "Neuspjelo dodavanje sobe na uređaj: {error}\n\nSoba možda još nije oglašena.\nPokušajte pričekati da soba emitira.",
|
||||||
|
|
||||||
|
"direct": "Izravno",
|
||||||
|
|
||||||
|
"flood": "Poplava",
|
||||||
|
|
||||||
|
"admin": "Administrator",
|
||||||
|
|
||||||
|
"loggedIn": "Prijavljen",
|
||||||
|
|
||||||
|
"noGpsData": "Nema GPS podataka",
|
||||||
|
|
||||||
|
"distance": "Udaljenost",
|
||||||
|
|
||||||
|
"pingingDirect": "Pingiranje {name} (izravno putem puta)...",
|
||||||
|
|
||||||
|
"pingingFlood": "Pingiranje {name} (poplava - nema puta)...",
|
||||||
|
|
||||||
|
"directPingTimeout": "Istek izravnog pinga - ponovni pokušaj {name} s poplavom...",
|
||||||
|
|
||||||
|
"pingSuccessful": "Ping uspješan prema {name}{fallback}",
|
||||||
|
|
||||||
|
"viaFloodingFallback": " (putem rezervnog plavljenja)",
|
||||||
|
|
||||||
|
"pingFailed": "Ping neuspješan prema {name} - nije primljen odgovor",
|
||||||
|
|
||||||
|
"deleteContactConfirmation": "Jeste li sigurni da želite izbrisati \"{name}\"?\n\nOvo će ukloniti kontakt iz aplikacije i pratećeg radio uređaja.",
|
||||||
|
|
||||||
|
"removingContact": "Uklanjanje {name}...",
|
||||||
|
|
||||||
|
"contactRemoved": "Kontakt \"{name}\" uklonjen",
|
||||||
|
|
||||||
|
"failedToRemoveContact": "Neuspjelo uklanjanje kontakta: {error}",
|
||||||
|
|
||||||
|
"type": "Vrsta",
|
||||||
|
|
||||||
|
"publicKey": "Javni ključ",
|
||||||
|
|
||||||
|
"lastSeen": "Zadnje viđen",
|
||||||
|
|
||||||
|
"roomStatus": "Status sobe",
|
||||||
|
|
||||||
|
"loginStatus": "Status prijave",
|
||||||
|
|
||||||
|
"notLoggedIn": "Nije prijavljen",
|
||||||
|
|
||||||
|
"adminAccess": "Administratorski pristup",
|
||||||
|
|
||||||
|
"yes": "Da",
|
||||||
|
|
||||||
|
"no": "Ne",
|
||||||
|
|
||||||
|
"permissions": "Dopuštenja",
|
||||||
|
|
||||||
|
"passwordSaved": "Lozinka spremljena",
|
||||||
|
|
||||||
|
"locationColon": "Lokacija:",
|
||||||
|
|
||||||
|
"telemetry": "Telemetrija",
|
||||||
|
|
||||||
|
"requestingTelemetry": "Zahtijevanje telemetrije od {name}...",
|
||||||
|
|
||||||
|
"voltage": "Napon",
|
||||||
|
|
||||||
|
"battery": "Baterija",
|
||||||
|
|
||||||
|
"temperature": "Temperatura",
|
||||||
|
|
||||||
|
"humidity": "Vlažnost",
|
||||||
|
|
||||||
|
"pressure": "Tlak",
|
||||||
|
|
||||||
|
"gpsTelemetry": "GPS (Telemetrija)",
|
||||||
|
|
||||||
|
"updated": "Ažurirano",
|
||||||
|
|
||||||
|
"pathResetInfo": "Put resetiran za {name}. Sljedeća poruka će pronaći novu rutu.",
|
||||||
|
|
||||||
|
"reLoginToRoom": "Ponovna prijava u sobu",
|
||||||
|
|
||||||
|
"heading": "Smjer",
|
||||||
|
|
||||||
|
"elevation": "Nadmorska visina",
|
||||||
|
|
||||||
|
"accuracy": "Točnost",
|
||||||
|
|
||||||
|
"filterMarkers": "Filtriraj markere",
|
||||||
|
|
||||||
|
"filterMarkersTooltip": "Filtriraj markere",
|
||||||
|
|
||||||
|
"contactsFilter": "Kontakti",
|
||||||
|
|
||||||
|
"sarMarkers": "SAR markeri",
|
||||||
|
|
||||||
|
"foundPerson": "Pronađena osoba",
|
||||||
|
|
||||||
|
"fire": "Požar",
|
||||||
|
|
||||||
|
"stagingArea": "Zbirno mjesto",
|
||||||
|
|
||||||
|
"showAll": "Prikaži sve",
|
||||||
|
|
||||||
|
"nearbyContacts": "Obližnji kontakti",
|
||||||
|
|
||||||
|
"locationUnavailable": "Lokacija nije dostupna",
|
||||||
|
|
||||||
|
"ahead": "naprijed",
|
||||||
|
|
||||||
|
"degreesRight": "{degrees}° desno",
|
||||||
|
|
||||||
|
"degreesLeft": "{degrees}° lijevo",
|
||||||
|
|
||||||
|
"latLonFormat": "Ššir: {latitude} Dužina: {longitude}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -800,6 +800,486 @@ abstract class AppLocalizations {
|
|||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Type your message...'**
|
/// **'Type your message...'**
|
||||||
String get typeYourMessage;
|
String get typeYourMessage;
|
||||||
|
|
||||||
|
/// Subtitle for SAR marker sheet header
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Quick location marker'**
|
||||||
|
String get quickLocationMarker;
|
||||||
|
|
||||||
|
/// Label for marker type selection section
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Marker Type'**
|
||||||
|
String get markerType;
|
||||||
|
|
||||||
|
/// Label for destination selection section
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Send To'**
|
||||||
|
String get sendTo;
|
||||||
|
|
||||||
|
/// Warning when no rooms or channels exist
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No destinations available.'**
|
||||||
|
String get noDestinationsAvailable;
|
||||||
|
|
||||||
|
/// Placeholder for destination dropdown
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Select destination...'**
|
||||||
|
String get selectDestination;
|
||||||
|
|
||||||
|
/// Information about ephemeral channel broadcasts
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Ephemeral: Broadcast over-the-air only. Not stored - nodes must be online.'**
|
||||||
|
String get ephemeralBroadcastInfo;
|
||||||
|
|
||||||
|
/// Information about persistent room storage
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Persistent: Stored immutably in room. Synced automatically and preserved offline.'**
|
||||||
|
String get persistentRoomInfo;
|
||||||
|
|
||||||
|
/// Label for location section
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Location'**
|
||||||
|
String get location;
|
||||||
|
|
||||||
|
/// Badge showing location is from map tap
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'From Map'**
|
||||||
|
String get fromMap;
|
||||||
|
|
||||||
|
/// Loading message while fetching GPS location
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Getting location...'**
|
||||||
|
String get gettingLocation;
|
||||||
|
|
||||||
|
/// Title for location error messages
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Location Error'**
|
||||||
|
String get locationError;
|
||||||
|
|
||||||
|
/// Retry button label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Retry'**
|
||||||
|
String get retry;
|
||||||
|
|
||||||
|
/// Tooltip for refresh location button
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Refresh location'**
|
||||||
|
String get refreshLocation;
|
||||||
|
|
||||||
|
/// Display of GPS accuracy in meters
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Accuracy: ±{accuracy}m'**
|
||||||
|
String accuracyMeters(int accuracy);
|
||||||
|
|
||||||
|
/// Label for optional notes field
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Notes (optional)'**
|
||||||
|
String get notesOptional;
|
||||||
|
|
||||||
|
/// Placeholder for notes field
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Add additional information...'**
|
||||||
|
String get addAdditionalInformation;
|
||||||
|
|
||||||
|
/// Warning dialog content for low GPS accuracy
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Location accuracy is ±{accuracy}m. This may not be accurate enough for SAR operations.\n\nContinue anyway?'**
|
||||||
|
String lowAccuracyWarning(int accuracy);
|
||||||
|
|
||||||
|
/// Title for room login dialog
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Login to Room'**
|
||||||
|
String get loginToRoom;
|
||||||
|
|
||||||
|
/// Information about room password
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Enter the password to access this room. The password will be saved for future use.'**
|
||||||
|
String get enterPasswordInfo;
|
||||||
|
|
||||||
|
/// Password field label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Password'**
|
||||||
|
String get password;
|
||||||
|
|
||||||
|
/// Password field hint
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Enter room password'**
|
||||||
|
String get enterRoomPassword;
|
||||||
|
|
||||||
|
/// Button text while logging in
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Logging in...'**
|
||||||
|
String get loggingInDots;
|
||||||
|
|
||||||
|
/// Login button label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Login'**
|
||||||
|
String get login;
|
||||||
|
|
||||||
|
/// Error message when adding room fails
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Failed to add room to device: {error}\n\nThe room may not have advertised yet.\nTry waiting for the room to broadcast.'**
|
||||||
|
String failedToAddRoom(String error);
|
||||||
|
|
||||||
|
/// Direct routing indicator
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Direct'**
|
||||||
|
String get direct;
|
||||||
|
|
||||||
|
/// Flood routing indicator
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Flood'**
|
||||||
|
String get flood;
|
||||||
|
|
||||||
|
/// Admin badge label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Admin'**
|
||||||
|
String get admin;
|
||||||
|
|
||||||
|
/// Logged in status badge
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Logged In'**
|
||||||
|
String get loggedIn;
|
||||||
|
|
||||||
|
/// Message when GPS data is not available
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No GPS data'**
|
||||||
|
String get noGpsData;
|
||||||
|
|
||||||
|
/// Distance label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Distance'**
|
||||||
|
String get distance;
|
||||||
|
|
||||||
|
/// Status message for direct ping
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Pinging {name} (direct via path)...'**
|
||||||
|
String pingingDirect(String name);
|
||||||
|
|
||||||
|
/// Status message for flood ping
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Pinging {name} (flooding - no path)...'**
|
||||||
|
String pingingFlood(String name);
|
||||||
|
|
||||||
|
/// Warning when direct ping times out
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Direct ping timeout - retrying {name} with flooding...'**
|
||||||
|
String directPingTimeout(String name);
|
||||||
|
|
||||||
|
/// Success message for ping
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Ping successful to {name}{fallback}'**
|
||||||
|
String pingSuccessful(String name, String fallback);
|
||||||
|
|
||||||
|
/// Suffix for ping success with fallback
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **' (via flooding fallback)'**
|
||||||
|
String get viaFloodingFallback;
|
||||||
|
|
||||||
|
/// Error message when ping fails
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Ping failed to {name} - no response received'**
|
||||||
|
String pingFailed(String name);
|
||||||
|
|
||||||
|
/// Confirmation message for deleting contact
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Are you sure you want to delete \"{name}\"?\n\nThis will remove the contact from both the app and the companion radio device.'**
|
||||||
|
String deleteContactConfirmation(String name);
|
||||||
|
|
||||||
|
/// Status message while removing contact
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Removing {name}...'**
|
||||||
|
String removingContact(String name);
|
||||||
|
|
||||||
|
/// Success message after removing contact
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Contact \"{name}\" removed'**
|
||||||
|
String contactRemoved(String name);
|
||||||
|
|
||||||
|
/// Error message when contact removal fails
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Failed to remove contact: {error}'**
|
||||||
|
String failedToRemoveContact(String error);
|
||||||
|
|
||||||
|
/// Contact type label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Type'**
|
||||||
|
String get type;
|
||||||
|
|
||||||
|
/// Public key label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Public Key'**
|
||||||
|
String get publicKey;
|
||||||
|
|
||||||
|
/// Last seen label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Last Seen'**
|
||||||
|
String get lastSeen;
|
||||||
|
|
||||||
|
/// Room status section header
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Room Status'**
|
||||||
|
String get roomStatus;
|
||||||
|
|
||||||
|
/// Login status label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Login Status'**
|
||||||
|
String get loginStatus;
|
||||||
|
|
||||||
|
/// Not logged in status
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Not Logged In'**
|
||||||
|
String get notLoggedIn;
|
||||||
|
|
||||||
|
/// Admin access label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Admin Access'**
|
||||||
|
String get adminAccess;
|
||||||
|
|
||||||
|
/// Yes answer
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Yes'**
|
||||||
|
String get yes;
|
||||||
|
|
||||||
|
/// No answer
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No'**
|
||||||
|
String get no;
|
||||||
|
|
||||||
|
/// Permissions label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Permissions'**
|
||||||
|
String get permissions;
|
||||||
|
|
||||||
|
/// Password saved label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Password Saved'**
|
||||||
|
String get passwordSaved;
|
||||||
|
|
||||||
|
/// Location section header
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Location:'**
|
||||||
|
String get locationColon;
|
||||||
|
|
||||||
|
/// Telemetry section header
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Telemetry'**
|
||||||
|
String get telemetry;
|
||||||
|
|
||||||
|
/// Status message while requesting telemetry
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Requesting telemetry from {name}...'**
|
||||||
|
String requestingTelemetry(String name);
|
||||||
|
|
||||||
|
/// Voltage label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Voltage'**
|
||||||
|
String get voltage;
|
||||||
|
|
||||||
|
/// Battery label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Battery'**
|
||||||
|
String get battery;
|
||||||
|
|
||||||
|
/// Temperature label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Temperature'**
|
||||||
|
String get temperature;
|
||||||
|
|
||||||
|
/// Humidity label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Humidity'**
|
||||||
|
String get humidity;
|
||||||
|
|
||||||
|
/// Pressure label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Pressure'**
|
||||||
|
String get pressure;
|
||||||
|
|
||||||
|
/// GPS from telemetry label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'GPS (Telemetry)'**
|
||||||
|
String get gpsTelemetry;
|
||||||
|
|
||||||
|
/// Updated timestamp label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Updated'**
|
||||||
|
String get updated;
|
||||||
|
|
||||||
|
/// Info message after path reset
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Path reset for {name}. Next message will find a new route.'**
|
||||||
|
String pathResetInfo(String name);
|
||||||
|
|
||||||
|
/// Button to re-login to room
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Re-Login to Room'**
|
||||||
|
String get reLoginToRoom;
|
||||||
|
|
||||||
|
/// Compass heading label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Heading'**
|
||||||
|
String get heading;
|
||||||
|
|
||||||
|
/// Elevation/altitude label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Elevation'**
|
||||||
|
String get elevation;
|
||||||
|
|
||||||
|
/// GPS accuracy label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Accuracy'**
|
||||||
|
String get accuracy;
|
||||||
|
|
||||||
|
/// Title for filter markers dialog
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Filter Markers'**
|
||||||
|
String get filterMarkers;
|
||||||
|
|
||||||
|
/// Tooltip for filter button
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Filter markers'**
|
||||||
|
String get filterMarkersTooltip;
|
||||||
|
|
||||||
|
/// Filter option for contacts
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Contacts'**
|
||||||
|
String get contactsFilter;
|
||||||
|
|
||||||
|
/// SAR markers section header
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'SAR Markers'**
|
||||||
|
String get sarMarkers;
|
||||||
|
|
||||||
|
/// Found person SAR marker type
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Found Person'**
|
||||||
|
String get foundPerson;
|
||||||
|
|
||||||
|
/// Fire SAR marker type
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Fire'**
|
||||||
|
String get fire;
|
||||||
|
|
||||||
|
/// Staging area SAR marker type
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Staging Area'**
|
||||||
|
String get stagingArea;
|
||||||
|
|
||||||
|
/// Button to show all filters
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Show All'**
|
||||||
|
String get showAll;
|
||||||
|
|
||||||
|
/// Title for nearby contacts list in compass
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Nearby Contacts'**
|
||||||
|
String get nearbyContacts;
|
||||||
|
|
||||||
|
/// Message when GPS location is unavailable
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Location unavailable'**
|
||||||
|
String get locationUnavailable;
|
||||||
|
|
||||||
|
/// Relative bearing direction - ahead
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'ahead'**
|
||||||
|
String get ahead;
|
||||||
|
|
||||||
|
/// Relative bearing direction - right
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{degrees}° right'**
|
||||||
|
String degreesRight(int degrees);
|
||||||
|
|
||||||
|
/// Relative bearing direction - left
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{degrees}° left'**
|
||||||
|
String degreesLeft(int degrees);
|
||||||
|
|
||||||
|
/// Latitude and longitude display format
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Lat: {latitude} Lon: {longitude}'**
|
||||||
|
String latLonFormat(String latitude, String longitude);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|||||||
@@ -404,4 +404,281 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get typeYourMessage => 'Type your message...';
|
String get typeYourMessage => 'Type your message...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get quickLocationMarker => 'Quick location marker';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get markerType => 'Marker Type';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sendTo => 'Send To';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noDestinationsAvailable => 'No destinations available.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectDestination => 'Select destination...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get ephemeralBroadcastInfo =>
|
||||||
|
'Ephemeral: Broadcast over-the-air only. Not stored - nodes must be online.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get persistentRoomInfo =>
|
||||||
|
'Persistent: Stored immutably in room. Synced automatically and preserved offline.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get location => 'Location';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get fromMap => 'From Map';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get gettingLocation => 'Getting location...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get locationError => 'Location Error';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get retry => 'Retry';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get refreshLocation => 'Refresh location';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String accuracyMeters(int accuracy) {
|
||||||
|
return 'Accuracy: ±${accuracy}m';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notesOptional => 'Notes (optional)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addAdditionalInformation => 'Add additional information...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String lowAccuracyWarning(int accuracy) {
|
||||||
|
return 'Location accuracy is ±${accuracy}m. This may not be accurate enough for SAR operations.\n\nContinue anyway?';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get loginToRoom => 'Login to Room';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterPasswordInfo =>
|
||||||
|
'Enter the password to access this room. The password will be saved for future use.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get password => 'Password';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterRoomPassword => 'Enter room password';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get loggingInDots => 'Logging in...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get login => 'Login';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String failedToAddRoom(String error) {
|
||||||
|
return 'Failed to add room to device: $error\n\nThe room may not have advertised yet.\nTry waiting for the room to broadcast.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get direct => 'Direct';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get flood => 'Flood';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get admin => 'Admin';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get loggedIn => 'Logged In';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noGpsData => 'No GPS data';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get distance => 'Distance';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String pingingDirect(String name) {
|
||||||
|
return 'Pinging $name (direct via path)...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String pingingFlood(String name) {
|
||||||
|
return 'Pinging $name (flooding - no path)...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String directPingTimeout(String name) {
|
||||||
|
return 'Direct ping timeout - retrying $name with flooding...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String pingSuccessful(String name, String fallback) {
|
||||||
|
return 'Ping successful to $name$fallback';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viaFloodingFallback => ' (via flooding fallback)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String pingFailed(String name) {
|
||||||
|
return 'Ping failed to $name - no response received';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String deleteContactConfirmation(String name) {
|
||||||
|
return 'Are you sure you want to delete \"$name\"?\n\nThis will remove the contact from both the app and the companion radio device.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String removingContact(String name) {
|
||||||
|
return 'Removing $name...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contactRemoved(String name) {
|
||||||
|
return 'Contact \"$name\" removed';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String failedToRemoveContact(String error) {
|
||||||
|
return 'Failed to remove contact: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get type => 'Type';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get publicKey => 'Public Key';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastSeen => 'Last Seen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get roomStatus => 'Room Status';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get loginStatus => 'Login Status';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notLoggedIn => 'Not Logged In';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminAccess => 'Admin Access';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get yes => 'Yes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get no => 'No';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get permissions => 'Permissions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get passwordSaved => 'Password Saved';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get locationColon => 'Location:';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get telemetry => 'Telemetry';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String requestingTelemetry(String name) {
|
||||||
|
return 'Requesting telemetry from $name...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get voltage => 'Voltage';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get battery => 'Battery';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get temperature => 'Temperature';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get humidity => 'Humidity';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pressure => 'Pressure';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get gpsTelemetry => 'GPS (Telemetry)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get updated => 'Updated';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String pathResetInfo(String name) {
|
||||||
|
return 'Path reset for $name. Next message will find a new route.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get reLoginToRoom => 'Re-Login to Room';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get heading => 'Heading';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get elevation => 'Elevation';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get accuracy => 'Accuracy';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get filterMarkers => 'Filter Markers';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get filterMarkersTooltip => 'Filter markers';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsFilter => 'Contacts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sarMarkers => 'SAR Markers';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get foundPerson => 'Found Person';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get fire => 'Fire';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get stagingArea => 'Staging Area';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get showAll => 'Show All';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get nearbyContacts => 'Nearby Contacts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get locationUnavailable => 'Location unavailable';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get ahead => 'ahead';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String degreesRight(int degrees) {
|
||||||
|
return '$degrees° right';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String degreesLeft(int degrees) {
|
||||||
|
return '$degrees° left';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String latLonFormat(String latitude, String longitude) {
|
||||||
|
return 'Lat: $latitude Lon: $longitude';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -404,4 +404,281 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get typeYourMessage => 'Upišite svoju poruku...';
|
String get typeYourMessage => 'Upišite svoju poruku...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get quickLocationMarker => 'Brzi označitelj lokacije';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get markerType => 'Vrsta markera';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sendTo => 'Pošalji na';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noDestinationsAvailable => 'Nema dostupnih odredišta.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectDestination => 'Odaberite odredište...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get ephemeralBroadcastInfo =>
|
||||||
|
'Privremeno: Samo emitiranje. Nije pohranjeno - čvorovi moraju biti online.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get persistentRoomInfo =>
|
||||||
|
'Trajno: Nepromjenjivo pohranjeno u sobi. Automatski sinkronizirano i očuvano offline.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get location => 'Lokacija';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get fromMap => 'S karte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get gettingLocation => 'Dohvaćanje lokacije...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get locationError => 'Greška lokacije';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get retry => 'Pokušaj ponovno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get refreshLocation => 'Osvježi lokaciju';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String accuracyMeters(int accuracy) {
|
||||||
|
return 'Točnost: ±${accuracy}m';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notesOptional => 'Napomene (opcionalno)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addAdditionalInformation => 'Dodajte dodatne informacije...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String lowAccuracyWarning(int accuracy) {
|
||||||
|
return 'Točnost lokacije je ±${accuracy}m. Ovo možda nije dovoljno precizno za SAR operacije.\n\nNastaviti svejedno?';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get loginToRoom => 'Prijava u sobu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterPasswordInfo =>
|
||||||
|
'Unesite lozinku za pristup ovoj sobi. Lozinka će biti spremljena za buduću upotrebu.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get password => 'Lozinka';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterRoomPassword => 'Unesite lozinku sobe';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get loggingInDots => 'Prijavljivanje...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get login => 'Prijava';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String failedToAddRoom(String error) {
|
||||||
|
return 'Neuspjelo dodavanje sobe na uređaj: $error\n\nSoba možda još nije oglašena.\nPokušajte pričekati da soba emitira.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get direct => 'Izravno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get flood => 'Poplava';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get admin => 'Administrator';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get loggedIn => 'Prijavljen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noGpsData => 'Nema GPS podataka';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get distance => 'Udaljenost';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String pingingDirect(String name) {
|
||||||
|
return 'Pingiranje $name (izravno putem puta)...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String pingingFlood(String name) {
|
||||||
|
return 'Pingiranje $name (poplava - nema puta)...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String directPingTimeout(String name) {
|
||||||
|
return 'Istek izravnog pinga - ponovni pokušaj $name s poplavom...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String pingSuccessful(String name, String fallback) {
|
||||||
|
return 'Ping uspješan prema $name$fallback';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viaFloodingFallback => ' (putem rezervnog plavljenja)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String pingFailed(String name) {
|
||||||
|
return 'Ping neuspješan prema $name - nije primljen odgovor';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String deleteContactConfirmation(String name) {
|
||||||
|
return 'Jeste li sigurni da želite izbrisati \"$name\"?\n\nOvo će ukloniti kontakt iz aplikacije i pratećeg radio uređaja.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String removingContact(String name) {
|
||||||
|
return 'Uklanjanje $name...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contactRemoved(String name) {
|
||||||
|
return 'Kontakt \"$name\" uklonjen';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String failedToRemoveContact(String error) {
|
||||||
|
return 'Neuspjelo uklanjanje kontakta: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get type => 'Vrsta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get publicKey => 'Javni ključ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastSeen => 'Zadnje viđen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get roomStatus => 'Status sobe';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get loginStatus => 'Status prijave';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notLoggedIn => 'Nije prijavljen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminAccess => 'Administratorski pristup';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get yes => 'Da';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get no => 'Ne';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get permissions => 'Dopuštenja';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get passwordSaved => 'Lozinka spremljena';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get locationColon => 'Lokacija:';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get telemetry => 'Telemetrija';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String requestingTelemetry(String name) {
|
||||||
|
return 'Zahtijevanje telemetrije od $name...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get voltage => 'Napon';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get battery => 'Baterija';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get temperature => 'Temperatura';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get humidity => 'Vlažnost';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pressure => 'Tlak';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get gpsTelemetry => 'GPS (Telemetrija)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get updated => 'Ažurirano';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String pathResetInfo(String name) {
|
||||||
|
return 'Put resetiran za $name. Sljedeća poruka će pronaći novu rutu.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get reLoginToRoom => 'Ponovna prijava u sobu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get heading => 'Smjer';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get elevation => 'Nadmorska visina';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get accuracy => 'Točnost';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get filterMarkers => 'Filtriraj markere';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get filterMarkersTooltip => 'Filtriraj markere';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsFilter => 'Kontakti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sarMarkers => 'SAR markeri';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get foundPerson => 'Pronađena osoba';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get fire => 'Požar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get stagingArea => 'Zbirno mjesto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get showAll => 'Prikaži sve';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get nearbyContacts => 'Obližnji kontakti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get locationUnavailable => 'Lokacija nije dostupna';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get ahead => 'naprijed';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String degreesRight(int degrees) {
|
||||||
|
return '$degrees° desno';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String degreesLeft(int degrees) {
|
||||||
|
return '$degrees° lijevo';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String latLonFormat(String latitude, String longitude) {
|
||||||
|
return 'Ššir: $latitude Dužina: $longitude';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -404,4 +404,281 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get typeYourMessage => 'Vnesite svoje sporočilo...';
|
String get typeYourMessage => 'Vnesite svoje sporočilo...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get quickLocationMarker => 'Hitri označevalec lokacije';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get markerType => 'Vrsta označevalca';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sendTo => 'Pošlji na';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noDestinationsAvailable => 'Ni dostopnih ciljev.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectDestination => 'Izberite cilj...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get ephemeralBroadcastInfo =>
|
||||||
|
'Začasno: Samo oddajanje. Ni shranjeno - vozlišča morajo biti povezana.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get persistentRoomInfo =>
|
||||||
|
'Trajno: Nespremenljivo shranjeno v sobi. Samodejno sinhronizirano in ohranjeno brez povezave.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get location => 'Lokacija';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get fromMap => 'Z zemljevida';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get gettingLocation => 'Pridobivanje lokacije...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get locationError => 'Napaka lokacije';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get retry => 'Poskusi znova';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get refreshLocation => 'Osveži lokacijo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String accuracyMeters(int accuracy) {
|
||||||
|
return 'Natančnost: ±${accuracy}m';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notesOptional => 'Opombe (neobvezno)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addAdditionalInformation => 'Dodajte dodatne informacije...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String lowAccuracyWarning(int accuracy) {
|
||||||
|
return 'Natančnost lokacije je ±${accuracy}m. To morda ni dovolj natančno za SAR operacije.\n\nVseeno nadaljuj?';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get loginToRoom => 'Prijava v sobo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterPasswordInfo =>
|
||||||
|
'Vnesite geslo za dostop do te sobe. Geslo bo shranjeno za prihodnjo uporabo.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get password => 'Geslo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterRoomPassword => 'Vnesite geslo sobe';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get loggingInDots => 'Prijavljanje...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get login => 'Prijava';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String failedToAddRoom(String error) {
|
||||||
|
return 'Dodajanje sobe v napravo ni uspelo: $error\n\nSoba morda še ni oglašena.\nPoskusite počakati, da soba odda.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get direct => 'Neposredno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get flood => 'Poplava';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get admin => 'Administrator';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get loggedIn => 'Prijavljen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noGpsData => 'Ni GPS podatkov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get distance => 'Razdalja';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String pingingDirect(String name) {
|
||||||
|
return 'Pinganje $name (neposredno preko poti)...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String pingingFlood(String name) {
|
||||||
|
return 'Pinganje $name (poplava - brez poti)...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String directPingTimeout(String name) {
|
||||||
|
return 'Časovna omejitev neposrednega pinga - ponovni poskus $name s poplavo...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String pingSuccessful(String name, String fallback) {
|
||||||
|
return 'Ping uspešen do $name$fallback';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viaFloodingFallback => ' (preko rezervnega poplavljanja)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String pingFailed(String name) {
|
||||||
|
return 'Ping neuspešen do $name - odgovor ni prejet';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String deleteContactConfirmation(String name) {
|
||||||
|
return 'Ste prepričani, da želite izbrisati \"$name\"?\n\nTo bo odstranilo stik iz aplikacije in spremljevalne radijske naprave.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String removingContact(String name) {
|
||||||
|
return 'Odstranjevanje $name...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contactRemoved(String name) {
|
||||||
|
return 'Stik \"$name\" odstranjen';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String failedToRemoveContact(String error) {
|
||||||
|
return 'Odstranjevanje stika ni uspelo: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get type => 'Vrsta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get publicKey => 'Javni ključ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastSeen => 'Nazadnje viden';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get roomStatus => 'Status sobe';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get loginStatus => 'Status prijave';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get notLoggedIn => 'Ni prijavljen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminAccess => 'Administratorski dostop';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get yes => 'Da';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get no => 'Ne';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get permissions => 'Dovoljenja';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get passwordSaved => 'Geslo shranjeno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get locationColon => 'Lokacija:';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get telemetry => 'Telemetrija';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String requestingTelemetry(String name) {
|
||||||
|
return 'Zahtevanje telemetrije od $name...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get voltage => 'Napetost';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get battery => 'Baterija';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get temperature => 'Temperatura';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get humidity => 'Vlažnost';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pressure => 'Tlak';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get gpsTelemetry => 'GPS (Telemetrija)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get updated => 'Posodobljeno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String pathResetInfo(String name) {
|
||||||
|
return 'Pot ponastavljena za $name. Naslednje sporočilo bo našlo novo pot.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get reLoginToRoom => 'Ponovna prijava v sobo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get heading => 'Smer';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get elevation => 'Nadmorska višina';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get accuracy => 'Natančnost';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get filterMarkers => 'Filtriraj označevalce';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get filterMarkersTooltip => 'Filtriraj označevalce';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsFilter => 'Stiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sarMarkers => 'SAR označevalci';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get foundPerson => 'Najdena oseba';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get fire => 'Ogenj';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get stagingArea => 'Zbirališče';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get showAll => 'Prikaži vse';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get nearbyContacts => 'Bližnji stiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get locationUnavailable => 'Lokacija ni na voljo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get ahead => 'naravnost';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String degreesRight(int degrees) {
|
||||||
|
return '$degrees° desno';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String degreesLeft(int degrees) {
|
||||||
|
return '$degrees° levo';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String latLonFormat(String latitude, String longitude) {
|
||||||
|
return 'Šir: $latitude Dolž: $longitude';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -231,5 +231,165 @@
|
|||||||
|
|
||||||
"directMessageInfo": "To sporočilo bo poslano neposredno {contactName}. Prikazalo se bo tudi v glavnem viru sporočil.",
|
"directMessageInfo": "To sporočilo bo poslano neposredno {contactName}. Prikazalo se bo tudi v glavnem viru sporočil.",
|
||||||
|
|
||||||
"typeYourMessage": "Vnesite svoje sporočilo..."
|
"typeYourMessage": "Vnesite svoje sporočilo...",
|
||||||
|
|
||||||
|
"quickLocationMarker": "Hitri označevalec lokacije",
|
||||||
|
|
||||||
|
"markerType": "Vrsta označevalca",
|
||||||
|
|
||||||
|
"sendTo": "Pošlji na",
|
||||||
|
|
||||||
|
"noDestinationsAvailable": "Ni dostopnih ciljev.",
|
||||||
|
|
||||||
|
"selectDestination": "Izberite cilj...",
|
||||||
|
|
||||||
|
"ephemeralBroadcastInfo": "Začasno: Samo oddajanje. Ni shranjeno - vozlišča morajo biti povezana.",
|
||||||
|
|
||||||
|
"persistentRoomInfo": "Trajno: Nespremenljivo shranjeno v sobi. Samodejno sinhronizirano in ohranjeno brez povezave.",
|
||||||
|
|
||||||
|
"location": "Lokacija",
|
||||||
|
|
||||||
|
"fromMap": "Z zemljevida",
|
||||||
|
|
||||||
|
"gettingLocation": "Pridobivanje lokacije...",
|
||||||
|
|
||||||
|
"locationError": "Napaka lokacije",
|
||||||
|
|
||||||
|
"retry": "Poskusi znova",
|
||||||
|
|
||||||
|
"refreshLocation": "Osveži lokacijo",
|
||||||
|
|
||||||
|
"accuracyMeters": "Natančnost: ±{accuracy}m",
|
||||||
|
|
||||||
|
"notesOptional": "Opombe (neobvezno)",
|
||||||
|
|
||||||
|
"addAdditionalInformation": "Dodajte dodatne informacije...",
|
||||||
|
|
||||||
|
"lowAccuracyWarning": "Natančnost lokacije je ±{accuracy}m. To morda ni dovolj natančno za SAR operacije.\n\nVseeno nadaljuj?",
|
||||||
|
|
||||||
|
"loginToRoom": "Prijava v sobo",
|
||||||
|
|
||||||
|
"enterPasswordInfo": "Vnesite geslo za dostop do te sobe. Geslo bo shranjeno za prihodnjo uporabo.",
|
||||||
|
|
||||||
|
"password": "Geslo",
|
||||||
|
|
||||||
|
"enterRoomPassword": "Vnesite geslo sobe",
|
||||||
|
|
||||||
|
"loggingInDots": "Prijavljanje...",
|
||||||
|
|
||||||
|
"login": "Prijava",
|
||||||
|
|
||||||
|
"failedToAddRoom": "Dodajanje sobe v napravo ni uspelo: {error}\n\nSoba morda še ni oglašena.\nPoskusite počakati, da soba odda.",
|
||||||
|
|
||||||
|
"direct": "Neposredno",
|
||||||
|
|
||||||
|
"flood": "Poplava",
|
||||||
|
|
||||||
|
"admin": "Administrator",
|
||||||
|
|
||||||
|
"loggedIn": "Prijavljen",
|
||||||
|
|
||||||
|
"noGpsData": "Ni GPS podatkov",
|
||||||
|
|
||||||
|
"distance": "Razdalja",
|
||||||
|
|
||||||
|
"pingingDirect": "Pinganje {name} (neposredno preko poti)...",
|
||||||
|
|
||||||
|
"pingingFlood": "Pinganje {name} (poplava - brez poti)...",
|
||||||
|
|
||||||
|
"directPingTimeout": "Časovna omejitev neposrednega pinga - ponovni poskus {name} s poplavo...",
|
||||||
|
|
||||||
|
"pingSuccessful": "Ping uspešen do {name}{fallback}",
|
||||||
|
|
||||||
|
"viaFloodingFallback": " (preko rezervnega poplavljanja)",
|
||||||
|
|
||||||
|
"pingFailed": "Ping neuspešen do {name} - odgovor ni prejet",
|
||||||
|
|
||||||
|
"deleteContactConfirmation": "Ste prepričani, da želite izbrisati \"{name}\"?\n\nTo bo odstranilo stik iz aplikacije in spremljevalne radijske naprave.",
|
||||||
|
|
||||||
|
"removingContact": "Odstranjevanje {name}...",
|
||||||
|
|
||||||
|
"contactRemoved": "Stik \"{name}\" odstranjen",
|
||||||
|
|
||||||
|
"failedToRemoveContact": "Odstranjevanje stika ni uspelo: {error}",
|
||||||
|
|
||||||
|
"type": "Vrsta",
|
||||||
|
|
||||||
|
"publicKey": "Javni ključ",
|
||||||
|
|
||||||
|
"lastSeen": "Nazadnje viden",
|
||||||
|
|
||||||
|
"roomStatus": "Status sobe",
|
||||||
|
|
||||||
|
"loginStatus": "Status prijave",
|
||||||
|
|
||||||
|
"notLoggedIn": "Ni prijavljen",
|
||||||
|
|
||||||
|
"adminAccess": "Administratorski dostop",
|
||||||
|
|
||||||
|
"yes": "Da",
|
||||||
|
|
||||||
|
"no": "Ne",
|
||||||
|
|
||||||
|
"permissions": "Dovoljenja",
|
||||||
|
|
||||||
|
"passwordSaved": "Geslo shranjeno",
|
||||||
|
|
||||||
|
"locationColon": "Lokacija:",
|
||||||
|
|
||||||
|
"telemetry": "Telemetrija",
|
||||||
|
|
||||||
|
"requestingTelemetry": "Zahtevanje telemetrije od {name}...",
|
||||||
|
|
||||||
|
"voltage": "Napetost",
|
||||||
|
|
||||||
|
"battery": "Baterija",
|
||||||
|
|
||||||
|
"temperature": "Temperatura",
|
||||||
|
|
||||||
|
"humidity": "Vlažnost",
|
||||||
|
|
||||||
|
"pressure": "Tlak",
|
||||||
|
|
||||||
|
"gpsTelemetry": "GPS (Telemetrija)",
|
||||||
|
|
||||||
|
"updated": "Posodobljeno",
|
||||||
|
|
||||||
|
"pathResetInfo": "Pot ponastavljena za {name}. Naslednje sporočilo bo našlo novo pot.",
|
||||||
|
|
||||||
|
"reLoginToRoom": "Ponovna prijava v sobo",
|
||||||
|
|
||||||
|
"heading": "Smer",
|
||||||
|
|
||||||
|
"elevation": "Nadmorska višina",
|
||||||
|
|
||||||
|
"accuracy": "Natančnost",
|
||||||
|
|
||||||
|
"filterMarkers": "Filtriraj označevalce",
|
||||||
|
|
||||||
|
"filterMarkersTooltip": "Filtriraj označevalce",
|
||||||
|
|
||||||
|
"contactsFilter": "Stiki",
|
||||||
|
|
||||||
|
"sarMarkers": "SAR označevalci",
|
||||||
|
|
||||||
|
"foundPerson": "Najdena oseba",
|
||||||
|
|
||||||
|
"fire": "Ogenj",
|
||||||
|
|
||||||
|
"stagingArea": "Zbirališče",
|
||||||
|
|
||||||
|
"showAll": "Prikaži vse",
|
||||||
|
|
||||||
|
"nearbyContacts": "Bližnji stiki",
|
||||||
|
|
||||||
|
"locationUnavailable": "Lokacija ni na voljo",
|
||||||
|
|
||||||
|
"ahead": "naravnost",
|
||||||
|
|
||||||
|
"degreesRight": "{degrees}° desno",
|
||||||
|
|
||||||
|
"degreesLeft": "{degrees}° levo",
|
||||||
|
|
||||||
|
"latLonFormat": "Šir: {latitude} Dolž: {longitude}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,22 +76,27 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
|
|
||||||
// Helper instances
|
// Helper instances
|
||||||
final RoomLoginManager _roomLoginManager = RoomLoginManager();
|
final RoomLoginManager _roomLoginManager = RoomLoginManager();
|
||||||
final MessageDeliveryTracker _messageDeliveryTracker = MessageDeliveryTracker();
|
final MessageDeliveryTracker _messageDeliveryTracker =
|
||||||
|
MessageDeliveryTracker();
|
||||||
final PingTracker _pingTracker = PingTracker();
|
final PingTracker _pingTracker = PingTracker();
|
||||||
|
|
||||||
// Expose room login states
|
// Expose room login states
|
||||||
Map<String, RoomLoginState> get roomLoginStates => _roomLoginManager.roomLoginStates;
|
Map<String, RoomLoginState> get roomLoginStates =>
|
||||||
|
_roomLoginManager.roomLoginStates;
|
||||||
|
|
||||||
// Callbacks for other providers
|
// Callbacks for other providers
|
||||||
Function(Contact)? onContactReceived;
|
Function(Contact)? onContactReceived;
|
||||||
Function(List<Contact>)? onContactsComplete;
|
Function(List<Contact>)? onContactsComplete;
|
||||||
Function(Message)? onMessageReceived;
|
Function(Message)? onMessageReceived;
|
||||||
Function(Uint8List publicKey, Uint8List lppData)? onTelemetryReceived;
|
Function(Uint8List publicKey, Uint8List lppData)? onTelemetryReceived;
|
||||||
Function(Uint8List publicKeyPrefix, int tag, Uint8List responseData)? onBinaryResponse;
|
Function(Uint8List publicKeyPrefix, int tag, Uint8List responseData)?
|
||||||
|
onBinaryResponse;
|
||||||
Function(Uint8List publicKey)? onPathUpdated;
|
Function(Uint8List publicKey)? onPathUpdated;
|
||||||
Function(Uint8List publicKeyPrefix, int permissions, bool isAdmin, int tag)? onLoginSuccess;
|
Function(Uint8List publicKeyPrefix, int permissions, bool isAdmin, int tag)?
|
||||||
|
onLoginSuccess;
|
||||||
Function(Uint8List publicKeyPrefix)? onLoginFail;
|
Function(Uint8List publicKeyPrefix)? onLoginFail;
|
||||||
Function(String messageId, int expectedAckTag, int suggestedTimeoutMs)? onMessageSent;
|
Function(String messageId, int expectedAckTag, int suggestedTimeoutMs)?
|
||||||
|
onMessageSent;
|
||||||
Function(int ackCode, int roundTripTimeMs)? onMessageDelivered;
|
Function(int ackCode, int roundTripTimeMs)? onMessageDelivered;
|
||||||
Function(Uint8List publicKeyPrefix, Uint8List statusData)? onStatusResponse;
|
Function(Uint8List publicKeyPrefix, Uint8List statusData)? onStatusResponse;
|
||||||
|
|
||||||
@@ -106,11 +111,13 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
connectionState: isConnected
|
connectionState: isConnected
|
||||||
? ConnectionState.connected
|
? ConnectionState.connected
|
||||||
: (_bleService.isReconnecting
|
: (_bleService.isReconnecting
|
||||||
? ConnectionState.connecting
|
? ConnectionState.connecting
|
||||||
: ConnectionState.disconnected),
|
: ConnectionState.disconnected),
|
||||||
lastUpdate: DateTime.now(),
|
lastUpdate: DateTime.now(),
|
||||||
);
|
);
|
||||||
print(' Updated deviceInfo.connectionState: ${_deviceInfo.connectionState}');
|
print(
|
||||||
|
' Updated deviceInfo.connectionState: ${_deviceInfo.connectionState}',
|
||||||
|
);
|
||||||
print(' Updated deviceInfo.isConnected: ${_deviceInfo.isConnected}');
|
print(' Updated deviceInfo.isConnected: ${_deviceInfo.isConnected}');
|
||||||
print(' isReconnecting: ${_bleService.isReconnecting}');
|
print(' isReconnecting: ${_bleService.isReconnecting}');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -137,7 +144,9 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
connectionState: ConnectionState.error,
|
connectionState: ConnectionState.error,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
print(' Keeping connection state as connected (ignoring data parsing error)');
|
print(
|
||||||
|
' Keeping connection state as connected (ignoring data parsing error)',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -165,7 +174,9 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
|
|
||||||
_bleService.onBinaryResponse = (publicKeyPrefix, tag, responseData) {
|
_bleService.onBinaryResponse = (publicKeyPrefix, tag, responseData) {
|
||||||
print('📥 [Provider] Binary response received');
|
print('📥 [Provider] Binary response received');
|
||||||
print(' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
|
print(
|
||||||
|
' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}',
|
||||||
|
);
|
||||||
print(' Tag: $tag');
|
print(' Tag: $tag');
|
||||||
print(' Response data: ${responseData.length} bytes');
|
print(' Response data: ${responseData.length} bytes');
|
||||||
// Mark ping as successful if this was a ping request
|
// Mark ping as successful if this was a ping request
|
||||||
@@ -180,15 +191,20 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_bleService.onMessageWaiting = () {
|
_bleService.onMessageWaiting = () {
|
||||||
print('📥 [Provider] PUSH_CODE_MSG_WAITING received - auto-fetching messages via event');
|
print(
|
||||||
|
'📥 [Provider] PUSH_CODE_MSG_WAITING received - auto-fetching messages via event',
|
||||||
|
);
|
||||||
// Automatically fetch messages when push notification received
|
// Automatically fetch messages when push notification received
|
||||||
// This is the CORRECT way to receive messages - room server pushes them
|
// This is the CORRECT way to receive messages - room server pushes them
|
||||||
syncAllMessages();
|
syncAllMessages();
|
||||||
};
|
};
|
||||||
|
|
||||||
_bleService.onLoginSuccess = (publicKeyPrefix, permissions, isAdmin, tag) async {
|
_bleService
|
||||||
|
.onLoginSuccess = (publicKeyPrefix, permissions, isAdmin, tag) async {
|
||||||
print('📥 [Provider] Login successful to room');
|
print('📥 [Provider] Login successful to room');
|
||||||
print(' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
|
print(
|
||||||
|
' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}',
|
||||||
|
);
|
||||||
print(' Permissions: $permissions, Admin: $isAdmin, Tag: $tag');
|
print(' Permissions: $permissions, Admin: $isAdmin, Tag: $tag');
|
||||||
|
|
||||||
// Update room login state via helper
|
// Update room login state via helper
|
||||||
@@ -205,12 +221,12 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
|
|
||||||
_bleService.onLoginFail = (publicKeyPrefix) {
|
_bleService.onLoginFail = (publicKeyPrefix) {
|
||||||
print('📥 [Provider] Login failed to room');
|
print('📥 [Provider] Login failed to room');
|
||||||
print(' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
|
print(
|
||||||
|
' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}',
|
||||||
|
);
|
||||||
|
|
||||||
// Update room login state to logged out via helper
|
// Update room login state to logged out via helper
|
||||||
_roomLoginManager.handleLoginFail(
|
_roomLoginManager.handleLoginFail(publicKeyPrefix: publicKeyPrefix);
|
||||||
publicKeyPrefix: publicKeyPrefix,
|
|
||||||
);
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
onLoginFail?.call(publicKeyPrefix);
|
onLoginFail?.call(publicKeyPrefix);
|
||||||
@@ -218,22 +234,33 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
|
|
||||||
_bleService.onAdvertReceived = (publicKey) {
|
_bleService.onAdvertReceived = (publicKey) {
|
||||||
print('📥 [Provider] Advert received from node');
|
print('📥 [Provider] Advert received from node');
|
||||||
print(' Public key: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...');
|
print(
|
||||||
print(' Note: Waiting for PUSH_CODE_NEW_ADVERT (0x8A) with full contact details');
|
' Public key: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...',
|
||||||
|
);
|
||||||
|
print(
|
||||||
|
' Note: Waiting for PUSH_CODE_NEW_ADVERT (0x8A) with full contact details',
|
||||||
|
);
|
||||||
// The companion radio will automatically send PUSH_CODE_NEW_ADVERT if manual_add_contacts=0
|
// The companion radio will automatically send PUSH_CODE_NEW_ADVERT if manual_add_contacts=0
|
||||||
// which will trigger onContactReceived callback and add/update the contact
|
// which will trigger onContactReceived callback and add/update the contact
|
||||||
};
|
};
|
||||||
|
|
||||||
_bleService.onPathUpdated = (publicKey) {
|
_bleService.onPathUpdated = (publicKey) {
|
||||||
print('📥 [Provider] Path updated for contact');
|
print('📥 [Provider] Path updated for contact');
|
||||||
print(' Public key: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...');
|
print(
|
||||||
print(' Note: Mesh network discovered a new/better routing path to this contact');
|
' Public key: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...',
|
||||||
|
);
|
||||||
|
print(
|
||||||
|
' Note: Mesh network discovered a new/better routing path to this contact',
|
||||||
|
);
|
||||||
// Forward the callback to ContactsProvider to trigger contact sync
|
// Forward the callback to ContactsProvider to trigger contact sync
|
||||||
onPathUpdated?.call(publicKey);
|
onPathUpdated?.call(publicKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
_bleService.onMessageSent = (expectedAckTag, suggestedTimeoutMs, isFloodMode) {
|
_bleService
|
||||||
print('📥 [Provider] Message sent - ACK tag: $expectedAckTag, timeout: ${suggestedTimeoutMs}ms');
|
.onMessageSent = (expectedAckTag, suggestedTimeoutMs, isFloodMode) {
|
||||||
|
print(
|
||||||
|
'📥 [Provider] Message sent - ACK tag: $expectedAckTag, timeout: ${suggestedTimeoutMs}ms',
|
||||||
|
);
|
||||||
|
|
||||||
// Pop the first pending message ID from the queue (FIFO) via helper
|
// Pop the first pending message ID from the queue (FIFO) via helper
|
||||||
final messageId = _messageDeliveryTracker.popPendingMessageId();
|
final messageId = _messageDeliveryTracker.popPendingMessageId();
|
||||||
@@ -247,18 +274,24 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
// Notify callback with message ID
|
// Notify callback with message ID
|
||||||
onMessageSent?.call(messageId, expectedAckTag, suggestedTimeoutMs);
|
onMessageSent?.call(messageId, expectedAckTag, suggestedTimeoutMs);
|
||||||
} else {
|
} else {
|
||||||
print('⚠️ [Provider] SENT response received but no pending message IDs');
|
print(
|
||||||
|
'⚠️ [Provider] SENT response received but no pending message IDs',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_bleService.onMessageDelivered = (ackCode, roundTripTimeMs) {
|
_bleService.onMessageDelivered = (ackCode, roundTripTimeMs) {
|
||||||
print('📥 [Provider] Message delivered - ACK code: $ackCode, RTT: ${roundTripTimeMs}ms');
|
print(
|
||||||
|
'📥 [Provider] Message delivered - ACK code: $ackCode, RTT: ${roundTripTimeMs}ms',
|
||||||
|
);
|
||||||
onMessageDelivered?.call(ackCode, roundTripTimeMs);
|
onMessageDelivered?.call(ackCode, roundTripTimeMs);
|
||||||
};
|
};
|
||||||
|
|
||||||
_bleService.onStatusResponse = (publicKeyPrefix, statusData) {
|
_bleService.onStatusResponse = (publicKeyPrefix, statusData) {
|
||||||
print('📥 [Provider] Status response received from node');
|
print('📥 [Provider] Status response received from node');
|
||||||
print(' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
|
print(
|
||||||
|
' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}',
|
||||||
|
);
|
||||||
print(' Status data: ${statusData.length} bytes');
|
print(' Status data: ${statusData.length} bytes');
|
||||||
// Forward the callback to whoever needs it (e.g., ContactsProvider)
|
// Forward the callback to whoever needs it (e.g., ContactsProvider)
|
||||||
onStatusResponse?.call(publicKeyPrefix, statusData);
|
onStatusResponse?.call(publicKeyPrefix, statusData);
|
||||||
@@ -289,9 +322,15 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
|
|
||||||
_bleService.onSelfInfoReceived = (selfInfo) {
|
_bleService.onSelfInfoReceived = (selfInfo) {
|
||||||
print('📥 [Provider] Received SelfInfo:');
|
print('📥 [Provider] Received SelfInfo:');
|
||||||
print(' TX Power: ${selfInfo['txPower']} / ${selfInfo['maxTxPower']} dBm');
|
print(
|
||||||
print(' Radio: freq=${selfInfo['radioFreq']}, bw=${selfInfo['radioBw']}, sf=${selfInfo['radioSf']}, cr=${selfInfo['radioCr']}');
|
' TX Power: ${selfInfo['txPower']} / ${selfInfo['maxTxPower']} dBm',
|
||||||
print(' Position: ${selfInfo['advLat'] / 1000000.0}, ${selfInfo['advLon'] / 1000000.0}');
|
);
|
||||||
|
print(
|
||||||
|
' Radio: freq=${selfInfo['radioFreq']}, bw=${selfInfo['radioBw']}, sf=${selfInfo['radioSf']}, cr=${selfInfo['radioCr']}',
|
||||||
|
);
|
||||||
|
print(
|
||||||
|
' Position: ${selfInfo['advLat'] / 1000000.0}, ${selfInfo['advLon'] / 1000000.0}',
|
||||||
|
);
|
||||||
print(' Self Name: ${selfInfo['selfName']}');
|
print(' Self Name: ${selfInfo['selfName']}');
|
||||||
|
|
||||||
_deviceInfo = _deviceInfo.copyWith(
|
_deviceInfo = _deviceInfo.copyWith(
|
||||||
@@ -316,7 +355,9 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
|
|
||||||
_bleService.onBatteryAndStorage = (millivolts, usedKb, totalKb) {
|
_bleService.onBatteryAndStorage = (millivolts, usedKb, totalKb) {
|
||||||
print('📥 [Provider] Received BatteryAndStorage:');
|
print('📥 [Provider] Received BatteryAndStorage:');
|
||||||
print(' Battery: ${millivolts}mV (${(millivolts / 1000.0).toStringAsFixed(2)}V)');
|
print(
|
||||||
|
' Battery: ${millivolts}mV (${(millivolts / 1000.0).toStringAsFixed(2)}V)',
|
||||||
|
);
|
||||||
if (usedKb != null) {
|
if (usedKb != null) {
|
||||||
print(' Storage Used: ${usedKb}KB');
|
print(' Storage Used: ${usedKb}KB');
|
||||||
}
|
}
|
||||||
@@ -362,7 +403,6 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_bleService.onRssiUpdate = (rssi) {
|
_bleService.onRssiUpdate = (rssi) {
|
||||||
print('📡 [Provider] RSSI updated: $rssi dBm');
|
|
||||||
_deviceInfo = _deviceInfo.copyWith(
|
_deviceInfo = _deviceInfo.copyWith(
|
||||||
signalRssi: rssi,
|
signalRssi: rssi,
|
||||||
lastUpdate: DateTime.now(),
|
lastUpdate: DateTime.now(),
|
||||||
@@ -381,25 +421,34 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
print('✅ [Provider] Scan state initialized, notifying listeners');
|
print('✅ [Provider] Scan state initialized, notifying listeners');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await for (final scanResult
|
await for (final scanResult in _bleService.scanForDevices(
|
||||||
in _bleService.scanForDevices(timeout: const Duration(seconds: 10))) {
|
timeout: const Duration(seconds: 10),
|
||||||
|
)) {
|
||||||
print('📱 [Provider] Scan result received from scan stream');
|
print('📱 [Provider] Scan result received from scan stream');
|
||||||
final device = scanResult.device;
|
final device = scanResult.device;
|
||||||
final rssi = scanResult.rssi;
|
final rssi = scanResult.rssi;
|
||||||
|
|
||||||
if (!_scannedDevices.any((d) => d.device.remoteId == device.remoteId)) {
|
if (!_scannedDevices.any((d) => d.device.remoteId == device.remoteId)) {
|
||||||
_scannedDevices.add(ScannedDevice(device: device, rssi: rssi));
|
_scannedDevices.add(ScannedDevice(device: device, rssi: rssi));
|
||||||
print('✅ [Provider] Added device to list: ${device.platformName} (RSSI: $rssi dBm), total: ${_scannedDevices.length}');
|
print(
|
||||||
|
'✅ [Provider] Added device to list: ${device.platformName} (RSSI: $rssi dBm), total: ${_scannedDevices.length}',
|
||||||
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} else {
|
} else {
|
||||||
// Update RSSI if device already exists
|
// Update RSSI if device already exists
|
||||||
final index = _scannedDevices.indexWhere((d) => d.device.remoteId == device.remoteId);
|
final index = _scannedDevices.indexWhere(
|
||||||
|
(d) => d.device.remoteId == device.remoteId,
|
||||||
|
);
|
||||||
if (index != -1 && _scannedDevices[index].rssi != rssi) {
|
if (index != -1 && _scannedDevices[index].rssi != rssi) {
|
||||||
_scannedDevices[index] = ScannedDevice(device: device, rssi: rssi);
|
_scannedDevices[index] = ScannedDevice(device: device, rssi: rssi);
|
||||||
print(' 🔄 [Provider] Updated RSSI for ${device.platformName}: $rssi dBm');
|
print(
|
||||||
|
' 🔄 [Provider] Updated RSSI for ${device.platformName}: $rssi dBm',
|
||||||
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} else {
|
} else {
|
||||||
print(' ⏭️ [Provider] Device already in list with same RSSI, skipping');
|
print(
|
||||||
|
' ⏭️ [Provider] Device already in list with same RSSI, skipping',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -426,7 +475,9 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
|
|
||||||
_deviceInfo = _deviceInfo.copyWith(
|
_deviceInfo = _deviceInfo.copyWith(
|
||||||
deviceId: device.remoteId.toString(),
|
deviceId: device.remoteId.toString(),
|
||||||
deviceName: device.platformName.isNotEmpty ? device.platformName : 'Unknown',
|
deviceName: device.platformName.isNotEmpty
|
||||||
|
? device.platformName
|
||||||
|
: 'Unknown',
|
||||||
connectionState: ConnectionState.connecting,
|
connectionState: ConnectionState.connecting,
|
||||||
);
|
);
|
||||||
_error = null;
|
_error = null;
|
||||||
@@ -457,10 +508,9 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
|
|
||||||
await _bleService.disconnect();
|
await _bleService.disconnect();
|
||||||
|
|
||||||
_deviceInfo = DeviceInfo(
|
_deviceInfo = DeviceInfo(connectionState: ConnectionState.disconnected);
|
||||||
connectionState: ConnectionState.disconnected,
|
_roomLoginManager
|
||||||
);
|
.clearRoomLoginStates(); // Clear login states on disconnect
|
||||||
_roomLoginManager.clearRoomLoginStates(); // Clear login states on disconnect
|
|
||||||
_pingTracker.clearAll(); // Clear pending pings on disconnect
|
_pingTracker.clearAll(); // Clear pending pings on disconnect
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -579,10 +629,7 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _bleService.sendChannelMessage(
|
await _bleService.sendChannelMessage(channelIdx: channelIdx, text: text);
|
||||||
channelIdx: channelIdx,
|
|
||||||
text: text,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Channel messages are ephemeral (not persisted) - mark as "sent" immediately
|
// Channel messages are ephemeral (not persisted) - mark as "sent" immediately
|
||||||
// They don't have ACK/TAG mechanism like direct messages
|
// They don't have ACK/TAG mechanism like direct messages
|
||||||
@@ -622,7 +669,10 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
/// deprecated and should continue to be used for telemetry requests.
|
/// deprecated and should continue to be used for telemetry requests.
|
||||||
///
|
///
|
||||||
/// [zeroHop] - if true, only direct connection (no mesh forwarding)
|
/// [zeroHop] - if true, only direct connection (no mesh forwarding)
|
||||||
Future<void> requestTelemetry(Uint8List contactPublicKey, {bool zeroHop = false}) async {
|
Future<void> requestTelemetry(
|
||||||
|
Uint8List contactPublicKey, {
|
||||||
|
bool zeroHop = false,
|
||||||
|
}) async {
|
||||||
if (!_bleService.isConnected) {
|
if (!_bleService.isConnected) {
|
||||||
_error = 'Not connected to device';
|
_error = 'Not connected to device';
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -684,7 +734,9 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
|
|
||||||
// First attempt timed out - retry with flooding if first was direct
|
// First attempt timed out - retry with flooding if first was direct
|
||||||
if (firstAttemptDirect) {
|
if (firstAttemptDirect) {
|
||||||
print('⚠️ [Provider] Ping timeout on direct attempt, retrying with flooding...');
|
print(
|
||||||
|
'⚠️ [Provider] Ping timeout on direct attempt, retrying with flooding...',
|
||||||
|
);
|
||||||
onRetryWithFlooding?.call();
|
onRetryWithFlooding?.call();
|
||||||
|
|
||||||
// Track the retry
|
// Track the retry
|
||||||
@@ -708,11 +760,7 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// First attempt was already flooding and it timed out
|
// First attempt was already flooding and it timed out
|
||||||
return PingResult(
|
return PingResult(success: false, usedFlooding: true, timedOut: true);
|
||||||
success: false,
|
|
||||||
usedFlooding: true,
|
|
||||||
timedOut: true,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_error = 'Failed to ping contact: $e';
|
_error = 'Failed to ping contact: $e';
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -1003,14 +1051,19 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
// Keep syncing until we get NoMoreMessages response
|
// Keep syncing until we get NoMoreMessages response
|
||||||
// The device will send ContactMsgRecv or ChannelMsgRecv responses
|
// The device will send ContactMsgRecv or ChannelMsgRecv responses
|
||||||
// until it sends NoMoreMessages
|
// until it sends NoMoreMessages
|
||||||
for (int i = 0; i < 100; i++) { // Safety limit
|
for (int i = 0; i < 100; i++) {
|
||||||
|
// Safety limit
|
||||||
// Check flag BEFORE sending (not after)
|
// Check flag BEFORE sending (not after)
|
||||||
if (_noMoreMessages) {
|
if (_noMoreMessages) {
|
||||||
print('✅ [Provider] Message sync complete - NoMoreMessages flag set after $count requests');
|
print(
|
||||||
|
'✅ [Provider] Message sync complete - NoMoreMessages flag set after $count requests',
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
print('📤 [Provider] Sync iteration ${i + 1}: Sending CMD_SYNC_NEXT_MESSAGE');
|
print(
|
||||||
|
'📤 [Provider] Sync iteration ${i + 1}: Sending CMD_SYNC_NEXT_MESSAGE',
|
||||||
|
);
|
||||||
|
|
||||||
await _bleService.syncNextMessage();
|
await _bleService.syncNextMessage();
|
||||||
count++;
|
count++;
|
||||||
@@ -1022,10 +1075,14 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!_noMoreMessages && count >= 100) {
|
if (!_noMoreMessages && count >= 100) {
|
||||||
print('⚠️ [Provider] Message sync stopped - reached safety limit of 100 requests without NoMoreMessages');
|
print(
|
||||||
|
'⚠️ [Provider] Message sync stopped - reached safety limit of 100 requests without NoMoreMessages',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
print('🏁 [Provider] Message sync finished: sent $count sync requests, _noMoreMessages=$_noMoreMessages');
|
print(
|
||||||
|
'🏁 [Provider] Message sync finished: sent $count sync requests, _noMoreMessages=$_noMoreMessages',
|
||||||
|
);
|
||||||
return count;
|
return count;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('❌ [Provider] Failed to sync messages: $e');
|
print('❌ [Provider] Failed to sync messages: $e');
|
||||||
|
|||||||
@@ -78,7 +78,10 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
|
|
||||||
if (!connectionProvider.deviceInfo.isConnected) {
|
if (!connectionProvider.deviceInfo.isConnected) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ToastLogger.error(context, AppLocalizations.of(context)!.deviceNotConnected);
|
ToastLogger.error(
|
||||||
|
context,
|
||||||
|
AppLocalizations.of(context)!.deviceNotConnected,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -102,7 +105,10 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
permission = await Geolocator.requestPermission();
|
permission = await Geolocator.requestPermission();
|
||||||
if (permission == LocationPermission.denied) {
|
if (permission == LocationPermission.denied) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ToastLogger.error(context, AppLocalizations.of(context)!.locationPermissionDenied);
|
ToastLogger.error(
|
||||||
|
context,
|
||||||
|
AppLocalizations.of(context)!.locationPermissionDenied,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -130,7 +136,10 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('❌ Failed to get GPS position: $e');
|
print('❌ Failed to get GPS position: $e');
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ToastLogger.error(context, AppLocalizations.of(context)!.failedToGetGpsLocation);
|
ToastLogger.error(
|
||||||
|
context,
|
||||||
|
AppLocalizations.of(context)!.failedToGetGpsLocation,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -159,7 +168,10 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('❌ Failed to advertise device: $e');
|
print('❌ Failed to advertise device: $e');
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ToastLogger.error(context, AppLocalizations.of(context)!.failedToAdvertise(e.toString()));
|
ToastLogger.error(
|
||||||
|
context,
|
||||||
|
AppLocalizations.of(context)!.failedToAdvertise(e.toString()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -307,7 +319,9 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
connectionProvider.startScan();
|
connectionProvider.startScan();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
label: Text(AppLocalizations.of(context)!.scanAgain),
|
label: Text(
|
||||||
|
AppLocalizations.of(context)!.scanAgain,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -564,7 +578,10 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
),
|
),
|
||||||
text: AppLocalizations.of(context)!.contacts,
|
text: AppLocalizations.of(context)!.contacts,
|
||||||
),
|
),
|
||||||
Tab(icon: const Icon(Icons.map), text: AppLocalizations.of(context)!.map),
|
Tab(
|
||||||
|
icon: const Icon(Icons.map),
|
||||||
|
text: AppLocalizations.of(context)!.map,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -579,10 +596,6 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
final deviceInfo = provider.deviceInfo;
|
final deviceInfo = provider.deviceInfo;
|
||||||
final isConnected = deviceInfo.isConnected;
|
final isConnected = deviceInfo.isConnected;
|
||||||
|
|
||||||
print(
|
|
||||||
'🎨 [UI] Building status bar - isConnected: $isConnected, state: ${deviceInfo.connectionState}',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
// Disconnected state: show connect button
|
// Disconnected state: show connect button
|
||||||
return Row(
|
return Row(
|
||||||
@@ -590,7 +603,10 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context)!.appTitle,
|
AppLocalizations.of(context)!.appTitle,
|
||||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
@@ -654,8 +670,12 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
deviceInfo.selfName ?? AppLocalizations.of(context)!.appTitle,
|
deviceInfo.selfName ??
|
||||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
AppLocalizations.of(context)!.appTitle,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
@@ -674,7 +694,9 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
'${deviceInfo.signalRssi}',
|
'${deviceInfo.signalRssi}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: _getSignalColor(deviceInfo.signalRssi!),
|
color: _getSignalColor(
|
||||||
|
deviceInfo.signalRssi!,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -682,7 +704,9 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Icon(
|
Icon(
|
||||||
_getBatteryIcon(deviceInfo.batteryPercent!),
|
_getBatteryIcon(deviceInfo.batteryPercent!),
|
||||||
color: _getBatteryColor(deviceInfo.batteryPercent!),
|
color: _getBatteryColor(
|
||||||
|
deviceInfo.batteryPercent!,
|
||||||
|
),
|
||||||
size: 13,
|
size: 13,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 3),
|
const SizedBox(width: 3),
|
||||||
@@ -690,7 +714,9 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
'${deviceInfo.batteryPercent!.round()}%',
|
'${deviceInfo.batteryPercent!.round()}%',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: _getBatteryColor(deviceInfo.batteryPercent!),
|
color: _getBatteryColor(
|
||||||
|
deviceInfo.batteryPercent!,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -811,7 +837,9 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
const SizedBox(width: 52), // Placeholder to maintain layout balance
|
const SizedBox(
|
||||||
|
width: 52,
|
||||||
|
), // Placeholder to maintain layout balance
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -842,7 +870,8 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
isConnected
|
isConnected
|
||||||
? deviceInfo.displayName ?? AppLocalizations.of(context)!.connect
|
? deviceInfo.displayName ??
|
||||||
|
AppLocalizations.of(context)!.connect
|
||||||
: AppLocalizations.of(context)!.deviceNotConnected,
|
: AppLocalizations.of(context)!.deviceNotConnected,
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import '../meshcore_constants.dart';
|
|||||||
/// Callback types for connection events
|
/// Callback types for connection events
|
||||||
typedef OnConnectionStateCallback = void Function(bool isConnected);
|
typedef OnConnectionStateCallback = void Function(bool isConnected);
|
||||||
typedef OnErrorCallback = void Function(String error);
|
typedef OnErrorCallback = void Function(String error);
|
||||||
typedef OnReconnectionAttemptCallback = void Function(int attemptNumber, int maxAttempts);
|
typedef OnReconnectionAttemptCallback =
|
||||||
|
void Function(int attemptNumber, int maxAttempts);
|
||||||
typedef OnRssiUpdateCallback = void Function(int rssi);
|
typedef OnRssiUpdateCallback = void Function(int rssi);
|
||||||
|
|
||||||
/// Manages BLE connection lifecycle with automatic reconnection
|
/// Manages BLE connection lifecycle with automatic reconnection
|
||||||
@@ -30,13 +31,13 @@ class BleConnectionManager {
|
|||||||
// Pattern: Fast retries first (for temporary issues), then slower retries (for extended disconnections)
|
// Pattern: Fast retries first (for temporary issues), then slower retries (for extended disconnections)
|
||||||
static const int _maxReconnectionAttempts = 30;
|
static const int _maxReconnectionAttempts = 30;
|
||||||
static const List<int> _reconnectionDelaysMs = [
|
static const List<int> _reconnectionDelaysMs = [
|
||||||
2000, // 2s - immediate retry
|
2000, // 2s - immediate retry
|
||||||
3000, // 3s - quick retry
|
3000, // 3s - quick retry
|
||||||
5000, // 5s - fast retry
|
5000, // 5s - fast retry
|
||||||
10000, // 10s - moderate retry
|
10000, // 10s - moderate retry
|
||||||
15000, // 15s - longer retry
|
15000, // 15s - longer retry
|
||||||
30000, // 30s - extended retry
|
30000, // 30s - extended retry
|
||||||
30000, // 30s - keep trying every 30s after this
|
30000, // 30s - keep trying every 30s after this
|
||||||
]; // Total: ~15 minutes of reconnection attempts
|
]; // Total: ~15 minutes of reconnection attempts
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
@@ -55,7 +56,9 @@ class BleConnectionManager {
|
|||||||
BluetoothCharacteristic? get txCharacteristic => _txCharacteristic;
|
BluetoothCharacteristic? get txCharacteristic => _txCharacteristic;
|
||||||
|
|
||||||
/// Scan for MeshCore devices
|
/// Scan for MeshCore devices
|
||||||
Stream<ScanResult> scanForDevices({Duration timeout = const Duration(seconds: 10)}) async* {
|
Stream<ScanResult> scanForDevices({
|
||||||
|
Duration timeout = const Duration(seconds: 10),
|
||||||
|
}) async* {
|
||||||
try {
|
try {
|
||||||
print('🔍 [BLE] Starting scan for MeshCore devices...');
|
print('🔍 [BLE] Starting scan for MeshCore devices...');
|
||||||
print(' Service UUID: ${MeshCoreConstants.bleServiceUuid}');
|
print(' Service UUID: ${MeshCoreConstants.bleServiceUuid}');
|
||||||
@@ -69,14 +72,19 @@ class BleConnectionManager {
|
|||||||
|
|
||||||
int deviceCount = 0;
|
int deviceCount = 0;
|
||||||
await for (final scanResult in FlutterBluePlus.scanResults) {
|
await for (final scanResult in FlutterBluePlus.scanResults) {
|
||||||
print('📡 [BLE] Scan results batch received: ${scanResult.length} results');
|
print(
|
||||||
|
'📡 [BLE] Scan results batch received: ${scanResult.length} results',
|
||||||
|
);
|
||||||
for (final result in scanResult) {
|
for (final result in scanResult) {
|
||||||
print(' Device: ${result.device.platformName} (${result.device.remoteId})');
|
print(
|
||||||
|
' Device: ${result.device.platformName} (${result.device.remoteId})',
|
||||||
|
);
|
||||||
print(' RSSI: ${result.rssi}');
|
print(' RSSI: ${result.rssi}');
|
||||||
print(' Service UUIDs: ${result.advertisementData.serviceUuids}');
|
print(' Service UUIDs: ${result.advertisementData.serviceUuids}');
|
||||||
|
|
||||||
if (result.advertisementData.serviceUuids
|
if (result.advertisementData.serviceUuids.contains(
|
||||||
.contains(Guid(MeshCoreConstants.bleServiceUuid))) {
|
Guid(MeshCoreConstants.bleServiceUuid),
|
||||||
|
)) {
|
||||||
deviceCount++;
|
deviceCount++;
|
||||||
print(' ✅ MeshCore device found! Total: $deviceCount');
|
print(' ✅ MeshCore device found! Total: $deviceCount');
|
||||||
yield result;
|
yield result;
|
||||||
@@ -95,7 +103,9 @@ class BleConnectionManager {
|
|||||||
/// Connect to a MeshCore device
|
/// Connect to a MeshCore device
|
||||||
Future<bool> connect(BluetoothDevice device) async {
|
Future<bool> connect(BluetoothDevice device) async {
|
||||||
try {
|
try {
|
||||||
print('🔵 [BLE] Starting connection to device: ${device.platformName} (${device.remoteId})');
|
print(
|
||||||
|
'🔵 [BLE] Starting connection to device: ${device.platformName} (${device.remoteId})',
|
||||||
|
);
|
||||||
_device = device;
|
_device = device;
|
||||||
|
|
||||||
// Connect to device
|
// Connect to device
|
||||||
@@ -121,7 +131,9 @@ class BleConnectionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find MeshCore service
|
// Find MeshCore service
|
||||||
print('🔵 [BLE] Looking for MeshCore service: ${MeshCoreConstants.bleServiceUuid}');
|
print(
|
||||||
|
'🔵 [BLE] Looking for MeshCore service: ${MeshCoreConstants.bleServiceUuid}',
|
||||||
|
);
|
||||||
BluetoothService? meshCoreService;
|
BluetoothService? meshCoreService;
|
||||||
for (final service in services) {
|
for (final service in services) {
|
||||||
if (service.uuid.toString().toLowerCase() ==
|
if (service.uuid.toString().toLowerCase() ==
|
||||||
@@ -169,7 +181,8 @@ class BleConnectionManager {
|
|||||||
print('✅ [BLE] Notifications enabled');
|
print('✅ [BLE] Notifications enabled');
|
||||||
|
|
||||||
_isConnected = true;
|
_isConnected = true;
|
||||||
_reconnectionAttempt = 0; // Reset reconnection counter on successful connection
|
_reconnectionAttempt =
|
||||||
|
0; // Reset reconnection counter on successful connection
|
||||||
print('🔵 [BLE] Notifying connection state change: connected');
|
print('🔵 [BLE] Notifying connection state change: connected');
|
||||||
onConnectionStateChanged?.call(true);
|
onConnectionStateChanged?.call(true);
|
||||||
|
|
||||||
@@ -213,7 +226,9 @@ class BleConnectionManager {
|
|||||||
|
|
||||||
/// Setup connection monitoring for automatic reconnection
|
/// Setup connection monitoring for automatic reconnection
|
||||||
void _setupConnectionMonitoring() {
|
void _setupConnectionMonitoring() {
|
||||||
print('🔵 [BLE] Setting up connection monitoring for device: ${_device?.platformName}');
|
print(
|
||||||
|
'🔵 [BLE] Setting up connection monitoring for device: ${_device?.platformName}',
|
||||||
|
);
|
||||||
|
|
||||||
// Cancel any existing subscription
|
// Cancel any existing subscription
|
||||||
_connectionStateSubscription?.cancel();
|
_connectionStateSubscription?.cancel();
|
||||||
@@ -251,21 +266,32 @@ class BleConnectionManager {
|
|||||||
_isReconnecting = true;
|
_isReconnecting = true;
|
||||||
_reconnectionAttempt++;
|
_reconnectionAttempt++;
|
||||||
|
|
||||||
print('🔄 [BLE] Reconnection attempt $_reconnectionAttempt of $_maxReconnectionAttempts');
|
print(
|
||||||
|
'🔄 [BLE] Reconnection attempt $_reconnectionAttempt of $_maxReconnectionAttempts',
|
||||||
|
);
|
||||||
onReconnectionAttempt?.call(_reconnectionAttempt, _maxReconnectionAttempts);
|
onReconnectionAttempt?.call(_reconnectionAttempt, _maxReconnectionAttempts);
|
||||||
|
|
||||||
if (_reconnectionAttempt > _maxReconnectionAttempts) {
|
if (_reconnectionAttempt > _maxReconnectionAttempts) {
|
||||||
print('❌ [BLE] Max reconnection attempts reached after ~15 minutes. Giving up.');
|
print(
|
||||||
|
'❌ [BLE] Max reconnection attempts reached after ~15 minutes. Giving up.',
|
||||||
|
);
|
||||||
_isReconnecting = false;
|
_isReconnecting = false;
|
||||||
onError?.call('Connection lost. Unable to reconnect after 15 minutes ($_maxReconnectionAttempts attempts).');
|
onError?.call(
|
||||||
|
'Connection lost. Unable to reconnect after 15 minutes ($_maxReconnectionAttempts attempts).',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate delay with exponential backoff (uses last delay for attempts beyond array length)
|
// Calculate delay with exponential backoff (uses last delay for attempts beyond array length)
|
||||||
final delayIndex = (_reconnectionAttempt - 1).clamp(0, _reconnectionDelaysMs.length - 1);
|
final delayIndex = (_reconnectionAttempt - 1).clamp(
|
||||||
|
0,
|
||||||
|
_reconnectionDelaysMs.length - 1,
|
||||||
|
);
|
||||||
final delayMs = _reconnectionDelaysMs[delayIndex];
|
final delayMs = _reconnectionDelaysMs[delayIndex];
|
||||||
|
|
||||||
print('🔄 [BLE] Waiting ${(delayMs / 1000).toStringAsFixed(0)}s before reconnection attempt $_reconnectionAttempt...');
|
print(
|
||||||
|
'🔄 [BLE] Waiting ${(delayMs / 1000).toStringAsFixed(0)}s before reconnection attempt $_reconnectionAttempt...',
|
||||||
|
);
|
||||||
|
|
||||||
// Wait before attempting reconnection
|
// Wait before attempting reconnection
|
||||||
_reconnectionTimer = Timer(Duration(milliseconds: delayMs), () async {
|
_reconnectionTimer = Timer(Duration(milliseconds: delayMs), () async {
|
||||||
@@ -293,7 +319,9 @@ class BleConnectionManager {
|
|||||||
if (_reconnectionAttempt < _maxReconnectionAttempts) {
|
if (_reconnectionAttempt < _maxReconnectionAttempts) {
|
||||||
_attemptReconnection();
|
_attemptReconnection();
|
||||||
} else {
|
} else {
|
||||||
onError?.call('Connection lost. Unable to reconnect after 15 minutes ($_maxReconnectionAttempts attempts).');
|
onError?.call(
|
||||||
|
'Connection lost. Unable to reconnect after 15 minutes ($_maxReconnectionAttempts attempts).',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -304,7 +332,9 @@ class BleConnectionManager {
|
|||||||
if (_reconnectionAttempt < _maxReconnectionAttempts) {
|
if (_reconnectionAttempt < _maxReconnectionAttempts) {
|
||||||
_attemptReconnection();
|
_attemptReconnection();
|
||||||
} else {
|
} else {
|
||||||
onError?.call('Connection lost. Unable to reconnect after 15 minutes: $e');
|
onError?.call(
|
||||||
|
'Connection lost. Unable to reconnect after 15 minutes: $e',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -338,7 +368,6 @@ class BleConnectionManager {
|
|||||||
final rssi = await _device!.readRssi();
|
final rssi = await _device!.readRssi();
|
||||||
if (_lastRssi != rssi) {
|
if (_lastRssi != rssi) {
|
||||||
_lastRssi = rssi;
|
_lastRssi = rssi;
|
||||||
print('📡 [BLE] RSSI updated: $rssi dBm');
|
|
||||||
onRssiUpdate?.call(rssi);
|
onRssiUpdate?.call(rssi);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ class ContactTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 2),
|
const SizedBox(width: 2),
|
||||||
Text(
|
Text(
|
||||||
contact.hasPath ? 'Direct' : 'Flood',
|
contact.hasPath ? AppLocalizations.of(context)!.direct : AppLocalizations.of(context)!.flood,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -189,7 +189,7 @@ class ContactTile extends StatelessWidget {
|
|||||||
const Icon(Icons.admin_panel_settings, size: 10, color: Colors.red),
|
const Icon(Icons.admin_panel_settings, size: 10, color: Colors.red),
|
||||||
const SizedBox(width: 2),
|
const SizedBox(width: 2),
|
||||||
Text(
|
Text(
|
||||||
'Admin',
|
AppLocalizations.of(context)!.admin,
|
||||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -213,7 +213,7 @@ class ContactTile extends StatelessWidget {
|
|||||||
const Icon(Icons.check_circle, size: 10, color: Colors.green),
|
const Icon(Icons.check_circle, size: 10, color: Colors.green),
|
||||||
const SizedBox(width: 2),
|
const SizedBox(width: 2),
|
||||||
Text(
|
Text(
|
||||||
'Logged In',
|
AppLocalizations.of(context)!.loggedIn,
|
||||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
color: Colors.green,
|
color: Colors.green,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -263,7 +263,7 @@ class ContactTile extends StatelessWidget {
|
|||||||
const Icon(Icons.sensors_off, size: 12, color: Colors.grey),
|
const Icon(Icons.sensors_off, size: 12, color: Colors.grey),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'No GPS data',
|
AppLocalizations.of(context)!.noGpsData,
|
||||||
style: Theme.of(context).textTheme.labelSmall,
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -277,7 +277,7 @@ class ContactTile extends StatelessWidget {
|
|||||||
const Icon(Icons.straighten, size: 12, color: Colors.blue),
|
const Icon(Icons.straighten, size: 12, color: Colors.blue),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'Distance: $distanceText',
|
'${AppLocalizations.of(context)!.distance}: $distanceText',
|
||||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
@@ -300,8 +300,8 @@ class ContactTile extends StatelessWidget {
|
|||||||
ToastLogger.info(
|
ToastLogger.info(
|
||||||
context,
|
context,
|
||||||
hasPath
|
hasPath
|
||||||
? 'Pinging ${contact.displayName} (direct via path)...'
|
? AppLocalizations.of(context)!.pingingDirect(contact.displayName)
|
||||||
: 'Pinging ${contact.displayName} (flooding - no path)...',
|
: AppLocalizations.of(context)!.pingingFlood(contact.displayName),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Use smart ping with automatic fallback
|
// Use smart ping with automatic fallback
|
||||||
@@ -313,7 +313,7 @@ class ContactTile extends StatelessWidget {
|
|||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ToastLogger.warning(
|
ToastLogger.warning(
|
||||||
context,
|
context,
|
||||||
'Direct ping timeout - retrying ${contact.displayName} with flooding...',
|
AppLocalizations.of(context)!.directPingTimeout(contact.displayName),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -324,12 +324,15 @@ class ContactTile extends StatelessWidget {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
ToastLogger.success(
|
ToastLogger.success(
|
||||||
context,
|
context,
|
||||||
'Ping successful to ${contact.displayName}${result.retriedWithFlooding ? ' (via flooding fallback)' : ''}',
|
AppLocalizations.of(context)!.pingSuccessful(
|
||||||
|
contact.displayName,
|
||||||
|
result.retriedWithFlooding ? AppLocalizations.of(context)!.viaFloodingFallback : '',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ToastLogger.error(
|
ToastLogger.error(
|
||||||
context,
|
context,
|
||||||
'Ping failed to ${contact.displayName} - no response received',
|
AppLocalizations.of(context)!.pingFailed(contact.displayName),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -362,8 +365,7 @@ class ContactTile extends StatelessWidget {
|
|||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: Text(AppLocalizations.of(context)!.deleteContact),
|
title: Text(AppLocalizations.of(context)!.deleteContact),
|
||||||
content: Text(
|
content: Text(
|
||||||
'Are you sure you want to delete "${contact.displayName}"?\n\n'
|
AppLocalizations.of(context)!.deleteContactConfirmation(contact.displayName),
|
||||||
'This will remove the contact from both the app and the companion radio device.',
|
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -390,7 +392,7 @@ class ContactTile extends StatelessWidget {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Show loading toast
|
// Show loading toast
|
||||||
ToastLogger.info(context, 'Removing ${contact.displayName}...');
|
ToastLogger.info(context, AppLocalizations.of(context)!.removingContact(contact.displayName));
|
||||||
|
|
||||||
// Remove contact from provider (which will also remove from device)
|
// Remove contact from provider (which will also remove from device)
|
||||||
await contactsProvider.removeContact(
|
await contactsProvider.removeContact(
|
||||||
@@ -403,11 +405,11 @@ class ContactTile extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ToastLogger.success(context, 'Contact "${contact.displayName}" removed');
|
ToastLogger.success(context, AppLocalizations.of(context)!.contactRemoved(contact.displayName));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ToastLogger.error(context, 'Failed to remove contact: $e');
|
ToastLogger.error(context, AppLocalizations.of(context)!.failedToRemoveContact(e.toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -483,18 +485,18 @@ class ContactTile extends StatelessWidget {
|
|||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
_DetailRow('Type', contact.type.displayName),
|
_DetailRow(AppLocalizations.of(context)!.type, contact.type.displayName),
|
||||||
// Public Key with copy button
|
// Public Key with copy button
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
SizedBox(
|
||||||
width: 100,
|
width: 100,
|
||||||
child: Text(
|
child: Text(
|
||||||
'Public Key:',
|
'${AppLocalizations.of(context)!.publicKey}:',
|
||||||
style: TextStyle(fontWeight: FontWeight.w500),
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -524,40 +526,40 @@ class ContactTile extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_DetailRow('Last Seen', contact.timeSinceLastSeen),
|
_DetailRow(AppLocalizations.of(context)!.lastSeen, contact.timeSinceLastSeen),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// Room Login Status
|
// Room Login Status
|
||||||
if (roomLoginState != null) ...[
|
if (roomLoginState != null) ...[
|
||||||
const Text(
|
Text(
|
||||||
'Room Status:',
|
'${AppLocalizations.of(context)!.roomStatus}:',
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_DetailRow(
|
_DetailRow(
|
||||||
'Login Status',
|
AppLocalizations.of(context)!.loginStatus,
|
||||||
roomLoginState.isLoggedIn ? 'Logged In' : 'Not Logged In',
|
roomLoginState.isLoggedIn ? AppLocalizations.of(context)!.loggedIn : AppLocalizations.of(context)!.notLoggedIn,
|
||||||
),
|
),
|
||||||
if (roomLoginState.isLoggedIn) ...[
|
if (roomLoginState.isLoggedIn) ...[
|
||||||
_DetailRow(
|
_DetailRow(
|
||||||
'Admin Access',
|
AppLocalizations.of(context)!.adminAccess,
|
||||||
roomLoginState.isAdmin ? 'Yes' : 'No',
|
roomLoginState.isAdmin ? AppLocalizations.of(context)!.yes : AppLocalizations.of(context)!.no,
|
||||||
),
|
),
|
||||||
_DetailRow(
|
_DetailRow(
|
||||||
'Permissions',
|
AppLocalizations.of(context)!.permissions,
|
||||||
roomLoginState.permissions.toString(),
|
roomLoginState.permissions.toString(),
|
||||||
),
|
),
|
||||||
if (roomLoginState.loginDurationFormatted != null)
|
if (roomLoginState.loginDurationFormatted != null)
|
||||||
_DetailRow(
|
_DetailRow(
|
||||||
'Logged In',
|
AppLocalizations.of(context)!.loggedIn,
|
||||||
roomLoginState.loginDurationFormatted!,
|
roomLoginState.loginDurationFormatted!,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
_DetailRow(
|
_DetailRow(
|
||||||
'Password Saved',
|
AppLocalizations.of(context)!.passwordSaved,
|
||||||
roomLoginState.hasPassword ? 'Yes' : 'No',
|
roomLoginState.hasPassword ? AppLocalizations.of(context)!.yes : AppLocalizations.of(context)!.no,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
@@ -565,9 +567,9 @@ class ContactTile extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
'Location:',
|
AppLocalizations.of(context)!.locationColon,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
@@ -632,9 +634,9 @@ class ContactTile extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
'Telemetry:',
|
'${AppLocalizations.of(context)!.telemetry}:',
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
@@ -643,7 +645,7 @@ class ContactTile extends StatelessWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
final connectionProvider = context.read<ConnectionProvider>();
|
final connectionProvider = context.read<ConnectionProvider>();
|
||||||
connectionProvider.requestTelemetry(contact.publicKey, zeroHop: true);
|
connectionProvider.requestTelemetry(contact.publicKey, zeroHop: true);
|
||||||
ToastLogger.info(context, 'Requesting telemetry from ${contact.displayName}...');
|
ToastLogger.info(context, AppLocalizations.of(context)!.requestingTelemetry(contact.displayName));
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.refresh, size: 18),
|
icon: const Icon(Icons.refresh, size: 18),
|
||||||
label: Text(AppLocalizations.of(context)!.refresh),
|
label: Text(AppLocalizations.of(context)!.refresh),
|
||||||
@@ -656,25 +658,25 @@ class ContactTile extends StatelessWidget {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
if (contact.telemetry!.batteryMilliVolts != null)
|
if (contact.telemetry!.batteryMilliVolts != null)
|
||||||
_DetailRow(
|
_DetailRow(
|
||||||
'Voltage',
|
AppLocalizations.of(context)!.voltage,
|
||||||
'${(contact.telemetry!.batteryMilliVolts! / 1000).toStringAsFixed(3)}V'
|
'${(contact.telemetry!.batteryMilliVolts! / 1000).toStringAsFixed(3)}V'
|
||||||
'${contact.telemetry!.batteryPercentage != null ? ' (${contact.telemetry!.batteryPercentage!.toStringAsFixed(1)}%)' : ''}',
|
'${contact.telemetry!.batteryPercentage != null ? ' (${contact.telemetry!.batteryPercentage!.toStringAsFixed(1)}%)' : ''}',
|
||||||
)
|
)
|
||||||
else if (contact.telemetry!.batteryPercentage != null)
|
else if (contact.telemetry!.batteryPercentage != null)
|
||||||
_DetailRow('Battery', '${contact.telemetry!.batteryPercentage!.toStringAsFixed(1)}%'),
|
_DetailRow(AppLocalizations.of(context)!.battery, '${contact.telemetry!.batteryPercentage!.toStringAsFixed(1)}%'),
|
||||||
if (contact.telemetry!.temperature != null)
|
if (contact.telemetry!.temperature != null)
|
||||||
_DetailRow('Temperature', '${contact.telemetry!.temperature!.toStringAsFixed(1)}°C'),
|
_DetailRow(AppLocalizations.of(context)!.temperature, '${contact.telemetry!.temperature!.toStringAsFixed(1)}°C'),
|
||||||
if (contact.telemetry!.humidity != null)
|
if (contact.telemetry!.humidity != null)
|
||||||
_DetailRow('Humidity', '${contact.telemetry!.humidity!.toStringAsFixed(1)}%'),
|
_DetailRow(AppLocalizations.of(context)!.humidity, '${contact.telemetry!.humidity!.toStringAsFixed(1)}%'),
|
||||||
if (contact.telemetry!.pressure != null)
|
if (contact.telemetry!.pressure != null)
|
||||||
_DetailRow('Pressure', '${contact.telemetry!.pressure!.toStringAsFixed(1)} hPa'),
|
_DetailRow(AppLocalizations.of(context)!.pressure, '${contact.telemetry!.pressure!.toStringAsFixed(1)} hPa'),
|
||||||
if (contact.telemetry!.gpsLocation != null)
|
if (contact.telemetry!.gpsLocation != null)
|
||||||
_DetailRow(
|
_DetailRow(
|
||||||
'GPS (Telemetry)',
|
AppLocalizations.of(context)!.gpsTelemetry,
|
||||||
'${contact.telemetry!.gpsLocation!.latitude.toStringAsFixed(6)}, ${contact.telemetry!.gpsLocation!.longitude.toStringAsFixed(6)}',
|
'${contact.telemetry!.gpsLocation!.latitude.toStringAsFixed(6)}, ${contact.telemetry!.gpsLocation!.longitude.toStringAsFixed(6)}',
|
||||||
),
|
),
|
||||||
_DetailRow(
|
_DetailRow(
|
||||||
'Updated',
|
AppLocalizations.of(context)!.updated,
|
||||||
'${_formatTimestamp(contact.telemetry!.timestamp)} (${_formatTimeAgo(contact.telemetry!.timestamp)})',
|
'${_formatTimestamp(contact.telemetry!.timestamp)} (${_formatTimeAgo(contact.telemetry!.timestamp)})',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -703,7 +705,7 @@ class ContactTile extends StatelessWidget {
|
|||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
connectionProvider.resetPath(contact.publicKey);
|
connectionProvider.resetPath(contact.publicKey);
|
||||||
ToastLogger.info(context, 'Path reset for ${contact.displayName}. Next message will find a new route.');
|
ToastLogger.info(context, AppLocalizations.of(context)!.pathResetInfo(contact.displayName));
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.route),
|
icon: const Icon(Icons.route),
|
||||||
label: Text(AppLocalizations.of(context)!.resetPath),
|
label: Text(AppLocalizations.of(context)!.resetPath),
|
||||||
@@ -726,7 +728,7 @@ class ContactTile extends StatelessWidget {
|
|||||||
_showRoomLoginDialog(context, contact);
|
_showRoomLoginDialog(context, contact);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.login),
|
icon: const Icon(Icons.login),
|
||||||
label: Text(roomLoginState?.isLoggedIn == true ? 'Re-Login to Room' : 'Login to Room'),
|
label: Text(roomLoginState?.isLoggedIn == true ? AppLocalizations.of(context)!.reLoginToRoom : AppLocalizations.of(context)!.loginToRoom),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
backgroundColor: _getTypeColor(contact.type, context),
|
backgroundColor: _getTypeColor(contact.type, context),
|
||||||
|
|||||||
@@ -151,11 +151,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
|||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(AppLocalizations.of(context)!.failedToAddRoom(e.toString())),
|
||||||
'Failed to add room to device: $e\n\n'
|
|
||||||
'The room may not have advertised yet.\n'
|
|
||||||
'Try waiting for the room to broadcast.',
|
|
||||||
),
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.error,
|
backgroundColor: Theme.of(context).colorScheme.error,
|
||||||
duration: const Duration(seconds: 7),
|
duration: const Duration(seconds: 7),
|
||||||
),
|
),
|
||||||
@@ -316,7 +312,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Login to Room',
|
AppLocalizations.of(context)!.loginToRoom,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: colorScheme.onSurface,
|
color: colorScheme.onSurface,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
@@ -358,7 +354,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
|||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Enter the password to access this room. The password will be saved for future use.',
|
AppLocalizations.of(context)!.enterPasswordInfo,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: colorScheme.onPrimaryContainer,
|
color: colorScheme.onPrimaryContainer,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
@@ -392,9 +388,9 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
|||||||
maxLengthEnforcement: MaxLengthEnforcement.enforced,
|
maxLengthEnforcement: MaxLengthEnforcement.enforced,
|
||||||
style: TextStyle(color: colorScheme.onSurface),
|
style: TextStyle(color: colorScheme.onSurface),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Password',
|
labelText: AppLocalizations.of(context)!.password,
|
||||||
labelStyle: TextStyle(color: colorScheme.onSurfaceVariant),
|
labelStyle: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||||
hintText: 'Enter room password',
|
hintText: AppLocalizations.of(context)!.enterRoomPassword,
|
||||||
hintStyle: TextStyle(color: colorScheme.onSurfaceVariant),
|
hintStyle: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
@@ -436,7 +432,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
|||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: const Icon(Icons.login),
|
: const Icon(Icons.login),
|
||||||
label: Text(_isLoggingIn ? 'Logging in...' : 'Login'),
|
label: Text(_isLoggingIn ? AppLocalizations.of(context)!.loggingInDots : AppLocalizations.of(context)!.login),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import '../../../models/contact.dart';
|
import '../../../models/contact.dart';
|
||||||
|
|
||||||
/// Contact list section for the compass dialog.
|
/// Contact list section for the compass dialog.
|
||||||
@@ -28,7 +29,7 @@ class CompassContactList extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (position == null) {
|
if (position == null) {
|
||||||
return const Text('Location unavailable');
|
return Text(AppLocalizations.of(context)!.locationUnavailable);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate bearings and distances
|
// Calculate bearings and distances
|
||||||
@@ -60,13 +61,14 @@ class CompassContactList extends StatelessWidget {
|
|||||||
contactsWithBearing.sort((a, b) =>
|
contactsWithBearing.sort((a, b) =>
|
||||||
(a['distance'] as double).compareTo(b['distance'] as double));
|
(a['distance'] as double).compareTo(b['distance'] as double));
|
||||||
|
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 16, bottom: 8),
|
padding: const EdgeInsets.only(left: 16, bottom: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Nearby Contacts',
|
l10n.nearbyContacts,
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -120,7 +122,7 @@ class CompassContactList extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
if (heading != null)
|
if (heading != null)
|
||||||
Text(
|
Text(
|
||||||
_formatRelativeBearing(bearing, heading!),
|
_formatRelativeBearing(bearing, heading!, context),
|
||||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
@@ -189,7 +191,8 @@ class CompassContactList extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatRelativeBearing(double bearing, double heading) {
|
String _formatRelativeBearing(double bearing, double heading, BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
// Calculate relative bearing (how much to turn from current heading)
|
// Calculate relative bearing (how much to turn from current heading)
|
||||||
double relative = bearing - heading;
|
double relative = bearing - heading;
|
||||||
|
|
||||||
@@ -204,11 +207,11 @@ class CompassContactList extends StatelessWidget {
|
|||||||
final absRelative = relative.abs().round();
|
final absRelative = relative.abs().round();
|
||||||
|
|
||||||
if (absRelative < 10) {
|
if (absRelative < 10) {
|
||||||
return 'ahead';
|
return l10n.ahead;
|
||||||
} else if (relative > 0) {
|
} else if (relative > 0) {
|
||||||
return '$absRelative° right';
|
return l10n.degreesRight(absRelative);
|
||||||
} else {
|
} else {
|
||||||
return '$absRelative° left';
|
return l10n.degreesLeft(absRelative);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../../l10n/app_localizations.dart';
|
||||||
|
|
||||||
/// Filter controls for the compass dialog.
|
/// Filter controls for the compass dialog.
|
||||||
/// Allows filtering of contacts and SAR marker types.
|
/// Allows filtering of contacts and SAR marker types.
|
||||||
@@ -32,15 +33,16 @@ class CompassFilters extends StatefulWidget {
|
|||||||
|
|
||||||
class _CompassFiltersState extends State<CompassFilters> {
|
class _CompassFiltersState extends State<CompassFilters> {
|
||||||
void _showFilterDialog() {
|
void _showFilterDialog() {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => StatefulBuilder(
|
builder: (context) => StatefulBuilder(
|
||||||
builder: (context, setDialogState) => AlertDialog(
|
builder: (context, setDialogState) => AlertDialog(
|
||||||
title: const Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.filter_list, size: 20),
|
const Icon(Icons.filter_list, size: 20),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text('Filter Markers'),
|
Text(l10n.filterMarkers),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
|
contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
|
||||||
@@ -52,7 +54,7 @@ class _CompassFiltersState extends State<CompassFilters> {
|
|||||||
_CompactFilterItem(
|
_CompactFilterItem(
|
||||||
icon: Icons.person,
|
icon: Icons.person,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
label: 'Contacts',
|
label: l10n.contactsFilter,
|
||||||
value: widget.showContacts,
|
value: widget.showContacts,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
widget.onShowContactsChanged(value);
|
widget.onShowContactsChanged(value);
|
||||||
@@ -66,7 +68,7 @@ class _CompassFiltersState extends State<CompassFilters> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 8, bottom: 8, top: 4),
|
padding: const EdgeInsets.only(left: 8, bottom: 8, top: 4),
|
||||||
child: Text(
|
child: Text(
|
||||||
'SAR Markers',
|
l10n.sarMarkers,
|
||||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -75,7 +77,7 @@ class _CompassFiltersState extends State<CompassFilters> {
|
|||||||
_CompactFilterItem(
|
_CompactFilterItem(
|
||||||
icon: Icons.person_pin,
|
icon: Icons.person_pin,
|
||||||
color: Colors.green,
|
color: Colors.green,
|
||||||
label: 'Found Person',
|
label: l10n.foundPerson,
|
||||||
value: widget.showFoundPerson,
|
value: widget.showFoundPerson,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
widget.onShowFoundPersonChanged(value);
|
widget.onShowFoundPersonChanged(value);
|
||||||
@@ -86,7 +88,7 @@ class _CompassFiltersState extends State<CompassFilters> {
|
|||||||
_CompactFilterItem(
|
_CompactFilterItem(
|
||||||
icon: Icons.local_fire_department,
|
icon: Icons.local_fire_department,
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
label: 'Fire',
|
label: l10n.fire,
|
||||||
value: widget.showFire,
|
value: widget.showFire,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
widget.onShowFireChanged(value);
|
widget.onShowFireChanged(value);
|
||||||
@@ -97,7 +99,7 @@ class _CompassFiltersState extends State<CompassFilters> {
|
|||||||
_CompactFilterItem(
|
_CompactFilterItem(
|
||||||
icon: Icons.home_work,
|
icon: Icons.home_work,
|
||||||
color: Colors.orange,
|
color: Colors.orange,
|
||||||
label: 'Staging Area',
|
label: l10n.stagingArea,
|
||||||
value: widget.showStagingArea,
|
value: widget.showStagingArea,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
widget.onShowStagingAreaChanged(value);
|
widget.onShowStagingAreaChanged(value);
|
||||||
@@ -112,11 +114,11 @@ class _CompassFiltersState extends State<CompassFilters> {
|
|||||||
widget.onShowAll();
|
widget.onShowAll();
|
||||||
setDialogState(() {});
|
setDialogState(() {});
|
||||||
},
|
},
|
||||||
child: const Text('Show All'),
|
child: Text(l10n.showAll),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: const Text('Close'),
|
child: Text(l10n.close),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -126,9 +128,10 @@ class _CompassFiltersState extends State<CompassFilters> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
return IconButton(
|
return IconButton(
|
||||||
icon: const Icon(Icons.filter_list),
|
icon: const Icon(Icons.filter_list),
|
||||||
tooltip: 'Filter markers',
|
tooltip: l10n.filterMarkersTooltip,
|
||||||
onPressed: () => _showFilterDialog(),
|
onPressed: () => _showFilterDialog(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:ui' as ui;
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
import '../../../l10n/app_localizations.dart';
|
||||||
import '../../../models/contact.dart';
|
import '../../../models/contact.dart';
|
||||||
import '../../../models/sar_marker.dart';
|
import '../../../models/sar_marker.dart';
|
||||||
|
|
||||||
@@ -76,18 +77,19 @@ class CompassHeader extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildInfoRow(BuildContext context, double? heading, Position? position) {
|
Widget _buildInfoRow(BuildContext context, double? heading, Position? position) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
_buildInfoCard(
|
_buildInfoCard(
|
||||||
context,
|
context,
|
||||||
'Heading',
|
l10n.heading,
|
||||||
heading != null ? '${heading.round()}°' : '--',
|
heading != null ? '${heading.round()}°' : '--',
|
||||||
Icons.explore,
|
Icons.explore,
|
||||||
),
|
),
|
||||||
_buildInfoCard(
|
_buildInfoCard(
|
||||||
context,
|
context,
|
||||||
'Elevation',
|
l10n.elevation,
|
||||||
position?.altitude != null
|
position?.altitude != null
|
||||||
? '${position!.altitude.round()}m'
|
? '${position!.altitude.round()}m'
|
||||||
: '--',
|
: '--',
|
||||||
@@ -95,7 +97,7 @@ class CompassHeader extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
_buildInfoCard(
|
_buildInfoCard(
|
||||||
context,
|
context,
|
||||||
'Accuracy',
|
l10n.accuracy,
|
||||||
position?.accuracy != null
|
position?.accuracy != null
|
||||||
? '±${position!.accuracy.round()}m'
|
? '±${position!.accuracy.round()}m'
|
||||||
: '--',
|
: '--',
|
||||||
@@ -566,12 +568,16 @@ class _LocationFormatToggleState extends State<_LocationFormatToggle> {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final String displayText;
|
final String displayText;
|
||||||
|
|
||||||
if (_showDMS) {
|
if (_showDMS) {
|
||||||
displayText = '${_formatDMS(position.latitude, true)} ${_formatDMS(position.longitude, false)}';
|
displayText = '${_formatDMS(position.latitude, true)} ${_formatDMS(position.longitude, false)}';
|
||||||
} else {
|
} else {
|
||||||
displayText = 'Lat: ${position.latitude.toStringAsFixed(5)} Lon: ${position.longitude.toStringAsFixed(5)}';
|
displayText = l10n.latLonFormat(
|
||||||
|
position.latitude.toStringAsFixed(5),
|
||||||
|
position.longitude.toStringAsFixed(5),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import '../../../models/sar_marker.dart';
|
import '../../../models/sar_marker.dart';
|
||||||
|
|
||||||
/// SAR marker list section for the compass dialog.
|
/// SAR marker list section for the compass dialog.
|
||||||
@@ -28,7 +29,7 @@ class CompassSarList extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (position == null) {
|
if (position == null) {
|
||||||
return const Text('Location unavailable');
|
return Text(AppLocalizations.of(context)!.locationUnavailable);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate bearings and distances for SAR markers
|
// Calculate bearings and distances for SAR markers
|
||||||
@@ -58,13 +59,14 @@ class CompassSarList extends StatelessWidget {
|
|||||||
markersWithBearing.sort((a, b) =>
|
markersWithBearing.sort((a, b) =>
|
||||||
(a['distance'] as double).compareTo(b['distance'] as double));
|
(a['distance'] as double).compareTo(b['distance'] as double));
|
||||||
|
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 16, top: 16, bottom: 8),
|
padding: const EdgeInsets.only(left: 16, top: 16, bottom: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
'SAR Markers',
|
l10n.sarMarkers,
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -139,7 +141,7 @@ class CompassSarList extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
if (heading != null)
|
if (heading != null)
|
||||||
Text(
|
Text(
|
||||||
_formatRelativeBearing(bearing, heading!),
|
_formatRelativeBearing(bearing, heading!, context),
|
||||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
@@ -208,7 +210,8 @@ class CompassSarList extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatRelativeBearing(double bearing, double heading) {
|
String _formatRelativeBearing(double bearing, double heading, BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
// Calculate relative bearing (how much to turn from current heading)
|
// Calculate relative bearing (how much to turn from current heading)
|
||||||
double relative = bearing - heading;
|
double relative = bearing - heading;
|
||||||
|
|
||||||
@@ -223,11 +226,11 @@ class CompassSarList extends StatelessWidget {
|
|||||||
final absRelative = relative.abs().round();
|
final absRelative = relative.abs().round();
|
||||||
|
|
||||||
if (absRelative < 10) {
|
if (absRelative < 10) {
|
||||||
return 'ahead';
|
return l10n.ahead;
|
||||||
} else if (relative > 0) {
|
} else if (relative > 0) {
|
||||||
return '$absRelative° right';
|
return l10n.degreesRight(absRelative);
|
||||||
} else {
|
} else {
|
||||||
return '$absRelative° left';
|
return l10n.degreesLeft(absRelative);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'Quick location marker',
|
AppLocalizations.of(context)!.quickLocationMarker,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: colorScheme.onSurfaceVariant,
|
color: colorScheme.onSurfaceVariant,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@@ -203,7 +203,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
|||||||
children: [
|
children: [
|
||||||
// Marker type selection
|
// Marker type selection
|
||||||
Text(
|
Text(
|
||||||
'Marker Type',
|
AppLocalizations.of(context)!.markerType,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: colorScheme.onSurface,
|
color: colorScheme.onSurface,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@@ -238,7 +238,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
|||||||
|
|
||||||
// Destination selection (compact dropdown with rooms and channel)
|
// Destination selection (compact dropdown with rooms and channel)
|
||||||
Text(
|
Text(
|
||||||
'Send To',
|
AppLocalizations.of(context)!.sendTo,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: colorScheme.onSurface,
|
color: colorScheme.onSurface,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@@ -262,14 +262,14 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
|||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.error_outline, color: Colors.red, size: 20),
|
const Icon(Icons.error_outline, color: Colors.red, size: 20),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'No destinations available.',
|
AppLocalizations.of(context)!.noDestinationsAvailable,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white70,
|
color: Colors.white70,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
),
|
),
|
||||||
@@ -298,7 +298,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
|||||||
Icon(Icons.arrow_drop_down_circle, size: 18, color: colorScheme.onSurfaceVariant),
|
Icon(Icons.arrow_drop_down_circle, size: 18, color: colorScheme.onSurfaceVariant),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text(
|
Text(
|
||||||
'Select destination...',
|
AppLocalizations.of(context)!.selectDestination,
|
||||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -373,8 +373,8 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
_selectedContact!.isChannel
|
_selectedContact!.isChannel
|
||||||
? 'Ephemeral: Broadcast over-the-air only. Not stored - nodes must be online.'
|
? AppLocalizations.of(context)!.ephemeralBroadcastInfo
|
||||||
: 'Persistent: Stored immutably in room. Synced automatically and preserved offline.',
|
: AppLocalizations.of(context)!.persistentRoomInfo,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white70,
|
color: Colors.white70,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
@@ -390,7 +390,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Location',
|
AppLocalizations.of(context)!.location,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: colorScheme.onSurface,
|
color: colorScheme.onSurface,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@@ -409,9 +409,9 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
|||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'From Map',
|
AppLocalizations.of(context)!.fromMap,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -441,7 +441,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Text(
|
Text(
|
||||||
'Getting location...',
|
AppLocalizations.of(context)!.gettingLocation,
|
||||||
style: TextStyle(color: colorScheme.onSurface),
|
style: TextStyle(color: colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -466,9 +466,9 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
'Location Error',
|
AppLocalizations.of(context)!.locationError,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
@@ -488,7 +488,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.refresh, color: Colors.red),
|
icon: const Icon(Icons.refresh, color: Colors.red),
|
||||||
onPressed: _getCurrentLocation,
|
onPressed: _getCurrentLocation,
|
||||||
tooltip: 'Retry',
|
tooltip: AppLocalizations.of(context)!.retry,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -529,7 +529,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
|||||||
onPressed: _getCurrentLocation,
|
onPressed: _getCurrentLocation,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
tooltip: 'Refresh location',
|
tooltip: AppLocalizations.of(context)!.refreshLocation,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -544,7 +544,9 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
'Accuracy: ±${_currentPosition!.accuracy!.round()}m',
|
AppLocalizations.of(context)!.accuracyMeters(
|
||||||
|
_currentPosition!.accuracy!.round(),
|
||||||
|
),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: colorScheme.onSurfaceVariant,
|
color: colorScheme.onSurfaceVariant,
|
||||||
@@ -560,7 +562,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
|||||||
|
|
||||||
// Optional notes
|
// Optional notes
|
||||||
Text(
|
Text(
|
||||||
'Notes (optional)',
|
AppLocalizations.of(context)!.notesOptional,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: colorScheme.onSurface,
|
color: colorScheme.onSurface,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@@ -574,7 +576,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
|||||||
maxLength: 100,
|
maxLength: 100,
|
||||||
style: TextStyle(fontSize: 14, color: colorScheme.onSurface),
|
style: TextStyle(fontSize: 14, color: colorScheme.onSurface),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Add additional information...',
|
hintText: AppLocalizations.of(context)!.addAdditionalInformation,
|
||||||
hintStyle: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant),
|
hintStyle: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: colorScheme.surfaceContainerHighest,
|
fillColor: colorScheme.surfaceContainerHighest,
|
||||||
@@ -648,9 +650,9 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
|||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: Text(AppLocalizations.of(context)!.lowLocationAccuracy),
|
title: Text(AppLocalizations.of(context)!.lowLocationAccuracy),
|
||||||
content: Text(
|
content: Text(
|
||||||
'Location accuracy is ±${_currentPosition!.accuracy!.round()}m. '
|
AppLocalizations.of(context)!.lowAccuracyWarning(
|
||||||
'This may not be accurate enough for SAR operations.\n\n'
|
_currentPosition!.accuracy!.round(),
|
||||||
'Continue anyway?',
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
|
|||||||
Reference in New Issue
Block a user