Add localization support for various UI elements and messages

- Integrated localization for alert dialogs, snack bars, and button labels in PacketLogScreen and SettingsScreen.
- Updated location tracking service messages to use localized strings.
- Enhanced ContactTile and DirectMessageSheet with localized text for user interactions.
- Implemented localization in DrawingLayer and DrawingToolbar for map-related actions.
- Updated SAR Update Sheet to utilize localized strings for user prompts and actions.
- Ensured consistent use of localization across all relevant widgets and screens.
This commit is contained in:
Janez T
2025-10-16 15:19:05 +02:00
parent 9def8daf0c
commit 1056380f3a
24 changed files with 1246 additions and 236 deletions

View File

@@ -8,7 +8,8 @@
"Bash(dart pub global activate:*)", "Bash(dart pub global activate:*)",
"Bash(dart pub global run intl_utils:generate:*)", "Bash(dart pub global run intl_utils:generate:*)",
"Bash(dart pub global deactivate:*)", "Bash(dart pub global deactivate:*)",
"Bash(flutter precache:*)" "Bash(flutter precache:*)",
"Bash(flutter analyze:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@@ -457,5 +457,225 @@
"defaultPinInfo": "The default pin for devices without a screen is 123456. Trouble pairing? Forget the bluetooth device in system settings.", "defaultPinInfo": "The default pin for devices without a screen is 123456. Trouble pairing? Forget the bluetooth device in system settings.",
"@defaultPinInfo": { "@defaultPinInfo": {
"description": "Information about default PIN for pairing" "description": "Information about default PIN for pairing"
},
"noMessagesYet": "No messages yet",
"@noMessagesYet": {
"description": "Empty state message when there are no messages"
},
"pullDownToSync": "Pull down to sync messages",
"@pullDownToSync": {
"description": "Instruction to pull down to refresh messages"
},
"deleteContact": "Delete Contact",
"@deleteContact": {
"description": "Delete contact action label"
},
"delete": "Delete",
"@delete": {
"description": "Delete button label"
},
"viewOnMap": "View on Map",
"@viewOnMap": {
"description": "Action to view contact location on map"
},
"refresh": "Refresh",
"@refresh": {
"description": "Refresh button label"
},
"sendDirectMessage": "Send Direct Message",
"@sendDirectMessage": {
"description": "Action to send direct message to contact"
},
"resetPath": "Reset Path (Re-route)",
"@resetPath": {
"description": "Action to reset contact path for re-routing"
},
"publicKeyCopied": "Public key copied to clipboard",
"@publicKeyCopied": {
"description": "Success message when public key is copied"
},
"copiedToClipboard": "{label} copied to clipboard",
"@copiedToClipboard": {
"description": "Success message when a value is copied to clipboard",
"placeholders": {
"label": {
"type": "String"
}
}
},
"pleaseEnterPassword": "Please enter a password",
"@pleaseEnterPassword": {
"description": "Validation message for empty password field"
},
"failedToSyncContacts": "Failed to sync contacts: {error}",
"@failedToSyncContacts": {
"description": "Error message when contact sync fails",
"placeholders": {
"error": {
"type": "String"
}
}
},
"loggedInSuccessfully": "Logged in successfully! Waiting for room messages...",
"@loggedInSuccessfully": {
"description": "Success message after successful room login"
},
"loginFailed": "Login failed - incorrect password",
"@loginFailed": {
"description": "Error message when room login fails"
},
"loggingIn": "Logging in to {roomName}...",
"@loggingIn": {
"description": "Status message during room login process",
"placeholders": {
"roomName": {
"type": "String"
}
}
},
"failedToSendLogin": "Failed to send login: {error}",
"@failedToSendLogin": {
"description": "Error message when login command fails to send",
"placeholders": {
"error": {
"type": "String"
}
}
},
"lowLocationAccuracy": "Low Location Accuracy",
"@lowLocationAccuracy": {
"description": "Warning title for low GPS accuracy"
},
"continue_": "Continue",
"@continue_": {
"description": "Continue button label"
},
"sendSarMarker": "Send SAR marker",
"@sendSarMarker": {
"description": "Action to send SAR marker"
},
"deleteDrawing": "Delete Drawing",
"@deleteDrawing": {
"description": "Action to delete a map drawing"
},
"drawLine": "Draw Line",
"@drawLine": {
"description": "Map drawing mode: line"
},
"drawLineDesc": "Draw a freehand line on the map",
"@drawLineDesc": {
"description": "Description for line drawing mode"
},
"drawRectangle": "Draw Rectangle",
"@drawRectangle": {
"description": "Map drawing mode: rectangle"
},
"drawRectangleDesc": "Draw a rectangular area on the map",
"@drawRectangleDesc": {
"description": "Description for rectangle drawing mode"
},
"shareDrawings": "Share Drawings",
"@shareDrawings": {
"description": "Action to share drawings to network"
},
"clearAllDrawings": "Clear All Drawings",
"@clearAllDrawings": {
"description": "Action to clear all local drawings"
},
"clearAll": "Clear All",
"@clearAll": {
"description": "Clear all button label"
},
"noLocalDrawings": "No local drawings to share",
"@noLocalDrawings": {
"description": "Message when there are no drawings to share"
},
"publicChannel": "Public Channel",
"@publicChannel": {
"description": "Public channel option for sharing"
},
"broadcastToAll": "Broadcast to all nearby nodes (ephemeral)",
"@broadcastToAll": {
"description": "Description for public channel broadcast"
},
"storedPermanently": "Stored permanently in room",
"@storedPermanently": {
"description": "Description for room storage permanence"
},
"notConnectedToDevice": "Not connected to device",
"@notConnectedToDevice": {
"description": "Error message when device is not connected for direct messaging"
},
"directMessage": "Direct Message",
"@directMessage": {
"description": "Title for direct message sheet"
},
"directMessageSentTo": "Direct message sent to {contactName}",
"@directMessageSentTo": {
"description": "Success message after sending direct message",
"placeholders": {
"contactName": {
"type": "String"
}
}
},
"failedToSend": "Failed to send: {error}",
"@failedToSend": {
"description": "Error message when sending direct message fails",
"placeholders": {
"error": {
"type": "String"
}
}
},
"directMessageInfo": "This message will be sent directly to {contactName}. It will also appear in the main messages feed.",
"@directMessageInfo": {
"description": "Information about direct messaging behavior",
"placeholders": {
"contactName": {
"type": "String"
}
}
},
"typeYourMessage": "Type your message...",
"@typeYourMessage": {
"description": "Placeholder text for message input field"
} }
} }

View File

@@ -157,5 +157,79 @@
"locationBroadcast": "Emitiranje lokacije: {latitude}, {longitude}", "locationBroadcast": "Emitiranje lokacije: {latitude}, {longitude}",
"defaultPinInfo": "Zadani PIN za uređaje bez zaslona je 123456. Problemi s uparivanjem? Zaboravite bluetooth uređaj u postavkama sustava." "defaultPinInfo": "Zadani PIN za uređaje bez zaslona je 123456. Problemi s uparivanjem? Zaboravite bluetooth uređaj u postavkama sustava.",
"noMessagesYet": "Još nema poruka",
"pullDownToSync": "Povucite prema dolje za sinkronizaciju",
"deleteContact": "Izbriši kontakt",
"delete": "Izbriši",
"viewOnMap": "Prikaži na karti",
"refresh": "Osvježi",
"sendDirectMessage": "Pošalji izravnu poruku",
"resetPath": "Resetiraj put (preusmjeri)",
"publicKeyCopied": "Javni ključ kopiran u međuspremnik",
"copiedToClipboard": "{label} kopirano u međuspremnik",
"pleaseEnterPassword": "Molimo unesite lozinku",
"failedToSyncContacts": "Neuspjela sinkronizacija kontakata: {error}",
"loggedInSuccessfully": "Uspješno prijavljen! Čekanje na poruke sobe...",
"loginFailed": "Prijava neuspjela - netočna lozinka",
"loggingIn": "Prijavljivanje u {roomName}...",
"failedToSendLogin": "Neuspjelo slanje prijave: {error}",
"lowLocationAccuracy": "Niska točnost lokacije",
"continue_": "Nastavi",
"sendSarMarker": "Pošalji SAR marker",
"deleteDrawing": "Izbriši crtež",
"drawLine": "Nacrtaj liniju",
"drawLineDesc": "Nacrtaj slobodnu liniju na karti",
"drawRectangle": "Nacrtaj pravokutnik",
"drawRectangleDesc": "Nacrtaj pravokutno područje na karti",
"shareDrawings": "Podijeli crteže",
"clearAllDrawings": "Očisti sve crteže",
"clearAll": "Očisti sve",
"noLocalDrawings": "Nema lokalnih crteža za dijeljenje",
"publicChannel": "Javni kanal",
"broadcastToAll": "Emitiraj svim obližnjim čvorovima (privremeno)",
"storedPermanently": "Trajno pohranjeno u sobi",
"notConnectedToDevice": "Nije povezano s uređajem",
"directMessage": "Izravna poruka",
"directMessageSentTo": "Izravna poruka poslana {contactName}",
"failedToSend": "Neuspjelo slanje: {error}",
"directMessageInfo": "Ova poruka će biti poslana izravno {contactName}. Također će se prikazati u glavnom feedu poruka.",
"typeYourMessage": "Upišite svoju poruku..."
} }

View File

