fix: Update import path for AppLocalizations in CompassSarList widget

This commit is contained in:
Janez T
2025-10-16 16:07:22 +02:00
parent 01a1bbec44
commit f95edcb477
21 changed files with 2055 additions and 91 deletions

View File

@@ -32,6 +32,11 @@ AI assistant guide for the MeshCore SAR Flutter application.
```
lib/
├── l10n/ # Internationalization (i18n)
│ ├── app_localizations.dart # Generated localization class
│ ├── app_en.arb # English (default)
│ ├── app_hr.arb # Croatian (Hrvatski)
│ └── app_sl.arb # Slovenian (Slovenščina)
├── models/ # Data models
│ ├── contact.dart, message.dart, sar_marker.dart
│ ├── map_drawing.dart # MapDrawing, LineDrawing, RectangleDrawing
@@ -55,8 +60,9 @@ lib/
│ ├── map_markers.dart # Map marker rendering
│ ├── map/ # Map-specific widgets
│ │ ├── drawing_layer.dart # Drawing rendering on map
│ │ ── drawing_toolbar.dart # Drawing UI controls
├── messages/, contacts/ # Feature-specific widgets
│ │ ── drawing_toolbar.dart # Drawing UI controls
│ └── compass/ # Compass dialog components (fully localized)
│ ├── messages/, contacts/ # Feature-specific widgets (fully localized)
└── utils/ # Utilities
├── sar_message_parser.dart # SAR marker parsing
└── drawing_message_parser.dart # Drawing message parsing
@@ -418,12 +424,78 @@ Ultra-compact location display, tap to toggle DD/DMS formats, no close button (t
- Provider: `lib/providers/drawing_provider.dart` (280 lines)
- UI: `lib/widgets/map/drawing_toolbar.dart`, `lib/widgets/map/drawing_layer.dart`
## Internationalization (i18n)
### Supported Languages
- **English (en)**: Default language
- **Croatian (hr)**: Hrvatski - Full localization
- **Slovenian (sl)**: Slovenščina - Full localization
### Localization Files
- **ARB files**: `lib/l10n/app_{locale}.arb` (Application Resource Bundle)
- **Generated class**: `lib/l10n/app_localizations.dart` (auto-generated, do not edit)
- **Configuration**: `l10n.yaml` in project root
### Adding Localized Strings
1. **Add to English ARB** (`lib/l10n/app_en.arb`):
```json
{
"myNewString": "My new text",
"@myNewString": {
"description": "Description of what this string is for"
},
"stringWithParam": "Hello {name}",
"@stringWithParam": {
"description": "Greeting with name parameter",
"placeholders": {
"name": {"type": "String"}
}
}
}
```
2. **Add translations** to `app_hr.arb` and `app_sl.arb`
3. **Generate localization files**:
```bash
flutter gen-l10n
```
4. **Use in code**:
```dart
import '../l10n/app_localizations.dart';
// In build method:
Text(AppLocalizations.of(context)!.myNewString)
Text(AppLocalizations.of(context)!.stringWithParam('John'))
```
### Important Notes
- **Import path**: Always use `import '../l10n/app_localizations.dart'` (relative path from widget location)
- **Do NOT use**: `import 'package:flutter_gen/gen_l10n/app_localizations.dart'` (incorrect)
- **Generate after changes**: Run `flutter gen-l10n` after modifying ARB files
- **Null safety**: Use `AppLocalizations.of(context)!` (with null assertion operator)
### Localized Components
All major UI components are fully localized:
- Home screen and status indicators
- Settings screen and preferences
- Messages tab and SAR markers
- Contacts tab and contact details
- Map screen and compass dialog
- Drawing tools and filters
- All dialogs and confirmation messages
## Build Commands
```bash
# Dependencies
flutter pub get
# Localization
flutter gen-l10n # Generate localization files after ARB changes
# Development
flutter run # Debug mode
flutter run -d <device> # Specific device

View File

