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:
Janez T
2025-10-16 15:39:48 +02:00
parent 1056380f3a
commit 01a1bbec44
17 changed files with 2469 additions and 217 deletions

View File

@@ -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"
}
}
}
}

View File

@@ -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}"
}

View File

@@ -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

View File

@@ -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';
}
}

View File

@@ -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';
}
}

View File

@@ -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';
}
}

View File

@@ -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}"
}

View File

@@ -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');

View File

@@ -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,
),

View File

@@ -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) {

View File

@@ -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),

View File

@@ -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),
),

View File

@@ -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);
}
}
}

View File

@@ -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(),
);
}

View File

@@ -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(

View File

@@ -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);
}
}
}

View File

@@ -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(