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

@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import 'package:geolocator/geolocator.dart';
import '../providers/connection_provider.dart';
import '../services/validation_service.dart';
import '../l10n/app_localizations.dart';
class DeviceConfigScreen extends StatefulWidget {
const DeviceConfigScreen({super.key});
@@ -174,8 +175,8 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Public info saved'),
SnackBar(
content: Text(AppLocalizations.of(context)!.save),
backgroundColor: Colors.green,
),
);
@@ -244,8 +245,8 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Radio settings saved'),
SnackBar(
content: Text(AppLocalizations.of(context)!.save),
backgroundColor: Colors.green,
),
);
@@ -268,7 +269,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
if (!serviceEnabled) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Location services disabled')),
SnackBar(content: Text(AppLocalizations.of(context)!.locationServicesDisabled)),
);
}
return;
@@ -280,7 +281,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
if (permission == LocationPermission.denied) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Location permission denied')),
SnackBar(content: Text(AppLocalizations.of(context)!.locationPermissionDenied)),
);
}
return;
@@ -290,7 +291,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
if (permission == LocationPermission.deniedForever) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Location permission permanently denied')),
SnackBar(content: Text(AppLocalizations.of(context)!.locationPermissionPermanentlyDenied)),
);
}
return;
@@ -310,8 +311,11 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Location updated'),
SnackBar(
content: Text(AppLocalizations.of(context)!.locationBroadcast(
position.latitude.toStringAsFixed(6),
position.longitude.toStringAsFixed(6),
)),
backgroundColor: Colors.green,
),
);
@@ -331,8 +335,8 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
if (!connectionProvider.deviceInfo.isConnected) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Not connected to device'),
SnackBar(
content: Text(AppLocalizations.of(context)!.deviceNotConnected),
backgroundColor: Colors.orange,
),
);
@@ -347,9 +351,12 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
await connectionProvider.sendSelfAdvert(floodMode: true);
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(
const SnackBar(
content: Text('Advertisement broadcast to mesh network'),
SnackBar(
content: Text(AppLocalizations.of(context)!.advertisedAtLocation(lat, lon)),
backgroundColor: Colors.green,
),
);
@@ -358,7 +365,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to broadcast: $e'),
content: Text(AppLocalizations.of(context)!.failedToAdvertise(e.toString())),
backgroundColor: Colors.red,
),
);
@@ -377,7 +384,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
return Scaffold(
appBar: AppBar(
title: const Text('Device Settings'),
title: Text(AppLocalizations.of(context)!.settings),
),
body: ListView(
padding: const EdgeInsets.all(16),
@@ -441,13 +448,13 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.sensors),
tooltip: 'Broadcast',
tooltip: AppLocalizations.of(context)!.settings,
),
const SizedBox(width: 8),
IconButton.filled(
onPressed: _savePublicInfo,
icon: const Icon(Icons.save),
tooltip: 'Save',
tooltip: AppLocalizations.of(context)!.save,
),
],
),
@@ -555,7 +562,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
IconButton.filled(
onPressed: _saveRadioSettings,
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 'packet_log_screen.dart';
import '../utils/toast_logger.dart';
import '../l10n/app_localizations.dart';
class HomeScreen extends StatefulWidget {
final Function(AppThemeMode) onThemeChanged;
@@ -77,7 +78,7 @@ class _HomeScreenState extends State<HomeScreen>
if (!connectionProvider.deviceInfo.isConnected) {
if (context.mounted) {
ToastLogger.error(context, 'Device not connected');
ToastLogger.error(context, AppLocalizations.of(context)!.deviceNotConnected);
}
return;
}
@@ -89,7 +90,7 @@ class _HomeScreenState extends State<HomeScreen>
if (context.mounted) {
ToastLogger.error(
context,
'Location services are disabled. Please enable them in Settings.',
AppLocalizations.of(context)!.locationServicesDisabled,
);
}
return;
@@ -101,7 +102,7 @@ class _HomeScreenState extends State<HomeScreen>
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
if (context.mounted) {
ToastLogger.error(context, 'Location permission denied');
ToastLogger.error(context, AppLocalizations.of(context)!.locationPermissionDenied);
}
return;
}
@@ -111,7 +112,7 @@ class _HomeScreenState extends State<HomeScreen>
if (context.mounted) {
ToastLogger.error(
context,
'Location permission permanently denied. Please enable in Settings.',
AppLocalizations.of(context)!.locationPermissionPermanentlyDenied,
);
}
return;
@@ -129,7 +130,7 @@ class _HomeScreenState extends State<HomeScreen>
} catch (e) {
print('❌ Failed to get GPS position: $e');
if (context.mounted) {
ToastLogger.error(context, 'Failed to get GPS location');
ToastLogger.error(context, AppLocalizations.of(context)!.failedToGetGpsLocation);
}
return;
}
@@ -149,13 +150,16 @@ class _HomeScreenState extends State<HomeScreen>
if (context.mounted) {
ToastLogger.success(
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) {
print('❌ Failed to advertise device: $e');
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(
children: [
Text(
'MeshCore',
AppLocalizations.of(context)!.appTitle,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontSize: 18,
@@ -211,7 +215,7 @@ class _HomeScreenState extends State<HomeScreen>
),
),
Text(
'Scanning for devices...',
AppLocalizations.of(context)!.scanningForDevices,
style: TextStyle(
color: Theme.of(
context,
@@ -253,7 +257,7 @@ class _HomeScreenState extends State<HomeScreen>
const SizedBox(width: 12),
Expanded(
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(
color: Theme.of(context).colorScheme.onPrimaryContainer,
fontSize: 13,
@@ -288,7 +292,7 @@ class _HomeScreenState extends State<HomeScreen>
),
const SizedBox(height: 16),
Text(
'No devices found',
AppLocalizations.of(context)!.noDevicesFound,
style: TextStyle(
color: Theme.of(
context,
@@ -303,7 +307,7 @@ class _HomeScreenState extends State<HomeScreen>
connectionProvider.startScan();
},
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(
children: [
Text(
'Tap to connect',
AppLocalizations.of(context)!.tapToConnect,
style: TextStyle(
color: Theme.of(
context,
@@ -450,7 +454,7 @@ class _HomeScreenState extends State<HomeScreen>
await provider.disconnect();
},
icon: const Icon(Icons.power_settings_new),
tooltip: 'Disconnect',
tooltip: AppLocalizations.of(context)!.disconnect,
color: Colors.red.shade700,
);
}
@@ -461,11 +465,11 @@ class _HomeScreenState extends State<HomeScreen>
icon: const Icon(Icons.more_vert),
itemBuilder: (context) => [
PopupMenuItem(
child: const Row(
child: Row(
children: [
Icon(Icons.map),
SizedBox(width: 8),
Text('Map Management'),
const Icon(Icons.map),
const SizedBox(width: 8),
Text(AppLocalizations.of(context)!.mapManagement),
],
),
onTap: () {
@@ -483,11 +487,11 @@ class _HomeScreenState extends State<HomeScreen>
},
),
PopupMenuItem(
child: const Row(
child: Row(
children: [
Icon(Icons.settings),
SizedBox(width: 8),
Text('Settings'),
const Icon(Icons.settings),
const SizedBox(width: 8),
Text(AppLocalizations.of(context)!.settings),
],
),
onTap: () {
@@ -551,16 +555,16 @@ class _HomeScreenState extends State<HomeScreen>
Icons.message,
unreadCount,
),
text: 'Messages',
text: AppLocalizations.of(context)!.messages,
),
Tab(
icon: _buildTabIconWithBadge(
Icons.contacts,
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
return Row(
children: [
const Expanded(
Expanded(
child: Text(
'MeshCore',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
AppLocalizations.of(context)!.appTitle,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
),
ElevatedButton.icon(
@@ -608,7 +612,7 @@ class _HomeScreenState extends State<HomeScreen>
label: Text(
provider.isReconnecting
? '${provider.reconnectionAttempt}/${provider.maxReconnectionAttempts}'
: 'Connect',
: AppLocalizations.of(context)!.connect,
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
@@ -624,7 +628,7 @@ class _HomeScreenState extends State<HomeScreen>
IconButton(
onPressed: () => provider.cancelReconnection(),
icon: const Icon(Icons.close, size: 20),
tooltip: 'Cancel reconnection',
tooltip: AppLocalizations.of(context)!.cancelReconnection,
style: IconButton.styleFrom(
backgroundColor: Colors.red.shade700,
foregroundColor: Colors.white,
@@ -650,7 +654,7 @@ class _HomeScreenState extends State<HomeScreen>
mainAxisSize: MainAxisSize.min,
children: [
Text(
deviceInfo.selfName ?? 'MeshCore',
deviceInfo.selfName ?? AppLocalizations.of(context)!.appTitle,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
@@ -838,8 +842,8 @@ class _HomeScreenState extends State<HomeScreen>
Expanded(
child: Text(
isConnected
? deviceInfo.displayName ?? 'Connected'
: 'Not Connected',
? deviceInfo.displayName ?? AppLocalizations.of(context)!.connect
: AppLocalizations.of(context)!.deviceNotConnected,
style: Theme.of(context).textTheme.bodyMedium,
),
),
@@ -881,7 +885,7 @@ class _HomeScreenState extends State<HomeScreen>
? null
: () => _showConnectionDialog(context),
icon: const Icon(Icons.bluetooth_searching, size: 18),
label: const Text('Connect'),
label: Text(AppLocalizations.of(context)!.connect),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 8),
),
@@ -896,7 +900,7 @@ class _HomeScreenState extends State<HomeScreen>
await provider.disconnect();
},
icon: const Icon(Icons.bluetooth_disabled, size: 18),
label: const Text('Disconnect'),
label: Text(AppLocalizations.of(context)!.disconnect),
style: OutlinedButton.styleFrom(
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/validation_service.dart';
import '../models/map_layer.dart';
import '../l10n/app_localizations.dart';
class MapManagementScreen extends StatefulWidget {
final TileCacheService tileCacheService;
@@ -217,8 +218,8 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Download cancelled'),
SnackBar(
content: Text(AppLocalizations.of(context)!.cancel),
backgroundColor: Colors.orange,
),
);
@@ -237,19 +238,19 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Clear Cache'),
title: Text(AppLocalizations.of(context)!.clear),
content: const Text(
'Are you sure you want to delete all downloaded maps? This action cannot be undone.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
child: Text(AppLocalizations.of(context)!.cancel),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
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) {
return Scaffold(
appBar: AppBar(
title: const Text('Map Management'),
title: Text(AppLocalizations.of(context)!.mapManagement),
),
body: _isLoading
? 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_toolbar.dart';
import '../widgets/messages/sar_update_sheet.dart';
import '../l10n/app_localizations.dart';
import 'map_management_screen.dart';
class MapTab extends StatefulWidget {
@@ -643,8 +644,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
_saveSettings();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Failed to start background tracking. Check permissions and BLE connection.'),
SnackBar(
content: Text(AppLocalizations.of(context)!.failedToStartBackgroundTracking),
duration: Duration(seconds: 3),
),
);
@@ -710,8 +711,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
if (!connectionProvider.deviceInfo.isConnected) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Not connected to device'),
SnackBar(
content: Text(AppLocalizations.of(context)!.deviceNotConnected),
backgroundColor: Colors.red,
),
);

View File

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

View File

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