@@ -1168,5 +1168,535 @@
"type": "String"
}
}
},
"noContactsYet": "No contacts yet",
"@noContactsYet": {
"description": "Empty state message when there are no contacts"
},
"connectToDeviceToLoadContacts": "Connect to a device to load contacts",
"@connectToDeviceToLoadContacts": {
"description": "Instruction to connect device to load contacts"
},
"teamMembers": "Team Members",
"@teamMembers": {
"description": "Section header for team members (chat contacts)"
},
"repeaters": "Repeaters",
"@repeaters": {
"description": "Section header for repeater nodes"
},
"rooms": "Rooms",
"@rooms": {
"description": "Section header for rooms/channels"
},
"cacheStatistics": "Cache Statistics",
"@cacheStatistics": {
"description": "Title for cache statistics section"
},
"totalTiles": "Total Tiles",
"@totalTiles": {
"description": "Label for total number of cached tiles"
},
"cacheSize": "Cache Size",
"@cacheSize": {
"description": "Label for cache size in MB"
},
"storeName": "Store Name",
"@storeName": {
"description": "Label for cache store name"
},
"noCacheStatistics": "No cache statistics available",
"@noCacheStatistics": {
"description": "Message when cache statistics are unavailable"
},
"downloadRegion": "Download Region",
"@downloadRegion": {
"description": "Title for download region section"
},
"mapLayer": "Map Layer",
"@mapLayer": {
"description": "Label for map layer selection"
},
"regionBounds": "Region Bounds",
"@regionBounds": {
"description": "Title for region bounds input section"
},
"north": "North",
"@north": {
"description": "Label for north coordinate"
},
"south": "South",
"@south": {
"description": "Label for south coordinate"
},
"east": "East",
"@east": {
"description": "Label for east coordinate"
},
"west": "West",
"@west": {
"description": "Label for west coordinate"
},
"zoomLevels": "Zoom Levels",
"@zoomLevels": {
"description": "Title for zoom levels section"
},
"minZoom": "Min: {zoom}",
"@minZoom": {
"description": "Label for minimum zoom level",
"placeholders": {
"zoom": {
"type": "int"
}
}
},
"maxZoom": "Max: {zoom}",
"@maxZoom": {
"description": "Label for maximum zoom level",
"placeholders": {
"zoom": {
"type": "int"
}
}
},
"downloadingDots": "Downloading...",
"@downloadingDots": {
"description": "Status message during download"
},
"cancelDownload": "Cancel Download",
"@cancelDownload": {
"description": "Button to cancel download"
},
"downloadRegionButton": "Download Region",
"@downloadRegionButton": {
"description": "Button to start region download"
},
"downloadNote": "Note: Large regions or high zoom levels may take significant time and storage.",
"@downloadNote": {
"description": "Warning about download size and time"
},
"cacheManagement": "Cache Management",
"@cacheManagement": {
"description": "Title for cache management section"
},
"clearAllMaps": "Clear All Maps",
"@clearAllMaps": {
"description": "Button to clear all cached maps"
},
"clearMapsConfirmTitle": "Clear All Maps",
"@clearMapsConfirmTitle": {
"description": "Title for clear maps confirmation dialog"
},
"clearMapsConfirmMessage": "Are you sure you want to delete all downloaded maps? This action cannot be undone.",
"@clearMapsConfirmMessage": {
"description": "Confirmation message for clearing maps"
},
"mapDownloadCompleted": "Map download completed!",
"@mapDownloadCompleted": {
"description": "Success message after map download"
},
"cacheClearedSuccessfully": "Cache cleared successfully!",
"@cacheClearedSuccessfully": {
"description": "Success message after clearing cache"
},
"downloadCancelled": "Download cancelled",
"@downloadCancelled": {
"description": "Message when download is cancelled"
},
"startingDownload": "Starting download...",
"@startingDownload": {
"description": "Initial status when download begins"
},
"downloadingMapTiles": "Downloading map tiles...",
"@downloadingMapTiles": {
"description": "Status during tile download"
},
"downloadCompletedSuccessfully": "Download completed successfully!",
"@downloadCompletedSuccessfully": {
"description": "Status after successful download"
},
"cancellingDownload": "Cancelling download...",
"@cancellingDownload": {
"description": "Status while cancelling download"
},
"errorLoadingStats": "Error loading stats: {error}",
"@errorLoadingStats": {
"description": "Error message when loading cache stats fails",
"placeholders": {
"error": {
"type": "String"
}
}
},
"downloadFailed": "Download failed: {error}",
"@downloadFailed": {
"description": "Error message when download fails",
"placeholders": {
"error": {
"type": "String"
}
}
},
"cancelFailed": "Cancel failed: {error}",
"@cancelFailed": {
"description": "Error message when cancel fails",
"placeholders": {
"error": {
"type": "String"
}
}
},
"clearCacheFailed": "Clear cache failed: {error}",
"@clearCacheFailed": {
"description": "Error message when clearing cache fails",
"placeholders": {
"error": {
"type": "String"
}
}
},
"minZoomError": "Min zoom: {error}",
"@minZoomError": {
"description": "Validation error for minimum zoom",
"placeholders": {
"error": {
"type": "String"
}
}
},
"maxZoomError": "Max zoom: {error}",
"@maxZoomError": {
"description": "Validation error for maximum zoom",
"placeholders": {
"error": {
"type": "String"
}
}
},
"minZoomGreaterThanMax": "Minimum zoom must be less than or equal to maximum zoom",
"@minZoomGreaterThanMax": {
"description": "Validation error when min zoom > max zoom"
},
"selectMapLayer": "Select Map Layer",
"@selectMapLayer": {
"description": "Title for map layer selection dialog"
},
"mapOptions": "Map Options",
"@mapOptions": {
"description": "Title for map options dialog"
},
"showLegend": "Show Legend",
"@showLegend": {
"description": "Toggle for showing map legend"
},
"displayMarkerTypeCounts": "Display marker type counts",
"@displayMarkerTypeCounts": {
"description": "Description for show legend toggle"
},
"rotateMapWithHeading": "Rotate Map with Heading",
"@rotateMapWithHeading": {
"description": "Toggle for rotating map with compass heading"
},
"mapFollowsDirection": "Map follows your direction when moving",
"@mapFollowsDirection": {
"description": "Description for rotate map toggle"
},
"showMapDebugInfo": "Show Map Debug Info",
"@showMapDebugInfo": {
"description": "Toggle for showing map debug information"
},
"displayZoomLevelBounds": "Display zoom level and bounds",
"@displayZoomLevelBounds": {
"description": "Description for debug info toggle"
},
"fullscreenMode": "Fullscreen Mode",
"@fullscreenMode": {
"description": "Toggle for fullscreen map mode"
},
"hideUiFullMapView": "Hide all UI controls for full map view",
"@hideUiFullMapView": {
"description": "Description for fullscreen mode toggle"
},
"openStreetMap": "OpenStreetMap",
"@openStreetMap": {
"description": "OpenStreetMap layer name"
},
"openTopoMap": "OpenTopoMap",
"@openTopoMap": {
"description": "OpenTopoMap layer name"
},
"esriSatellite": "ESRI Satellite",
"@esriSatellite": {
"description": "ESRI Satellite imagery layer name"
},
"downloadVisibleArea": "Download visible area",
"@downloadVisibleArea": {
"description": "Tooltip for download visible area button"
},
"initializingMap": "Initializing map...",
"@initializingMap": {
"description": "Loading message for map initialization"
},
"dragToPosition": "Drag to Position",
"@dragToPosition": {
"description": "Label when dragging a pin on map"
},
"createSarMarker": "Create SAR Marker",
"@createSarMarker": {
"description": "Label for creating SAR marker from pin"
},
"compass": "Compass",
"@compass": {
"description": "Compass title in detailed compass dialog"
},
"navigationAndContacts": "Navigation & Contacts",
"@navigationAndContacts": {
"description": "Subtitle for compass dialog"
},
"sarAlert": "SAR ALERT",
"@sarAlert": {
"description": "Label for SAR alert badge on messages"
},
"messageSentToPublicChannel": "Message sent to public channel",
"@messageSentToPublicChannel": {
"description": "Success message when message is sent to public channel"
},
"pleaseSelectRoomToSendSar": "Please select a room to send SAR marker",
"@pleaseSelectRoomToSendSar": {
"description": "Error when no room is selected for SAR marker"
},
"failedToSendSarMarker": "Failed to send SAR marker: {error}",
"@failedToSendSarMarker": {
"description": "Error message when SAR marker fails to send",
"placeholders": {
"error": {
"type": "String"
}
}
},
"sarMarkerSentTo": "SAR marker sent to {roomName}",
"@sarMarkerSentTo": {
"description": "Success message when SAR marker is sent to room",
"placeholders": {
"roomName": {
"type": "String"
}
}
},
"notConnectedCannotSync": "Not connected - cannot sync messages",
"@notConnectedCannotSync": {
"description": "Warning when trying to sync messages while not connected"
},
"syncedMessageCount": "Synced {count} message(s)",
"@syncedMessageCount": {
"description": "Success message showing number of synced messages",
"placeholders": {
"count": {
"type": "int"
}
}
},
"noNewMessages": "No new messages",
"@noNewMessages": {
"description": "Info message when no new messages to sync"
},
"syncFailed": "Sync failed: {error}",
"@syncFailed": {
"description": "Error message when sync fails",
"placeholders": {
"error": {
"type": "String"
}
}
},
"failedToResendMessage": "Failed to resend message",
"@failedToResendMessage": {
"description": "Error when message retry fails"
},
"retryingMessage": "Retrying message...",
"@retryingMessage": {
"description": "Info message when retrying a failed message"
},
"retryFailed": "Retry failed: {error}",
"@retryFailed": {
"description": "Error message when retry fails",
"placeholders": {
"error": {
"type": "String"
}
}
},
"textCopiedToClipboard": "Text copied to clipboard",
"@textCopiedToClipboard": {
"description": "Success message when text is copied"
},
"cannotReplySenderMissing": "Cannot reply: sender information missing",
"@cannotReplySenderMissing": {
"description": "Error when sender info is missing for reply"
},
"cannotReplyContactNotFound": "Cannot reply: contact not found",
"@cannotReplyContactNotFound": {
"description": "Error when contact not found for reply"
},
"messageDeleted": "Message deleted",
"@messageDeleted": {
"description": "Info message when message is deleted"
},
"refreshedContacts": "Refreshed contacts",
"@refreshedContacts": {
"description": "Success message when contacts are refreshed"
},
"justNow": "Just now",
"@justNow": {
"description": "Time indicator for very recent activity"
},
"minutesAgo": "{minutes}m ago",
"@minutesAgo": {
"description": "Time indicator for minutes ago",
"placeholders": {
"minutes": {
"type": "int"
}
}
},
"hoursAgo": "{hours}h ago",
"@hoursAgo": {
"description": "Time indicator for hours ago",
"placeholders": {
"hours": {
"type": "int"
}
}
},
"daysAgo": "{days}d ago",
"@daysAgo": {
"description": "Time indicator for days ago",
"placeholders": {
"days": {
"type": "int"
}
}
},
"secondsAgo": "{seconds}s ago",
"@secondsAgo": {
"description": "Time indicator for seconds ago",
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"sending": "Sending...",
"@sending": {
"description": "Delivery status: sending"
},
"sent": "Sent",
"@sent": {
"description": "Delivery status: sent"
},
"delivered": "Delivered",
"@delivered": {
"description": "Delivery status: delivered"
},
"deliveredWithTime": "Delivered ({time}ms)",
"@deliveredWithTime": {
"description": "Delivery status with round-trip time",
"placeholders": {
"time": {
"type": "int"
}
}
},
"failed": "Failed",
"@failed": {
"description": "Delivery status: failed"
}
}

View File

