Add Croatian and Slovenian localizations, implement locale preferences, and update settings screen

- Created `app_localizations_hr.dart` and `app_localizations_sl.dart` for Croatian and Slovenian translations.
- Added `app_sl.arb` file for Slovenian localization strings.
- Enhanced `main.dart` to support locale selection and initialization.
- Updated `home_screen.dart` and `settings_screen.dart` to handle locale changes and display current language.
- Implemented `locale_preferences.dart` service for managing locale settings using SharedPreferences.
- Modified `pubspec.yaml` to include `flutter_localizations` and enable localization file generation.
This commit is contained in:
Janez T
2025-10-16 14:42:43 +02:00
parent 95333a13b8
commit 4c8e0e5d61
15 changed files with 2615 additions and 214 deletions

View File

@@ -18,12 +18,16 @@ import '../utils/toast_logger.dart';
class HomeScreen extends StatefulWidget {
final Function(AppThemeMode) onThemeChanged;
final Function(Locale?) onLocaleChanged;
final AppThemeMode currentTheme;
final Locale? currentLocale;
const HomeScreen({
super.key,
required this.onThemeChanged,
required this.onLocaleChanged,
required this.currentTheme,
required this.currentLocale,
});
@override
@@ -493,7 +497,9 @@ class _HomeScreenState extends State<HomeScreen>
MaterialPageRoute(
builder: (context) => SettingsScreen(
onThemeChanged: widget.onThemeChanged,
onLocaleChanged: widget.onLocaleChanged,
currentTheme: widget.currentTheme,
currentLocale: widget.currentLocale,
),
),
);
@@ -575,72 +581,131 @@ class _HomeScreenState extends State<HomeScreen>
return Row(
children: [
// LEFT SECTION: Name + BT/Battery status + Settings cog
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'MeshCore',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
if (isConnected)
Row(
// Name and status group
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// BLE connection strength indicator with RSSI
Icon(
Icons.bluetooth_connected,
color: deviceInfo.signalRssi != null
? _getSignalColor(deviceInfo.signalRssi!)
: Colors.grey,
size: 14,
Text(
isConnected && deviceInfo.selfName != null
? deviceInfo.selfName!
: 'MeshCore',
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
const SizedBox(width: 3),
if (deviceInfo.signalRssi != null)
Flexible(
child: Text(
'${deviceInfo.signalRssi}',
style: TextStyle(
fontSize: 12,
color: _getSignalColor(deviceInfo.signalRssi!),
fontWeight: FontWeight.w500,
if (isConnected)
Row(
mainAxisSize: MainAxisSize.min,
children: [
// BLE connection strength indicator with RSSI
Icon(
Icons.bluetooth_connected,
color: deviceInfo.signalRssi != null
? _getSignalColor(deviceInfo.signalRssi!)
: Colors.grey,
size: 14,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
// Battery indicator
if (deviceInfo.batteryPercent != null) ...[
Icon(
_getBatteryIcon(deviceInfo.batteryPercent!),
color: _getBatteryColor(deviceInfo.batteryPercent!),
size: 14,
),
const SizedBox(width: 3),
Flexible(
child: Text(
'${deviceInfo.batteryPercent!.round()}%',
style: TextStyle(
fontSize: 12,
color: _getBatteryColor(
deviceInfo.batteryPercent!,
const SizedBox(width: 3),
if (deviceInfo.signalRssi != null)
Flexible(
child: Text(
'${deviceInfo.signalRssi}',
style: TextStyle(
fontSize: 12,
color: _getSignalColor(deviceInfo.signalRssi!),
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
),
),
overflow: TextOverflow.ellipsis,
),
const SizedBox(width: 8),
// Battery indicator
if (deviceInfo.batteryPercent != null) ...[
Icon(
_getBatteryIcon(deviceInfo.batteryPercent!),
color: _getBatteryColor(deviceInfo.batteryPercent!),
size: 14,
),
const SizedBox(width: 3),
Flexible(
child: Text(
'${deviceInfo.batteryPercent!.round()}%',
style: TextStyle(
fontSize: 12,
color: _getBatteryColor(
deviceInfo.batteryPercent!,
),
),
overflow: TextOverflow.ellipsis,
),
),
],
],
)
else if (provider.isReconnecting)
Text(
'Reconnecting...',
style: TextStyle(fontSize: 12, color: Colors.orange[600]),
overflow: TextOverflow.ellipsis,
),
],
],
)
else if (provider.isReconnecting)
Text(
'Reconnecting... (${provider.reconnectionAttempt}/${provider.maxReconnectionAttempts})',
style: TextStyle(fontSize: 14, color: Colors.orange[600]),
),
),
// Settings cog - part of left group
if (isConnected) ...[
const SizedBox(width: 8),
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DeviceConfigScreen(),
),
);
},
onLongPress: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
PacketLogScreen(bleService: provider.bleService),
),
);
},
child: Container(
width: 36,
height: 36,
alignment: Alignment.center,
child: const Icon(Icons.settings, size: 20),
),
),
],
],
),
),
// CENTER SECTION: Broadcast button (when connected)
if (isConnected) ...[
FilledButton(
onPressed: () => _advertiseDevice(context),
style: FilledButton.styleFrom(
backgroundColor: Colors.blue.shade700,
foregroundColor: Colors.white,
padding: const EdgeInsets.all(10),
minimumSize: const Size(40, 40),
shape: const CircleBorder(),
),
child: const Icon(Icons.campaign, size: 20),
),
],
// RIGHT SECTION: Connect button OR RX/TX indicators
if (!isConnected)
Row(
mainAxisSize: MainAxisSize.min,
@@ -663,7 +728,7 @@ class _HomeScreenState extends State<HomeScreen>
: const Icon(Icons.bluetooth, size: 18),
label: Text(
provider.isReconnecting
? 'Reconnecting (${provider.reconnectionAttempt}/${provider.maxReconnectionAttempts})'
? '${provider.reconnectionAttempt}/${provider.maxReconnectionAttempts}'
: 'Connect',
),
style: ElevatedButton.styleFrom(
@@ -691,123 +756,72 @@ class _HomeScreenState extends State<HomeScreen>
],
],
)
else
Row(
mainAxisSize: MainAxisSize.min,
children: [
// Advertise button (broadcast location)
FilledButton(
onPressed: () => _advertiseDevice(context),
style: FilledButton.styleFrom(
backgroundColor: Colors.blue.shade700,
foregroundColor: Colors.white,
padding: const EdgeInsets.all(10),
minimumSize: const Size(40, 40),
shape: const CircleBorder(),
else if (_showRxTxIndicators)
GestureDetector(
onLongPress: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
PacketLogScreen(bleService: provider.bleService),
),
child: const Icon(Icons.campaign, size: 20),
),
if (_showRxTxIndicators) ...[
const SizedBox(width: 8),
// RX/TX indicators with long press to open packet log
GestureDetector(
onLongPress: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
PacketLogScreen(bleService: provider.bleService),
);
},
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// RX indicator
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: provider.rxActivity
? Colors.green
: Colors.grey.withOpacity(0.3),
),
);
},
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// RX indicator
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: provider.rxActivity
? Colors.green
: Colors.grey.withOpacity(0.3),
),
),
const SizedBox(width: 4),
Text(
'RX:${provider.rxPacketCount}',
style: const TextStyle(
fontSize: 11,
color: Colors.grey,
),
),
],
),
const SizedBox(width: 4),
Text(
'RX:${provider.rxPacketCount}',
style: const TextStyle(
fontSize: 11,
color: Colors.grey,
),
const SizedBox(height: 4),
// TX indicator
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: provider.txActivity
? Colors.blue
: Colors.grey.withOpacity(0.3),
),
),
const SizedBox(width: 4),
Text(
'TX:${provider.txPacketCount}',
style: const TextStyle(
fontSize: 11,
color: Colors.grey,
),
),
],
),
],
),
),
],
),
const SizedBox(height: 4),
// TX indicator
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: provider.txActivity
? Colors.blue
: Colors.grey.withOpacity(0.3),
),
),
const SizedBox(width: 4),
Text(
'TX:${provider.txPacketCount}',
style: const TextStyle(
fontSize: 11,
color: Colors.grey,
),
),
],
),
const SizedBox(width: 12),
],
// Settings button (tap for device settings, long press for packet logs)
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DeviceConfigScreen(),
),
);
},
onLongPress: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
PacketLogScreen(bleService: provider.bleService),
),
);
},
child: Container(
width: 36,
height: 36,
alignment: Alignment.center,
child: const Icon(Icons.settings, size: 20),
),
),
const SizedBox(width: 16),
// Disconnect button (prominent, icon only) - pushed to far right edge
],
),
),
],
);