@@ -578,6 +578,228 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'The default pin for devices without a screen is 123456. Trouble pairing? Forget the bluetooth device in system settings.'** /// **'The default pin for devices without a screen is 123456. Trouble pairing? Forget the bluetooth device in system settings.'**
String get defaultPinInfo; String get defaultPinInfo;
/// Empty state message when there are no messages
///
/// In en, this message translates to:
/// **'No messages yet'**
String get noMessagesYet;
/// Instruction to pull down to refresh messages
///
/// In en, this message translates to:
/// **'Pull down to sync messages'**
String get pullDownToSync;
/// Delete contact action label
///
/// In en, this message translates to:
/// **'Delete Contact'**
String get deleteContact;
/// Delete button label
///
/// In en, this message translates to:
/// **'Delete'**
String get delete;
/// Action to view contact location on map
///
/// In en, this message translates to:
/// **'View on Map'**
String get viewOnMap;
/// Refresh button label
///
/// In en, this message translates to:
/// **'Refresh'**
String get refresh;
/// Action to send direct message to contact
///
/// In en, this message translates to:
/// **'Send Direct Message'**
String get sendDirectMessage;
/// Action to reset contact path for re-routing
///
/// In en, this message translates to:
/// **'Reset Path (Re-route)'**
String get resetPath;
/// Success message when public key is copied
///
/// In en, this message translates to:
/// **'Public key copied to clipboard'**
String get publicKeyCopied;
/// Success message when a value is copied to clipboard
///
/// In en, this message translates to:
/// **'{label} copied to clipboard'**
String copiedToClipboard(String label);
/// Validation message for empty password field
///
/// In en, this message translates to:
/// **'Please enter a password'**
String get pleaseEnterPassword;
/// Error message when contact sync fails
///
/// In en, this message translates to:
/// **'Failed to sync contacts: {error}'**
String failedToSyncContacts(String error);
/// Success message after successful room login
///
/// In en, this message translates to:
/// **'Logged in successfully! Waiting for room messages...'**
String get loggedInSuccessfully;
/// Error message when room login fails
///
/// In en, this message translates to:
/// **'Login failed - incorrect password'**
String get loginFailed;
/// Status message during room login process
///
/// In en, this message translates to:
/// **'Logging in to {roomName}...'**
String loggingIn(String roomName);
/// Error message when login command fails to send
///
/// In en, this message translates to:
/// **'Failed to send login: {error}'**
String failedToSendLogin(String error);
/// Warning title for low GPS accuracy
///
/// In en, this message translates to:
/// **'Low Location Accuracy'**
String get lowLocationAccuracy;
/// Continue button label
///
/// In en, this message translates to:
/// **'Continue'**
String get continue_;
/// Action to send SAR marker
///
/// In en, this message translates to:
/// **'Send SAR marker'**
String get sendSarMarker;
/// Action to delete a map drawing
///
/// In en, this message translates to:
/// **'Delete Drawing'**
String get deleteDrawing;
/// Map drawing mode: line
///
/// In en, this message translates to:
/// **'Draw Line'**
String get drawLine;
/// Description for line drawing mode
///
/// In en, this message translates to:
/// **'Draw a freehand line on the map'**
String get drawLineDesc;
/// Map drawing mode: rectangle
///
/// In en, this message translates to:
/// **'Draw Rectangle'**
String get drawRectangle;
/// Description for rectangle drawing mode
///
/// In en, this message translates to:
/// **'Draw a rectangular area on the map'**
String get drawRectangleDesc;
/// Action to share drawings to network
///
/// In en, this message translates to:
/// **'Share Drawings'**
String get shareDrawings;
/// Action to clear all local drawings
///
/// In en, this message translates to:
/// **'Clear All Drawings'**
String get clearAllDrawings;
/// Clear all button label
///
/// In en, this message translates to:
/// **'Clear All'**
String get clearAll;
/// Message when there are no drawings to share
///
/// In en, this message translates to:
/// **'No local drawings to share'**
String get noLocalDrawings;
/// Public channel option for sharing
///
/// In en, this message translates to:
/// **'Public Channel'**
String get publicChannel;
/// Description for public channel broadcast
///
/// In en, this message translates to:
/// **'Broadcast to all nearby nodes (ephemeral)'**
String get broadcastToAll;
/// Description for room storage permanence
///
/// In en, this message translates to:
/// **'Stored permanently in room'**
String get storedPermanently;
/// Error message when device is not connected for direct messaging
///
/// In en, this message translates to:
/// **'Not connected to device'**
String get notConnectedToDevice;
/// Title for direct message sheet
///
/// In en, this message translates to:
/// **'Direct Message'**
String get directMessage;
/// Success message after sending direct message
///
/// In en, this message translates to:
/// **'Direct message sent to {contactName}'**
String directMessageSentTo(String contactName);
/// Error message when sending direct message fails
///
/// In en, this message translates to:
/// **'Failed to send: {error}'**
String failedToSend(String error);
/// Information about direct messaging behavior
///
/// In en, this message translates to:
/// **'This message will be sent directly to {contactName}. It will also appear in the main messages feed.'**
String directMessageInfo(String contactName);
/// Placeholder text for message input field
///
/// In en, this message translates to:
/// **'Type your message...'**
String get typeYourMessage;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View File

@@ -278,4 +278,130 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get defaultPinInfo => String get defaultPinInfo =>
'The default pin for devices without a screen is 123456. Trouble pairing? Forget the bluetooth device in system settings.'; 'The default pin for devices without a screen is 123456. Trouble pairing? Forget the bluetooth device in system settings.';
@override
String get noMessagesYet => 'No messages yet';
@override
String get pullDownToSync => 'Pull down to sync messages';
@override
String get deleteContact => 'Delete Contact';
@override
String get delete => 'Delete';
@override
String get viewOnMap => 'View on Map';
@override
String get refresh => 'Refresh';
@override
String get sendDirectMessage => 'Send Direct Message';
@override
String get resetPath => 'Reset Path (Re-route)';
@override
String get publicKeyCopied => 'Public key copied to clipboard';
@override
String copiedToClipboard(String label) {
return '$label copied to clipboard';
}
@override
String get pleaseEnterPassword => 'Please enter a password';
@override
String failedToSyncContacts(String error) {
return 'Failed to sync contacts: $error';
}
@override
String get loggedInSuccessfully =>
'Logged in successfully! Waiting for room messages...';
@override
String get loginFailed => 'Login failed - incorrect password';
@override
String loggingIn(String roomName) {
return 'Logging in to $roomName...';
}
@override
String failedToSendLogin(String error) {
return 'Failed to send login: $error';
}
@override
String get lowLocationAccuracy => 'Low Location Accuracy';
@override
String get continue_ => 'Continue';
@override
String get sendSarMarker => 'Send SAR marker';
@override
String get deleteDrawing => 'Delete Drawing';
@override
String get drawLine => 'Draw Line';
@override
String get drawLineDesc => 'Draw a freehand line on the map';
@override
String get drawRectangle => 'Draw Rectangle';
@override
String get drawRectangleDesc => 'Draw a rectangular area on the map';
@override
String get shareDrawings => 'Share Drawings';
@override
String get clearAllDrawings => 'Clear All Drawings';
@override
String get clearAll => 'Clear All';
@override
String get noLocalDrawings => 'No local drawings to share';
@override
String get publicChannel => 'Public Channel';
@override
String get broadcastToAll => 'Broadcast to all nearby nodes (ephemeral)';
@override
String get storedPermanently => 'Stored permanently in room';
@override
String get notConnectedToDevice => 'Not connected to device';
@override
String get directMessage => 'Direct Message';
@override
String directMessageSentTo(String contactName) {
return 'Direct message sent to $contactName';
}
@override
String failedToSend(String error) {
return 'Failed to send: $error';
}
@override
String directMessageInfo(String contactName) {
return 'This message will be sent directly to $contactName. It will also appear in the main messages feed.';
}
@override
String get typeYourMessage => 'Type your message...';
} }

View File

@@ -278,4 +278,130 @@ class AppLocalizationsHr extends AppLocalizations {
@override @override
String get defaultPinInfo => String get defaultPinInfo =>
'Zadani PIN za uređaje bez zaslona je 123456. Problemi s uparivanjem? Zaboravite bluetooth uređaj u postavkama sustava.'; 'Zadani PIN za uređaje bez zaslona je 123456. Problemi s uparivanjem? Zaboravite bluetooth uređaj u postavkama sustava.';
@override
String get noMessagesYet => 'Još nema poruka';
@override
String get pullDownToSync => 'Povucite prema dolje za sinkronizaciju';
@override
String get deleteContact => 'Izbriši kontakt';
@override
String get delete => 'Izbriši';
@override
String get viewOnMap => 'Prikaži na karti';
@override
String get refresh => 'Osvježi';
@override
String get sendDirectMessage => 'Pošalji izravnu poruku';
@override
String get resetPath => 'Resetiraj put (preusmjeri)';
@override
String get publicKeyCopied => 'Javni ključ kopiran u međuspremnik';
@override
String copiedToClipboard(String label) {
return '$label kopirano u međuspremnik';
}
@override
String get pleaseEnterPassword => 'Molimo unesite lozinku';
@override
String failedToSyncContacts(String error) {
return 'Neuspjela sinkronizacija kontakata: $error';
}
@override
String get loggedInSuccessfully =>
'Uspješno prijavljen! Čekanje na poruke sobe...';
@override
String get loginFailed => 'Prijava neuspjela - netočna lozinka';
@override
String loggingIn(String roomName) {
return 'Prijavljivanje u $roomName...';
}
@override
String failedToSendLogin(String error) {
return 'Neuspjelo slanje prijave: $error';
}
@override
String get lowLocationAccuracy => 'Niska točnost lokacije';
@override
String get continue_ => 'Nastavi';
@override
String get sendSarMarker => 'Pošalji SAR marker';
@override
String get deleteDrawing => 'Izbriši crtež';
@override
String get drawLine => 'Nacrtaj liniju';
@override
String get drawLineDesc => 'Nacrtaj slobodnu liniju na karti';
@override
String get drawRectangle => 'Nacrtaj pravokutnik';
@override
String get drawRectangleDesc => 'Nacrtaj pravokutno područje na karti';
@override
String get shareDrawings => 'Podijeli crteže';
@override
String get clearAllDrawings => 'Očisti sve crteže';
@override
String get clearAll => 'Očisti sve';
@override
String get noLocalDrawings => 'Nema lokalnih crteža za dijeljenje';
@override
String get publicChannel => 'Javni kanal';
@override
String get broadcastToAll => 'Emitiraj svim obližnjim čvorovima (privremeno)';
@override
String get storedPermanently => 'Trajno pohranjeno u sobi';
@override
String get notConnectedToDevice => 'Nije povezano s uređajem';
@override
String get directMessage => 'Izravna poruka';
@override
String directMessageSentTo(String contactName) {
return 'Izravna poruka poslana $contactName';
}
@override
String failedToSend(String error) {
return 'Neuspjelo slanje: $error';
}
@override
String directMessageInfo(String contactName) {
return 'Ova poruka će biti poslana izravno $contactName. Također će se prikazati u glavnom feedu poruka.';
}
@override
String get typeYourMessage => 'Upišite svoju poruku...';
} }

View File

@@ -278,4 +278,130 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get defaultPinInfo => String get defaultPinInfo =>
'Privzeta PIN koda za naprave brez zaslona je 123456. Težave s seznanitvijo? Pozabite napravo bluetooth v sistemskih nastavitvah.'; 'Privzeta PIN koda za naprave brez zaslona je 123456. Težave s seznanitvijo? Pozabite napravo bluetooth v sistemskih nastavitvah.';
@override
String get noMessagesYet => 'Še ni sporočil';
@override
String get pullDownToSync => 'Potegnite navzdol za sinhronizacijo';
@override
String get deleteContact => 'Izbriši stik';
@override
String get delete => 'Izbriši';
@override
String get viewOnMap => 'Poglej na zemljevidu';
@override
String get refresh => 'Osveži';
@override
String get sendDirectMessage => 'Pošlji neposredno sporočilo';
@override
String get resetPath => 'Ponastavi pot (preusmeri)';
@override
String get publicKeyCopied => 'Javni ključ kopiran v odložišče';
@override
String copiedToClipboard(String label) {
return '$label kopirano v odložišče';
}
@override
String get pleaseEnterPassword => 'Prosimo, vnesite geslo';
@override
String failedToSyncContacts(String error) {
return 'Sinhronizacija stikov ni uspela: $error';
}
@override
String get loggedInSuccessfully =>
'Uspešno prijavljen! Čakanje na sporočila sobe...';
@override
String get loginFailed => 'Prijava ni uspela - nepravilno geslo';
@override
String loggingIn(String roomName) {
return 'Prijavljanje v $roomName...';
}
@override
String failedToSendLogin(String error) {
return 'Pošiljanje prijave ni uspelo: $error';
}
@override
String get lowLocationAccuracy => 'Nizka natančnost lokacije';
@override
String get continue_ => 'Nadaljuj';
@override
String get sendSarMarker => 'Pošlji SAR označevalec';
@override
String get deleteDrawing => 'Izbriši risbo';
@override
String get drawLine => 'Nariši črto';
@override
String get drawLineDesc => 'Nariši prosto črto na zemljevidu';
@override
String get drawRectangle => 'Nariši pravokotnik';
@override
String get drawRectangleDesc => 'Nariši pravokotno področje na zemljevidu';
@override
String get shareDrawings => 'Deli risbe';
@override
String get clearAllDrawings => 'Počisti vse risbe';
@override
String get clearAll => 'Počisti vse';
@override
String get noLocalDrawings => 'Ni lokalnih risb za deljenje';
@override
String get publicChannel => 'Javni kanal';
@override
String get broadcastToAll => 'Oddajaj vsem bližnjim vozliščem (začasno)';
@override
String get storedPermanently => 'Trajno shranjeno v sobi';
@override
String get notConnectedToDevice => 'Ni povezano z napravo';
@override
String get directMessage => 'Neposredno sporočilo';
@override
String directMessageSentTo(String contactName) {
return 'Neposredno sporočilo poslano $contactName';
}
@override
String failedToSend(String error) {
return 'Pošiljanje ni uspelo: $error';
}
@override
String directMessageInfo(String contactName) {
return 'To sporočilo bo poslano neposredno $contactName. Prikazalo se bo tudi v glavnem viru sporočil.';
}
@override
String get typeYourMessage => 'Vnesite svoje sporočilo...';
} }