@@ -391,5 +391,149 @@
"degreesLeft": "{degrees}° lijevo",
"latLonFormat": "Ššir: {latitude} Dužina: {longitude}"
"latLonFormat": "Ššir: {latitude} Dužina: {longitude}",
"noContactsYet": "Još nema kontakata",
"connectToDeviceToLoadContacts": "Povežite se s uređajem da učitate kontakte",
"teamMembers": "Članovi tima",
"repeaters": "Repetitori",
"rooms": "Sobe",
"cacheStatistics": "Statistika predmemorije",
"totalTiles": "Ukupno pločica",
"cacheSize": "Veličina predmemorije",
"storeName": "Ime pohrane",
"noCacheStatistics": "Statistika predmemorije nije dostupna",
"downloadRegion": "Preuzmi regiju",
"mapLayer": "Sloj karte",
"regionBounds": "Granice regije",
"north": "Sjever",
"south": "Jug",
"east": "Istok",
"west": "Zapad",
"zoomLevels": "Razine zumiranja",
"minZoom": "Min: {zoom}",
"maxZoom": "Maks: {zoom}",
"downloadingDots": "Preuzimanje...",
"cancelDownload": "Otkaži preuzimanje",
"downloadRegionButton": "Preuzmi regiju",
"downloadNote": "Napomena: Velike regije ili visoke razine zumiranja mogu zahtijevati značajno vrijeme i prostor za pohranu.",
"cacheManagement": "Upravljanje predmemorijom",
"clearAllMaps": "Očisti sve karte",
"clearMapsConfirmTitle": "Očisti sve karte",
"clearMapsConfirmMessage": "Jeste li sigurni da želite izbrisati sve preuzete karte? Ova radnja se ne može poništiti.",
"mapDownloadCompleted": "Preuzimanje karte završeno!",
"cacheClearedSuccessfully": "Predmemorija uspješno očišćena!",
"downloadCancelled": "Preuzimanje otkazano",
"startingDownload": "Pokretanje preuzimanja...",
"downloadingMapTiles": "Preuzimanje pločica karte...",
"downloadCompletedSuccessfully": "Preuzimanje uspješno završeno!",
"cancellingDownload": "Otkazivanje preuzimanja...",
"errorLoadingStats": "Greška pri učitavanju statistike: {error}",
"downloadFailed": "Preuzimanje nije uspjelo: {error}",
"cancelFailed": "Otkazivanje nije uspjelo: {error}",
"clearCacheFailed": "Čišćenje predmemorije nije uspjelo: {error}",
"minZoomError": "Min zumiranje: {error}",
"maxZoomError": "Maks zumiranje: {error}",
"minZoomGreaterThanMax": "Minimalno zumiranje mora biti manje ili jednako maksimalnom zumiranju",
"selectMapLayer": "Odaberite sloj karte",
"mapOptions": "Opcije karte",
"showLegend": "Prikaži legendu",
"displayMarkerTypeCounts": "Prikaži broj vrsta markera",
"rotateMapWithHeading": "Rotiraj kartu sa smjerom",
"mapFollowsDirection": "Karta slijedi vaš smjer pri kretanju",
"showMapDebugInfo": "Prikaži debug informacije karte",
"displayZoomLevelBounds": "Prikaži razinu zumiranja i granice",
"fullscreenMode": "Način cijelog zaslona",
"hideUiFullMapView": "Sakrij sve UI kontrole za prikaz cijele karte",
"openStreetMap": "OpenStreetMap",
"openTopoMap": "OpenTopoMap",
"esriSatellite": "ESRI satelit",
"downloadVisibleArea": "Preuzmi vidljivo područje",
"initializingMap": "Inicijalizacija karte...",
"dragToPosition": "Povuci na poziciju",
"createSarMarker": "Kreiraj SAR marker",
"compass": "Kompas",
"navigationAndContacts": "Navigacija i kontakti",
"sarAlert": "SAR UZBUNA",
"justNow": "Upravo sada",
"minutesAgo": "prije {minutes}m",
"hoursAgo": "prije {hours}h",
"daysAgo": "prije {days}d",
"secondsAgo": "prije {seconds}s",
"sending": "Slanje...",
"sent": "Poslano",
"delivered": "Dostavljeno",
"deliveredWithTime": "Dostavljeno ({time}ms)",
"failed": "Neuspjelo"
}

View File

@@ -1280,6 +1280,378 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Lat: {latitude} Lon: {longitude}'**
String latLonFormat(String latitude, String longitude);
/// Empty state message when there are no contacts
///
/// In en, this message translates to:
/// **'No contacts yet'**
String get noContactsYet;
/// Instruction to connect device to load contacts
///
/// In en, this message translates to:
/// **'Connect to a device to load contacts'**
String get connectToDeviceToLoadContacts;
/// Section header for team members (chat contacts)
///
/// In en, this message translates to:
/// **'Team Members'**
String get teamMembers;
/// Section header for repeater nodes
///
/// In en, this message translates to:
/// **'Repeaters'**
String get repeaters;
/// Section header for rooms/channels
///
/// In en, this message translates to:
/// **'Rooms'**
String get rooms;
/// Title for cache statistics section
///
/// In en, this message translates to:
/// **'Cache Statistics'**
String get cacheStatistics;
/// Label for total number of cached tiles
///
/// In en, this message translates to:
/// **'Total Tiles'**
String get totalTiles;
/// Label for cache size in MB
///
/// In en, this message translates to:
/// **'Cache Size'**
String get cacheSize;
/// Label for cache store name
///
/// In en, this message translates to:
/// **'Store Name'**
String get storeName;
/// Message when cache statistics are unavailable
///
/// In en, this message translates to:
/// **'No cache statistics available'**
String get noCacheStatistics;
/// Title for download region section
///
/// In en, this message translates to:
/// **'Download Region'**
String get downloadRegion;
/// Label for map layer selection
///
/// In en, this message translates to:
/// **'Map Layer'**
String get mapLayer;
/// Title for region bounds input section
///
/// In en, this message translates to:
/// **'Region Bounds'**
String get regionBounds;
/// Label for north coordinate
///
/// In en, this message translates to:
/// **'North'**
String get north;
/// Label for south coordinate
///
/// In en, this message translates to:
/// **'South'**
String get south;
/// Label for east coordinate
///
/// In en, this message translates to:
/// **'East'**
String get east;
/// Label for west coordinate
///
/// In en, this message translates to:
/// **'West'**
String get west;
/// Title for zoom levels section
///
/// In en, this message translates to:
/// **'Zoom Levels'**
String get zoomLevels;
/// Label for minimum zoom level
///
/// In en, this message translates to:
/// **'Min: {zoom}'**
String minZoom(int zoom);
/// Label for maximum zoom level
///
/// In en, this message translates to:
/// **'Max: {zoom}'**
String maxZoom(int zoom);
/// Status message during download
///
/// In en, this message translates to:
/// **'Downloading...'**
String get downloadingDots;
/// Button to cancel download
///
/// In en, this message translates to:
/// **'Cancel Download'**
String get cancelDownload;
/// Button to start region download
///
/// In en, this message translates to:
/// **'Download Region'**
String get downloadRegionButton;
/// Warning about download size and time
///
/// In en, this message translates to:
/// **'Note: Large regions or high zoom levels may take significant time and storage.'**
String get downloadNote;
/// Title for cache management section
///
/// In en, this message translates to:
/// **'Cache Management'**
String get cacheManagement;
/// Button to clear all cached maps
///
/// In en, this message translates to:
/// **'Clear All Maps'**
String get clearAllMaps;
/// Title for clear maps confirmation dialog
///
/// In en, this message translates to:
/// **'Clear All Maps'**
String get clearMapsConfirmTitle;
/// Confirmation message for clearing maps
///
/// In en, this message translates to:
/// **'Are you sure you want to delete all downloaded maps? This action cannot be undone.'**
String get clearMapsConfirmMessage;
/// Success message after map download
///
/// In en, this message translates to:
/// **'Map download completed!'**
String get mapDownloadCompleted;
/// Success message after clearing cache
///
/// In en, this message translates to:
/// **'Cache cleared successfully!'**
String get cacheClearedSuccessfully;
/// Message when download is cancelled
///
/// In en, this message translates to:
/// **'Download cancelled'**
String get downloadCancelled;
/// Initial status when download begins
///
/// In en, this message translates to:
/// **'Starting download...'**
String get startingDownload;
/// Status during tile download
///
/// In en, this message translates to:
/// **'Downloading map tiles...'**
String get downloadingMapTiles;
/// Status after successful download
///
/// In en, this message translates to:
/// **'Download completed successfully!'**
String get downloadCompletedSuccessfully;
/// Status while cancelling download
///
/// In en, this message translates to:
/// **'Cancelling download...'**
String get cancellingDownload;
/// Error message when loading cache stats fails
///
/// In en, this message translates to:
/// **'Error loading stats: {error}'**
String errorLoadingStats(String error);
/// Error message when download fails
///
/// In en, this message translates to:
/// **'Download failed: {error}'**
String downloadFailed(String error);
/// Error message when cancel fails
///
/// In en, this message translates to:
/// **'Cancel failed: {error}'**
String cancelFailed(String error);
/// Error message when clearing cache fails
///
/// In en, this message translates to:
/// **'Clear cache failed: {error}'**
String clearCacheFailed(String error);
/// Validation error for minimum zoom
///
/// In en, this message translates to:
/// **'Min zoom: {error}'**
String minZoomError(String error);
/// Validation error for maximum zoom
///
/// In en, this message translates to:
/// **'Max zoom: {error}'**
String maxZoomError(String error);
/// Validation error when min zoom > max zoom
///
/// In en, this message translates to:
/// **'Minimum zoom must be less than or equal to maximum zoom'**
String get minZoomGreaterThanMax;
/// Title for map layer selection dialog
///
/// In en, this message translates to:
/// **'Select Map Layer'**
String get selectMapLayer;
/// Title for map options dialog
///
/// In en, this message translates to:
/// **'Map Options'**
String get mapOptions;
/// Toggle for showing map legend
///
/// In en, this message translates to:
/// **'Show Legend'**
String get showLegend;
/// Description for show legend toggle
///
/// In en, this message translates to:
/// **'Display marker type counts'**
String get displayMarkerTypeCounts;
/// Toggle for rotating map with compass heading
///
/// In en, this message translates to:
/// **'Rotate Map with Heading'**
String get rotateMapWithHeading;
/// Description for rotate map toggle
///
/// In en, this message translates to:
/// **'Map follows your direction when moving'**
String get mapFollowsDirection;
/// Toggle for showing map debug information
///
/// In en, this message translates to:
/// **'Show Map Debug Info'**
String get showMapDebugInfo;
/// Description for debug info toggle
///
/// In en, this message translates to:
/// **'Display zoom level and bounds'**
String get displayZoomLevelBounds;
/// Toggle for fullscreen map mode
///
/// In en, this message translates to:
/// **'Fullscreen Mode'**
String get fullscreenMode;
/// Description for fullscreen mode toggle
///
/// In en, this message translates to:
/// **'Hide all UI controls for full map view'**
String get hideUiFullMapView;
/// OpenStreetMap layer name
///
/// In en, this message translates to:
/// **'OpenStreetMap'**
String get openStreetMap;
/// OpenTopoMap layer name
///
/// In en, this message translates to:
/// **'OpenTopoMap'**
String get openTopoMap;
/// ESRI Satellite imagery layer name
///
/// In en, this message translates to:
/// **'ESRI Satellite'**
String get esriSatellite;
/// Tooltip for download visible area button
///
/// In en, this message translates to:
/// **'Download visible area'**
String get downloadVisibleArea;
/// Loading message for map initialization
///
/// In en, this message translates to:
/// **'Initializing map...'**
String get initializingMap;
/// Label when dragging a pin on map
///
/// In en, this message translates to:
/// **'Drag to Position'**
String get dragToPosition;
/// Label for creating SAR marker from pin
///
/// In en, this message translates to:
/// **'Create SAR Marker'**
String get createSarMarker;
/// Compass title in detailed compass dialog
///
/// In en, this message translates to:
/// **'Compass'**
String get compass;
/// Subtitle for compass dialog
///
/// In en, this message translates to:
/// **'Navigation & Contacts'**
String get navigationAndContacts;
/// Label for SAR alert badge on messages
///
/// In en, this message translates to:
/// **'SAR ALERT'**
String get sarAlert;
}
class _AppLocalizationsDelegate