View File

@@ -8,17 +8,22 @@ import '../providers/contacts_provider.dart';
import '../providers/messages_provider.dart';
import '../providers/app_provider.dart';
import '../services/location_tracking_service.dart';
import '../services/locale_preferences.dart';
import '../utils/sample_data_generator.dart';
import '../theme/app_theme.dart';
class SettingsScreen extends StatefulWidget {
final Function(AppThemeMode) onThemeChanged;
final Function(Locale?) onLocaleChanged;
final AppThemeMode currentTheme;
final Locale? currentLocale;
const SettingsScreen({
super.key,
required this.onThemeChanged,
required this.onLocaleChanged,
required this.currentTheme,
required this.currentLocale,
});
@override
@@ -27,6 +32,7 @@ class SettingsScreen extends StatefulWidget {
class _SettingsScreenState extends State<SettingsScreen> {
late AppThemeMode _selectedTheme;
late Locale? _selectedLocale;
PackageInfo? _packageInfo;
bool _isLoadingSampleData = false;
bool _showRxTxIndicators = true;
@@ -36,6 +42,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
void initState() {
super.initState();
_selectedTheme = widget.currentTheme;
_selectedLocale = widget.currentLocale;
_loadPackageInfo();
_initializeLocationService();
_loadRxTxPreference();
@@ -135,6 +142,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
}
}
Future<void> _saveLocalePreference(Locale? locale) async {
await LocalePreferences.setLocale(locale);
}
void _handleLocaleChange(Locale? locale) {
setState(() {
_selectedLocale = locale;
});
_saveLocalePreference(locale);
widget.onLocaleChanged(locale);
}
Future<void> _loadSampleData() async {
setState(() => _isLoadingSampleData = true);
@@ -322,6 +341,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
await _saveRxTxPreference(value);
},
),
ListTile(
leading: const Icon(Icons.language),
title: const Text('Language'),
subtitle: Text(LocalePreferences.getDisplayName(_selectedLocale)),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showLanguageDialog(),
),
const Divider(),
// Location Settings Section
@@ -761,6 +787,50 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
void _showLanguageDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Choose Language'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile<Locale?>(
title: const Text('System Default'),
subtitle: const Text('Follow system language'),
value: null,
groupValue: _selectedLocale,
onChanged: (value) {
_handleLocaleChange(value);
Navigator.pop(context);
},
),
const Divider(),
...LocalePreferences.supportedLocales.map((locale) {
return RadioListTile<Locale?>(
title: Text(LocalePreferences.getNativeDisplayName(locale)),
value: locale,
groupValue: _selectedLocale,
onChanged: (value) {
_handleLocaleChange(value);
Navigator.pop(context);
},
);
}),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
],
),
);
}
void _showAboutDialog() {
showDialog(
context: context,