View File

@@ -157,5 +157,79 @@
"locationBroadcast": "Oddajanje lokacije: {latitude}, {longitude}", "locationBroadcast": "Oddajanje lokacije: {latitude}, {longitude}",
"defaultPinInfo": "Privzeta PIN koda za naprave brez zaslona je 123456. Težave s seznanitvijo? Pozabite napravo bluetooth v sistemskih nastavitvah." "defaultPinInfo": "Privzeta PIN koda za naprave brez zaslona je 123456. Težave s seznanitvijo? Pozabite napravo bluetooth v sistemskih nastavitvah.",
"noMessagesYet": "Še ni sporočil",
"pullDownToSync": "Potegnite navzdol za sinhronizacijo",
"deleteContact": "Izbriši stik",
"delete": "Izbriši",
"viewOnMap": "Poglej na zemljevidu",
"refresh": "Osveži",
"sendDirectMessage": "Pošlji neposredno sporočilo",
"resetPath": "Ponastavi pot (preusmeri)",
"publicKeyCopied": "Javni ključ kopiran v odložišče",
"copiedToClipboard": "{label} kopirano v odložišče",
"pleaseEnterPassword": "Prosimo, vnesite geslo",
"failedToSyncContacts": "Sinhronizacija stikov ni uspela: {error}",
"loggedInSuccessfully": "Uspešno prijavljen! Čakanje na sporočila sobe...",
"loginFailed": "Prijava ni uspela - nepravilno geslo",
"loggingIn": "Prijavljanje v {roomName}...",
"failedToSendLogin": "Pošiljanje prijave ni uspelo: {error}",
"lowLocationAccuracy": "Nizka natančnost lokacije",
"continue_": "Nadaljuj",
"sendSarMarker": "Pošlji SAR označevalec",
"deleteDrawing": "Izbriši risbo",
"drawLine": "Nariši črto",
"drawLineDesc": "Nariši prosto črto na zemljevidu",
"drawRectangle": "Nariši pravokotnik",
"drawRectangleDesc": "Nariši pravokotno področje na zemljevidu",
"shareDrawings": "Deli risbe",
"clearAllDrawings": "Počisti vse risbe",
"clearAll": "Počisti vse",
"noLocalDrawings": "Ni lokalnih risb za deljenje",
"publicChannel": "Javni kanal",
"broadcastToAll": "Oddajaj vsem bližnjim vozliščem (začasno)",
"storedPermanently": "Trajno shranjeno v sobi",
"notConnectedToDevice": "Ni povezano z napravo",
"directMessage": "Neposredno sporočilo",
"directMessageSentTo": "Neposredno sporočilo poslano {contactName}",
"failedToSend": "Pošiljanje ni uspelo: {error}",
"directMessageInfo": "To sporočilo bo poslano neposredno {contactName}. Prikazalo se bo tudi v glavnem viru sporočil.",
"typeYourMessage": "Vnesite svoje sporočilo..."
} }

View File

@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import '../providers/connection_provider.dart'; import '../providers/connection_provider.dart';
import '../services/validation_service.dart'; import '../services/validation_service.dart';
import '../l10n/app_localizations.dart';
class DeviceConfigScreen extends StatefulWidget { class DeviceConfigScreen extends StatefulWidget {
const DeviceConfigScreen({super.key}); const DeviceConfigScreen({super.key});
@@ -174,8 +175,8 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Public info saved'), content: Text(AppLocalizations.of(context)!.save),
backgroundColor: Colors.green, backgroundColor: Colors.green,
), ),
); );
@@ -244,8 +245,8 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Radio settings saved'), content: Text(AppLocalizations.of(context)!.save),
backgroundColor: Colors.green, backgroundColor: Colors.green,
), ),
); );
@@ -268,7 +269,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
if (!serviceEnabled) { if (!serviceEnabled) {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Location services disabled')), SnackBar(content: Text(AppLocalizations.of(context)!.locationServicesDisabled)),
); );
} }
return; return;
@@ -280,7 +281,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
if (permission == LocationPermission.denied) { if (permission == LocationPermission.denied) {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Location permission denied')), SnackBar(content: Text(AppLocalizations.of(context)!.locationPermissionDenied)),
); );
} }
return; return;
@@ -290,7 +291,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
if (permission == LocationPermission.deniedForever) { if (permission == LocationPermission.deniedForever) {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Location permission permanently denied')), SnackBar(content: Text(AppLocalizations.of(context)!.locationPermissionPermanentlyDenied)),
); );
} }
return; return;
@@ -310,8 +311,11 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Location updated'), content: Text(AppLocalizations.of(context)!.locationBroadcast(
position.latitude.toStringAsFixed(6),
position.longitude.toStringAsFixed(6),
)),
backgroundColor: Colors.green, backgroundColor: Colors.green,
), ),
); );
@@ -331,8 +335,8 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
if (!connectionProvider.deviceInfo.isConnected) { if (!connectionProvider.deviceInfo.isConnected) {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Not connected to device'), content: Text(AppLocalizations.of(context)!.deviceNotConnected),
backgroundColor: Colors.orange, backgroundColor: Colors.orange,
), ),
); );
@@ -347,9 +351,12 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
await connectionProvider.sendSelfAdvert(floodMode: true); await connectionProvider.sendSelfAdvert(floodMode: true);
if (context.mounted) { if (context.mounted) {
final deviceInfo = connectionProvider.deviceInfo;
final lat = deviceInfo.advLat != null ? (deviceInfo.advLat! / 1000000).toStringAsFixed(6) : '0.0';
final lon = deviceInfo.advLon != null ? (deviceInfo.advLon! / 1000000).toStringAsFixed(6) : '0.0';
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Advertisement broadcast to mesh network'), content: Text(AppLocalizations.of(context)!.advertisedAtLocation(lat, lon)),
backgroundColor: Colors.green, backgroundColor: Colors.green,
), ),
); );
@@ -358,7 +365,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Failed to broadcast: $e'), content: Text(AppLocalizations.of(context)!.failedToAdvertise(e.toString())),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
@@ -377,7 +384,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Device Settings'), title: Text(AppLocalizations.of(context)!.settings),
), ),
body: ListView( body: ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@@ -441,13 +448,13 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2),
) )
: const Icon(Icons.sensors), : const Icon(Icons.sensors),
tooltip: 'Broadcast', tooltip: AppLocalizations.of(context)!.settings,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
IconButton.filled( IconButton.filled(
onPressed: _savePublicInfo, onPressed: _savePublicInfo,
icon: const Icon(Icons.save), icon: const Icon(Icons.save),
tooltip: 'Save', tooltip: AppLocalizations.of(context)!.save,
), ),
], ],
), ),
@@ -555,7 +562,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
IconButton.filled( IconButton.filled(
onPressed: _saveRadioSettings, onPressed: _saveRadioSettings,
icon: const Icon(Icons.save), icon: const Icon(Icons.save),
tooltip: 'Save', tooltip: AppLocalizations.of(context)!.save,
), ),
], ],
), ),

View File