View File

@@ -681,4 +681,211 @@ class AppLocalizationsEn extends AppLocalizations {
String latLonFormat(String latitude, String longitude) {
return 'Lat: $latitude Lon: $longitude';
}
@override
String get noContactsYet => 'No contacts yet';
@override
String get connectToDeviceToLoadContacts =>
'Connect to a device to load contacts';
@override
String get teamMembers => 'Team Members';
@override
String get repeaters => 'Repeaters';
@override
String get rooms => 'Rooms';
@override
String get cacheStatistics => 'Cache Statistics';
@override
String get totalTiles => 'Total Tiles';
@override
String get cacheSize => 'Cache Size';
@override
String get storeName => 'Store Name';
@override
String get noCacheStatistics => 'No cache statistics available';
@override
String get downloadRegion => 'Download Region';
@override
String get mapLayer => 'Map Layer';
@override
String get regionBounds => 'Region Bounds';
@override
String get north => 'North';
@override
String get south => 'South';
@override
String get east => 'East';
@override
String get west => 'West';
@override
String get zoomLevels => 'Zoom Levels';
@override
String minZoom(int zoom) {
return 'Min: $zoom';
}
@override
String maxZoom(int zoom) {
return 'Max: $zoom';
}
@override
String get downloadingDots => 'Downloading...';
@override
String get cancelDownload => 'Cancel Download';
@override
String get downloadRegionButton => 'Download Region';
@override
String get downloadNote =>
'Note: Large regions or high zoom levels may take significant time and storage.';
@override
String get cacheManagement => 'Cache Management';
@override
String get clearAllMaps => 'Clear All Maps';
@override
String get clearMapsConfirmTitle => 'Clear All Maps';
@override
String get clearMapsConfirmMessage =>
'Are you sure you want to delete all downloaded maps? This action cannot be undone.';
@override
String get mapDownloadCompleted => 'Map download completed!';
@override
String get cacheClearedSuccessfully => 'Cache cleared successfully!';
@override
String get downloadCancelled => 'Download cancelled';
@override
String get startingDownload => 'Starting download...';
@override
String get downloadingMapTiles => 'Downloading map tiles...';
@override
String get downloadCompletedSuccessfully =>
'Download completed successfully!';
@override
String get cancellingDownload => 'Cancelling download...';
@override
String errorLoadingStats(String error) {
return 'Error loading stats: $error';
}
@override
String downloadFailed(String error) {
return 'Download failed: $error';
}
@override
String cancelFailed(String error) {
return 'Cancel failed: $error';
}
@override
String clearCacheFailed(String error) {
return 'Clear cache failed: $error';
}
@override
String minZoomError(String error) {
return 'Min zoom: $error';
}
@override
String maxZoomError(String error) {
return 'Max zoom: $error';
}
@override
String get minZoomGreaterThanMax =>
'Minimum zoom must be less than or equal to maximum zoom';
@override
String get selectMapLayer => 'Select Map Layer';
@override
String get mapOptions => 'Map Options';
@override
String get showLegend => 'Show Legend';
@override
String get displayMarkerTypeCounts => 'Display marker type counts';
@override
String get rotateMapWithHeading => 'Rotate Map with Heading';
@override
String get mapFollowsDirection => 'Map follows your direction when moving';
@override
String get showMapDebugInfo => 'Show Map Debug Info';
@override
String get displayZoomLevelBounds => 'Display zoom level and bounds';
@override
String get fullscreenMode => 'Fullscreen Mode';
@override
String get hideUiFullMapView => 'Hide all UI controls for full map view';
@override
String get openStreetMap => 'OpenStreetMap';
@override
String get openTopoMap => 'OpenTopoMap';
@override
String get esriSatellite => 'ESRI Satellite';
@override
String get downloadVisibleArea => 'Download visible area';
@override
String get initializingMap => 'Initializing map...';
@override
String get dragToPosition => 'Drag to Position';
@override
String get createSarMarker => 'Create SAR Marker';
@override
String get compass => 'Compass';
@override
String get navigationAndContacts => 'Navigation & Contacts';
@override
String get sarAlert => 'SAR ALERT';
}

View File

