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": {
|
||||
"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.",
|
||||
|
||||
"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:
|
||||
/// **'Type your message...'**
|
||||
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
|
||||
|
||||
@@ -404,4 +404,281 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
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
|
||||
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
|
||||
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.",
|
||||
|
||||
"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
|
||||
final RoomLoginManager _roomLoginManager = RoomLoginManager();
|
||||
final MessageDeliveryTracker _messageDeliveryTracker = MessageDeliveryTracker();
|
||||
final MessageDeliveryTracker _messageDeliveryTracker =
|
||||
MessageDeliveryTracker();
|
||||
final PingTracker _pingTracker = PingTracker();
|
||||
|
||||
// Expose room login states
|
||||
Map<String, RoomLoginState> get roomLoginStates => _roomLoginManager.roomLoginStates;
|
||||
Map<String, RoomLoginState> get roomLoginStates =>
|
||||
_roomLoginManager.roomLoginStates;
|
||||
|
||||
// Callbacks for other providers
|
||||
Function(Contact)? onContactReceived;
|
||||
Function(List<Contact>)? onContactsComplete;
|
||||
Function(Message)? onMessageReceived;
|
||||
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 publicKeyPrefix, int permissions, bool isAdmin, int tag)? onLoginSuccess;
|
||||
Function(Uint8List publicKeyPrefix, int permissions, bool isAdmin, int tag)?
|
||||
onLoginSuccess;
|
||||
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(Uint8List publicKeyPrefix, Uint8List statusData)? onStatusResponse;
|
||||
|
||||
@@ -106,11 +111,13 @@ class ConnectionProvider with ChangeNotifier {
|
||||
connectionState: isConnected
|
||||
? ConnectionState.connected
|
||||
: (_bleService.isReconnecting
|
||||
? ConnectionState.connecting
|
||||
: ConnectionState.disconnected),
|
||||
? ConnectionState.connecting
|
||||
: ConnectionState.disconnected),
|
||||
lastUpdate: DateTime.now(),
|
||||
);
|
||||
print(' Updated deviceInfo.connectionState: ${_deviceInfo.connectionState}');
|
||||
print(
|
||||
' Updated deviceInfo.connectionState: ${_deviceInfo.connectionState}',
|
||||
);
|
||||
print(' Updated deviceInfo.isConnected: ${_deviceInfo.isConnected}');
|
||||
print(' isReconnecting: ${_bleService.isReconnecting}');
|
||||
notifyListeners();
|
||||
@@ -137,7 +144,9 @@ class ConnectionProvider with ChangeNotifier {
|
||||
connectionState: ConnectionState.error,
|
||||
);
|
||||
} else {
|
||||
print(' Keeping connection state as connected (ignoring data parsing error)');
|
||||
print(
|
||||
' Keeping connection state as connected (ignoring data parsing error)',
|
||||
);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
@@ -165,7 +174,9 @@ class ConnectionProvider with ChangeNotifier {
|
||||
|
||||
_bleService.onBinaryResponse = (publicKeyPrefix, tag, responseData) {
|
||||
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(' Response data: ${responseData.length} bytes');
|
||||
// Mark ping as successful if this was a ping request
|
||||
@@ -180,15 +191,20 @@ class ConnectionProvider with ChangeNotifier {
|
||||
};
|
||||
|
||||
_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
|
||||
// This is the CORRECT way to receive messages - room server pushes them
|
||||
syncAllMessages();
|
||||
};
|
||||
|
||||
_bleService.onLoginSuccess = (publicKeyPrefix, permissions, isAdmin, tag) async {
|
||||
_bleService
|
||||
.onLoginSuccess = (publicKeyPrefix, permissions, isAdmin, tag) async {
|
||||
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');
|
||||
|
||||
// Update room login state via helper
|
||||
@@ -205,12 +221,12 @@ class ConnectionProvider with ChangeNotifier {
|
||||
|
||||
_bleService.onLoginFail = (publicKeyPrefix) {
|
||||
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
|
||||
_roomLoginManager.handleLoginFail(
|
||||
publicKeyPrefix: publicKeyPrefix,
|
||||
);
|
||||
_roomLoginManager.handleLoginFail(publicKeyPrefix: publicKeyPrefix);
|
||||
notifyListeners();
|
||||
|
||||
onLoginFail?.call(publicKeyPrefix);
|
||||
@@ -218,22 +234,33 @@ class ConnectionProvider with ChangeNotifier {
|
||||
|
||||
_bleService.onAdvertReceived = (publicKey) {
|
||||
print('📥 [Provider] Advert received from node');
|
||||
print(' 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');
|
||||
print(
|
||||
' 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
|
||||
// which will trigger onContactReceived callback and add/update the contact
|
||||
};
|
||||
|
||||
_bleService.onPathUpdated = (publicKey) {
|
||||
print('📥 [Provider] Path updated for contact');
|
||||
print(' 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');
|
||||
print(
|
||||
' 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
|
||||
onPathUpdated?.call(publicKey);
|
||||
};
|
||||
|
||||
_bleService.onMessageSent = (expectedAckTag, suggestedTimeoutMs, isFloodMode) {
|
||||
print('📥 [Provider] Message sent - ACK tag: $expectedAckTag, timeout: ${suggestedTimeoutMs}ms');
|
||||
_bleService
|
||||
.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
|
||||
final messageId = _messageDeliveryTracker.popPendingMessageId();
|
||||
@@ -247,18 +274,24 @@ class ConnectionProvider with ChangeNotifier {
|
||||
// Notify callback with message ID
|
||||
onMessageSent?.call(messageId, expectedAckTag, suggestedTimeoutMs);
|
||||
} 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) {
|
||||
print('📥 [Provider] Message delivered - ACK code: $ackCode, RTT: ${roundTripTimeMs}ms');
|
||||
print(
|
||||
'📥 [Provider] Message delivered - ACK code: $ackCode, RTT: ${roundTripTimeMs}ms',
|
||||
);
|
||||
onMessageDelivered?.call(ackCode, roundTripTimeMs);
|
||||
};
|
||||
|
||||
_bleService.onStatusResponse = (publicKeyPrefix, statusData) {
|
||||
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');
|
||||
// Forward the callback to whoever needs it (e.g., ContactsProvider)
|
||||
onStatusResponse?.call(publicKeyPrefix, statusData);
|
||||
@@ -289,9 +322,15 @@ class ConnectionProvider with ChangeNotifier {
|
||||
|
||||
_bleService.onSelfInfoReceived = (selfInfo) {
|
||||
print('📥 [Provider] Received SelfInfo:');
|
||||
print(' TX Power: ${selfInfo['txPower']} / ${selfInfo['maxTxPower']} dBm');
|
||||
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(
|
||||
' TX Power: ${selfInfo['txPower']} / ${selfInfo['maxTxPower']} dBm',
|
||||
);
|
||||
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']}');
|
||||
|
||||
_deviceInfo = _deviceInfo.copyWith(
|
||||
@@ -316,7 +355,9 @@ class ConnectionProvider with ChangeNotifier {
|
||||
|
||||
_bleService.onBatteryAndStorage = (millivolts, usedKb, totalKb) {
|
||||
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) {
|
||||
print(' Storage Used: ${usedKb}KB');
|
||||
}
|
||||
@@ -362,7 +403,6 @@ class ConnectionProvider with ChangeNotifier {
|
||||
};
|
||||
|
||||
_bleService.onRssiUpdate = (rssi) {
|
||||
print('📡 [Provider] RSSI updated: $rssi dBm');
|
||||
_deviceInfo = _deviceInfo.copyWith(
|
||||
signalRssi: rssi,
|
||||
lastUpdate: DateTime.now(),
|
||||
@@ -381,25 +421,34 @@ class ConnectionProvider with ChangeNotifier {
|
||||
print('✅ [Provider] Scan state initialized, notifying listeners');
|
||||
|
||||
try {
|
||||
await for (final scanResult
|
||||
in _bleService.scanForDevices(timeout: const Duration(seconds: 10))) {
|
||||
await for (final scanResult in _bleService.scanForDevices(
|
||||
timeout: const Duration(seconds: 10),
|
||||
)) {
|
||||
print('📱 [Provider] Scan result received from scan stream');
|
||||
final device = scanResult.device;
|
||||
final rssi = scanResult.rssi;
|
||||
|
||||
if (!_scannedDevices.any((d) => d.device.remoteId == device.remoteId)) {
|
||||
_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();
|
||||
} else {
|
||||
// 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) {
|
||||
_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();
|
||||
} 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(
|
||||
deviceId: device.remoteId.toString(),
|
||||
deviceName: device.platformName.isNotEmpty ? device.platformName : 'Unknown',
|
||||
deviceName: device.platformName.isNotEmpty
|
||||
? device.platformName
|
||||
: 'Unknown',
|
||||
connectionState: ConnectionState.connecting,
|
||||
);
|
||||
_error = null;
|
||||
@@ -457,10 +508,9 @@ class ConnectionProvider with ChangeNotifier {
|
||||
|
||||
await _bleService.disconnect();
|
||||
|
||||
_deviceInfo = DeviceInfo(
|
||||
connectionState: ConnectionState.disconnected,
|
||||
);
|
||||
_roomLoginManager.clearRoomLoginStates(); // Clear login states on disconnect
|
||||
_deviceInfo = DeviceInfo(connectionState: ConnectionState.disconnected);
|
||||
_roomLoginManager
|
||||
.clearRoomLoginStates(); // Clear login states on disconnect
|
||||
_pingTracker.clearAll(); // Clear pending pings on disconnect
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -579,10 +629,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
try {
|
||||
await _bleService.sendChannelMessage(
|
||||
channelIdx: channelIdx,
|
||||
text: text,
|
||||
);
|
||||
await _bleService.sendChannelMessage(channelIdx: channelIdx, text: text);
|
||||
|
||||
// Channel messages are ephemeral (not persisted) - mark as "sent" immediately
|
||||
// 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.
|
||||
///
|
||||
/// [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) {
|
||||
_error = 'Not connected to device';
|
||||
notifyListeners();
|
||||
@@ -684,7 +734,9 @@ class ConnectionProvider with ChangeNotifier {
|
||||
|
||||
// First attempt timed out - retry with flooding if first was direct
|
||||
if (firstAttemptDirect) {
|
||||
print('⚠️ [Provider] Ping timeout on direct attempt, retrying with flooding...');
|
||||
print(
|
||||
'⚠️ [Provider] Ping timeout on direct attempt, retrying with flooding...',
|
||||
);
|
||||
onRetryWithFlooding?.call();
|
||||
|
||||
// Track the retry
|
||||
@@ -708,11 +760,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
// First attempt was already flooding and it timed out
|
||||
return PingResult(
|
||||
success: false,
|
||||
usedFlooding: true,
|
||||
timedOut: true,
|
||||
);
|
||||
return PingResult(success: false, usedFlooding: true, timedOut: true);
|
||||
} catch (e) {
|
||||
_error = 'Failed to ping contact: $e';
|
||||
notifyListeners();
|
||||
@@ -1003,14 +1051,19 @@ class ConnectionProvider with ChangeNotifier {
|
||||
// Keep syncing until we get NoMoreMessages response
|
||||
// The device will send ContactMsgRecv or ChannelMsgRecv responses
|
||||
// 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)
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
count++;
|
||||
@@ -1022,10 +1075,14 @@ class ConnectionProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
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;
|
||||
} catch (e) {
|
||||
print('❌ [Provider] Failed to sync messages: $e');
|
||||
|
||||
@@ -78,7 +78,10 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
|
||||
if (!connectionProvider.deviceInfo.isConnected) {
|
||||
if (context.mounted) {
|
||||
ToastLogger.error(context, AppLocalizations.of(context)!.deviceNotConnected);
|
||||
ToastLogger.error(
|
||||
context,
|
||||
AppLocalizations.of(context)!.deviceNotConnected,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -102,7 +105,10 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
if (context.mounted) {
|
||||
ToastLogger.error(context, AppLocalizations.of(context)!.locationPermissionDenied);
|
||||
ToastLogger.error(
|
||||
context,
|
||||
AppLocalizations.of(context)!.locationPermissionDenied,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -130,7 +136,10 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
} catch (e) {
|
||||
print('❌ Failed to get GPS position: $e');
|
||||
if (context.mounted) {
|
||||
ToastLogger.error(context, AppLocalizations.of(context)!.failedToGetGpsLocation);
|
||||
ToastLogger.error(
|
||||
context,
|
||||
AppLocalizations.of(context)!.failedToGetGpsLocation,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -159,7 +168,10 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
} catch (e) {
|
||||
print('❌ Failed to advertise device: $e');
|
||||
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();
|
||||
},
|
||||
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,
|
||||
),
|
||||
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 isConnected = deviceInfo.isConnected;
|
||||
|
||||
print(
|
||||
'🎨 [UI] Building status bar - isConnected: $isConnected, state: ${deviceInfo.connectionState}',
|
||||
);
|
||||
|
||||
if (!isConnected) {
|
||||
// Disconnected state: show connect button
|
||||
return Row(
|
||||
@@ -590,7 +603,10 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
Expanded(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.appTitle,
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
@@ -654,8 +670,12 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
deviceInfo.selfName ?? AppLocalizations.of(context)!.appTitle,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
deviceInfo.selfName ??
|
||||
AppLocalizations.of(context)!.appTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Row(
|
||||
@@ -674,7 +694,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
'${deviceInfo.signalRssi}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: _getSignalColor(deviceInfo.signalRssi!),
|
||||
color: _getSignalColor(
|
||||
deviceInfo.signalRssi!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -682,7 +704,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
_getBatteryIcon(deviceInfo.batteryPercent!),
|
||||
color: _getBatteryColor(deviceInfo.batteryPercent!),
|
||||
color: _getBatteryColor(
|
||||
deviceInfo.batteryPercent!,
|
||||
),
|
||||
size: 13,
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
@@ -690,7 +714,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
'${deviceInfo.batteryPercent!.round()}%',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: _getBatteryColor(deviceInfo.batteryPercent!),
|
||||
color: _getBatteryColor(
|
||||
deviceInfo.batteryPercent!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -811,7 +837,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
)
|
||||
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(
|
||||
child: Text(
|
||||
isConnected
|
||||
? deviceInfo.displayName ?? AppLocalizations.of(context)!.connect
|
||||
? deviceInfo.displayName ??
|
||||
AppLocalizations.of(context)!.connect
|
||||
: AppLocalizations.of(context)!.deviceNotConnected,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
|
||||
@@ -5,7 +5,8 @@ import '../meshcore_constants.dart';
|
||||
/// Callback types for connection events
|
||||
typedef OnConnectionStateCallback = void Function(bool isConnected);
|
||||
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);
|
||||
|
||||
/// 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)
|
||||
static const int _maxReconnectionAttempts = 30;
|
||||
static const List<int> _reconnectionDelaysMs = [
|
||||
2000, // 2s - immediate retry
|
||||
3000, // 3s - quick retry
|
||||
5000, // 5s - fast retry
|
||||
10000, // 10s - moderate retry
|
||||
15000, // 15s - longer retry
|
||||
30000, // 30s - extended retry
|
||||
30000, // 30s - keep trying every 30s after this
|
||||
2000, // 2s - immediate retry
|
||||
3000, // 3s - quick retry
|
||||
5000, // 5s - fast retry
|
||||
10000, // 10s - moderate retry
|
||||
15000, // 15s - longer retry
|
||||
30000, // 30s - extended retry
|
||||
30000, // 30s - keep trying every 30s after this
|
||||
]; // Total: ~15 minutes of reconnection attempts
|
||||
|
||||
// Callbacks
|
||||
@@ -55,7 +56,9 @@ class BleConnectionManager {
|
||||
BluetoothCharacteristic? get txCharacteristic => _txCharacteristic;
|
||||
|
||||
/// 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 {
|
||||
print('🔍 [BLE] Starting scan for MeshCore devices...');
|
||||
print(' Service UUID: ${MeshCoreConstants.bleServiceUuid}');
|
||||
@@ -69,14 +72,19 @@ class BleConnectionManager {
|
||||
|
||||
int deviceCount = 0;
|
||||
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) {
|
||||
print(' Device: ${result.device.platformName} (${result.device.remoteId})');
|
||||
print(
|
||||
' Device: ${result.device.platformName} (${result.device.remoteId})',
|
||||
);
|
||||
print(' RSSI: ${result.rssi}');
|
||||
print(' Service UUIDs: ${result.advertisementData.serviceUuids}');
|
||||
|
||||
if (result.advertisementData.serviceUuids
|
||||
.contains(Guid(MeshCoreConstants.bleServiceUuid))) {
|
||||
if (result.advertisementData.serviceUuids.contains(
|
||||
Guid(MeshCoreConstants.bleServiceUuid),
|
||||
)) {
|
||||
deviceCount++;
|
||||
print(' ✅ MeshCore device found! Total: $deviceCount');
|
||||
yield result;
|
||||
@@ -95,7 +103,9 @@ class BleConnectionManager {
|
||||
/// Connect to a MeshCore device
|
||||
Future<bool> connect(BluetoothDevice device) async {
|
||||
try {
|
||||
print('🔵 [BLE] Starting connection to device: ${device.platformName} (${device.remoteId})');
|
||||
print(
|
||||
'🔵 [BLE] Starting connection to device: ${device.platformName} (${device.remoteId})',
|
||||
);
|
||||
_device = device;
|
||||
|
||||
// Connect to device
|
||||
@@ -121,7 +131,9 @@ class BleConnectionManager {
|
||||
}
|
||||
|
||||
// Find MeshCore service
|
||||
print('🔵 [BLE] Looking for MeshCore service: ${MeshCoreConstants.bleServiceUuid}');
|
||||
print(
|
||||
'🔵 [BLE] Looking for MeshCore service: ${MeshCoreConstants.bleServiceUuid}',
|
||||
);
|
||||
BluetoothService? meshCoreService;
|
||||
for (final service in services) {
|
||||
if (service.uuid.toString().toLowerCase() ==
|
||||
@@ -169,7 +181,8 @@ class BleConnectionManager {
|
||||
print('✅ [BLE] Notifications enabled');
|
||||
|
||||
_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');
|
||||
onConnectionStateChanged?.call(true);
|
||||
|
||||
@@ -213,7 +226,9 @@ class BleConnectionManager {
|
||||
|
||||
/// Setup connection monitoring for automatic reconnection
|
||||
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
|
||||
_connectionStateSubscription?.cancel();
|
||||
@@ -251,21 +266,32 @@ class BleConnectionManager {
|
||||
_isReconnecting = true;
|
||||
_reconnectionAttempt++;
|
||||
|
||||
print('🔄 [BLE] Reconnection attempt $_reconnectionAttempt of $_maxReconnectionAttempts');
|
||||
print(
|
||||
'🔄 [BLE] Reconnection attempt $_reconnectionAttempt of $_maxReconnectionAttempts',
|
||||
);
|
||||
onReconnectionAttempt?.call(_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;
|
||||
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;
|
||||
}
|
||||
|
||||
// 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];
|
||||
|
||||
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
|
||||
_reconnectionTimer = Timer(Duration(milliseconds: delayMs), () async {
|
||||
@@ -293,7 +319,9 @@ class BleConnectionManager {
|
||||
if (_reconnectionAttempt < _maxReconnectionAttempts) {
|
||||
_attemptReconnection();
|
||||
} 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) {
|
||||
@@ -304,7 +332,9 @@ class BleConnectionManager {
|
||||
if (_reconnectionAttempt < _maxReconnectionAttempts) {
|
||||
_attemptReconnection();
|
||||
} 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();
|
||||
if (_lastRssi != rssi) {
|
||||
_lastRssi = rssi;
|
||||
print('📡 [BLE] RSSI updated: $rssi dBm');
|
||||
onRssiUpdate?.call(rssi);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -154,7 +154,7 @@ class ContactTile extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
contact.hasPath ? 'Direct' : 'Flood',
|
||||
contact.hasPath ? AppLocalizations.of(context)!.direct : AppLocalizations.of(context)!.flood,
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -189,7 +189,7 @@ class ContactTile extends StatelessWidget {
|
||||
const Icon(Icons.admin_panel_settings, size: 10, color: Colors.red),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
'Admin',
|
||||
AppLocalizations.of(context)!.admin,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -213,7 +213,7 @@ class ContactTile extends StatelessWidget {
|
||||
const Icon(Icons.check_circle, size: 10, color: Colors.green),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
'Logged In',
|
||||
AppLocalizations.of(context)!.loggedIn,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Colors.green,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -263,7 +263,7 @@ class ContactTile extends StatelessWidget {
|
||||
const Icon(Icons.sensors_off, size: 12, color: Colors.grey),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'No GPS data',
|
||||
AppLocalizations.of(context)!.noGpsData,
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
@@ -277,7 +277,7 @@ class ContactTile extends StatelessWidget {
|
||||
const Icon(Icons.straighten, size: 12, color: Colors.blue),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Distance: $distanceText',
|
||||
'${AppLocalizations.of(context)!.distance}: $distanceText',
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Colors.blue,
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -300,8 +300,8 @@ class ContactTile extends StatelessWidget {
|
||||
ToastLogger.info(
|
||||
context,
|
||||
hasPath
|
||||
? 'Pinging ${contact.displayName} (direct via path)...'
|
||||
: 'Pinging ${contact.displayName} (flooding - no path)...',
|
||||
? AppLocalizations.of(context)!.pingingDirect(contact.displayName)
|
||||
: AppLocalizations.of(context)!.pingingFlood(contact.displayName),
|
||||
);
|
||||
|
||||
// Use smart ping with automatic fallback
|
||||
@@ -313,7 +313,7 @@ class ContactTile extends StatelessWidget {
|
||||
if (context.mounted) {
|
||||
ToastLogger.warning(
|
||||
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) {
|
||||
ToastLogger.success(
|
||||
context,
|
||||
'Ping successful to ${contact.displayName}${result.retriedWithFlooding ? ' (via flooding fallback)' : ''}',
|
||||
AppLocalizations.of(context)!.pingSuccessful(
|
||||
contact.displayName,
|
||||
result.retriedWithFlooding ? AppLocalizations.of(context)!.viaFloodingFallback : '',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ToastLogger.error(
|
||||
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(
|
||||
title: Text(AppLocalizations.of(context)!.deleteContact),
|
||||
content: Text(
|
||||
'Are you sure you want to delete "${contact.displayName}"?\n\n'
|
||||
'This will remove the contact from both the app and the companion radio device.',
|
||||
AppLocalizations.of(context)!.deleteContactConfirmation(contact.displayName),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -390,7 +392,7 @@ class ContactTile extends StatelessWidget {
|
||||
|
||||
try {
|
||||
// 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)
|
||||
await contactsProvider.removeContact(
|
||||
@@ -403,11 +405,11 @@ class ContactTile extends StatelessWidget {
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
ToastLogger.success(context, 'Contact "${contact.displayName}" removed');
|
||||
ToastLogger.success(context, AppLocalizations.of(context)!.contactRemoved(contact.displayName));
|
||||
}
|
||||
} catch (e) {
|
||||
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,
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_DetailRow('Type', contact.type.displayName),
|
||||
_DetailRow(AppLocalizations.of(context)!.type, contact.type.displayName),
|
||||
// Public Key with copy button
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
'Public Key:',
|
||||
style: TextStyle(fontWeight: FontWeight.w500),
|
||||
'${AppLocalizations.of(context)!.publicKey}:',
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
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),
|
||||
// Room Login Status
|
||||
if (roomLoginState != null) ...[
|
||||
const Text(
|
||||
'Room Status:',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
'${AppLocalizations.of(context)!.roomStatus}:',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_DetailRow(
|
||||
'Login Status',
|
||||
roomLoginState.isLoggedIn ? 'Logged In' : 'Not Logged In',
|
||||
AppLocalizations.of(context)!.loginStatus,
|
||||
roomLoginState.isLoggedIn ? AppLocalizations.of(context)!.loggedIn : AppLocalizations.of(context)!.notLoggedIn,
|
||||
),
|
||||
if (roomLoginState.isLoggedIn) ...[
|
||||
_DetailRow(
|
||||
'Admin Access',
|
||||
roomLoginState.isAdmin ? 'Yes' : 'No',
|
||||
AppLocalizations.of(context)!.adminAccess,
|
||||
roomLoginState.isAdmin ? AppLocalizations.of(context)!.yes : AppLocalizations.of(context)!.no,
|
||||
),
|
||||
_DetailRow(
|
||||
'Permissions',
|
||||
AppLocalizations.of(context)!.permissions,
|
||||
roomLoginState.permissions.toString(),
|
||||
),
|
||||
if (roomLoginState.loginDurationFormatted != null)
|
||||
_DetailRow(
|
||||
'Logged In',
|
||||
AppLocalizations.of(context)!.loggedIn,
|
||||
roomLoginState.loginDurationFormatted!,
|
||||
),
|
||||
],
|
||||
_DetailRow(
|
||||
'Password Saved',
|
||||
roomLoginState.hasPassword ? 'Yes' : 'No',
|
||||
AppLocalizations.of(context)!.passwordSaved,
|
||||
roomLoginState.hasPassword ? AppLocalizations.of(context)!.yes : AppLocalizations.of(context)!.no,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
@@ -565,9 +567,9 @@ class ContactTile extends StatelessWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Location:',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
AppLocalizations.of(context)!.locationColon,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
@@ -632,9 +634,9 @@ class ContactTile extends StatelessWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Telemetry:',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
'${AppLocalizations.of(context)!.telemetry}:',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
@@ -643,7 +645,7 @@ class ContactTile extends StatelessWidget {
|
||||
onPressed: () {
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
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),
|
||||
label: Text(AppLocalizations.of(context)!.refresh),
|
||||
@@ -656,25 +658,25 @@ class ContactTile extends StatelessWidget {
|
||||
const SizedBox(height: 8),
|
||||
if (contact.telemetry!.batteryMilliVolts != null)
|
||||
_DetailRow(
|
||||
'Voltage',
|
||||
AppLocalizations.of(context)!.voltage,
|
||||
'${(contact.telemetry!.batteryMilliVolts! / 1000).toStringAsFixed(3)}V'
|
||||
'${contact.telemetry!.batteryPercentage != null ? ' (${contact.telemetry!.batteryPercentage!.toStringAsFixed(1)}%)' : ''}',
|
||||
)
|
||||
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)
|
||||
_DetailRow('Temperature', '${contact.telemetry!.temperature!.toStringAsFixed(1)}°C'),
|
||||
_DetailRow(AppLocalizations.of(context)!.temperature, '${contact.telemetry!.temperature!.toStringAsFixed(1)}°C'),
|
||||
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)
|
||||
_DetailRow('Pressure', '${contact.telemetry!.pressure!.toStringAsFixed(1)} hPa'),
|
||||
_DetailRow(AppLocalizations.of(context)!.pressure, '${contact.telemetry!.pressure!.toStringAsFixed(1)} hPa'),
|
||||
if (contact.telemetry!.gpsLocation != null)
|
||||
_DetailRow(
|
||||
'GPS (Telemetry)',
|
||||
AppLocalizations.of(context)!.gpsTelemetry,
|
||||
'${contact.telemetry!.gpsLocation!.latitude.toStringAsFixed(6)}, ${contact.telemetry!.gpsLocation!.longitude.toStringAsFixed(6)}',
|
||||
),
|
||||
_DetailRow(
|
||||
'Updated',
|
||||
AppLocalizations.of(context)!.updated,
|
||||
'${_formatTimestamp(contact.telemetry!.timestamp)} (${_formatTimeAgo(contact.telemetry!.timestamp)})',
|
||||
),
|
||||
],
|
||||
@@ -703,7 +705,7 @@ class ContactTile extends StatelessWidget {
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
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),
|
||||
label: Text(AppLocalizations.of(context)!.resetPath),
|
||||
@@ -726,7 +728,7 @@ class ContactTile extends StatelessWidget {
|
||||
_showRoomLoginDialog(context, contact);
|
||||
},
|
||||
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(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
backgroundColor: _getTypeColor(contact.type, context),
|
||||
|
||||
@@ -151,11 +151,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Failed to add room to device: $e\n\n'
|
||||
'The room may not have advertised yet.\n'
|
||||
'Try waiting for the room to broadcast.',
|
||||
),
|
||||
content: Text(AppLocalizations.of(context)!.failedToAddRoom(e.toString())),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
duration: const Duration(seconds: 7),
|
||||
),
|
||||
@@ -316,7 +312,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Login to Room',
|
||||
AppLocalizations.of(context)!.loginToRoom,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 18,
|
||||
@@ -358,7 +354,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Enter the password to access this room. The password will be saved for future use.',
|
||||
AppLocalizations.of(context)!.enterPasswordInfo,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
fontSize: 12,
|
||||
@@ -392,9 +388,9 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
||||
maxLengthEnforcement: MaxLengthEnforcement.enforced,
|
||||
style: TextStyle(color: colorScheme.onSurface),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
labelText: AppLocalizations.of(context)!.password,
|
||||
labelStyle: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
hintText: 'Enter room password',
|
||||
hintText: AppLocalizations.of(context)!.enterRoomPassword,
|
||||
hintStyle: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -436,7 +432,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.login),
|
||||
label: Text(_isLoggingIn ? 'Logging in...' : 'Login'),
|
||||
label: Text(_isLoggingIn ? AppLocalizations.of(context)!.loggingInDots : AppLocalizations.of(context)!.login),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import '../../../models/contact.dart';
|
||||
|
||||
/// Contact list section for the compass dialog.
|
||||
@@ -28,7 +29,7 @@ class CompassContactList extends StatelessWidget {
|
||||
}
|
||||
|
||||
if (position == null) {
|
||||
return const Text('Location unavailable');
|
||||
return Text(AppLocalizations.of(context)!.locationUnavailable);
|
||||
}
|
||||
|
||||
// Calculate bearings and distances
|
||||
@@ -60,13 +61,14 @@ class CompassContactList extends StatelessWidget {
|
||||
contactsWithBearing.sort((a, b) =>
|
||||
(a['distance'] as double).compareTo(b['distance'] as double));
|
||||
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, bottom: 8),
|
||||
child: Text(
|
||||
'Nearby Contacts',
|
||||
l10n.nearbyContacts,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -120,7 +122,7 @@ class CompassContactList extends StatelessWidget {
|
||||
),
|
||||
if (heading != null)
|
||||
Text(
|
||||
_formatRelativeBearing(bearing, heading!),
|
||||
_formatRelativeBearing(bearing, heading!, context),
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
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)
|
||||
double relative = bearing - heading;
|
||||
|
||||
@@ -204,11 +207,11 @@ class CompassContactList extends StatelessWidget {
|
||||
final absRelative = relative.abs().round();
|
||||
|
||||
if (absRelative < 10) {
|
||||
return 'ahead';
|
||||
return l10n.ahead;
|
||||
} else if (relative > 0) {
|
||||
return '$absRelative° right';
|
||||
return l10n.degreesRight(absRelative);
|
||||
} else {
|
||||
return '$absRelative° left';
|
||||
return l10n.degreesLeft(absRelative);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
|
||||
/// Filter controls for the compass dialog.
|
||||
/// Allows filtering of contacts and SAR marker types.
|
||||
@@ -32,15 +33,16 @@ class CompassFilters extends StatefulWidget {
|
||||
|
||||
class _CompassFiltersState extends State<CompassFilters> {
|
||||
void _showFilterDialog() {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setDialogState) => AlertDialog(
|
||||
title: const Row(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.filter_list, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text('Filter Markers'),
|
||||
const Icon(Icons.filter_list, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(l10n.filterMarkers),
|
||||
],
|
||||
),
|
||||
contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
|
||||
@@ -52,7 +54,7 @@ class _CompassFiltersState extends State<CompassFilters> {
|
||||
_CompactFilterItem(
|
||||
icon: Icons.person,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
label: 'Contacts',
|
||||
label: l10n.contactsFilter,
|
||||
value: widget.showContacts,
|
||||
onChanged: (value) {
|
||||
widget.onShowContactsChanged(value);
|
||||
@@ -66,7 +68,7 @@ class _CompassFiltersState extends State<CompassFilters> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8, bottom: 8, top: 4),
|
||||
child: Text(
|
||||
'SAR Markers',
|
||||
l10n.sarMarkers,
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -75,7 +77,7 @@ class _CompassFiltersState extends State<CompassFilters> {
|
||||
_CompactFilterItem(
|
||||
icon: Icons.person_pin,
|
||||
color: Colors.green,
|
||||
label: 'Found Person',
|
||||
label: l10n.foundPerson,
|
||||
value: widget.showFoundPerson,
|
||||
onChanged: (value) {
|
||||
widget.onShowFoundPersonChanged(value);
|
||||
@@ -86,7 +88,7 @@ class _CompassFiltersState extends State<CompassFilters> {
|
||||
_CompactFilterItem(
|
||||
icon: Icons.local_fire_department,
|
||||
color: Colors.red,
|
||||
label: 'Fire',
|
||||
label: l10n.fire,
|
||||
value: widget.showFire,
|
||||
onChanged: (value) {
|
||||
widget.onShowFireChanged(value);
|
||||
@@ -97,7 +99,7 @@ class _CompassFiltersState extends State<CompassFilters> {
|
||||
_CompactFilterItem(
|
||||
icon: Icons.home_work,
|
||||
color: Colors.orange,
|
||||
label: 'Staging Area',
|
||||
label: l10n.stagingArea,
|
||||
value: widget.showStagingArea,
|
||||
onChanged: (value) {
|
||||
widget.onShowStagingAreaChanged(value);
|
||||
@@ -112,11 +114,11 @@ class _CompassFiltersState extends State<CompassFilters> {
|
||||
widget.onShowAll();
|
||||
setDialogState(() {});
|
||||
},
|
||||
child: const Text('Show All'),
|
||||
child: Text(l10n.showAll),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Close'),
|
||||
child: Text(l10n.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -126,9 +128,10 @@ class _CompassFiltersState extends State<CompassFilters> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.filter_list),
|
||||
tooltip: 'Filter markers',
|
||||
tooltip: l10n.filterMarkersTooltip,
|
||||
onPressed: () => _showFilterDialog(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import '../../../models/contact.dart';
|
||||
import '../../../models/sar_marker.dart';
|
||||
|
||||
@@ -76,18 +77,19 @@ class CompassHeader extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(BuildContext context, double? heading, Position? position) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildInfoCard(
|
||||
context,
|
||||
'Heading',
|
||||
l10n.heading,
|
||||
heading != null ? '${heading.round()}°' : '--',
|
||||
Icons.explore,
|
||||
),
|
||||
_buildInfoCard(
|
||||
context,
|
||||
'Elevation',
|
||||
l10n.elevation,
|
||||
position?.altitude != null
|
||||
? '${position!.altitude.round()}m'
|
||||
: '--',
|
||||
@@ -95,7 +97,7 @@ class CompassHeader extends StatelessWidget {
|
||||
),
|
||||
_buildInfoCard(
|
||||
context,
|
||||
'Accuracy',
|
||||
l10n.accuracy,
|
||||
position?.accuracy != null
|
||||
? '±${position!.accuracy.round()}m'
|
||||
: '--',
|
||||
@@ -566,12 +568,16 @@ class _LocationFormatToggleState extends State<_LocationFormatToggle> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final String displayText;
|
||||
|
||||
if (_showDMS) {
|
||||
displayText = '${_formatDMS(position.latitude, true)} ${_formatDMS(position.longitude, false)}';
|
||||
} 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(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import '../../../models/sar_marker.dart';
|
||||
|
||||
/// SAR marker list section for the compass dialog.
|
||||
@@ -28,7 +29,7 @@ class CompassSarList extends StatelessWidget {
|
||||
}
|
||||
|
||||
if (position == null) {
|
||||
return const Text('Location unavailable');
|
||||
return Text(AppLocalizations.of(context)!.locationUnavailable);
|
||||
}
|
||||
|
||||
// Calculate bearings and distances for SAR markers
|
||||
@@ -58,13 +59,14 @@ class CompassSarList extends StatelessWidget {
|
||||
markersWithBearing.sort((a, b) =>
|
||||
(a['distance'] as double).compareTo(b['distance'] as double));
|
||||
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, top: 16, bottom: 8),
|
||||
child: Text(
|
||||
'SAR Markers',
|
||||
l10n.sarMarkers,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -139,7 +141,7 @@ class CompassSarList extends StatelessWidget {
|
||||
),
|
||||
if (heading != null)
|
||||
Text(
|
||||
_formatRelativeBearing(bearing, heading!),
|
||||
_formatRelativeBearing(bearing, heading!, context),
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
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)
|
||||
double relative = bearing - heading;
|
||||
|
||||
@@ -223,11 +226,11 @@ class CompassSarList extends StatelessWidget {
|
||||
final absRelative = relative.abs().round();
|
||||
|
||||
if (absRelative < 10) {
|
||||
return 'ahead';
|
||||
return l10n.ahead;
|
||||
} else if (relative > 0) {
|
||||
return '$absRelative° right';
|
||||
return l10n.degreesRight(absRelative);
|
||||
} else {
|
||||
return '$absRelative° left';
|
||||
return l10n.degreesLeft(absRelative);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Quick location marker',
|
||||
AppLocalizations.of(context)!.quickLocationMarker,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
@@ -203,7 +203,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
||||
children: [
|
||||
// Marker type selection
|
||||
Text(
|
||||
'Marker Type',
|
||||
AppLocalizations.of(context)!.markerType,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 16,
|
||||
@@ -238,7 +238,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
||||
|
||||
// Destination selection (compact dropdown with rooms and channel)
|
||||
Text(
|
||||
'Send To',
|
||||
AppLocalizations.of(context)!.sendTo,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 16,
|
||||
@@ -262,14 +262,14 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: const Row(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: Colors.red, size: 20),
|
||||
SizedBox(width: 8),
|
||||
const Icon(Icons.error_outline, color: Colors.red, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'No destinations available.',
|
||||
style: TextStyle(
|
||||
AppLocalizations.of(context)!.noDestinationsAvailable,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 11,
|
||||
),
|
||||
@@ -298,7 +298,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
||||
Icon(Icons.arrow_drop_down_circle, size: 18, color: colorScheme.onSurfaceVariant),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Select destination...',
|
||||
AppLocalizations.of(context)!.selectDestination,
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
@@ -373,8 +373,8 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
_selectedContact!.isChannel
|
||||
? 'Ephemeral: Broadcast over-the-air only. Not stored - nodes must be online.'
|
||||
: 'Persistent: Stored immutably in room. Synced automatically and preserved offline.',
|
||||
? AppLocalizations.of(context)!.ephemeralBroadcastInfo
|
||||
: AppLocalizations.of(context)!.persistentRoomInfo,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 11,
|
||||
@@ -390,7 +390,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Location',
|
||||
AppLocalizations.of(context)!.location,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 16,
|
||||
@@ -409,9 +409,9 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'From Map',
|
||||
style: TextStyle(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.fromMap,
|
||||
style: const TextStyle(
|
||||
color: Colors.blue,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -441,7 +441,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
'Getting location...',
|
||||
AppLocalizations.of(context)!.gettingLocation,
|
||||
style: TextStyle(color: colorScheme.onSurface),
|
||||
),
|
||||
],
|
||||
@@ -466,9 +466,9 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Location Error',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
AppLocalizations.of(context)!.locationError,
|
||||
style: const TextStyle(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
@@ -488,7 +488,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh, color: Colors.red),
|
||||
onPressed: _getCurrentLocation,
|
||||
tooltip: 'Retry',
|
||||
tooltip: AppLocalizations.of(context)!.retry,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -529,7 +529,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
||||
onPressed: _getCurrentLocation,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
tooltip: 'Refresh location',
|
||||
tooltip: AppLocalizations.of(context)!.refreshLocation,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -544,7 +544,9 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Accuracy: ±${_currentPosition!.accuracy!.round()}m',
|
||||
AppLocalizations.of(context)!.accuracyMeters(
|
||||
_currentPosition!.accuracy!.round(),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
@@ -560,7 +562,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
||||
|
||||
// Optional notes
|
||||
Text(
|
||||
'Notes (optional)',
|
||||
AppLocalizations.of(context)!.notesOptional,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 16,
|
||||
@@ -574,7 +576,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
||||
maxLength: 100,
|
||||
style: TextStyle(fontSize: 14, color: colorScheme.onSurface),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Add additional information...',
|
||||
hintText: AppLocalizations.of(context)!.addAdditionalInformation,
|
||||
hintStyle: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant),
|
||||
filled: true,
|
||||
fillColor: colorScheme.surfaceContainerHighest,
|
||||
@@ -648,9 +650,9 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!.lowLocationAccuracy),
|
||||
content: Text(
|
||||
'Location accuracy is ±${_currentPosition!.accuracy!.round()}m. '
|
||||
'This may not be accurate enough for SAR operations.\n\n'
|
||||
'Continue anyway?',
|
||||
AppLocalizations.of(context)!.lowAccuracyWarning(
|
||||
_currentPosition!.accuracy!.round(),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
||||
Reference in New Issue
Block a user