@@ -15,6 +15,7 @@ import 'settings_screen.dart';
import 'device_config_screen.dart'; import 'device_config_screen.dart';
import 'packet_log_screen.dart'; import 'packet_log_screen.dart';
import '../utils/toast_logger.dart'; import '../utils/toast_logger.dart';
import '../l10n/app_localizations.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
final Function(AppThemeMode) onThemeChanged; final Function(AppThemeMode) onThemeChanged;
@@ -77,7 +78,7 @@ class _HomeScreenState extends State<HomeScreen>
if (!connectionProvider.deviceInfo.isConnected) { if (!connectionProvider.deviceInfo.isConnected) {
if (context.mounted) { if (context.mounted) {
ToastLogger.error(context, 'Device not connected'); ToastLogger.error(context, AppLocalizations.of(context)!.deviceNotConnected);
} }
return; return;
} }
@@ -89,7 +90,7 @@ class _HomeScreenState extends State<HomeScreen>
if (context.mounted) { if (context.mounted) {
ToastLogger.error( ToastLogger.error(
context, context,
'Location services are disabled. Please enable them in Settings.', AppLocalizations.of(context)!.locationServicesDisabled,
); );
} }
return; return;
@@ -101,7 +102,7 @@ class _HomeScreenState extends State<HomeScreen>
permission = await Geolocator.requestPermission(); permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) { if (permission == LocationPermission.denied) {
if (context.mounted) { if (context.mounted) {
ToastLogger.error(context, 'Location permission denied'); ToastLogger.error(context, AppLocalizations.of(context)!.locationPermissionDenied);
} }
return; return;
} }
@@ -111,7 +112,7 @@ class _HomeScreenState extends State<HomeScreen>
if (context.mounted) { if (context.mounted) {
ToastLogger.error( ToastLogger.error(
context, context,
'Location permission permanently denied. Please enable in Settings.', AppLocalizations.of(context)!.locationPermissionPermanentlyDenied,
); );
} }
return; return;
@@ -129,7 +130,7 @@ class _HomeScreenState extends State<HomeScreen>
} catch (e) { } catch (e) {
print('❌ Failed to get GPS position: $e'); print('❌ Failed to get GPS position: $e');
if (context.mounted) { if (context.mounted) {
ToastLogger.error(context, 'Failed to get GPS location'); ToastLogger.error(context, AppLocalizations.of(context)!.failedToGetGpsLocation);
} }
return; return;
} }
@@ -149,13 +150,16 @@ class _HomeScreenState extends State<HomeScreen>
if (context.mounted) { if (context.mounted) {
ToastLogger.success( ToastLogger.success(
context, context,
'Advertised at ${position.latitude.toStringAsFixed(6)}, ${position.longitude.toStringAsFixed(6)}', AppLocalizations.of(context)!.advertisedAtLocation(
position.latitude.toStringAsFixed(6),
position.longitude.toStringAsFixed(6),
),
); );
} }
} catch (e) { } catch (e) {
print('❌ Failed to advertise device: $e'); print('❌ Failed to advertise device: $e');
if (context.mounted) { if (context.mounted) {
ToastLogger.error(context, 'Failed to advertise: $e'); ToastLogger.error(context, AppLocalizations.of(context)!.failedToAdvertise(e.toString()));
} }
} }
} }
@@ -203,7 +207,7 @@ class _HomeScreenState extends State<HomeScreen>
child: Column( child: Column(
children: [ children: [
Text( Text(
'MeshCore', AppLocalizations.of(context)!.appTitle,
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
fontSize: 18, fontSize: 18,
@@ -211,7 +215,7 @@ class _HomeScreenState extends State<HomeScreen>
), ),
), ),
Text( Text(
'Scanning for devices...', AppLocalizations.of(context)!.scanningForDevices,
style: TextStyle( style: TextStyle(
color: Theme.of( color: Theme.of(
context, context,
@@ -253,7 +257,7 @@ class _HomeScreenState extends State<HomeScreen>
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Text( child: Text(
'The default pin for devices without a screen is 123456. Trouble pairing? Forget the bluetooth device in system settings.', AppLocalizations.of(context)!.defaultPinInfo,
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer, color: Theme.of(context).colorScheme.onPrimaryContainer,
fontSize: 13, fontSize: 13,
@@ -288,7 +292,7 @@ class _HomeScreenState extends State<HomeScreen>
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'No devices found', AppLocalizations.of(context)!.noDevicesFound,
style: TextStyle( style: TextStyle(
color: Theme.of( color: Theme.of(
context, context,
@@ -303,7 +307,7 @@ class _HomeScreenState extends State<HomeScreen>
connectionProvider.startScan(); connectionProvider.startScan();
}, },
icon: const Icon(Icons.refresh), icon: const Icon(Icons.refresh),
label: const Text('Scan Again'), label: Text(AppLocalizations.of(context)!.scanAgain),
), ),
], ],
), ),
@@ -358,7 +362,7 @@ class _HomeScreenState extends State<HomeScreen>
subtitle: Row( subtitle: Row(
children: [ children: [
Text( Text(
'Tap to connect', AppLocalizations.of(context)!.tapToConnect,
style: TextStyle( style: TextStyle(
color: Theme.of( color: Theme.of(
context, context,
@@ -450,7 +454,7 @@ class _HomeScreenState extends State<HomeScreen>
await provider.disconnect(); await provider.disconnect();
}, },
icon: const Icon(Icons.power_settings_new), icon: const Icon(Icons.power_settings_new),
tooltip: 'Disconnect', tooltip: AppLocalizations.of(context)!.disconnect,
color: Colors.red.shade700, color: Colors.red.shade700,
); );
} }
@@ -461,11 +465,11 @@ class _HomeScreenState extends State<HomeScreen>
icon: const Icon(Icons.more_vert), icon: const Icon(Icons.more_vert),
itemBuilder: (context) => [ itemBuilder: (context) => [
PopupMenuItem( PopupMenuItem(
child: const Row( child: Row(
children: [ children: [
Icon(Icons.map), const Icon(Icons.map),
SizedBox(width: 8), const SizedBox(width: 8),
Text('Map Management'), Text(AppLocalizations.of(context)!.mapManagement),
], ],
), ),
onTap: () { onTap: () {
@@ -483,11 +487,11 @@ class _HomeScreenState extends State<HomeScreen>
}, },
), ),
PopupMenuItem( PopupMenuItem(
child: const Row( child: Row(
children: [ children: [
Icon(Icons.settings), const Icon(Icons.settings),
SizedBox(width: 8), const SizedBox(width: 8),
Text('Settings'), Text(AppLocalizations.of(context)!.settings),
], ],
), ),
onTap: () { onTap: () {
@@ -551,16 +555,16 @@ class _HomeScreenState extends State<HomeScreen>
Icons.message, Icons.message,
unreadCount, unreadCount,
), ),
text: 'Messages', text: AppLocalizations.of(context)!.messages,
), ),
Tab( Tab(
icon: _buildTabIconWithBadge( icon: _buildTabIconWithBadge(
Icons.contacts, Icons.contacts,
newContactsCount, newContactsCount,
), ),
text: 'Contacts', text: AppLocalizations.of(context)!.contacts,
), ),
const Tab(icon: Icon(Icons.map), text: 'Map'), Tab(icon: const Icon(Icons.map), text: AppLocalizations.of(context)!.map),
], ],
), ),
); );
@@ -583,10 +587,10 @@ class _HomeScreenState extends State<HomeScreen>
// Disconnected state: show connect button // Disconnected state: show connect button
return Row( return Row(
children: [ children: [
const Expanded( Expanded(
child: Text( child: Text(
'MeshCore', AppLocalizations.of(context)!.appTitle,
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
), ),
), ),
ElevatedButton.icon( ElevatedButton.icon(
@@ -608,7 +612,7 @@ class _HomeScreenState extends State<HomeScreen>
label: Text( label: Text(
provider.isReconnecting provider.isReconnecting
? '${provider.reconnectionAttempt}/${provider.maxReconnectionAttempts}' ? '${provider.reconnectionAttempt}/${provider.maxReconnectionAttempts}'
: 'Connect', : AppLocalizations.of(context)!.connect,
), ),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.white, backgroundColor: Colors.white,
@@ -624,7 +628,7 @@ class _HomeScreenState extends State<HomeScreen>
IconButton( IconButton(
onPressed: () => provider.cancelReconnection(), onPressed: () => provider.cancelReconnection(),
icon: const Icon(Icons.close, size: 20), icon: const Icon(Icons.close, size: 20),
tooltip: 'Cancel reconnection', tooltip: AppLocalizations.of(context)!.cancelReconnection,
style: IconButton.styleFrom( style: IconButton.styleFrom(
backgroundColor: Colors.red.shade700, backgroundColor: Colors.red.shade700,
foregroundColor: Colors.white, foregroundColor: Colors.white,
@@ -650,7 +654,7 @@ class _HomeScreenState extends State<HomeScreen>
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
deviceInfo.selfName ?? 'MeshCore', deviceInfo.selfName ?? AppLocalizations.of(context)!.appTitle,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -838,8 +842,8 @@ class _HomeScreenState extends State<HomeScreen>
Expanded( Expanded(
child: Text( child: Text(
isConnected isConnected
? deviceInfo.displayName ?? 'Connected' ? deviceInfo.displayName ?? AppLocalizations.of(context)!.connect
: 'Not Connected', : AppLocalizations.of(context)!.deviceNotConnected,
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
), ),
), ),
@@ -881,7 +885,7 @@ class _HomeScreenState extends State<HomeScreen>
? null ? null
: () => _showConnectionDialog(context), : () => _showConnectionDialog(context),
icon: const Icon(Icons.bluetooth_searching, size: 18), icon: const Icon(Icons.bluetooth_searching, size: 18),
label: const Text('Connect'), label: Text(AppLocalizations.of(context)!.connect),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
), ),
@@ -896,7 +900,7 @@ class _HomeScreenState extends State<HomeScreen>
await provider.disconnect(); await provider.disconnect();
}, },
icon: const Icon(Icons.bluetooth_disabled, size: 18), icon: const Icon(Icons.bluetooth_disabled, size: 18),
label: const Text('Disconnect'), label: Text(AppLocalizations.of(context)!.disconnect),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
), ),

View File

@@ -4,6 +4,7 @@ import 'package:latlong2/latlong.dart';
import '../services/tile_cache_service.dart'; import '../services/tile_cache_service.dart';
import '../services/validation_service.dart'; import '../services/validation_service.dart';
import '../models/map_layer.dart'; import '../models/map_layer.dart';
import '../l10n/app_localizations.dart';
class MapManagementScreen extends StatefulWidget { class MapManagementScreen extends StatefulWidget {
final TileCacheService tileCacheService; final TileCacheService tileCacheService;
@@ -217,8 +218,8 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Download cancelled'), content: Text(AppLocalizations.of(context)!.cancel),
backgroundColor: Colors.orange, backgroundColor: Colors.orange,
), ),
); );
@@ -237,19 +238,19 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Clear Cache'), title: Text(AppLocalizations.of(context)!.clear),
content: const Text( content: const Text(
'Are you sure you want to delete all downloaded maps? This action cannot be undone.', 'Are you sure you want to delete all downloaded maps? This action cannot be undone.',
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context, false), onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'), child: Text(AppLocalizations.of(context)!.cancel),
), ),
TextButton( TextButton(
onPressed: () => Navigator.pop(context, true), onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(foregroundColor: Colors.red), style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Clear'), child: Text(AppLocalizations.of(context)!.clear),
), ),
], ],
), ),
@@ -295,7 +296,7 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Map Management'), title: Text(AppLocalizations.of(context)!.mapManagement),
), ),
body: _isLoading body: _isLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())

View File