@@ -681,4 +681,211 @@ class AppLocalizationsHr extends AppLocalizations {
String latLonFormat(String latitude, String longitude) {
return 'Ššir: $latitude Dužina: $longitude';
}
@override
String get noContactsYet => 'Još nema kontakata';
@override
String get connectToDeviceToLoadContacts =>
'Povežite se s uređajem da učitate kontakte';
@override
String get teamMembers => 'Članovi tima';
@override
String get repeaters => 'Repetitori';
@override
String get rooms => 'Sobe';
@override
String get cacheStatistics => 'Statistika predmemorije';
@override
String get totalTiles => 'Ukupno pločica';
@override
String get cacheSize => 'Veličina predmemorije';
@override
String get storeName => 'Ime pohrane';
@override
String get noCacheStatistics => 'Statistika predmemorije nije dostupna';
@override
String get downloadRegion => 'Preuzmi regiju';
@override
String get mapLayer => 'Sloj karte';
@override
String get regionBounds => 'Granice regije';
@override
String get north => 'Sjever';
@override
String get south => 'Jug';
@override
String get east => 'Istok';
@override
String get west => 'Zapad';
@override
String get zoomLevels => 'Razine zumiranja';
@override
String minZoom(int zoom) {
return 'Min: $zoom';
}
@override
String maxZoom(int zoom) {
return 'Maks: $zoom';
}
@override
String get downloadingDots => 'Preuzimanje...';
@override
String get cancelDownload => 'Otkaži preuzimanje';
@override
String get downloadRegionButton => 'Preuzmi regiju';
@override
String get downloadNote =>
'Napomena: Velike regije ili visoke razine zumiranja mogu zahtijevati značajno vrijeme i prostor za pohranu.';
@override
String get cacheManagement => 'Upravljanje predmemorijom';
@override
String get clearAllMaps => 'Očisti sve karte';
@override
String get clearMapsConfirmTitle => 'Očisti sve karte';
@override
String get clearMapsConfirmMessage =>
'Jeste li sigurni da želite izbrisati sve preuzete karte? Ova radnja se ne može poništiti.';
@override
String get mapDownloadCompleted => 'Preuzimanje karte završeno!';
@override
String get cacheClearedSuccessfully => 'Predmemorija uspješno očišćena!';
@override
String get downloadCancelled => 'Preuzimanje otkazano';
@override
String get startingDownload => 'Pokretanje preuzimanja...';
@override
String get downloadingMapTiles => 'Preuzimanje pločica karte...';
@override
String get downloadCompletedSuccessfully => 'Preuzimanje uspješno završeno!';
@override
String get cancellingDownload => 'Otkazivanje preuzimanja...';
@override
String errorLoadingStats(String error) {
return 'Greška pri učitavanju statistike: $error';
}
@override
String downloadFailed(String error) {
return 'Preuzimanje nije uspjelo: $error';
}
@override
String cancelFailed(String error) {
return 'Otkazivanje nije uspjelo: $error';
}
@override
String clearCacheFailed(String error) {
return 'Čišćenje predmemorije nije uspjelo: $error';
}
@override
String minZoomError(String error) {
return 'Min zumiranje: $error';
}
@override
String maxZoomError(String error) {
return 'Maks zumiranje: $error';
}
@override
String get minZoomGreaterThanMax =>
'Minimalno zumiranje mora biti manje ili jednako maksimalnom zumiranju';
@override
String get selectMapLayer => 'Odaberite sloj karte';
@override
String get mapOptions => 'Opcije karte';
@override
String get showLegend => 'Prikaži legendu';
@override
String get displayMarkerTypeCounts => 'Prikaži broj vrsta markera';
@override
String get rotateMapWithHeading => 'Rotiraj kartu sa smjerom';
@override
String get mapFollowsDirection => 'Karta slijedi vaš smjer pri kretanju';
@override
String get showMapDebugInfo => 'Prikaži debug informacije karte';
@override
String get displayZoomLevelBounds => 'Prikaži razinu zumiranja i granice';
@override
String get fullscreenMode => 'Način cijelog zaslona';
@override
String get hideUiFullMapView =>
'Sakrij sve UI kontrole za prikaz cijele karte';
@override
String get openStreetMap => 'OpenStreetMap';
@override
String get openTopoMap => 'OpenTopoMap';
@override
String get esriSatellite => 'ESRI satelit';
@override
String get downloadVisibleArea => 'Preuzmi vidljivo područje';
@override
String get initializingMap => 'Inicijalizacija karte...';
@override
String get dragToPosition => 'Povuci na poziciju';
@override
String get createSarMarker => 'Kreiraj SAR marker';
@override
String get compass => 'Kompas';
@override
String get navigationAndContacts => 'Navigacija i kontakti';
@override
String get sarAlert => 'SAR UZBUNA';
}

View File

@@ -681,4 +681,211 @@ class AppLocalizationsSl extends AppLocalizations {
String latLonFormat(String latitude, String longitude) {
return 'Šir: $latitude Dolž: $longitude';
}
@override
String get noContactsYet => 'Še ni stikov';
@override
String get connectToDeviceToLoadContacts =>
'Povežite se z napravo za nalaganje stikov';
@override
String get teamMembers => 'Člani ekipe';
@override
String get repeaters => 'Repetitorji';
@override
String get rooms => 'Sobe';
@override
String get cacheStatistics => 'Statistika predpomnilnika';
@override
String get totalTiles => 'Skupaj ploščic';
@override
String get cacheSize => 'Velikost predpomnilnika';
@override
String get storeName => 'Ime skladišča';
@override
String get noCacheStatistics => 'Statistika predpomnilnika ni na voljo';
@override
String get downloadRegion => 'Prenesi regijo';
@override
String get mapLayer => 'Sloj zemljevida';
@override
String get regionBounds => 'Meje regije';
@override
String get north => 'Sever';
@override
String get south => 'Jug';
@override
String get east => 'Vzhod';
@override
String get west => 'Zahod';
@override
String get zoomLevels => 'Nivoji povečave';
@override
String minZoom(int zoom) {
return 'Min: $zoom';
}
@override
String maxZoom(int zoom) {
return 'Maks: $zoom';
}
@override
String get downloadingDots => 'Prenašanje...';
@override
String get cancelDownload => 'Prekliči prenos';
@override
String get downloadRegionButton => 'Prenesi regijo';
@override
String get downloadNote =>
'Opomba: Velike regije ali visoki nivoji povečave lahko zahtevajo veliko časa in prostora za shranjevanje.';
@override
String get cacheManagement => 'Upravljanje predpomnilnika';
@override
String get clearAllMaps => 'Počisti vse zemljevide';
@override
String get clearMapsConfirmTitle => 'Počisti vse zemljevide';
@override
String get clearMapsConfirmMessage =>
'Ste prepričani, da želite izbrisati vse prenesene zemljevide? Tega dejanja ni mogoče razveljaviti.';
@override
String get mapDownloadCompleted => 'Prenos zemljevida končan!';
@override
String get cacheClearedSuccessfully => 'Predpomnilnik uspešno počiščen!';
@override
String get downloadCancelled => 'Prenos preklican';
@override
String get startingDownload => 'Začetek prenosa...';
@override
String get downloadingMapTiles => 'Prenašanje ploščic zemljevida...';
@override
String get downloadCompletedSuccessfully => 'Prenos uspešno končan!';
@override
String get cancellingDownload => 'Preklic prenosa...';
@override
String errorLoadingStats(String error) {
return 'Napaka pri nalaganju statistike: $error';
}
@override
String downloadFailed(String error) {
return 'Prenos ni uspel: $error';
}
@override
String cancelFailed(String error) {
return 'Preklic ni uspel: $error';
}
@override
String clearCacheFailed(String error) {
return 'Čiščenje predpomnilnika ni uspelo: $error';
}
@override
String minZoomError(String error) {
return 'Min povečava: $error';
}
@override
String maxZoomError(String error) {
return 'Maks povečava: $error';
}
@override
String get minZoomGreaterThanMax =>
'Minimalna povečava mora biti manjša ali enaka maksimalni povečavi';
@override
String get selectMapLayer => 'Izberite sloj zemljevida';
@override
String get mapOptions => 'Možnosti zemljevida';
@override
String get showLegend => 'Prikaži legendo';
@override
String get displayMarkerTypeCounts => 'Prikaži število vrst označevalcev';
@override
String get rotateMapWithHeading => 'Rotiraj zemljevid s smerjo';
@override
String get mapFollowsDirection => 'Zemljevid sledi vaši smeri pri gibanju';
@override
String get showMapDebugInfo => 'Prikaži debug informacije zemljevida';
@override
String get displayZoomLevelBounds => 'Prikaži nivo povečave in meje';
@override
String get fullscreenMode => 'Način celozaslonskega prikaza';
@override
String get hideUiFullMapView =>
'Skrij vse UI kontrole za poln prikaz zemljevida';
@override
String get openStreetMap => 'OpenStreetMap';
@override
String get openTopoMap => 'OpenTopoMap';
@override
String get esriSatellite => 'ESRI satelit';
@override
String get downloadVisibleArea => 'Prenesi vidno območje';
@override
String get initializingMap => 'Inicializacija zemljevida...';
@override
String get dragToPosition => 'Povleci na položaj';
@override
String get createSarMarker => 'Ustvari SAR označevalec';
@override
String get compass => 'Kompas';
@override
String get navigationAndContacts => 'Navigacija in stiki';
@override
String get sarAlert => 'SAR ALARM';
}

View File

@@ -391,5 +391,149 @@
"degreesLeft": "{degrees}° levo",
"latLonFormat": "Šir: {latitude} Dolž: {longitude}"
"latLonFormat": "Šir: {latitude} Dolž: {longitude}",
"noContactsYet": "Še ni stikov",
"connectToDeviceToLoadContacts": "Povežite se z napravo za nalaganje stikov",
"teamMembers": "Člani ekipe",
"repeaters": "Repetitorji",
"rooms": "Sobe",
"cacheStatistics": "Statistika predpomnilnika",
"totalTiles": "Skupaj ploščic",
"cacheSize": "Velikost predpomnilnika",
"storeName": "Ime skladišča",
"noCacheStatistics": "Statistika predpomnilnika ni na voljo",
"downloadRegion": "Prenesi regijo",
"mapLayer": "Sloj zemljevida",
"regionBounds": "Meje regije",
"north": "Sever",
"south": "Jug",
"east": "Vzhod",
"west": "Zahod",
"zoomLevels": "Nivoji povečave",
"minZoom": "Min: {zoom}",
"maxZoom": "Maks: {zoom}",
"downloadingDots": "Prenašanje...",
"cancelDownload": "Prekliči prenos",
"downloadRegionButton": "Prenesi regijo",
"downloadNote": "Opomba: Velike regije ali visoki nivoji povečave lahko zahtevajo veliko časa in prostora za shranjevanje.",
"cacheManagement": "Upravljanje predpomnilnika",
"clearAllMaps": "Počisti vse zemljevide",
"clearMapsConfirmTitle": "Počisti vse zemljevide",
"clearMapsConfirmMessage": "Ste prepričani, da želite izbrisati vse prenesene zemljevide? Tega dejanja ni mogoče razveljaviti.",
"mapDownloadCompleted": "Prenos zemljevida končan!",
"cacheClearedSuccessfully": "Predpomnilnik uspešno počiščen!",
"downloadCancelled": "Prenos preklican",
"startingDownload": "Začetek prenosa...",
"downloadingMapTiles": "Prenašanje ploščic zemljevida...",
"downloadCompletedSuccessfully": "Prenos uspešno končan!",
"cancellingDownload": "Preklic prenosa...",
"errorLoadingStats": "Napaka pri nalaganju statistike: {error}",
"downloadFailed": "Prenos ni uspel: {error}",
"cancelFailed": "Preklic ni uspel: {error}",
"clearCacheFailed": "Čiščenje predpomnilnika ni uspelo: {error}",
"minZoomError": "Min povečava: {error}",
"maxZoomError": "Maks povečava: {error}",
"minZoomGreaterThanMax": "Minimalna povečava mora biti manjša ali enaka maksimalni povečavi",
"selectMapLayer": "Izberite sloj zemljevida",
"mapOptions": "Možnosti zemljevida",
"showLegend": "Prikaži legendo",
"displayMarkerTypeCounts": "Prikaži število vrst označevalcev",
"rotateMapWithHeading": "Rotiraj zemljevid s smerjo",
"mapFollowsDirection": "Zemljevid sledi vaši smeri pri gibanju",
"showMapDebugInfo": "Prikaži debug informacije zemljevida",
"displayZoomLevelBounds": "Prikaži nivo povečave in meje",
"fullscreenMode": "Način celozaslonskega prikaza",
"hideUiFullMapView": "Skrij vse UI kontrole za poln prikaz zemljevida",
"openStreetMap": "OpenStreetMap",
"openTopoMap": "OpenTopoMap",
"esriSatellite": "ESRI satelit",
"downloadVisibleArea": "Prenesi vidno območje",
"initializingMap": "Inicializacija zemljevida...",
"dragToPosition": "Povleci na položaj",
"createSarMarker": "Ustvari SAR označevalec",
"compass": "Kompas",
"navigationAndContacts": "Navigacija in stiki",
"sarAlert": "SAR ALARM",
"justNow": "Pravkar",
"minutesAgo": "pred {minutes}m",
"hoursAgo": "pred {hours}h",
"daysAgo": "pred {days}d",
"secondsAgo": "pred {seconds}s",
"sending": "Pošiljanje...",
"sent": "Poslano",
"delivered": "Dostavljeno",
"deliveredWithTime": "Dostavljeno ({time}ms)",
"failed": "Neuspešno"
}

View File

@@ -1,3 +1,6 @@
import 'package:flutter/material.dart';
import '../l10n/app_localizations.dart';
enum MapLayerType {
openStreetMap,
openTopoMap,
@@ -19,6 +22,19 @@ class MapLayer {
required this.maxZoom,
});
/// Get localized name for the layer
String getLocalizedName(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
switch (type) {
case MapLayerType.openStreetMap:
return localizations.openStreetMap;
case MapLayerType.openTopoMap:
return localizations.openTopoMap;
case MapLayerType.esriWorldImagery:
return localizations.esriSatellite;
}
}
static const openStreetMap = MapLayer(
type: MapLayerType.openStreetMap,
name: 'OpenStreetMap',

View File

@@ -179,8 +179,8 @@ class AppProvider with ChangeNotifier {
);
}
// Sync device time
await connectionProvider.syncDeviceTime();
// Note: Device clock is automatically synced during connection in MeshCoreBleService
// No need to sync it again here
// Get battery and storage information
await connectionProvider.getBatteryAndStorage();

View File