@@ -28,6 +28,7 @@ import '../widgets/map/detailed_compass_dialog.dart';
import '../widgets/map/drawing_layer.dart'; import '../widgets/map/drawing_layer.dart';
import '../widgets/map/drawing_toolbar.dart'; import '../widgets/map/drawing_toolbar.dart';
import '../widgets/messages/sar_update_sheet.dart'; import '../widgets/messages/sar_update_sheet.dart';
import '../l10n/app_localizations.dart';
import 'map_management_screen.dart'; import 'map_management_screen.dart';
class MapTab extends StatefulWidget { class MapTab extends StatefulWidget {
@@ -643,8 +644,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
_saveSettings(); _saveSettings();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Failed to start background tracking. Check permissions and BLE connection.'), content: Text(AppLocalizations.of(context)!.failedToStartBackgroundTracking),
duration: Duration(seconds: 3), duration: Duration(seconds: 3),
), ),
); );
@@ -710,8 +711,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
if (!connectionProvider.deviceInfo.isConnected) { if (!connectionProvider.deviceInfo.isConnected) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Not connected to device'), content: Text(AppLocalizations.of(context)!.deviceNotConnected),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );

View File

@@ -12,6 +12,7 @@ import '../models/sar_marker.dart';
import '../widgets/messages/sar_update_sheet.dart'; import '../widgets/messages/sar_update_sheet.dart';
import '../widgets/contacts/direct_message_sheet.dart'; import '../widgets/contacts/direct_message_sheet.dart';
import '../utils/toast_logger.dart'; import '../utils/toast_logger.dart';
import '../l10n/app_localizations.dart';
class MessagesTab extends StatefulWidget { class MessagesTab extends StatefulWidget {
final VoidCallback onNavigateToMap; final VoidCallback onNavigateToMap;
@@ -304,14 +305,14 @@ class _MessagesTabState extends State<MessagesTab> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'No messages yet', AppLocalizations.of(context)!.noMessagesYet,
style: Theme.of( style: Theme.of(
context, context,
).textTheme.titleLarge, ).textTheme.titleLarge,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Pull down to sync messages', AppLocalizations.of(context)!.pullDownToSync,
style: Theme.of( style: Theme.of(
context, context,
).textTheme.bodyMedium, ).textTheme.bodyMedium,
@@ -374,7 +375,7 @@ class _MessagesTabState extends State<MessagesTab> {
// SAR quick action button // SAR quick action button
IconButton( IconButton(
icon: const Icon(Icons.add_location_alt), icon: const Icon(Icons.add_location_alt),
tooltip: 'Send SAR marker', tooltip: AppLocalizations.of(context)!.sendSarMarker,
onPressed: _showSarDialog, onPressed: _showSarDialog,
style: IconButton.styleFrom( style: IconButton.styleFrom(
backgroundColor: Theme.of( backgroundColor: Theme.of(
@@ -579,9 +580,9 @@ class _MessageBubble extends StatelessWidget {
// Delete message option // Delete message option
ListTile( ListTile(
leading: const Icon(Icons.delete, color: Colors.red), leading: const Icon(Icons.delete, color: Colors.red),
title: const Text( title: Text(
'Delete message', AppLocalizations.of(context)!.delete,
style: TextStyle(color: Colors.red), style: const TextStyle(color: Colors.red),
), ),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
@@ -633,6 +634,7 @@ class _MessageBubble extends StatelessWidget {
} }
void _showDeleteConfirmation(BuildContext context) { void _showDeleteConfirmation(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
@@ -641,7 +643,7 @@ class _MessageBubble extends StatelessWidget {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('Cancel'), child: Text(l10n.cancel),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {

View File

@@ -5,6 +5,7 @@ import 'dart:io';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import '../models/ble_packet_log.dart'; import '../models/ble_packet_log.dart';
import '../services/meshcore_ble_service.dart'; import '../services/meshcore_ble_service.dart';
import '../l10n/app_localizations.dart';
class PacketLogScreen extends StatefulWidget { class PacketLogScreen extends StatefulWidget {
final MeshCoreBleService bleService; final MeshCoreBleService bleService;
@@ -149,12 +150,12 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Clear Packet Logs'), title: Text(AppLocalizations.of(context)!.clearAllData),
content: const Text('Are you sure you want to clear all packet logs? This cannot be undone.'), content: const Text('Are you sure you want to clear all packet logs? This cannot be undone.'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('Cancel'), child: Text(AppLocalizations.of(context)!.cancel),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
@@ -166,7 +167,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
); );
}, },
style: TextButton.styleFrom(foregroundColor: Colors.red), style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Clear'), child: Text(AppLocalizations.of(context)!.clear),
), ),
], ],
), ),

View File

@@ -11,6 +11,7 @@ import '../services/location_tracking_service.dart';
import '../services/locale_preferences.dart'; import '../services/locale_preferences.dart';
import '../utils/sample_data_generator.dart'; import '../utils/sample_data_generator.dart';
import '../theme/app_theme.dart'; import '../theme/app_theme.dart';
import '../l10n/app_localizations.dart';
class SettingsScreen extends StatefulWidget { class SettingsScreen extends StatefulWidget {
final Function(AppThemeMode) onThemeChanged; final Function(AppThemeMode) onThemeChanged;
@@ -97,7 +98,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text(
'Location broadcast: ${position.latitude.toStringAsFixed(5)}, ${position.longitude.toStringAsFixed(5)}', AppLocalizations.of(context)!.locationBroadcast(
position.latitude.toStringAsFixed(5),
position.longitude.toStringAsFixed(5),
),
), ),
backgroundColor: Colors.green, backgroundColor: Colors.green,
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
@@ -220,7 +224,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text(
'Loaded $teamCount team members, $channelCount channels, ${sarMessages.length} SAR markers, ${channelMessages.length} messages', AppLocalizations.of(context)!.loadedSampleData(
teamCount,
channelCount,
sarMessages.length,
channelMessages.length,
),
), ),
backgroundColor: Colors.green, backgroundColor: Colors.green,
), ),
@@ -229,7 +238,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Failed to load sample data: $e'), content: Text(AppLocalizations.of(context)!.failedToLoadSampleData(e.toString())),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
@@ -247,11 +256,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (!success && mounted) { if (!success && mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text( content: Text(
'Failed to start background tracking. Check permissions and BLE connection.', AppLocalizations.of(context)!.failedToStartBackgroundTracking,
), ),
duration: Duration(seconds: 3), duration: const Duration(seconds: 3),
), ),
); );
} }
@@ -272,19 +281,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Clear All Data'), title: Text(AppLocalizations.of(context)!.clearAllDataConfirmTitle),
content: const Text( content: Text(
'This will clear all contacts and SAR markers. Are you sure?', AppLocalizations.of(context)!.clearAllDataConfirmMessage,
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context, false), onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'), child: Text(AppLocalizations.of(context)!.cancel),
), ),
TextButton( TextButton(
onPressed: () => Navigator.pop(context, true), onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(foregroundColor: Colors.red), style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Clear'), child: Text(AppLocalizations.of(context)!.clear),
), ),
], ],
), ),
@@ -307,8 +316,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('All data cleared'), content: Text(AppLocalizations.of(context)!.allDataCleared),
backgroundColor: Colors.orange, backgroundColor: Colors.orange,
), ),
); );
@@ -317,22 +326,22 @@ class _SettingsScreenState extends State<SettingsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Settings')), appBar: AppBar(title: Text(AppLocalizations.of(context)!.settings)),
body: ListView( body: ListView(
children: [ children: [
// General Settings Section // General Settings Section
_buildSectionHeader('General'), _buildSectionHeader(AppLocalizations.of(context)!.general),
ListTile( ListTile(
leading: const Icon(Icons.palette), leading: const Icon(Icons.palette),
title: const Text('Theme'), title: Text(AppLocalizations.of(context)!.theme),
subtitle: Text(AppTheme.getThemeDisplayName(_selectedTheme)), subtitle: Text(AppTheme.getThemeDisplayName(_selectedTheme)),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () => _showThemeDialog(), onTap: () => _showThemeDialog(),
), ),
SwitchListTile( SwitchListTile(
secondary: const Icon(Icons.radar), secondary: const Icon(Icons.radar),
title: const Text('Show RX/TX Indicators'), title: Text(AppLocalizations.of(context)!.showRxTxIndicators),
subtitle: const Text('Display packet activity indicators in top bar'), subtitle: Text(AppLocalizations.of(context)!.displayPacketActivity),
value: _showRxTxIndicators, value: _showRxTxIndicators,
onChanged: (value) async { onChanged: (value) async {
setState(() { setState(() {
@@ -343,7 +352,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
ListTile( ListTile(
leading: const Icon(Icons.language), leading: const Icon(Icons.language),
title: const Text('Language'), title: Text(AppLocalizations.of(context)!.language),
subtitle: Text(LocalePreferences.getDisplayName(_selectedLocale)), subtitle: Text(LocalePreferences.getDisplayName(_selectedLocale)),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () => _showLanguageDialog(), onTap: () => _showLanguageDialog(),
@@ -351,13 +360,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
const Divider(), const Divider(),
// Location Settings Section // Location Settings Section
_buildSectionHeader('Location Broadcasting'), _buildSectionHeader(AppLocalizations.of(context)!.locationBroadcasting),
// Automatic tracking settings // Automatic tracking settings
SwitchListTile( SwitchListTile(
secondary: const Icon(Icons.location_on), secondary: const Icon(Icons.location_on),
title: const Text('Auto Location Tracking'), title: Text(AppLocalizations.of(context)!.autoLocationTracking),
subtitle: const Text('Automatically broadcast position updates'), subtitle: Text(AppLocalizations.of(context)!.automaticallyBroadcastPosition),
value: _locationService.isTracking, value: _locationService.isTracking,
onChanged: (value) { onChanged: (value) {
if (value) { if (value) {
@@ -371,8 +380,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (_locationService.isTracking) ...[ if (_locationService.isTracking) ...[
ListTile( ListTile(
leading: const Icon(Icons.tune), leading: const Icon(Icons.tune),
title: const Text('Configure Tracking'), title: Text(AppLocalizations.of(context)!.configureTracking),
subtitle: const Text('Distance and time thresholds'), subtitle: Text(AppLocalizations.of(context)!.distanceAndTimeThresholds),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () => _showTrackingConfigDialog(), onTap: () => _showTrackingConfigDialog(),
), ),
@@ -381,10 +390,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
const Divider(), const Divider(),
// About Section // About Section
_buildSectionHeader('About'), _buildSectionHeader(AppLocalizations.of(context)!.about),
ListTile( ListTile(
leading: const Icon(Icons.info), leading: const Icon(Icons.info),
title: const Text('App Version'), title: Text(AppLocalizations.of(context)!.appVersion),
subtitle: Text( subtitle: Text(
_packageInfo != null _packageInfo != null
? '${_packageInfo!.version} (${_packageInfo!.buildNumber})' ? '${_packageInfo!.version} (${_packageInfo!.buildNumber})'
@@ -393,34 +402,34 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
ListTile( ListTile(
leading: const Icon(Icons.badge), leading: const Icon(Icons.badge),
title: const Text('App Name'), title: Text(AppLocalizations.of(context)!.appName),
subtitle: Text(_packageInfo?.appName ?? 'MeshCore SAR'), subtitle: Text(_packageInfo?.appName ?? 'MeshCore SAR'),
), ),
ListTile( ListTile(
leading: const Icon(Icons.description), leading: const Icon(Icons.description),
title: const Text('About MeshCore SAR'), title: Text(AppLocalizations.of(context)!.aboutMeshCoreSar),
subtitle: const Text( subtitle: Text(
'Search & Rescue application with BLE mesh networking and offline maps', AppLocalizations.of(context)!.aboutDescription.split('\n\n')[0],
), ),
onTap: () => _showAboutDialog(), onTap: () => _showAboutDialog(),
), ),
const Divider(), const Divider(),
// Developer Section // Developer Section
_buildSectionHeader('Developer'), _buildSectionHeader(AppLocalizations.of(context)!.developer),
ListTile( ListTile(
leading: const Icon(Icons.bug_report), leading: const Icon(Icons.bug_report),
title: const Text('Package Name'), title: Text(AppLocalizations.of(context)!.packageName),
subtitle: Text(_packageInfo?.packageName ?? 'com.meshcore.sar'), subtitle: Text(_packageInfo?.packageName ?? 'com.meshcore.sar'),
), ),
const Divider(), const Divider(),
// Sample Data Section // Sample Data Section
_buildSectionHeader('Sample Data'), _buildSectionHeader(AppLocalizations.of(context)!.sampleData),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text( child: Text(
'Load or clear sample contacts, channel messages, and SAR markers for testing', AppLocalizations.of(context)!.sampleDataDescription,
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
), ),
@@ -440,7 +449,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2),
) )
: const Icon(Icons.add_circle_outline), : const Icon(Icons.add_circle_outline),
label: const Text('Load Sample Data'), label: Text(AppLocalizations.of(context)!.loadSampleData),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
), ),
@@ -451,7 +460,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: _isLoadingSampleData ? null : _clearSampleData, onPressed: _isLoadingSampleData ? null : _clearSampleData,
icon: const Icon(Icons.delete_outline), icon: const Icon(Icons.delete_outline),
label: const Text('Clear All Data'), label: Text(AppLocalizations.of(context)!.clearAllData),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: Colors.red, foregroundColor: Colors.red,
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
@@ -488,7 +497,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
context: context, context: context,
builder: (context) => StatefulBuilder( builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog( builder: (context, setDialogState) => AlertDialog(
title: const Text('Location Tracking Configuration'), title: Text(AppLocalizations.of(context)!.locationTrackingConfiguration),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -496,7 +505,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
children: [ children: [
// Description // Description
Text( Text(
'Configure when location broadcasts are sent to the mesh network', AppLocalizations.of(context)!.configureWhenLocationBroadcasts,
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey, color: Colors.grey,
), ),
@@ -505,14 +514,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
// Minimum Distance // Minimum Distance
Text( Text(
'Minimum Distance', AppLocalizations.of(context)!.minimumDistance,
style: Theme.of(context).textTheme.titleSmall?.copyWith( style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Broadcast only after moving ${tempMinDistance.toStringAsFixed(0)} meters', AppLocalizations.of(context)!.broadcastAfterMoving(tempMinDistance.toStringAsFixed(0)),
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -551,14 +560,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
// Maximum Distance // Maximum Distance
Text( Text(
'Maximum Distance', AppLocalizations.of(context)!.maximumDistance,
style: Theme.of(context).textTheme.titleSmall?.copyWith( style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Always broadcast after moving ${tempMaxDistance.toStringAsFixed(0)} meters', AppLocalizations.of(context)!.alwaysBroadcastAfterMoving(tempMaxDistance.toStringAsFixed(0)),
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -594,14 +603,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
// Minimum Time Interval // Minimum Time Interval
Text( Text(
'Minimum Time Interval', AppLocalizations.of(context)!.minimumTimeInterval,
style: Theme.of(context).textTheme.titleSmall?.copyWith( style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Always broadcast every ${_formatDuration(tempTimeInterval)}', AppLocalizations.of(context)!.alwaysBroadcastEvery(_formatDuration(tempTimeInterval)),
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -638,7 +647,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('Cancel'), child: Text(AppLocalizations.of(context)!.cancel),
), ),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
@@ -663,7 +672,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {}); setState(() {});
} }
}, },
child: const Text('Save'), child: Text(AppLocalizations.of(context)!.save),
), ),
], ],
), ),
@@ -689,14 +698,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Choose Theme'), title: Text(AppLocalizations.of(context)!.chooseTheme),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
RadioListTile<AppThemeMode>( RadioListTile<AppThemeMode>(
title: const Text('Light'), title: Text(AppLocalizations.of(context)!.light),
subtitle: const Text('Blue light theme'), subtitle: Text(AppLocalizations.of(context)!.blueLightTheme),
value: AppThemeMode.light, value: AppThemeMode.light,
groupValue: _selectedTheme, groupValue: _selectedTheme,
onChanged: (value) { onChanged: (value) {
@@ -705,8 +714,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
}, },
), ),
RadioListTile<AppThemeMode>( RadioListTile<AppThemeMode>(
title: const Text('Dark'), title: Text(AppLocalizations.of(context)!.dark),
subtitle: const Text('Blue dark theme'), subtitle: Text(AppLocalizations.of(context)!.blueDarkTheme),
value: AppThemeMode.dark, value: AppThemeMode.dark,
groupValue: _selectedTheme, groupValue: _selectedTheme,
onChanged: (value) { onChanged: (value) {
@@ -718,7 +727,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
RadioListTile<AppThemeMode>( RadioListTile<AppThemeMode>(
title: Row( title: Row(
children: [ children: [
const Text('SAR Red'), Text(AppLocalizations.of(context)!.sarRed),
const SizedBox(width: 8), const SizedBox(width: 8),
Container( Container(
width: 16, width: 16,
@@ -731,7 +740,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
], ],
), ),
subtitle: const Text('Alert/Emergency mode'), subtitle: Text(AppLocalizations.of(context)!.alertEmergencyMode),
value: AppThemeMode.sarRed, value: AppThemeMode.sarRed,
groupValue: _selectedTheme, groupValue: _selectedTheme,
onChanged: (value) { onChanged: (value) {
@@ -742,7 +751,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
RadioListTile<AppThemeMode>( RadioListTile<AppThemeMode>(
title: Row( title: Row(
children: [ children: [
const Text('SAR Green'), Text(AppLocalizations.of(context)!.sarGreen),
const SizedBox(width: 8), const SizedBox(width: 8),
Container( Container(
width: 16, width: 16,
@@ -755,7 +764,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
], ],
), ),
subtitle: const Text('Safe/All Clear mode'), subtitle: Text(AppLocalizations.of(context)!.safeAllClearMode),
value: AppThemeMode.sarGreen, value: AppThemeMode.sarGreen,
groupValue: _selectedTheme, groupValue: _selectedTheme,
onChanged: (value) { onChanged: (value) {
@@ -765,8 +774,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
const Divider(), const Divider(),
RadioListTile<AppThemeMode>( RadioListTile<AppThemeMode>(
title: const Text('Auto (System)'), title: Text(AppLocalizations.of(context)!.autoSystem),
subtitle: const Text('Follow system theme'), subtitle: Text(AppLocalizations.of(context)!.followSystemTheme),
value: AppThemeMode.system, value: AppThemeMode.system,
groupValue: _selectedTheme, groupValue: _selectedTheme,
onChanged: (value) { onChanged: (value) {
@@ -780,7 +789,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('Cancel'), child: Text(AppLocalizations.of(context)!.cancel),
), ),
], ],
), ),
@@ -791,14 +800,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Choose Language'), title: Text(AppLocalizations.of(context)!.chooseLanguage),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
RadioListTile<Locale?>( RadioListTile<Locale?>(
title: const Text('System Default'), title: Text(LocalePreferences.getDisplayName(null)),
subtitle: const Text('Follow system language'), subtitle: Text(LocalePreferences.getDisplayName(null)),
value: null, value: null,
groupValue: _selectedLocale, groupValue: _selectedLocale,
onChanged: (value) { onChanged: (value) {
@@ -824,7 +833,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('Cancel'), child: Text(AppLocalizations.of(context)!.cancel),
), ),
], ],
), ),
@@ -835,7 +844,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('About MeshCore SAR'), title: Text(AppLocalizations.of(context)!.aboutMeshCoreSar),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -853,31 +862,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
const Text( Text(
'A Search & Rescue application designed for emergency response teams. ' AppLocalizations.of(context)!.aboutDescription,
'Features include:\n\n'
'• BLE mesh networking for device-to-device communication\n'
'• Offline maps with multiple layer options\n'
'• Real-time team member tracking\n'
'• SAR tactical markers (found person, fire, staging)\n'
'• Contact management and messaging\n'
'• GPS tracking with compass heading\n'
'• Map tile caching for offline use',
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'Technologies Used:', AppLocalizations.of(context)!.technologiesUsed,
style: Theme.of( style: Theme.of(
context, context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text( Text(
'• Flutter for cross-platform development\n' AppLocalizations.of(context)!.technologiesList,
'• BLE (Bluetooth Low Energy) for mesh networking\n'
'• OpenStreetMap for mapping\n'
'• Provider for state management\n'
'• SharedPreferences for local storage',
), ),
], ],
), ),
@@ -885,7 +882,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('Close'), child: Text(AppLocalizations.of(context)!.close),
), ),
], ],
), ),

View File

@@ -22,7 +22,8 @@ class LocationTrackingService {
// Singleton Pattern // Singleton Pattern
// ============================================================================ // ============================================================================
static final LocationTrackingService _instance = LocationTrackingService._internal(); static final LocationTrackingService _instance =
LocationTrackingService._internal();
/// Get the singleton instance /// Get the singleton instance
factory LocationTrackingService() => _instance; factory LocationTrackingService() => _instance;
@@ -52,7 +53,7 @@ class LocationTrackingService {
double maxDistanceMeters = 100.0; double maxDistanceMeters = 100.0;
/// Minimum time interval in seconds between broadcasts /// Minimum time interval in seconds between broadcasts
int minTimeIntervalSeconds = 30; int minTimeIntervalSeconds = 155;
/// GPS update distance filter for position stream /// GPS update distance filter for position stream
double gpsUpdateDistance = 10.0; double gpsUpdateDistance = 10.0;
@@ -154,7 +155,9 @@ class LocationTrackingService {
} }
if (permission == LocationPermission.deniedForever) { if (permission == LocationPermission.deniedForever) {
onError?.call('Location permission permanently denied. Please enable in settings.'); onError?.call(
'Location permission permanently denied. Please enable in settings.',
);
return false; return false;
} }
@@ -210,7 +213,9 @@ class LocationTrackingService {
/// Returns true if successful, false otherwise. /// Returns true if successful, false otherwise.
Future<bool> startTracking({double? distanceThreshold}) async { Future<bool> startTracking({double? distanceThreshold}) async {
if (!_isInitialized || _bleService == null) { if (!_isInitialized || _bleService == null) {
debugPrint('⚠️ [LocationTracking] Service not initialized or BLE service null'); debugPrint(
'⚠️ [LocationTracking] Service not initialized or BLE service null',
);
onError?.call('Location tracking service not initialized'); onError?.call('Location tracking service not initialized');
return false; return false;
} }
@@ -239,7 +244,8 @@ class LocationTrackingService {
// Start position stream // Start position stream
try { try {
_positionSubscription = getPositionStream(distanceFilter: threshold).listen( _positionSubscription = getPositionStream(distanceFilter: threshold)
.listen(
_handlePositionUpdate, _handlePositionUpdate,
onError: (error) { onError: (error) {
debugPrint('❌ [LocationTracking] Position stream error: $error'); debugPrint('❌ [LocationTracking] Position stream error: $error');
@@ -250,7 +256,9 @@ class LocationTrackingService {
isTracking = true; isTracking = true;
onTrackingStateChanged?.call(true); onTrackingStateChanged?.call(true);
debugPrint('✅ [LocationTracking] Tracking started with ${threshold}m threshold'); debugPrint(
'✅ [LocationTracking] Tracking started with ${threshold}m threshold',
);
return true; return true;
} catch (e) { } catch (e) {
debugPrint('❌ [LocationTracking] Failed to start tracking: $e'); debugPrint('❌ [LocationTracking] Failed to start tracking: $e');
@@ -281,7 +289,9 @@ class LocationTrackingService {
gpsUpdateDistance = meters; gpsUpdateDistance = meters;
await saveSettings(); await saveSettings();
debugPrint('📏 [LocationTracking] Distance threshold updated to ${meters}m'); debugPrint(
'📏 [LocationTracking] Distance threshold updated to ${meters}m',
);
// Restart tracking if currently active // Restart tracking if currently active
if (isTracking) { if (isTracking) {
@@ -296,7 +306,9 @@ class LocationTrackingService {
/// Handle incoming position updates from GPS stream /// Handle incoming position updates from GPS stream
void _handlePositionUpdate(Position position) { void _handlePositionUpdate(Position position) {
debugPrint('📍 [LocationTracking] New position: ${position.latitude}, ${position.longitude}'); debugPrint(
'📍 [LocationTracking] New position: ${position.latitude}, ${position.longitude}',
);
// Update current position // Update current position
currentPosition = position; currentPosition = position;
@@ -317,7 +329,9 @@ class LocationTrackingService {
} }
// Calculate time since last broadcast // Calculate time since last broadcast
final timeSinceLastBroadcast = DateTime.now().difference(_lastBroadcastTime!); final timeSinceLastBroadcast = DateTime.now().difference(
_lastBroadcastTime!,
);
final secondsSinceLastBroadcast = timeSinceLastBroadcast.inSeconds; final secondsSinceLastBroadcast = timeSinceLastBroadcast.inSeconds;
// Calculate distance from last broadcast position // Calculate distance from last broadcast position
@@ -328,12 +342,16 @@ class LocationTrackingService {
position.longitude, position.longitude,
); );
debugPrint(' Distance from last broadcast: ${distanceFromLastBroadcast.toStringAsFixed(1)}m'); debugPrint(
' Distance from last broadcast: ${distanceFromLastBroadcast.toStringAsFixed(1)}m',
);
debugPrint(' Time since last broadcast: ${secondsSinceLastBroadcast}s'); debugPrint(' Time since last broadcast: ${secondsSinceLastBroadcast}s');
// Broadcast if moved beyond max distance threshold // Broadcast if moved beyond max distance threshold
if (distanceFromLastBroadcast >= maxDistanceMeters) { if (distanceFromLastBroadcast >= maxDistanceMeters) {
debugPrint(' 📤 Triggering broadcast: exceeded max distance (${maxDistanceMeters}m)'); debugPrint(
' 📤 Triggering broadcast: exceeded max distance (${maxDistanceMeters}m)',
);
_broadcastPosition(position); _broadcastPosition(position);
return; return;
} }
@@ -341,7 +359,9 @@ class LocationTrackingService {
// Broadcast if minimum time interval passed AND moved beyond min distance // Broadcast if minimum time interval passed AND moved beyond min distance
if (secondsSinceLastBroadcast >= minTimeIntervalSeconds && if (secondsSinceLastBroadcast >= minTimeIntervalSeconds &&
distanceFromLastBroadcast >= minDistanceMeters) { distanceFromLastBroadcast >= minDistanceMeters) {
debugPrint(' 📤 Triggering broadcast: exceeded min time (${minTimeIntervalSeconds}s) and min distance (${minDistanceMeters}m)'); debugPrint(
' 📤 Triggering broadcast: exceeded min time (${minTimeIntervalSeconds}s) and min distance (${minDistanceMeters}m)',
);
_broadcastPosition(position); _broadcastPosition(position);
return; return;
} }

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import '../../l10n/app_localizations.dart';
/// Reusable location display widget with tap-to-show modal /// Reusable location display widget with tap-to-show modal
/// Shows coordinates in a compact format with ability to view all formats /// Shows coordinates in a compact format with ability to view all formats
@@ -149,7 +150,7 @@ class LocationDisplay extends StatelessWidget {
Clipboard.setData(ClipboardData(text: value)); Clipboard.setData(ClipboardData(text: value));
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('$label copied to clipboard'), content: Text(AppLocalizations.of(context)!.copiedToClipboard(label)),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
), ),
); );

View File

@@ -11,6 +11,7 @@ import '../../providers/map_provider.dart';
import 'direct_message_sheet.dart'; import 'direct_message_sheet.dart';
import 'room_login_sheet.dart'; import 'room_login_sheet.dart';
import '../../utils/toast_logger.dart'; import '../../utils/toast_logger.dart';
import '../../l10n/app_localizations.dart';
class ContactTile extends StatelessWidget { class ContactTile extends StatelessWidget {
final Contact contact; final Contact contact;
@@ -359,7 +360,7 @@ class ContactTile extends StatelessWidget {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Delete Contact'), title: Text(AppLocalizations.of(context)!.deleteContact),
content: Text( content: Text(
'Are you sure you want to delete "${contact.displayName}"?\n\n' '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.', 'This will remove the contact from both the app and the companion radio device.',
@@ -367,7 +368,7 @@ class ContactTile extends StatelessWidget {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('Cancel'), child: Text(AppLocalizations.of(context)!.cancel),
), ),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
@@ -376,7 +377,7 @@ class ContactTile extends StatelessWidget {
await _deleteContact(context, contact); await _deleteContact(context, contact);
}, },
style: TextButton.styleFrom(foregroundColor: Colors.red), style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Delete'), child: Text(AppLocalizations.of(context)!.delete),
), ),
], ],
), ),
@@ -504,9 +505,9 @@ class ContactTile extends StatelessWidget {
onTap: () { onTap: () {
Clipboard.setData(ClipboardData(text: contact.publicKeyHex)); Clipboard.setData(ClipboardData(text: contact.publicKeyHex));
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Public key copied to clipboard'), content: Text(AppLocalizations.of(context)!.publicKeyCopied),
duration: Duration(seconds: 2), duration: const Duration(seconds: 2),
), ),
); );
}, },
@@ -587,7 +588,7 @@ class ContactTile extends StatelessWidget {
DefaultTabController.of(context).animateTo(2); DefaultTabController.of(context).animateTo(2);
}, },
icon: const Icon(Icons.map, size: 18), icon: const Icon(Icons.map, size: 18),
label: const Text('View on Map'), label: Text(AppLocalizations.of(context)!.viewOnMap),
style: TextButton.styleFrom( style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
), ),
@@ -645,7 +646,7 @@ class ContactTile extends StatelessWidget {
ToastLogger.info(context, 'Requesting telemetry from ${contact.displayName}...'); ToastLogger.info(context, 'Requesting telemetry from ${contact.displayName}...');
}, },
icon: const Icon(Icons.refresh, size: 18), icon: const Icon(Icons.refresh, size: 18),
label: const Text('Refresh'), label: Text(AppLocalizations.of(context)!.refresh),
style: TextButton.styleFrom( style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
), ),
@@ -688,7 +689,7 @@ class ContactTile extends StatelessWidget {
_showDirectMessageDialog(context, contact); _showDirectMessageDialog(context, contact);
}, },
icon: const Icon(Icons.message), icon: const Icon(Icons.message),
label: const Text('Send Direct Message'), label: Text(AppLocalizations.of(context)!.sendDirectMessage),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14), padding: const EdgeInsets.symmetric(vertical: 14),
backgroundColor: _getTypeColor(contact.type, context), backgroundColor: _getTypeColor(contact.type, context),
@@ -705,7 +706,7 @@ class ContactTile extends StatelessWidget {
ToastLogger.info(context, 'Path reset for ${contact.displayName}. Next message will find a new route.'); ToastLogger.info(context, 'Path reset for ${contact.displayName}. Next message will find a new route.');
}, },
icon: const Icon(Icons.route), icon: const Icon(Icons.route),
label: const Text('Reset Path (Re-route)'), label: Text(AppLocalizations.of(context)!.resetPath),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14), padding: const EdgeInsets.symmetric(vertical: 14),
side: BorderSide(color: _getTypeColor(contact.type, context)), side: BorderSide(color: _getTypeColor(contact.type, context)),
@@ -742,7 +743,7 @@ class ContactTile extends StatelessWidget {
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: () => _showDeleteConfirmation(context, contact), onPressed: () => _showDeleteConfirmation(context, contact),
icon: const Icon(Icons.delete_outline), icon: const Icon(Icons.delete_outline),
label: const Text('Delete Contact'), label: Text(AppLocalizations.of(context)!.deleteContact),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14), padding: const EdgeInsets.symmetric(vertical: 14),
side: const BorderSide(color: Colors.red), side: const BorderSide(color: Colors.red),
@@ -803,7 +804,7 @@ class ContactTile extends StatelessWidget {
Clipboard.setData(ClipboardData(text: value)); Clipboard.setData(ClipboardData(text: value));
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('$label copied to clipboard'), content: Text(AppLocalizations.of(context)!.copiedToClipboard(label)),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
), ),
); );

View File

@@ -7,6 +7,7 @@ import '../../models/message.dart';
import '../../providers/connection_provider.dart'; import '../../providers/connection_provider.dart';
import '../../providers/messages_provider.dart'; import '../../providers/messages_provider.dart';
import '../../utils/toast_logger.dart'; import '../../utils/toast_logger.dart';
import '../../l10n/app_localizations.dart';
class DirectMessageSheet extends StatefulWidget { class DirectMessageSheet extends StatefulWidget {
final Contact contact; final Contact contact;
@@ -51,7 +52,7 @@ class _DirectMessageSheetState extends State<DirectMessageSheet> {
if (!connectionProvider.deviceInfo.isConnected) { if (!connectionProvider.deviceInfo.isConnected) {
if (!mounted) return; if (!mounted) return;
ToastLogger.error(context, 'Not connected to device'); ToastLogger.error(context, AppLocalizations.of(context)!.notConnectedToDevice);
return; return;
} }
@@ -100,10 +101,10 @@ class _DirectMessageSheetState extends State<DirectMessageSheet> {
if (!mounted) return; if (!mounted) return;
Navigator.pop(context); // Close the dialog Navigator.pop(context); // Close the dialog
ToastLogger.success(context, 'Direct message sent to ${widget.contact.displayName}'); ToastLogger.success(context, AppLocalizations.of(context)!.directMessageSentTo(widget.contact.displayName));
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
ToastLogger.error(context, 'Failed to send: $e'); ToastLogger.error(context, AppLocalizations.of(context)!.failedToSend(e.toString()));
} }
} }
@@ -137,7 +138,7 @@ class _DirectMessageSheetState extends State<DirectMessageSheet> {
child: Column( child: Column(
children: [ children: [
Text( Text(
'Direct Message', AppLocalizations.of(context)!.directMessage,
style: TextStyle( style: TextStyle(
color: colorScheme.onSurface, color: colorScheme.onSurface,
fontSize: 18, fontSize: 18,
@@ -173,7 +174,7 @@ class _DirectMessageSheetState extends State<DirectMessageSheet> {
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Text( child: Text(
'This message will be sent directly to ${widget.contact.displayName}. It will also appear in the main messages feed.', AppLocalizations.of(context)!.directMessageInfo(widget.contact.displayName),
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer, color: Theme.of(context).colorScheme.onPrimaryContainer,
fontSize: 13, fontSize: 13,
@@ -210,7 +211,7 @@ class _DirectMessageSheetState extends State<DirectMessageSheet> {
maxLengthEnforcement: MaxLengthEnforcement.enforced, maxLengthEnforcement: MaxLengthEnforcement.enforced,
style: TextStyle(color: colorScheme.onSurface), style: TextStyle(color: colorScheme.onSurface),
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Type your message...', hintText: AppLocalizations.of(context)!.typeYourMessage,
hintStyle: TextStyle(color: colorScheme.onSurfaceVariant), hintStyle: TextStyle(color: colorScheme.onSurfaceVariant),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@@ -246,7 +247,7 @@ class _DirectMessageSheetState extends State<DirectMessageSheet> {
? null ? null
: _sendDirectMessage, : _sendDirectMessage,
icon: const Icon(Icons.send), icon: const Icon(Icons.send),
label: const Text('Send Direct Message'), label: Text(AppLocalizations.of(context)!.sendDirectMessage),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14), padding: const EdgeInsets.symmetric(vertical: 14),
backgroundColor: colorScheme.primary, backgroundColor: colorScheme.primary,

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../../l10n/app_localizations.dart';
import '../../models/contact.dart'; import '../../models/contact.dart';
import '../../providers/connection_provider.dart'; import '../../providers/connection_provider.dart';
import '../../providers/contacts_provider.dart'; import '../../providers/contacts_provider.dart';
@@ -62,7 +63,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: const Text('Please enter a password'), content: Text(AppLocalizations.of(context)!.pleaseEnterPassword),
backgroundColor: Theme.of(context).colorScheme.error, backgroundColor: Theme.of(context).colorScheme.error,
), ),
); );
@@ -73,7 +74,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: const Text('Not connected to device'), content: Text(AppLocalizations.of(context)!.deviceNotConnected),
backgroundColor: Theme.of(context).colorScheme.error, backgroundColor: Theme.of(context).colorScheme.error,
), ),
); );
@@ -183,7 +184,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Failed to sync contacts: $e'), content: Text(AppLocalizations.of(context)!.failedToSyncContacts(e.toString())),
backgroundColor: Theme.of(context).colorScheme.error, backgroundColor: Theme.of(context).colorScheme.error,
), ),
); );
@@ -215,7 +216,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: const Text('Logged in successfully! Waiting for room messages...'), content: Text(AppLocalizations.of(context)!.loggedInSuccessfully),
backgroundColor: Theme.of(context).colorScheme.primary, backgroundColor: Theme.of(context).colorScheme.primary,
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),
), ),
@@ -233,7 +234,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: const Text('Login failed - incorrect password'), content: Text(AppLocalizations.of(context)!.loginFailed),
backgroundColor: Theme.of(context).colorScheme.error, backgroundColor: Theme.of(context).colorScheme.error,
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),
), ),
@@ -255,7 +256,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Logging in to ${widget.contact.displayName}...'), content: Text(AppLocalizations.of(context)!.loggingIn(widget.contact.displayName)),
backgroundColor: Theme.of(context).colorScheme.primary, backgroundColor: Theme.of(context).colorScheme.primary,
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
), ),
@@ -268,7 +269,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Failed to send login: $e'), content: Text(AppLocalizations.of(context)!.failedToSendLogin(e.toString())),
backgroundColor: Theme.of(context).colorScheme.error, backgroundColor: Theme.of(context).colorScheme.error,
), ),
); );

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import '../../models/map_drawing.dart'; import '../../models/map_drawing.dart';
import '../../l10n/app_localizations.dart';
/// Widget that renders map drawings as polylines /// Widget that renders map drawings as polylines
class DrawingLayer extends StatelessWidget { class DrawingLayer extends StatelessWidget {
@@ -212,14 +213,14 @@ class DrawingMarkersLayer extends StatelessWidget {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Delete Drawing'), title: Text(AppLocalizations.of(context)!.deleteDrawing),
content: Text( content: Text(
'Delete this ${drawing.type.name}?', 'Delete this ${drawing.type.name}?',
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('Cancel'), child: Text(AppLocalizations.of(context)!.cancel),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
@@ -229,7 +230,7 @@ class DrawingMarkersLayer extends StatelessWidget {
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: Colors.red, foregroundColor: Colors.red,
), ),
child: const Text('Delete'), child: Text(AppLocalizations.of(context)!.delete),
), ),
], ],
), ),

View File

@@ -7,6 +7,7 @@ import '../../providers/contacts_provider.dart';
import '../../providers/messages_provider.dart'; import '../../providers/messages_provider.dart';
import '../../models/map_drawing.dart'; import '../../models/map_drawing.dart';
import '../../models/contact.dart'; import '../../models/contact.dart';
import '../../l10n/app_localizations.dart';
/// Toolbar for drawing controls on the map /// Toolbar for drawing controls on the map
class DrawingToolbar extends StatelessWidget { class DrawingToolbar extends StatelessWidget {
@@ -53,7 +54,7 @@ class DrawingToolbar extends StatelessWidget {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
_getTitleForMode(drawingProvider.drawingMode), _getTitleForMode(drawingProvider.drawingMode, context),
style: Theme.of(context).textTheme.titleSmall?.copyWith( style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -119,7 +120,7 @@ class DrawingToolbar extends StatelessWidget {
IconButton( IconButton(
icon: const Icon(Icons.undo), icon: const Icon(Icons.undo),
onPressed: () => drawingProvider.cancelCurrentDrawing(), onPressed: () => drawingProvider.cancelCurrentDrawing(),
tooltip: 'Cancel', tooltip: AppLocalizations.of(context)!.cancel,
iconSize: 20, iconSize: 20,
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
@@ -184,8 +185,8 @@ class DrawingToolbar extends StatelessWidget {
const Divider(), const Divider(),
ListTile( ListTile(
leading: const Icon(Icons.show_chart), leading: const Icon(Icons.show_chart),
title: const Text('Draw Line'), title: Text(AppLocalizations.of(context)!.drawLine),
subtitle: const Text('Draw a freehand line on the map'), subtitle: Text(AppLocalizations.of(context)!.drawLineDesc),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
drawingProvider.setDrawingMode(DrawingMode.line); drawingProvider.setDrawingMode(DrawingMode.line);
@@ -193,8 +194,8 @@ class DrawingToolbar extends StatelessWidget {
), ),
ListTile( ListTile(
leading: const Icon(Icons.crop_square), leading: const Icon(Icons.crop_square),
title: const Text('Draw Rectangle'), title: Text(AppLocalizations.of(context)!.drawRectangle),
subtitle: const Text('Draw a rectangular area on the map'), subtitle: Text(AppLocalizations.of(context)!.drawRectangleDesc),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
drawingProvider.setDrawingMode(DrawingMode.rectangle); drawingProvider.setDrawingMode(DrawingMode.rectangle);
@@ -204,7 +205,7 @@ class DrawingToolbar extends StatelessWidget {
const Divider(), const Divider(),
ListTile( ListTile(
leading: const Icon(Icons.share, color: Colors.blue), leading: const Icon(Icons.share, color: Colors.blue),
title: const Text('Share Drawings'), title: Text(AppLocalizations.of(context)!.shareDrawings),
subtitle: Text( subtitle: Text(
'Broadcast ${drawingProvider.drawings.length} drawings to team', 'Broadcast ${drawingProvider.drawings.length} drawings to team',
), ),
@@ -220,7 +221,7 @@ class DrawingToolbar extends StatelessWidget {
), ),
ListTile( ListTile(
leading: const Icon(Icons.delete_sweep, color: Colors.red), leading: const Icon(Icons.delete_sweep, color: Colors.red),
title: const Text('Clear All Drawings'), title: Text(AppLocalizations.of(context)!.clearAllDrawings),
subtitle: Text( subtitle: Text(
'Remove all ${drawingProvider.drawings.length} drawings', 'Remove all ${drawingProvider.drawings.length} drawings',
), ),
@@ -244,14 +245,14 @@ class DrawingToolbar extends StatelessWidget {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Clear All Drawings'), title: Text(AppLocalizations.of(context)!.clearAllDrawings),
content: Text( content: Text(
'Delete all ${drawingProvider.drawings.length} drawings from the map?', 'Delete all ${drawingProvider.drawings.length} drawings from the map?',
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('Cancel'), child: Text(AppLocalizations.of(context)!.cancel),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
@@ -259,7 +260,7 @@ class DrawingToolbar extends StatelessWidget {
drawingProvider.clearAllDrawings(); drawingProvider.clearAllDrawings();
}, },
style: TextButton.styleFrom(foregroundColor: Colors.red), style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Clear All'), child: Text(AppLocalizations.of(context)!.clearAll),
), ),
], ],
), ),
@@ -279,12 +280,12 @@ class DrawingToolbar extends StatelessWidget {
} }
/// Get title for drawing mode /// Get title for drawing mode
String _getTitleForMode(DrawingMode mode) { String _getTitleForMode(DrawingMode mode, BuildContext context) {
switch (mode) { switch (mode) {
case DrawingMode.line: case DrawingMode.line:
return 'Draw Line'; return AppLocalizations.of(context)!.drawLine;
case DrawingMode.rectangle: case DrawingMode.rectangle:
return 'Draw Rectangle'; return AppLocalizations.of(context)!.drawRectangle;
case DrawingMode.none: case DrawingMode.none:
return 'Drawing'; return 'Drawing';
} }
@@ -321,8 +322,8 @@ class DrawingToolbar extends StatelessWidget {
if (!connectionProvider.deviceInfo.isConnected) { if (!connectionProvider.deviceInfo.isConnected) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Not connected to device'), content: Text(AppLocalizations.of(context)!.deviceNotConnected),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
@@ -342,8 +343,8 @@ class DrawingToolbar extends StatelessWidget {
if (localDrawings.isEmpty) { if (localDrawings.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('No local drawings to share'), content: Text(AppLocalizations.of(context)!.noLocalDrawings),
backgroundColor: Colors.orange, backgroundColor: Colors.orange,
), ),
); );
@@ -384,8 +385,8 @@ class DrawingToolbar extends StatelessWidget {
// Option: Send to Public Channel // Option: Send to Public Channel
ListTile( ListTile(
leading: const Icon(Icons.public, color: Colors.blue), leading: const Icon(Icons.public, color: Colors.blue),
title: const Text('Public Channel'), title: Text(AppLocalizations.of(context)!.publicChannel),
subtitle: const Text('Broadcast to all nearby nodes (ephemeral)'), subtitle: Text(AppLocalizations.of(context)!.broadcastToAll),
onTap: () async { onTap: () async {
debugPrint('📤 [DrawingToolbar] Public Channel tapped'); debugPrint('📤 [DrawingToolbar] Public Channel tapped');
// Share BEFORE popping the navigator // Share BEFORE popping the navigator
@@ -406,7 +407,7 @@ class DrawingToolbar extends StatelessWidget {
(room) => ListTile( (room) => ListTile(
leading: const Icon(Icons.meeting_room, color: Colors.green), leading: const Icon(Icons.meeting_room, color: Colors.green),
title: Text(room.advName), title: Text(room.advName),
subtitle: const Text('Stored permanently in room'), subtitle: Text(AppLocalizations.of(context)!.storedPermanently),
onTap: () async { onTap: () async {
debugPrint('📤 [DrawingToolbar] Room ${room.advName} tapped'); debugPrint('📤 [DrawingToolbar] Room ${room.advName} tapped');
// Share BEFORE popping the navigator // Share BEFORE popping the navigator

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import '../models/sar_marker.dart'; import '../models/sar_marker.dart';
import '../l10n/app_localizations.dart';
class MapMarkers { class MapMarkers {
static List<Marker> createTeamMemberMarkers( static List<Marker> createTeamMemberMarkers(
@@ -238,7 +239,7 @@ class MapMarkers {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('Close'), child: Text(AppLocalizations.of(context)!.close),
), ),
], ],
), ),
@@ -272,7 +273,7 @@ class MapMarkers {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('Close'), child: Text(AppLocalizations.of(context)!.close),
), ),
], ],
), ),

View File

@@ -6,6 +6,7 @@ import '../../providers/contacts_provider.dart';
import '../../models/contact.dart'; import '../../models/contact.dart';
import '../../models/sar_marker.dart'; import '../../models/sar_marker.dart';
import '../../services/validation_service.dart'; import '../../services/validation_service.dart';
import '../../l10n/app_localizations.dart';
/// SAR Update Sheet - Modal bottom sheet for creating and sending SAR markers /// SAR Update Sheet - Modal bottom sheet for creating and sending SAR markers
/// This widget is public so it can be used from both messages_tab.dart and map_tab.dart /// This widget is public so it can be used from both messages_tab.dart and map_tab.dart
@@ -167,7 +168,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
child: Column( child: Column(
children: [ children: [
Text( Text(
'Send SAR Marker', AppLocalizations.of(context)!.sendSarMarker,
style: TextStyle( style: TextStyle(
color: colorScheme.onSurface, color: colorScheme.onSurface,
fontSize: 18, fontSize: 18,
@@ -645,7 +646,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
final shouldContinue = await showDialog<bool>( final shouldContinue = await showDialog<bool>(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Low Location Accuracy'), title: Text(AppLocalizations.of(context)!.lowLocationAccuracy),
content: Text( content: Text(
'Location accuracy is ±${_currentPosition!.accuracy!.round()}m. ' 'Location accuracy is ±${_currentPosition!.accuracy!.round()}m. '
'This may not be accurate enough for SAR operations.\n\n' 'This may not be accurate enough for SAR operations.\n\n'
@@ -654,11 +655,11 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context, false), onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'), child: Text(AppLocalizations.of(context)!.cancel),
), ),
TextButton( TextButton(
onPressed: () => Navigator.pop(context, true), onPressed: () => Navigator.pop(context, true),
child: const Text('Continue'), child: Text(AppLocalizations.of(context)!.continue_),
), ),
], ],
), ),
@@ -688,9 +689,9 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
), ),
), ),
icon: const Icon(Icons.send, size: 20), icon: const Icon(Icons.send, size: 20),
label: const Text( label: Text(
'Send SAR Marker', AppLocalizations.of(context)!.sendSarMarker,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
), ),
), ),
), ),