@@ -2,6 +2,7 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:geolocator/geolocator.dart';
import '../l10n/app_localizations.dart';
import '../providers/contacts_provider.dart';
import '../providers/app_provider.dart';
import '../widgets/contacts/contact_tile.dart';
@@ -85,6 +86,7 @@ class _ContactsTabState extends State<ContactsTab> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Consumer<ContactsProvider>(
builder: (context, contactsProvider, child) {
final chatContacts = contactsProvider.chatContacts;
@@ -103,12 +105,12 @@ class _ContactsTabState extends State<ContactsTab> {
),
const SizedBox(height: 16),
Text(
'No contacts yet',
l10n.noContactsYet,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'Connect to a device to load contacts',
l10n.connectToDeviceToLoadContacts,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
@@ -125,7 +127,7 @@ class _ContactsTabState extends State<ContactsTab> {
// Team Members (Chat contacts)
if (chatContacts.isNotEmpty) ...[
_SectionHeader(
title: 'Team Members',
title: l10n.teamMembers,
count: chatContacts.length,
icon: Icons.people,
),
@@ -143,7 +145,7 @@ class _ContactsTabState extends State<ContactsTab> {
// Repeaters
if (repeaters.isNotEmpty) ...[
_SectionHeader(
title: 'Repeaters',
title: l10n.repeaters,
count: repeaters.length,
icon: Icons.router,
),
@@ -161,7 +163,7 @@ class _ContactsTabState extends State<ContactsTab> {
// Rooms/Channels
if (rooms.isNotEmpty) ...[
_SectionHeader(
title: 'Rooms',
title: l10n.rooms,
count: rooms.length,
icon: Icons.tag,
),

View File

@@ -101,7 +101,7 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
} catch (e) {
if (!mounted) return;
setState(() {
_statusMessage = 'Error loading stats: $e';
_statusMessage = AppLocalizations.of(context)!.errorLoadingStats(e.toString());
_isLoading = false;
});
}
@@ -133,18 +133,18 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
// Validate zoom levels
final minZoomResult = validator.validateZoomLevel(_minZoom);
if (!minZoomResult.isValid) {
_showError('Min zoom: ${minZoomResult.errorMessage}');
_showError(AppLocalizations.of(context)!.minZoomError(minZoomResult.errorMessage!));
return;
}
final maxZoomResult = validator.validateZoomLevel(_maxZoom);
if (!maxZoomResult.isValid) {
_showError('Max zoom: ${maxZoomResult.errorMessage}');
_showError(AppLocalizations.of(context)!.maxZoomError(maxZoomResult.errorMessage!));
return;
}
if (_minZoom > _maxZoom) {
_showError('Minimum zoom must be less than or equal to maximum zoom');
_showError(AppLocalizations.of(context)!.minZoomGreaterThanMax);
return;
}
@@ -157,7 +157,7 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
setState(() {
_isDownloading = true;
_downloadProgress = 0.0;
_statusMessage = 'Starting download...';
_statusMessage = AppLocalizations.of(context)!.startingDownload;
});
await widget.tileCacheService.downloadRegion(
@@ -170,7 +170,7 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
if (!mounted) return;
setState(() {
_downloadProgress = progress;
_statusMessage = 'Downloading map tiles...';
_statusMessage = AppLocalizations.of(context)!.downloadingMapTiles;
});
},
);
@@ -178,15 +178,15 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
if (!mounted) return;
setState(() {
_isDownloading = false;
_statusMessage = 'Download completed successfully!';
_statusMessage = AppLocalizations.of(context)!.downloadCompletedSuccessfully;
});
await _loadCacheStats();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Map download completed!'),
SnackBar(
content: Text(AppLocalizations.of(context)!.mapDownloadCompleted),
backgroundColor: Colors.green,
),
);
@@ -195,23 +195,23 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
if (!mounted) return;
setState(() {
_isDownloading = false;
_statusMessage = 'Download failed: $e';
_statusMessage = AppLocalizations.of(context)!.downloadFailed(e.toString());
});
_showError('Download failed: $e');
_showError(AppLocalizations.of(context)!.downloadFailed(e.toString()));
}
}
Future<void> _cancelDownload() async {
try {
if (!mounted) return;
setState(() => _statusMessage = 'Cancelling download...');
setState(() => _statusMessage = AppLocalizations.of(context)!.cancellingDownload);
await widget.tileCacheService.cancelDownload();
if (!mounted) return;
setState(() {
_isDownloading = false;
_statusMessage = 'Download cancelled';
_statusMessage = AppLocalizations.of(context)!.downloadCancelled;
});
await _loadCacheStats();
@@ -228,9 +228,9 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
if (!mounted) return;
setState(() {
_isDownloading = false;
_statusMessage = 'Cancel failed: $e';
_statusMessage = AppLocalizations.of(context)!.cancelFailed(e.toString());
});
_showError('Cancel failed: $e');
_showError(AppLocalizations.of(context)!.cancelFailed(e.toString()));
}
}
@@ -238,9 +238,9 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context)!.clear),
content: const Text(
'Are you sure you want to delete all downloaded maps? This action cannot be undone.',
title: Text(AppLocalizations.of(context)!.clearMapsConfirmTitle),
content: Text(
AppLocalizations.of(context)!.clearMapsConfirmMessage,
),
actions: [
TextButton(
@@ -268,15 +268,15 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Cache cleared successfully!'),
SnackBar(
content: Text(AppLocalizations.of(context)!.cacheClearedSuccessfully),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
setState(() => _isLoading = false);
_showError('Clear cache failed: $e');
_showError(AppLocalizations.of(context)!.clearCacheFailed(e.toString()));
}
}
@@ -332,7 +332,7 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Cache Statistics',
AppLocalizations.of(context)!.cacheStatistics,
style: Theme.of(context).textTheme.titleLarge,
),
IconButton(
@@ -344,22 +344,22 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
const SizedBox(height: 16),
if (_cacheStats != null) ...[
_buildStatRow(
'Total Tiles',
AppLocalizations.of(context)!.totalTiles,
'${_cacheStats!['tileCount'] ?? 0}',
Icons.grid_on,
),
_buildStatRow(
'Cache Size',
AppLocalizations.of(context)!.cacheSize,
'${(_cacheStats!['sizeMB'] ?? 0.0).toStringAsFixed(2)} MB',
Icons.storage,
),
_buildStatRow(
'Store Name',
AppLocalizations.of(context)!.storeName,
_cacheStats!['storeName'] ?? 'Unknown',
Icons.folder,
),
] else
const Text('No cache statistics available'),
Text(AppLocalizations.of(context)!.noCacheStatistics),
],
),
),
@@ -390,7 +390,7 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Download Region',
AppLocalizations.of(context)!.downloadRegion,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
@@ -398,14 +398,14 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
// Map Layer Selection
DropdownButtonFormField<MapLayer>(
value: _selectedLayer,
decoration: const InputDecoration(
labelText: 'Map Layer',
border: OutlineInputBorder(),
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.mapLayer,
border: const OutlineInputBorder(),
),
items: MapLayer.allLayers.map((layer) {
return DropdownMenuItem(
value: layer,
child: Text(layer.name),
child: Text(layer.getLocalizedName(context)),
);
}).toList(),
onChanged: _isDownloading ? null : (layer) {
@@ -418,7 +418,7 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
// Coordinates
Text(
'Region Bounds',
AppLocalizations.of(context)!.regionBounds,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
@@ -427,9 +427,9 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
Expanded(
child: TextField(
controller: _northController,
decoration: const InputDecoration(
labelText: 'North',
border: OutlineInputBorder(),
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.north,
border: const OutlineInputBorder(),
hintText: '46.1',
),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
@@ -440,9 +440,9 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
Expanded(
child: TextField(
controller: _southController,
decoration: const InputDecoration(
labelText: 'South',
border: OutlineInputBorder(),
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.south,
border: const OutlineInputBorder(),
hintText: '46.0',
),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
@@ -457,9 +457,9 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
Expanded(
child: TextField(
controller: _eastController,
decoration: const InputDecoration(
labelText: 'East',
border: OutlineInputBorder(),
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.east,
border: const OutlineInputBorder(),
hintText: '14.6',
),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
@@ -470,9 +470,9 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
Expanded(
child: TextField(
controller: _westController,
decoration: const InputDecoration(
labelText: 'West',
border: OutlineInputBorder(),
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.west,
border: const OutlineInputBorder(),
hintText: '14.4',
),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
@@ -485,7 +485,7 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
// Zoom Levels
Text(
'Zoom Levels',
AppLocalizations.of(context)!.zoomLevels,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
@@ -495,7 +495,7 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Min: $_minZoom'),
Text(AppLocalizations.of(context)!.minZoom(_minZoom)),
Slider(
value: _minZoom.toDouble(),
min: 1,
@@ -519,7 +519,7 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Max: $_maxZoom'),
Text(AppLocalizations.of(context)!.maxZoom(_maxZoom)),
Slider(
value: _maxZoom.toDouble(),
min: 1,
@@ -560,7 +560,7 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_statusMessage ?? 'Downloading...',
_statusMessage ?? AppLocalizations.of(context)!.downloadingDots,
style: TextStyle(
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onPrimaryContainer,
@@ -600,7 +600,7 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
ElevatedButton.icon(
onPressed: _cancelDownload,
icon: const Icon(Icons.cancel),
label: const Text('Cancel Download'),
label: Text(AppLocalizations.of(context)!.cancelDownload),
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(48),
backgroundColor: Colors.red,
@@ -611,7 +611,7 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
ElevatedButton.icon(
onPressed: _downloadRegion,
icon: const Icon(Icons.download),
label: const Text('Download Region'),
label: Text(AppLocalizations.of(context)!.downloadRegionButton),
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(48),
),
@@ -619,7 +619,7 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
const SizedBox(height: 8),
Text(
'Note: Large regions or high zoom levels may take significant time and storage.',
AppLocalizations.of(context)!.downloadNote,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
@@ -636,7 +636,7 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Cache Management',
AppLocalizations.of(context)!.cacheManagement,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
@@ -645,7 +645,7 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
OutlinedButton.icon(
onPressed: _isDownloading ? null : _clearCache,
icon: const Icon(Icons.delete_forever),
label: const Text('Clear All Maps'),
label: Text(AppLocalizations.of(context)!.clearAllMaps),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red,
minimumSize: const Size.fromHeight(48),

View File

@@ -360,7 +360,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
const SizedBox(width: 12),
Expanded(
child: Text(
'Select Map Layer',
AppLocalizations.of(context)!.selectMapLayer,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
@@ -368,7 +368,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
),
IconButton(
icon: const Icon(Icons.download),
tooltip: 'Download visible area',
tooltip: AppLocalizations.of(context)!.downloadVisibleArea,
onPressed: () {
Navigator.pop(context);
_navigateToDownload(context);
@@ -382,7 +382,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
leading: _currentLayer.type == layer.type
? const Icon(Icons.check_circle, color: Colors.green)
: const Icon(Icons.radio_button_unchecked),
title: Text(layer.name),
title: Text(layer.getLocalizedName(context)),
subtitle: Text(layer.attribution),
onTap: () {
setState(() {
@@ -442,7 +442,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
const Icon(Icons.settings),
const SizedBox(width: 12),
Text(
'Map Options',
AppLocalizations.of(context)!.mapOptions,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
@@ -454,8 +454,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
// Legend toggle
SwitchListTile(
secondary: const Icon(Icons.info_outline),
title: const Text('Show Legend'),
subtitle: const Text('Display marker type counts'),
title: Text(AppLocalizations.of(context)!.showLegend),
subtitle: Text(AppLocalizations.of(context)!.displayMarkerTypeCounts),
value: _showLegend,
onChanged: (value) {
setState(() {
@@ -469,8 +469,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
// Compass rotation toggle
SwitchListTile(
secondary: const Icon(Icons.explore),
title: const Text('Rotate Map with Heading'),
subtitle: const Text('Map follows your direction when moving'),
title: Text(AppLocalizations.of(context)!.rotateMapWithHeading),
subtitle: Text(AppLocalizations.of(context)!.mapFollowsDirection),
value: _rotateMarkerWithHeading,
onChanged: (value) {
setState(() {
@@ -502,8 +502,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
// Map Debug Info toggle
SwitchListTile(
secondary: const Icon(Icons.developer_mode),
title: const Text('Show Map Debug Info'),
subtitle: const Text('Display zoom level and bounds'),
title: Text(AppLocalizations.of(context)!.showMapDebugInfo),
subtitle: Text(AppLocalizations.of(context)!.displayZoomLevelBounds),
value: _showMapDebugInfo,
onChanged: (value) {
setState(() {
@@ -517,8 +517,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
// Fullscreen mode toggle
SwitchListTile(
secondary: const Icon(Icons.fullscreen),
title: const Text('Fullscreen Mode'),
subtitle: const Text('Hide all UI controls for full map view'),
title: Text(AppLocalizations.of(context)!.fullscreenMode),
subtitle: Text(AppLocalizations.of(context)!.hideUiFullMapView),
value: _isFullscreen,
onChanged: (value) {
setState(() {
@@ -1062,7 +1062,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
],
),
child: Text(
_isDraggingPin ? 'Drag to Position' : 'Create SAR Marker',
_isDraggingPin ? AppLocalizations.of(context)!.dragToPosition : AppLocalizations.of(context)!.createSarMarker,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
@@ -1109,7 +1109,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(
'Initializing map...',
AppLocalizations.of(context)!.initializingMap,
style: Theme.of(context).textTheme.bodyMedium,
),
],

View File

@@ -13,6 +13,7 @@ import '../widgets/messages/sar_update_sheet.dart';
import '../widgets/contacts/direct_message_sheet.dart';
import '../utils/toast_logger.dart';
import '../l10n/app_localizations.dart';
import '../utils/sar_marker_extensions.dart';
class MessagesTab extends StatefulWidget {
final VoidCallback onNavigateToMap;
@@ -179,7 +180,7 @@ class _MessagesTabState extends State<MessagesTab> {
if (!mounted) return;
ToastLogger.warning(
context,
'${sarType.displayName} marker broadcast to public channel',
'${sarType.getLocalizedName(context)} marker broadcast to public channel',
);
} else {
// Create message ID
@@ -231,7 +232,7 @@ class _MessagesTabState extends State<MessagesTab> {
if (!mounted) return;
ToastLogger.success(
context,
'${sarType.displayName} marker sent to room',
'${sarType.getLocalizedName(context)} marker sent to room',
);
}
} catch (e) {
@@ -397,7 +398,7 @@ class _MessagesTabState extends State<MessagesTab> {
maxLengthEnforcement: MaxLengthEnforcement.enforced,
style: const TextStyle(fontSize: 14),
decoration: InputDecoration(
hintText: 'Type a message...',
hintText: AppLocalizations.of(context)!.typeYourMessage,
hintStyle: const TextStyle(fontSize: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
@@ -860,7 +861,7 @@ class _MessageBubble extends StatelessWidget {
),
const SizedBox(width: 4),
Text(
'SAR ALERT',
AppLocalizations.of(context)!.sarAlert,
style: Theme.of(context).textTheme.labelSmall
?.copyWith(
color: Colors.white,
@@ -943,7 +944,7 @@ class _MessageBubble extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
message.sarMarkerType!.displayName,
message.sarMarkerType!.getLocalizedName(context),
style: Theme.of(context).textTheme.titleSmall
?.copyWith(fontWeight: FontWeight.bold),
),

View File

@@ -199,8 +199,17 @@ class MeshCoreBleService {
await _connectionManager.disconnect();
}
/// Send initial device query
/// Send initial device query and sync clock
Future<void> _sendDeviceQuery() async {
// CRITICAL: Set device clock FIRST, before any other commands
// This ensures the device has correct timestamps for all operations
print('⏰ [Service] Setting device clock before device query');
await _commandSender.writeData(FrameBuilder.buildSetDeviceTime());
// Small delay to ensure clock is set before proceeding
await Future.delayed(const Duration(milliseconds: 100));
// Now send device query and app start
await _commandSender.writeData(FrameBuilder.buildDeviceQuery());
await _commandSender.writeData(FrameBuilder.buildAppStart());
}

View File

@@ -0,0 +1,24 @@
import 'package:flutter/widgets.dart';
import '../models/sar_marker.dart';
import '../l10n/app_localizations.dart';
/// Extension for SarMarkerType to provide localized display names
extension SarMarkerTypeLocalization on SarMarkerType {
/// Get localized display name for this SAR marker type
String getLocalizedName(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
switch (this) {
case SarMarkerType.foundPerson:
return l10n.foundPerson;
case SarMarkerType.fire:
return l10n.fire;
case SarMarkerType.stagingArea:
return l10n.stagingArea;
case SarMarkerType.object:
return 'Object'; // Not commonly used, keeping English for now
case SarMarkerType.unknown:
return 'Unknown'; // Not commonly used, keeping English for now
}
}
}

View File

@@ -0,0 +1,25 @@
import 'package:flutter/widgets.dart';
import '../l10n/app_localizations.dart';
/// Extension to provide localized "time ago" formatting
extension TimeAgoExtension on Duration {
/// Get localized time ago string
String toLocalizedTimeAgo(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
if (inMinutes < 1) return l10n.justNow;
if (inMinutes < 60) return l10n.minutesAgo(inMinutes);
if (inHours < 24) return l10n.hoursAgo(inHours);
return l10n.daysAgo(inDays);
}
/// Get localized time ago string with seconds support
String toLocalizedTimeAgoWithSeconds(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
if (inSeconds < 60) return l10n.secondsAgo(inSeconds);
if (inMinutes < 60) return l10n.minutesAgo(inMinutes);
if (inHours < 24) return l10n.hoursAgo(inHours);
return l10n.daysAgo(inDays);
}
}

View File

@@ -1,7 +1,7 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../../l10n/app_localizations.dart';
import '../../../models/contact.dart';
/// Contact list section for the compass dialog.

View File

@@ -1,7 +1,7 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../../l10n/app_localizations.dart';
import '../../../models/sar_marker.dart';
/// SAR marker list section for the compass dialog.

View File

@@ -13,6 +13,7 @@ import 'compass/compass_header.dart';
import 'compass/compass_filters.dart';
import 'compass/compass_sar_list.dart';
import 'compass/compass_contact_list.dart';
import '../../l10n/app_localizations.dart';
enum HeadingAccuracySeverity { low, medium, high }
@@ -285,19 +286,19 @@ class _DetailedCompassDialogState extends State<DetailedCompassDialog> {
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
const Expanded(
Expanded(
child: Column(
children: [
Text(
'Compass',
style: TextStyle(
AppLocalizations.of(context)!.compass,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'Navigation & Contacts',
style: TextStyle(
AppLocalizations.of(context)!.navigationAndContacts,
style: const TextStyle(
color: Colors.grey,
fontSize: 14,
),

View File

@@ -73,6 +73,9 @@ dependencies:
# Background services
flutter_background_service: ^5.0.13
# Notifications
flutter_local_notifications: ^18.0.1
dev_dependencies:
flutter_test:
sdk: flutter