mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 08:20:36 +00:00
i18n: Add 247 missing translation keys across all 13 languages
Replace hardcoded English strings in 32 Dart source files with l10n references. Covers settings labels, UI actions, map/SAR features, device config, notifications, profiles, spectrum scan, and status messages for sl, de, hr, pl, es, fr, it, el, ru, tr, uk, zh, pt.
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
class AddContactScreen extends StatefulWidget {
|
||||
const AddContactScreen({super.key});
|
||||
@@ -80,7 +81,7 @@ class _AddContactScreenState extends State<AddContactScreen> {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Clipboard is empty')));
|
||||
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.clipboardIsEmpty)));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -141,7 +142,7 @@ class _AddContactScreenState extends State<AddContactScreen> {
|
||||
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Contact imported')));
|
||||
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.contactImported)));
|
||||
setState(() {
|
||||
_importSucceeded = true;
|
||||
});
|
||||
@@ -154,7 +155,7 @@ class _AddContactScreenState extends State<AddContactScreen> {
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Add Contact')),
|
||||
appBar: AppBar(title: Text(AppLocalizations.of(context)!.addContact)),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
@@ -205,18 +206,18 @@ class _AddContactScreenState extends State<AddContactScreen> {
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: const [
|
||||
children: [
|
||||
_ImportHintChip(
|
||||
icon: Icons.link_outlined,
|
||||
label: 'Accepts share links',
|
||||
label: AppLocalizations.of(context)!.acceptsShareLinks,
|
||||
),
|
||||
_ImportHintChip(
|
||||
icon: Icons.code_outlined,
|
||||
label: 'Supports raw hex',
|
||||
label: AppLocalizations.of(context)!.supportsRawHex,
|
||||
),
|
||||
_ImportHintChip(
|
||||
icon: Icons.content_paste_go_outlined,
|
||||
label: 'Clipboard-friendly',
|
||||
label: AppLocalizations.of(context)!.clipboardfriendly,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -285,8 +286,8 @@ class _AddContactScreenState extends State<AddContactScreen> {
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: _isImporting ? null : _pasteFromClipboard,
|
||||
icon: const Icon(Icons.content_paste_go_outlined),
|
||||
label: const Text('Paste'),
|
||||
icon: Icon(Icons.content_paste_go_outlined),
|
||||
label: Text(AppLocalizations.of(context)!.paste),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -493,7 +493,7 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.message_outlined),
|
||||
leading: Icon(Icons.message_outlined),
|
||||
title: Text(l10n.messages),
|
||||
onTap: () async {
|
||||
Navigator.pop(sheetContext);
|
||||
@@ -502,7 +502,7 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
),
|
||||
if (channel.displayLocation != null)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.map_outlined),
|
||||
leading: Icon(Icons.map_outlined),
|
||||
title: Text(l10n.viewOnMap),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
@@ -511,7 +511,7 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
),
|
||||
if (!channel.isPublicChannel)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete, color: Colors.red),
|
||||
leading: Icon(Icons.delete, color: Colors.red),
|
||||
title: Text(
|
||||
l10n.deleteChannel,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
@@ -655,12 +655,12 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
size: 64,
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.noContactsYet,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.connectToDeviceToLoadContacts,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
@@ -674,8 +674,8 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => _openAddContactScreen(context),
|
||||
icon: const Icon(Icons.person_add_alt_1_outlined),
|
||||
label: const Text('Add Contact'),
|
||||
icon: Icon(Icons.person_add_alt_1_outlined),
|
||||
label: Text(l10n.addContact),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -691,7 +691,7 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
// Favourites (contacts with firmware favourite flag set)
|
||||
if (contactsProvider.favouriteContacts.isNotEmpty) ...[
|
||||
_SectionHeader(
|
||||
title: 'Favourites',
|
||||
title: l10n.favourites,
|
||||
count: contactsProvider.favouriteContacts.length,
|
||||
icon: Icons.star,
|
||||
),
|
||||
@@ -766,8 +766,8 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
.read<ConnectionProvider>()
|
||||
.discoverNodeType(advertType: 2);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Repeater discovery sent'),
|
||||
SnackBar(
|
||||
content: Text(l10n.repeaterDiscoverySent),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -802,7 +802,7 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
_buildNoFilterResults(context)
|
||||
else if (showRepeatersOthersGroup)
|
||||
_InferredContactGroupCard(
|
||||
label: 'Others',
|
||||
label: l10n.others,
|
||||
contacts: ungroupedRepeaters,
|
||||
compactContacts: true,
|
||||
currentPosition: _currentPosition,
|
||||
@@ -822,7 +822,7 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
// Sensors
|
||||
if (showSensorsSection) ...[
|
||||
_SectionHeader(
|
||||
title: 'Sensors',
|
||||
title: l10n.sensors,
|
||||
count: sensors.length,
|
||||
icon: Icons.sensors,
|
||||
trailing: Row(
|
||||
@@ -841,8 +841,8 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
.read<ConnectionProvider>()
|
||||
.discoverNodeType(advertType: 4);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Sensor discovery sent'),
|
||||
SnackBar(
|
||||
content: Text(l10n.sensorDiscoverySent),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -946,8 +946,8 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => _openAddContactScreen(context),
|
||||
icon: const Icon(Icons.person_add_alt_1_outlined),
|
||||
label: const Text('Add Contact'),
|
||||
icon: Icon(Icons.person_add_alt_1_outlined),
|
||||
label: Text(l10n.addContact),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
@@ -960,7 +960,7 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => _showAddChannelDialog(context),
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
icon: Icon(Icons.add_circle_outline),
|
||||
label: Text(l10n.addChannel),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -1285,7 +1285,7 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
? colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(width: 8),
|
||||
Text(l10n.lastSeen),
|
||||
],
|
||||
),
|
||||
@@ -1301,7 +1301,7 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
? colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(width: 8),
|
||||
Text(l10n.distance),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -827,7 +827,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Wipe device data'),
|
||||
title: Text(AppLocalizations.of(context)!.wipeDeviceData),
|
||||
content: const Text(
|
||||
'This will erase all data on the connected device, including contacts, keys, and saved settings. This cannot be undone.',
|
||||
),
|
||||
@@ -842,7 +842,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
foregroundColor: Theme.of(context).colorScheme.onError,
|
||||
),
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: const Text('Wipe device'),
|
||||
child: Text(AppLocalizations.of(context)!.wipeDevice),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -893,7 +893,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
.toList();
|
||||
if (contacts.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('No device contacts to clear.')),
|
||||
SnackBar(content: Text(AppLocalizations.of(context)!.noDeviceContactsToClear)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -901,7 +901,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Clear all contacts'),
|
||||
title: Text(AppLocalizations.of(context)!.clearAllContacts),
|
||||
content: Text(
|
||||
'This will remove ${contacts.length} contact${contacts.length == 1 ? '' : 's'} from the connected device. Channels and radio settings will not be changed.',
|
||||
),
|
||||
@@ -916,7 +916,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
foregroundColor: Theme.of(context).colorScheme.onError,
|
||||
),
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: const Text('Clear contacts'),
|
||||
child: Text(AppLocalizations.of(context)!.clearContacts),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -988,7 +988,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
.toList();
|
||||
if (channels.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('No custom channels to clear.')),
|
||||
SnackBar(content: Text(AppLocalizations.of(context)!.noCustomChannelsToClear)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -996,7 +996,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Clear all channels'),
|
||||
title: Text(AppLocalizations.of(context)!.clearAllChannels),
|
||||
content: Text(
|
||||
'This will remove ${channels.length} custom channel${channels.length == 1 ? '' : 's'} from the connected device. Contacts and radio settings will not be changed.',
|
||||
),
|
||||
@@ -1011,7 +1011,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
foregroundColor: Theme.of(context).colorScheme.onError,
|
||||
),
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: const Text('Clear channels'),
|
||||
child: Text(AppLocalizations.of(context)!.clearChannels),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1119,23 +1119,23 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
'${_getDeviceTypeString(context, deviceInfo.deviceType)} • ${deviceInfo.semanticVersion ?? deviceInfo.manufacturerModel ?? AppLocalizations.of(context)!.unknown}',
|
||||
stats: [
|
||||
_HeroStatData(
|
||||
label: 'Location',
|
||||
label: AppLocalizations.of(context)!.location,
|
||||
value: locationSet ? 'Shared' : 'Hidden',
|
||||
icon: Icons.my_location_rounded,
|
||||
emphasized: locationSet,
|
||||
),
|
||||
_HeroStatData(
|
||||
label: 'Frequency',
|
||||
label: AppLocalizations.of(context)!.frequency,
|
||||
value: '${_freqController.text} MHz',
|
||||
icon: Icons.settings_input_antenna_rounded,
|
||||
),
|
||||
_HeroStatData(
|
||||
label: 'Bandwidth',
|
||||
label: AppLocalizations.of(context)!.bandwidth,
|
||||
value: _selectedBandwidth,
|
||||
icon: Icons.width_normal_rounded,
|
||||
),
|
||||
_HeroStatData(
|
||||
label: 'Model',
|
||||
label: AppLocalizations.of(context)!.model,
|
||||
value:
|
||||
deviceInfo.manufacturerModel ??
|
||||
AppLocalizations.of(context)!.unknown,
|
||||
@@ -1143,10 +1143,10 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(height: 20),
|
||||
_ConfigSectionCard(
|
||||
title: 'Storage',
|
||||
subtitle: 'Available space on this device.',
|
||||
title: AppLocalizations.of(context)!.storage,
|
||||
subtitle: AppLocalizations.of(context)!.availableSpaceOnThisDevice,
|
||||
icon: Icons.storage_rounded,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -1155,16 +1155,16 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: _StorageStat(
|
||||
label: 'Used',
|
||||
label: AppLocalizations.of(context)!.used,
|
||||
value: _formatStorage(
|
||||
deviceInfo.storageUsedKb ?? 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _StorageStat(
|
||||
label: 'Total',
|
||||
label: AppLocalizations.of(context)!.total,
|
||||
value: deviceInfo.storageTotalKb != null
|
||||
? _formatStorage(deviceInfo.storageTotalKb!)
|
||||
: AppLocalizations.of(context)!.unknown,
|
||||
@@ -1177,9 +1177,9 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(height: 20),
|
||||
_ConfigSectionCard(
|
||||
title: 'Auto discovery',
|
||||
title: AppLocalizations.of(context)!.autoDiscovery,
|
||||
subtitle:
|
||||
'Control how the radio auto-adds discovered nodes to its contacts table.',
|
||||
icon: Icons.person_search_rounded,
|
||||
@@ -1190,7 +1190,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
icon: _autoAddDiscoveredContactsEnabled
|
||||
? Icons.person_add_alt_1
|
||||
: Icons.person_add_disabled,
|
||||
title: 'Enable automatic adding',
|
||||
title: AppLocalizations.of(context)!.enableAutomaticAdding,
|
||||
description:
|
||||
'Turn this off to keep discoveries manual-only on the radio.',
|
||||
accentColor: _autoAddDiscoveredContactsEnabled
|
||||
@@ -1206,10 +1206,10 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
SizedBox(height: 18),
|
||||
_SettingHighlightCard(
|
||||
icon: Icons.person_outline_rounded,
|
||||
title: 'Auto-add users',
|
||||
title: AppLocalizations.of(context)!.autoaddUsers,
|
||||
description:
|
||||
'Automatically store discovered user/chat nodes.',
|
||||
accentColor: _autoAddUsersEnabled
|
||||
@@ -1227,10 +1227,10 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
SizedBox(height: 14),
|
||||
_SettingHighlightCard(
|
||||
icon: Icons.router_outlined,
|
||||
title: 'Auto-add repeaters',
|
||||
title: AppLocalizations.of(context)!.autoaddRepeaters,
|
||||
description:
|
||||
'Automatically store discovered repeater nodes.',
|
||||
accentColor: _autoAddRepeatersEnabled
|
||||
@@ -1248,10 +1248,10 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
SizedBox(height: 14),
|
||||
_SettingHighlightCard(
|
||||
icon: Icons.meeting_room_outlined,
|
||||
title: 'Auto-add room servers',
|
||||
title: AppLocalizations.of(context)!.autoaddRoomServers,
|
||||
description:
|
||||
'Automatically store discovered room/server nodes.',
|
||||
accentColor: _autoAddRoomServersEnabled
|
||||
@@ -1269,10 +1269,10 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
SizedBox(height: 14),
|
||||
_SettingHighlightCard(
|
||||
icon: Icons.sensors_outlined,
|
||||
title: 'Auto-add sensors',
|
||||
title: AppLocalizations.of(context)!.autoaddSensors,
|
||||
description:
|
||||
'Automatically store discovered sensor nodes.',
|
||||
accentColor: _autoAddSensorsEnabled
|
||||
@@ -1290,10 +1290,10 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
SizedBox(height: 14),
|
||||
_SettingHighlightCard(
|
||||
icon: Icons.history_toggle_off_rounded,
|
||||
title: 'Overwrite oldest when full',
|
||||
title: AppLocalizations.of(context)!.overwriteOldestWhenFull,
|
||||
description:
|
||||
'Allow the radio to replace the oldest contact when storage is full.',
|
||||
accentColor: _overwriteOldestAutoAddEnabled
|
||||
@@ -1329,16 +1329,16 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
: _saveAutoDiscoverySettings,
|
||||
isSaving: _isSavingAutoDiscoverySettings,
|
||||
isSaved: _autoDiscoverySettingsSaved,
|
||||
label: 'Save discovery settings',
|
||||
label: AppLocalizations.of(context)!.saveDiscoverySettings,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(height: 20),
|
||||
_ConfigSectionCard(
|
||||
title: AppLocalizations.of(context)!.publicInfo,
|
||||
subtitle: 'Choose the name and location this device shares.',
|
||||
subtitle: AppLocalizations.of(context)!.chooseTheNameAndLocationThisDeviceShares,
|
||||
icon: Icons.public_rounded,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -1371,7 +1371,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
icon: _gpsEnabled!
|
||||
? Icons.gps_fixed
|
||||
: Icons.gps_off,
|
||||
title: 'GPS Module',
|
||||
title: AppLocalizations.of(context)!.gpsModule,
|
||||
description:
|
||||
'Enable or disable the onboard GPS hardware.',
|
||||
accentColor: _gpsEnabled!
|
||||
@@ -1483,16 +1483,16 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
onPressed: _isSavingPublicInfo ? null : _savePublicInfo,
|
||||
isSaving: _isSavingPublicInfo,
|
||||
isSaved: _publicInfoSaved,
|
||||
label: 'Save public info',
|
||||
label: AppLocalizations.of(context)!.savePublicInfo,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(height: 20),
|
||||
_ConfigSectionCard(
|
||||
title: AppLocalizations.of(context)!.radioSettings,
|
||||
subtitle: 'Choose a preset or fine-tune custom radio settings.',
|
||||
subtitle: AppLocalizations.of(context)!.chooseAPresetOrFinetuneCustomRadioSettings,
|
||||
icon: Icons.settings_input_antenna_rounded,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -1512,9 +1512,9 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
),
|
||||
isExpanded: true,
|
||||
items: [
|
||||
const DropdownMenuItem<_RadioPreset?>(
|
||||
DropdownMenuItem<_RadioPreset?>(
|
||||
value: null,
|
||||
child: Text('Custom'),
|
||||
child: Text(AppLocalizations.of(context)!.custom),
|
||||
),
|
||||
..._radioPresets.map(
|
||||
(preset) => DropdownMenuItem<_RadioPreset?>(
|
||||
@@ -1713,10 +1713,10 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
),
|
||||
),
|
||||
if (deviceInfo.clientRepeat != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(height: 16),
|
||||
_SettingHighlightCard(
|
||||
icon: Icons.repeat_rounded,
|
||||
title: 'Repeat nearby traffic',
|
||||
title: AppLocalizations.of(context)!.repeatNearbyTraffic,
|
||||
description:
|
||||
deviceInfo.allowedRepeatFreqRanges != null &&
|
||||
deviceInfo.allowedRepeatFreqRanges!.isNotEmpty
|
||||
@@ -1753,16 +1753,16 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
: _saveRadioSettings,
|
||||
isSaving: _isSavingRadioSettings,
|
||||
isSaved: _radioSettingsSaved,
|
||||
label: 'Save radio settings',
|
||||
label: AppLocalizations.of(context)!.saveRadioSettings,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(height: 20),
|
||||
_ConfigSectionCard(
|
||||
title: 'Danger zone',
|
||||
subtitle: 'Destructive device actions.',
|
||||
title: AppLocalizations.of(context)!.dangerZone,
|
||||
subtitle: AppLocalizations.of(context)!.destructiveDeviceActions,
|
||||
icon: Icons.warning_amber_rounded,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -1830,8 +1830,8 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.people_alt_outlined),
|
||||
label: const Text('Clear all contacts'),
|
||||
: Icon(Icons.people_alt_outlined),
|
||||
label: Text(AppLocalizations.of(context)!.clearAllContacts),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -1854,8 +1854,8 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.forum_outlined),
|
||||
label: const Text('Clear all channels'),
|
||||
: Icon(Icons.forum_outlined),
|
||||
label: Text(AppLocalizations.of(context)!.clearAllChannels),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -1870,8 +1870,8 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
foregroundColor: colorScheme.onError,
|
||||
minimumSize: const Size.fromHeight(52),
|
||||
),
|
||||
icon: const Icon(Icons.delete_forever_rounded),
|
||||
label: const Text('Wipe device data'),
|
||||
icon: Icon(Icons.delete_forever_rounded),
|
||||
label: Text(AppLocalizations.of(context)!.wipeDeviceData),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -54,7 +54,7 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Clear discoveries'),
|
||||
title: Text(AppLocalizations.of(context)!.clearDiscoveries),
|
||||
content: Text(
|
||||
'Remove all $pendingCount pending discover${pendingCount == 1 ? 'y' : 'ies'} from this device?',
|
||||
),
|
||||
@@ -65,7 +65,7 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: const Text('Clear all'),
|
||||
child: Text(AppLocalizations.of(context)!.clearAllLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -80,7 +80,7 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
return;
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Cleared pending discoveries.')),
|
||||
SnackBar(content: Text(AppLocalizations.of(context)!.clearedPendingDiscoveries)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -363,7 +363,7 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Discovery'),
|
||||
title: Text(l10n.discovery),
|
||||
actions: [
|
||||
Consumer<ConnectionProvider>(
|
||||
builder: (context, connectionProvider, child) {
|
||||
@@ -393,8 +393,8 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.router_outlined),
|
||||
const SizedBox(width: 12),
|
||||
const Text('Discover repeaters'),
|
||||
SizedBox(width: 12),
|
||||
Text(l10n.discoverRepeaters),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -412,8 +412,8 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.sensors_outlined),
|
||||
const SizedBox(width: 12),
|
||||
const Text('Discover sensors'),
|
||||
SizedBox(width: 12),
|
||||
Text(l10n.discoverSensors),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -490,7 +490,7 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
: const Icon(
|
||||
Icons.download_for_offline_outlined,
|
||||
),
|
||||
label: const Text('Resolve all'),
|
||||
label: Text(l10n.resolveAll),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
@@ -499,8 +499,8 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
onPressed: pendingAdverts.isNotEmpty
|
||||
? _clearAllDiscoveries
|
||||
: null,
|
||||
icon: const Icon(Icons.clear_all_rounded),
|
||||
label: const Text('Clear all'),
|
||||
icon: Icon(Icons.clear_all_rounded),
|
||||
label: Text(l10n.clearAllLabel),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -362,7 +362,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
),
|
||||
title: Text(l10n.flood),
|
||||
subtitle: const Text('Relay through repeaters across the mesh'),
|
||||
subtitle: Text(l10n.relayThroughRepeatersAcrossTheMesh),
|
||||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
@@ -388,7 +388,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
),
|
||||
title: Text(l10n.direct),
|
||||
subtitle: const Text('Nearby only, without repeater flooding'),
|
||||
subtitle: Text(l10n.nearbyOnlyWithoutRepeaterFlooding),
|
||||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
@@ -609,7 +609,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
onPressed: () async {
|
||||
await provider.disconnect();
|
||||
},
|
||||
icon: const Icon(Icons.power_settings_new),
|
||||
icon: Icon(Icons.power_settings_new),
|
||||
tooltip: AppLocalizations.of(context)!.disconnect,
|
||||
color: Colors.red.shade700,
|
||||
);
|
||||
@@ -631,8 +631,8 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.radar),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Spectrum Scan'),
|
||||
SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.spectrumScan),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
@@ -653,11 +653,11 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
child: const Row(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.radar_outlined),
|
||||
SizedBox(width: 8),
|
||||
Text('Live Traffic'),
|
||||
const Icon(Icons.radar_outlined),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.liveTraffic),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
@@ -688,11 +688,11 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
child: const Row(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.router_outlined),
|
||||
SizedBox(width: 8),
|
||||
Text('Repeaters Map'),
|
||||
const Icon(Icons.router_outlined),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.repeatersMap),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
@@ -749,7 +749,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.settings),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.settings),
|
||||
],
|
||||
),
|
||||
@@ -777,11 +777,11 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
if (profilesEnabled) {
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
child: const Row(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.layers_outlined),
|
||||
SizedBox(width: 8),
|
||||
Text('Profiles'),
|
||||
const Icon(Icons.layers_outlined),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.profiles),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
@@ -889,7 +889,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
);
|
||||
case _HomeTab.map:
|
||||
return Tab(
|
||||
icon: const Icon(Icons.map),
|
||||
icon: Icon(Icons.map),
|
||||
text: AppLocalizations.of(context)!.map,
|
||||
);
|
||||
case _HomeTab.sensors:
|
||||
@@ -941,7 +941,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.bluetooth, size: 18),
|
||||
: Icon(Icons.bluetooth, size: 18),
|
||||
label: Text(
|
||||
provider.isReconnecting
|
||||
? '${provider.reconnectionAttempt}/${provider.maxReconnectionAttempts}'
|
||||
@@ -960,7 +960,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
onPressed: () => provider.cancelReconnection(),
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
icon: Icon(Icons.close, size: 20),
|
||||
tooltip: AppLocalizations.of(context)!.cancelReconnection,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade700,
|
||||
|
||||
@@ -15,6 +15,7 @@ import '../utils/log_rx_route_decoder.dart';
|
||||
import '../widgets/compact_signal_indicator.dart';
|
||||
import '../widgets/messages/message_trace_sheet.dart';
|
||||
import 'packet_log_screen.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
T? _maybeProvider<T>(BuildContext context) {
|
||||
try {
|
||||
@@ -147,7 +148,7 @@ class _LiveTrafficScreenState extends State<LiveTrafficScreen> {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Live Traffic'),
|
||||
title: Text(AppLocalizations.of(context)!.liveTraffic),
|
||||
actions: [
|
||||
if (widget.openPacketLogs != null)
|
||||
IconButton(
|
||||
@@ -327,17 +328,17 @@ class _SummaryPanel extends StatelessWidget {
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
_SummaryBadge(
|
||||
label: 'Mesh',
|
||||
label: AppLocalizations.of(context)!.mesh,
|
||||
value: busynessLabel,
|
||||
color: busynessColor,
|
||||
),
|
||||
_SummaryBadge(
|
||||
label: 'Rate',
|
||||
label: AppLocalizations.of(context)!.rate,
|
||||
value: '${snapshot.packetsPerMinute} pkt/min',
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
_SummaryBadge(
|
||||
label: 'Window',
|
||||
label: AppLocalizations.of(context)!.window,
|
||||
value: _windowLabel(snapshot.windowDuration),
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
onTap: onWindowTap,
|
||||
@@ -350,14 +351,14 @@ class _SummaryPanel extends StatelessWidget {
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
_MetricTile(
|
||||
label: 'RX packets',
|
||||
label: AppLocalizations.of(context)!.rxPackets,
|
||||
value: '${snapshot.rxCount}',
|
||||
subtitle: totalRxCount == null
|
||||
? _windowSummaryLabel(snapshot.windowDuration)
|
||||
: 'Device total $totalRxCount',
|
||||
),
|
||||
_MetricTile(
|
||||
label: 'RSSI',
|
||||
label: AppLocalizations.of(context)!.rssi,
|
||||
value: snapshot.latestRssiDbm == null
|
||||
? 'No RX data'
|
||||
: '${snapshot.latestRssiDbm} dBm',
|
||||
@@ -366,7 +367,7 @@ class _SummaryPanel extends StatelessWidget {
|
||||
: 'Avg ${snapshot.avgRssiDbm!.toStringAsFixed(1)} dBm',
|
||||
),
|
||||
_MetricTile(
|
||||
label: 'SNR',
|
||||
label: AppLocalizations.of(context)!.snr,
|
||||
value: snapshot.latestSnrDb == null
|
||||
? 'No RX data'
|
||||
: '${snapshot.latestSnrDb!.toStringAsFixed(1)} dB',
|
||||
@@ -375,7 +376,7 @@ class _SummaryPanel extends StatelessWidget {
|
||||
: 'Avg ${snapshot.avgSnrDb!.toStringAsFixed(1)} dB',
|
||||
),
|
||||
_MetricTile(
|
||||
label: 'Multi-hop',
|
||||
label: AppLocalizations.of(context)!.multihop,
|
||||
value: '${snapshot.multiHopCount}',
|
||||
subtitle: routeHashCounts.summaryLabel,
|
||||
footer: snapshot.avgHopCount == null
|
||||
@@ -455,7 +456,7 @@ class _PacketTypeFilterBar extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
_FilterChip(
|
||||
label: 'All',
|
||||
label: AppLocalizations.of(context)!.all,
|
||||
selected: selectedType == null,
|
||||
onTap: () => onSelected(null),
|
||||
),
|
||||
|
||||
@@ -519,7 +519,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.layers),
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.selectMapLayer,
|
||||
@@ -644,7 +644,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
!rootContext.read<MapProvider>().isUsingCustomMap &&
|
||||
_currentLayer == _dtk25Layer
|
||||
? const Icon(Icons.check_circle, color: Colors.green)
|
||||
: const Icon(Icons.radio_button_unchecked),
|
||||
: Icon(Icons.radio_button_unchecked),
|
||||
title: Text(AppLocalizations.of(context)!.topographicMap),
|
||||
subtitle: Text(_dtk25Layer.attribution),
|
||||
onTap: () async {
|
||||
@@ -872,8 +872,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
),
|
||||
if (!hasCustomMap)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add_photo_alternate),
|
||||
title: const Text('Load from gallery'),
|
||||
leading: Icon(Icons.add_photo_alternate),
|
||||
title: Text(AppLocalizations.of(context)!.loadFromGallery),
|
||||
subtitle: const Text(
|
||||
'Use a cave map image instead of GPS tiles',
|
||||
),
|
||||
@@ -904,8 +904,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.swap_horizontal_circle),
|
||||
title: const Text('Replace image'),
|
||||
leading: Icon(Icons.swap_horizontal_circle),
|
||||
title: Text(AppLocalizations.of(context)!.replaceImage),
|
||||
subtitle: const Text(
|
||||
'Pick a different map from the gallery',
|
||||
),
|
||||
@@ -940,8 +940,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
),
|
||||
if (customMapConfig.isCalibrated)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.clear),
|
||||
title: const Text('Clear scale'),
|
||||
leading: Icon(Icons.clear),
|
||||
title: Text(AppLocalizations.of(context)!.clearScale),
|
||||
onTap: () async {
|
||||
await mapProvider.clearCustomMapCalibration();
|
||||
if (!context.mounted) return;
|
||||
@@ -1218,7 +1218,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertDialog(
|
||||
title: const Text('Set map scale'),
|
||||
title: Text(AppLocalizations.of(context)!.setMapScale),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
@@ -1279,7 +1279,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Custom map scale saved')));
|
||||
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.customMapScaleSaved)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1417,9 +1417,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
if (message != null)
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.chat_bubble_outline),
|
||||
title: const Text('Open message'),
|
||||
subtitle: const Text('Jump to the related SAR message'),
|
||||
leading: Icon(Icons.chat_bubble_outline),
|
||||
title: Text(AppLocalizations.of(context)!.openMessage),
|
||||
subtitle: Text(AppLocalizations.of(context)!.jumpToTheRelatedSarMessage),
|
||||
onTap: () async {
|
||||
Navigator.pop(sheetContext);
|
||||
await _openSarMarkerMessage(
|
||||
@@ -1468,7 +1468,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Remove SAR marker'),
|
||||
title: Text(AppLocalizations.of(context)!.removeSarMarker),
|
||||
content: Text(
|
||||
hasMessage
|
||||
? 'This will remove the marker and its linked chat message.'
|
||||
@@ -1654,8 +1654,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
if (!sendToChannel && !sendToAllContacts && roomPublicKey == null) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Please select a destination to send SAR marker'),
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.pleaseSelectADestinationToSendSarMarker),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
@@ -1784,8 +1784,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('SAR marker broadcast to public channel'),
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.sarMarkerBroadcastToPublicChannel),
|
||||
backgroundColor: Colors.orange,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
@@ -1832,8 +1832,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('SAR marker sent to room'),
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.sarMarkerSentToRoom),
|
||||
backgroundColor: Colors.green,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
@@ -3026,7 +3026,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.measureDistance,
|
||||
style: const TextStyle(
|
||||
|
||||
@@ -535,7 +535,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
}).firstOrNull;
|
||||
|
||||
if (recipient == null) {
|
||||
ToastLogger.error(context, l10n.cannotReplyContactNotFound);
|
||||
ToastLogger.error(context, AppLocalizations.of(context)!.cannotReplyContactNotFound);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -564,13 +564,13 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
} else {
|
||||
final senderPrefix = message.senderPublicKeyPrefix;
|
||||
if (senderPrefix == null || senderPrefix.length < 6) {
|
||||
ToastLogger.error(context, l10n.cannotReplySenderMissing);
|
||||
ToastLogger.error(context, AppLocalizations.of(context)!.cannotReplySenderMissing);
|
||||
return;
|
||||
}
|
||||
|
||||
recipient = contactsProvider.findContactByPrefix(senderPrefix);
|
||||
if (recipient == null) {
|
||||
ToastLogger.error(context, l10n.cannotReplyContactNotFound);
|
||||
ToastLogger.error(context, AppLocalizations.of(context)!.cannotReplyContactNotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1376,7 +1376,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
final decision = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Send to Public Channel?'),
|
||||
title: Text(AppLocalizations.of(context)!.sendToPublicChannel),
|
||||
content: Text(
|
||||
'You are about to send $mediaType to the Public Channel. '
|
||||
'This is not advised because everyone on the mesh may receive it. '
|
||||
@@ -1389,7 +1389,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: const Text('Send anyway'),
|
||||
child: Text(AppLocalizations.of(context)!.sendAnyway),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1449,8 +1449,8 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.search),
|
||||
title: const Text('Search messages'),
|
||||
leading: Icon(Icons.search),
|
||||
title: Text(AppLocalizations.of(context)!.searchMessages),
|
||||
onTap: () async {
|
||||
await _runAfterSheetDismissal(sheetContext, () async {
|
||||
_showFilteredMessageSearch();
|
||||
@@ -1458,7 +1458,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add_location_alt),
|
||||
leading: Icon(Icons.add_location_alt),
|
||||
title: Text(AppLocalizations.of(context)!.sendSarMarker),
|
||||
onTap: () async {
|
||||
await _runAfterSheetDismissal(sheetContext, () async {
|
||||
@@ -1485,8 +1485,8 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
),
|
||||
ListTile(
|
||||
enabled: !_isSendingImage,
|
||||
leading: const Icon(Icons.photo_library),
|
||||
title: const Text('Send image from gallery'),
|
||||
leading: Icon(Icons.photo_library),
|
||||
title: Text(AppLocalizations.of(context)!.sendImageFromGallery),
|
||||
onTap: _isSendingImage
|
||||
? null
|
||||
: () async {
|
||||
@@ -1497,8 +1497,8 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
),
|
||||
ListTile(
|
||||
enabled: !_isSendingImage,
|
||||
leading: const Icon(Icons.camera_alt),
|
||||
title: const Text('Take photo'),
|
||||
leading: Icon(Icons.camera_alt),
|
||||
title: Text(AppLocalizations.of(context)!.takePhoto),
|
||||
onTap: _isSendingImage
|
||||
? null
|
||||
: () async {
|
||||
@@ -1508,9 +1508,9 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.grid_3x3),
|
||||
title: const Text('Start Tic-Tac-Toe'),
|
||||
subtitle: const Text('DM only'),
|
||||
leading: Icon(Icons.grid_3x3),
|
||||
title: Text(AppLocalizations.of(context)!.startTictactoe),
|
||||
subtitle: Text(AppLocalizations.of(context)!.dmOnly),
|
||||
onTap: () async {
|
||||
await _runAfterSheetDismissal(sheetContext, () async {
|
||||
await _startTicTacToeGame();
|
||||
|
||||
@@ -61,7 +61,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('No logs to export')));
|
||||
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.noLogsToExport)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -108,7 +108,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('No logs to export')));
|
||||
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.noLogsToExport)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -155,8 +155,8 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
void _copyToClipboard(BuildContext context, BlePacketLog log) {
|
||||
Clipboard.setData(ClipboardData(text: log.hexData));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Hex data copied to clipboard'),
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.hexDataCopiedToClipboard),
|
||||
duration: Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
@@ -184,7 +184,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
setState(() {});
|
||||
if (parentContext.mounted) {
|
||||
ScaffoldMessenger.of(parentContext).showSnackBar(
|
||||
const SnackBar(content: Text('Packet logs cleared')),
|
||||
SnackBar(content: Text(AppLocalizations.of(context)!.packetLogsCleared)),
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -205,7 +205,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('BLE Packet Logs'),
|
||||
Text(AppLocalizations.of(context)!.blePacketLogs),
|
||||
Text(
|
||||
'${logs.length} packets',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
@@ -316,23 +316,23 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
icon: const Icon(Icons.share),
|
||||
tooltip: 'Export logs',
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
PopupMenuItem(
|
||||
value: 'csv',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.table_chart),
|
||||
SizedBox(width: 8),
|
||||
Text('Export as CSV'),
|
||||
const Icon(Icons.table_chart),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.exportAsCsv),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
PopupMenuItem(
|
||||
value: 'txt',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.text_snippet),
|
||||
SizedBox(width: 8),
|
||||
Text('Export as Text'),
|
||||
const Icon(Icons.text_snippet),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.exportAsText),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -411,8 +411,8 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
_filterDirection = null;
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.clear_all),
|
||||
label: const Text('Clear filters'),
|
||||
icon: Icon(Icons.clear_all),
|
||||
label: Text(AppLocalizations.of(context)!.clearFilters),
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -519,24 +519,24 @@ class _PacketLogCard extends StatelessWidget {
|
||||
children: [
|
||||
_FactCard(
|
||||
icon: isRx ? Icons.call_received : Icons.call_made,
|
||||
label: 'Direction',
|
||||
label: AppLocalizations.of(context)!.direction,
|
||||
value: isRx ? 'RX' : 'TX',
|
||||
accent: directionColor,
|
||||
),
|
||||
_FactCard(
|
||||
icon: Icons.data_object,
|
||||
label: 'Size',
|
||||
label: AppLocalizations.of(context)!.size,
|
||||
value: '${log.rawData.length} bytes',
|
||||
),
|
||||
_FactCard(
|
||||
icon: Icons.schedule,
|
||||
label: 'Captured',
|
||||
label: AppLocalizations.of(context)!.captured,
|
||||
value: _formatTimestamp(log.timestamp),
|
||||
),
|
||||
if (log.responseCode != null)
|
||||
_FactCard(
|
||||
icon: Icons.sell,
|
||||
label: 'Opcode',
|
||||
label: AppLocalizations.of(context)!.opcode,
|
||||
value: log.opcodeName,
|
||||
),
|
||||
],
|
||||
@@ -565,7 +565,7 @@ class _PacketLogCard extends StatelessWidget {
|
||||
const SizedBox(height: 10),
|
||||
if (rxInfo?.rssiDbm != null)
|
||||
_SignalMeter(
|
||||
label: 'RSSI',
|
||||
label: AppLocalizations.of(context)!.rssi,
|
||||
valueLabel: '${rxInfo!.rssiDbm} dBm',
|
||||
normalized: _normalizeRssi(
|
||||
rxInfo.rssiDbm!.toDouble(),
|
||||
@@ -573,9 +573,9 @@ class _PacketLogCard extends StatelessWidget {
|
||||
color: _rssiColor(rxInfo.rssiDbm!.toDouble()),
|
||||
),
|
||||
if (rxInfo?.snrDb != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(height: 8),
|
||||
_SignalMeter(
|
||||
label: 'SNR',
|
||||
label: AppLocalizations.of(context)!.snr,
|
||||
valueLabel:
|
||||
'${rxInfo!.snrDb!.toStringAsFixed(1)} dB',
|
||||
normalized: _normalizeSnr(rxInfo.snrDb!),
|
||||
@@ -854,17 +854,17 @@ class _RouteSection extends StatelessWidget {
|
||||
children: [
|
||||
_FactCard(
|
||||
icon: Icons.route,
|
||||
label: 'Payload',
|
||||
label: AppLocalizations.of(context)!.payload,
|
||||
value: _payloadTypeLabel(route.payloadType),
|
||||
),
|
||||
_FactCard(
|
||||
icon: Icons.hub,
|
||||
label: 'Hops',
|
||||
label: AppLocalizations.of(context)!.hops,
|
||||
value: '${route.hopCount}',
|
||||
),
|
||||
_FactCard(
|
||||
icon: Icons.tag,
|
||||
label: 'Hash size',
|
||||
label: AppLocalizations.of(context)!.hashSize,
|
||||
value:
|
||||
'${route.hashSize} byte${route.hashSize == 1 ? '' : 's'}',
|
||||
),
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
||||
import '../models/config_profile.dart';
|
||||
import '../services/profile_manager.dart';
|
||||
import '../services/profile_workspace_coordinator.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
class ProfilesScreen extends StatelessWidget {
|
||||
const ProfilesScreen({super.key});
|
||||
@@ -15,7 +16,7 @@ class ProfilesScreen extends StatelessWidget {
|
||||
final profiles = profileManager.visibleProfiles;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Profiles'),
|
||||
title: Text(AppLocalizations.of(context)!.profiles),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
@@ -30,12 +31,12 @@ class ProfilesScreen extends StatelessWidget {
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => _createProfile(context),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('New Profile'),
|
||||
icon: Icon(Icons.add),
|
||||
label: Text(AppLocalizations.of(context)!.newProfile),
|
||||
),
|
||||
body: profiles.isEmpty
|
||||
? const Center(
|
||||
child: Text('Enable profiles to start managing them.'),
|
||||
? Center(
|
||||
child: Text(AppLocalizations.of(context)!.enableProfilesToStartManagingThem),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -73,7 +74,7 @@ class ProfilesScreen extends StatelessWidget {
|
||||
).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: const Text('Active'),
|
||||
child: Text(AppLocalizations.of(context)!.active),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -90,7 +91,7 @@ class ProfilesScreen extends StatelessWidget {
|
||||
.read<ProfileWorkspaceCoordinator>()
|
||||
.openProfile(profile.id);
|
||||
},
|
||||
child: const Text('Open'),
|
||||
child: Text(AppLocalizations.of(context)!.open),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () async {
|
||||
@@ -111,7 +112,7 @@ class ProfilesScreen extends StatelessWidget {
|
||||
.read<ProfileWorkspaceCoordinator>()
|
||||
.exportProfile(resolved);
|
||||
},
|
||||
child: const Text('Share'),
|
||||
child: Text(AppLocalizations.of(context)!.share),
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (value) async {
|
||||
@@ -132,14 +133,14 @@ class ProfilesScreen extends StatelessWidget {
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
PopupMenuItem(
|
||||
value: 'duplicate',
|
||||
child: Text('Duplicate'),
|
||||
child: Text(AppLocalizations.of(context)!.duplicate),
|
||||
),
|
||||
if (!profile.isDefault)
|
||||
const PopupMenuItem(
|
||||
PopupMenuItem(
|
||||
value: 'rename',
|
||||
child: Text('Rename'),
|
||||
child: Text(AppLocalizations.of(context)!.rename),
|
||||
),
|
||||
if (!profile.isDefault)
|
||||
const PopupMenuItem(
|
||||
@@ -187,7 +188,7 @@ class ProfilesScreen extends StatelessWidget {
|
||||
final name = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Create Profile'),
|
||||
title: Text(AppLocalizations.of(context)!.createProfile),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
@@ -200,7 +201,7 @@ class ProfilesScreen extends StatelessWidget {
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(controller.text.trim()),
|
||||
child: const Text('Create'),
|
||||
child: Text(AppLocalizations.of(context)!.create),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -221,7 +222,7 @@ class ProfilesScreen extends StatelessWidget {
|
||||
final name = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Rename Profile'),
|
||||
title: Text(AppLocalizations.of(context)!.renameProfile),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../models/contact.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../providers/contacts_provider.dart';
|
||||
import '../services/mesh_map_nodes_service.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
class RepeatersMapScreen extends StatefulWidget {
|
||||
const RepeatersMapScreen({super.key});
|
||||
@@ -191,18 +192,18 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(height: 12),
|
||||
_InfoLine(
|
||||
label: 'Public key',
|
||||
label: AppLocalizations.of(context)!.publicKeyLabel,
|
||||
value: repeater.publicKey,
|
||||
monospace: true,
|
||||
),
|
||||
_InfoLine(
|
||||
label: 'Coordinates',
|
||||
label: AppLocalizations.of(context)!.coordinates,
|
||||
value:
|
||||
'${repeater.latitude.toStringAsFixed(5)}, ${repeater.longitude.toStringAsFixed(5)}',
|
||||
),
|
||||
_InfoLine(label: 'Source', value: repeater.sourceLabel),
|
||||
_InfoLine(label: AppLocalizations.of(context)!.source, value: repeater.sourceLabel),
|
||||
const SizedBox(height: 12),
|
||||
if (canAdd)
|
||||
SizedBox(
|
||||
@@ -233,8 +234,8 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: null,
|
||||
icon: const Icon(Icons.check_circle),
|
||||
label: const Text('Already in contacts'),
|
||||
icon: Icon(Icons.check_circle),
|
||||
label: Text(AppLocalizations.of(context)!.alreadyInContacts),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -253,8 +254,8 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
||||
if (!connectionProvider.deviceInfo.isConnected) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Connect to a device before adding contacts'),
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.connectToADeviceBeforeAddingContacts),
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -366,7 +367,7 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Repeaters Map'),
|
||||
title: Text(AppLocalizations.of(context)!.repeatersMap),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: _isLoading
|
||||
@@ -446,18 +447,18 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
children: [
|
||||
_LegendRow(
|
||||
color: Color(0xFF7C3AED),
|
||||
label: 'From contacts',
|
||||
color: const Color(0xFF7C3AED),
|
||||
label: AppLocalizations.of(context)!.fromContacts,
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
const SizedBox(height: 8),
|
||||
_LegendRow(
|
||||
color: Color(0xFFE8681D),
|
||||
label: 'Online only',
|
||||
color: const Color(0xFFE8681D),
|
||||
label: AppLocalizations.of(context)!.onlineOnly,
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
_LegendRow(color: Color(0xFF1B8F4F), label: 'In both'),
|
||||
const SizedBox(height: 8),
|
||||
_LegendRow(color: const Color(0xFF1B8F4F), label: AppLocalizations.of(context)!.inBoth),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -240,7 +240,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
||||
PopupMenuItem(
|
||||
value: 'import',
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.download),
|
||||
leading: Icon(Icons.download),
|
||||
title: Text(l10n.importFromClipboard),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
@@ -248,7 +248,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
||||
PopupMenuItem(
|
||||
value: 'export',
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.upload),
|
||||
leading: Icon(Icons.upload),
|
||||
title: Text(l10n.exportToClipboard),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
@@ -257,7 +257,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
||||
PopupMenuItem(
|
||||
value: 'reset',
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.restart_alt),
|
||||
leading: Icon(Icons.restart_alt),
|
||||
title: Text(l10n.resetToDefaults),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
@@ -283,14 +283,14 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
||||
size: 64,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.3),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.noTemplates,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.tapAddToCreate,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
@@ -317,7 +317,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: _addTemplate,
|
||||
icon: const Icon(Icons.add),
|
||||
icon: Icon(Icons.add),
|
||||
label: Text(l10n.addTemplate),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../providers/connection_provider.dart';
|
||||
import '../providers/contacts_provider.dart';
|
||||
import '../providers/sensors_provider.dart';
|
||||
import '../widgets/sensors/sensor_telemetry_card.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
class SensorsTab extends StatefulWidget {
|
||||
final bool isActive;
|
||||
@@ -121,12 +122,12 @@ class _SensorsTabState extends State<SensorsTab> {
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
children: [
|
||||
const ListTile(
|
||||
title: Text(
|
||||
ListTile(
|
||||
title: const Text(
|
||||
'Add sensor node',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Text('Pick a relay or node to watch in Sensors.'),
|
||||
subtitle: Text(AppLocalizations.of(context)!.pickARelayOrNodeToWatchInSensors),
|
||||
),
|
||||
...candidates.map(
|
||||
(contact) => ListTile(
|
||||
@@ -209,7 +210,7 @@ class _SensorsTabState extends State<SensorsTab> {
|
||||
final didSave = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Rename value'),
|
||||
title: Text(AppLocalizations.of(context)!.renameValue),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -388,10 +389,10 @@ class _SensorCustomizeView extends StatelessWidget {
|
||||
title: Text('Customize ${contact?.displayName ?? 'Sensor'}'),
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
|
||||
padding: EdgeInsets.fromLTRB(16, 16, 16, 24),
|
||||
children: [
|
||||
_SensorCustomizeSectionCard(
|
||||
title: 'Live preview',
|
||||
title: AppLocalizations.of(context)!.livePreview,
|
||||
subtitle:
|
||||
'Changes apply immediately. This card matches the current dashboard layout for this sensor.',
|
||||
child: SensorTelemetryCard(
|
||||
@@ -414,7 +415,7 @@ class _SensorCustomizeView extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
_SensorCustomizeSectionCard(
|
||||
title: 'Refresh schedule',
|
||||
title: AppLocalizations.of(context)!.refreshSchedule,
|
||||
subtitle:
|
||||
'Choose how often this sensor should refresh while your device stays connected.',
|
||||
child: SensorAutoRefreshOptions(
|
||||
@@ -702,8 +703,8 @@ class SensorMetricSelectorItem extends StatelessWidget {
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.edit_outlined, size: 18),
|
||||
label: const Text('Rename'),
|
||||
icon: Icon(Icons.edit_outlined, size: 18),
|
||||
label: Text(AppLocalizations.of(context)!.rename),
|
||||
),
|
||||
if (showChannelChip)
|
||||
Container(
|
||||
@@ -911,7 +912,7 @@ class _EmptySensorsStateState extends State<_EmptySensorsState> {
|
||||
await connectionProvider.discoverNodeType(advertType: 4);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Sensor discovery sent')),
|
||||
SnackBar(content: Text(AppLocalizations.of(context)!.sensorDiscoverySent)),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -192,7 +192,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
});
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Developer mode disabled')));
|
||||
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.developerModeDisabled)));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
});
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Developer mode enabled')));
|
||||
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.developerModeEnabled)));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -328,7 +328,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final value = await showDialog<double>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Fast GPS movement threshold'),
|
||||
title: Text(AppLocalizations.of(context)!.fastGpsMovementThreshold),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
@@ -369,7 +369,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final value = await showDialog<int>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Fast GPS active-use interval'),
|
||||
title: Text(AppLocalizations.of(context)!.fastGpsActiveuseInterval),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.number,
|
||||
@@ -558,8 +558,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Update check is only available on Android'),
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.updateCheckIsOnlyAvailableOnAndroid),
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
@@ -583,8 +583,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
if (!updateInfo.isAvailable) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('You are running the latest version'),
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.youAreRunningTheLatestVersion),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
@@ -593,8 +593,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
if (updateInfo.downloadUrl == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Update available but download URL not found'),
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.updateAvailableButDownloadUrlNotFound),
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
@@ -736,7 +736,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
title: Row(
|
||||
children: [
|
||||
const Icon(Icons.settings, size: 24),
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(width: 12),
|
||||
Text(AppLocalizations.of(context)!.locationPermission),
|
||||
],
|
||||
),
|
||||
@@ -878,7 +878,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Clear Messages'),
|
||||
title: Text(AppLocalizations.of(context)!.clearMessages),
|
||||
content: const Text(
|
||||
'This will permanently delete all stored messages. Are you sure?',
|
||||
),
|
||||
@@ -907,8 +907,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
if (!mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('All messages cleared'),
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.allMessagesCleared),
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
@@ -918,7 +918,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Clear online trace database'),
|
||||
title: Text(AppLocalizations.of(context)!.clearOnlineTraceDatabase),
|
||||
content: const Text(
|
||||
'This removes the cached online node database used as a trace fallback.',
|
||||
),
|
||||
@@ -951,8 +951,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Online trace database cleared'),
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.onlineTraceDatabaseCleared),
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
@@ -978,7 +978,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final selected = await showDialog<int>(
|
||||
context: context,
|
||||
builder: (context) => SimpleDialog(
|
||||
title: const Text('Route path byte size'),
|
||||
title: Text(AppLocalizations.of(context)!.routePathByteSize),
|
||||
children: [
|
||||
for (final value in RouteHashPreferences.supportedSizes)
|
||||
SimpleDialogOption(
|
||||
@@ -1024,21 +1024,21 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_buildSectionHeader('Appearance'),
|
||||
_buildSettingsCard([
|
||||
ListTile(
|
||||
leading: const Icon(Icons.palette),
|
||||
leading: Icon(Icons.palette),
|
||||
title: Text(AppLocalizations.of(context)!.theme),
|
||||
subtitle: Text(AppTheme.getThemeDisplayName(_selectedTheme)),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showThemeDialog(),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.language),
|
||||
leading: Icon(Icons.language),
|
||||
title: Text(AppLocalizations.of(context)!.language),
|
||||
subtitle: Text(LocalePreferences.getDisplayName(_selectedLocale)),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showLanguageDialog(),
|
||||
),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.radar),
|
||||
secondary: Icon(Icons.radar),
|
||||
title: Text(AppLocalizations.of(context)!.showRxTxIndicators),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context)!.displayPacketActivity,
|
||||
@@ -1056,8 +1056,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_buildSectionHeader('Notifications'),
|
||||
_buildSettingsCard([
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.chat_bubble_outline),
|
||||
title: const Text('Message notifications'),
|
||||
secondary: Icon(Icons.chat_bubble_outline),
|
||||
title: Text(AppLocalizations.of(context)!.messageNotifications),
|
||||
subtitle: const Text(
|
||||
'Notify for incoming direct and channel messages',
|
||||
),
|
||||
@@ -1072,8 +1072,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.warning_amber_outlined),
|
||||
title: const Text('SAR alerts'),
|
||||
secondary: Icon(Icons.warning_amber_outlined),
|
||||
title: Text(AppLocalizations.of(context)!.sarAlerts),
|
||||
subtitle: const Text(
|
||||
'Notify for incoming SAR markers such as found person or fire',
|
||||
),
|
||||
@@ -1086,8 +1086,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.contact_page_outlined),
|
||||
title: const Text('Discovery notifications'),
|
||||
secondary: Icon(Icons.contact_page_outlined),
|
||||
title: Text(AppLocalizations.of(context)!.discoveryNotifications),
|
||||
subtitle: const Text(
|
||||
'Notify when new contacts appear in Discovery',
|
||||
),
|
||||
@@ -1102,8 +1102,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.system_update),
|
||||
title: const Text('Update notifications'),
|
||||
secondary: Icon(Icons.system_update),
|
||||
title: Text(AppLocalizations.of(context)!.updateNotifications),
|
||||
subtitle: const Text(
|
||||
'Notify when a newer app version is available',
|
||||
),
|
||||
@@ -1118,8 +1118,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.visibility_off_outlined),
|
||||
title: const Text('Mute while app is open'),
|
||||
secondary: Icon(Icons.visibility_off_outlined),
|
||||
title: Text(AppLocalizations.of(context)!.muteWhileAppIsOpen),
|
||||
subtitle: const Text(
|
||||
'Do not show local notifications while the app is in the foreground',
|
||||
),
|
||||
@@ -1139,7 +1139,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_buildSettingsCard([
|
||||
Consumer<AppProvider>(
|
||||
builder: (context, appProvider, child) => SwitchListTile(
|
||||
secondary: const Icon(Icons.map_outlined),
|
||||
secondary: Icon(Icons.map_outlined),
|
||||
title: Text(AppLocalizations.of(context)!.disableMap),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context)!.disableMapDescription,
|
||||
@@ -1152,8 +1152,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
Consumer<AppProvider>(
|
||||
builder: (context, appProvider, child) => SwitchListTile(
|
||||
secondary: const Icon(Icons.contacts_outlined),
|
||||
title: const Text('Disable Contacts'),
|
||||
secondary: Icon(Icons.contacts_outlined),
|
||||
title: Text(AppLocalizations.of(context)!.disableContacts),
|
||||
subtitle: const Text(
|
||||
'Hide the contacts tab to simplify navigation',
|
||||
),
|
||||
@@ -1165,8 +1165,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
Consumer<AppProvider>(
|
||||
builder: (context, appProvider, child) => SwitchListTile(
|
||||
secondary: const Icon(Icons.sensors),
|
||||
title: const Text('Enable Sensors tab'),
|
||||
secondary: Icon(Icons.sensors),
|
||||
title: Text(AppLocalizations.of(context)!.enableSensorsTab),
|
||||
subtitle: const Text(
|
||||
'Show a dedicated tab for watched relay and node telemetry',
|
||||
),
|
||||
@@ -1181,8 +1181,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_buildSectionHeader('Messaging'),
|
||||
_buildSettingsCard([
|
||||
ListTile(
|
||||
leading: const Icon(Icons.alt_route),
|
||||
title: const Text('Route path byte size'),
|
||||
leading: Icon(Icons.alt_route),
|
||||
title: Text(AppLocalizations.of(context)!.routePathByteSize),
|
||||
subtitle: Text(
|
||||
'$_routeHashSize byte${_routeHashSize == 1 ? '' : 's'} for manual contact routes',
|
||||
),
|
||||
@@ -1191,8 +1191,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
Consumer<AppProvider>(
|
||||
builder: (context, appProvider, child) => SwitchListTile(
|
||||
secondary: const Icon(Icons.swap_horiz),
|
||||
title: const Text('Auto route rotation'),
|
||||
secondary: Icon(Icons.swap_horiz),
|
||||
title: Text(AppLocalizations.of(context)!.autoRouteRotation),
|
||||
subtitle: const Text(
|
||||
'Rotate between best known direct paths and flood mode for room/contact sends',
|
||||
),
|
||||
@@ -1204,8 +1204,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
Consumer<AppProvider>(
|
||||
builder: (context, appProvider, child) => SwitchListTile(
|
||||
secondary: const Icon(Icons.route),
|
||||
title: const Text('Nearest repeater fallback'),
|
||||
secondary: Icon(Icons.route),
|
||||
title: Text(AppLocalizations.of(context)!.nearestRepeaterFallback),
|
||||
subtitle: const Text(
|
||||
'After normal retries fail, try one final resend through the nearest repeater',
|
||||
),
|
||||
@@ -1217,8 +1217,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
Consumer<AppProvider>(
|
||||
builder: (context, appProvider, child) => SwitchListTile(
|
||||
secondary: const Icon(Icons.route),
|
||||
title: const Text('Clear path on max retry'),
|
||||
secondary: Icon(Icons.route),
|
||||
title: Text(AppLocalizations.of(context)!.clearPathOnMaxRetry),
|
||||
subtitle: const Text(
|
||||
'Clear the route only after all retries and final router fallback fail',
|
||||
),
|
||||
@@ -1234,13 +1234,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
'Clear Messages',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
subtitle: const Text('Delete all stored message history'),
|
||||
subtitle: Text(AppLocalizations.of(context)!.deleteAllStoredMessageHistory),
|
||||
onTap: _clearMessages,
|
||||
),
|
||||
Consumer<AppProvider>(
|
||||
builder: (context, appProvider, child) => ListTile(
|
||||
leading: const Icon(Icons.format_size),
|
||||
title: const Text('Message font size'),
|
||||
leading: Icon(Icons.format_size),
|
||||
title: Text(AppLocalizations.of(context)!.messageFontSize),
|
||||
subtitle: Text(
|
||||
'${(appProvider.messageFontScale * 100).round()}% of default',
|
||||
),
|
||||
@@ -1264,8 +1264,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_buildSectionHeader('Tracing'),
|
||||
_buildSettingsCard([
|
||||
ListTile(
|
||||
leading: const Icon(Icons.cloud_sync),
|
||||
title: const Text('Online trace database'),
|
||||
leading: Icon(Icons.cloud_sync),
|
||||
title: Text(AppLocalizations.of(context)!.onlineTraceDatabase),
|
||||
subtitle: Text(_onlineTraceCacheSubtitle()),
|
||||
),
|
||||
ListTile(
|
||||
@@ -1299,8 +1299,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_buildSectionHeader('Map'),
|
||||
_buildSettingsCard([
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.explore),
|
||||
title: const Text('Rotate map with heading'),
|
||||
secondary: Icon(Icons.explore),
|
||||
title: Text(AppLocalizations.of(context)!.rotateMapWithHeading),
|
||||
subtitle: const Text(
|
||||
'Rotate the map based on your compass or movement heading',
|
||||
),
|
||||
@@ -1313,8 +1313,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.bug_report_outlined),
|
||||
title: const Text('Show map debug info'),
|
||||
secondary: Icon(Icons.bug_report_outlined),
|
||||
title: Text(AppLocalizations.of(context)!.showMapDebugInfo),
|
||||
subtitle: const Text(
|
||||
'Display extra map diagnostics and internal state overlays',
|
||||
),
|
||||
@@ -1327,8 +1327,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.fullscreen),
|
||||
title: const Text('Open map in fullscreen'),
|
||||
secondary: Icon(Icons.fullscreen),
|
||||
title: Text(AppLocalizations.of(context)!.openMapInFullscreen),
|
||||
subtitle: const Text(
|
||||
'Start the map tab in fullscreen mode by default',
|
||||
),
|
||||
@@ -1342,9 +1342,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
Consumer<DrawingProvider>(
|
||||
builder: (context, drawingProvider, child) => SwitchListTile(
|
||||
secondary: const Icon(Icons.fmd_good_outlined),
|
||||
title: const Text('Show SAR markers'),
|
||||
subtitle: const Text('Display SAR markers on the main map'),
|
||||
secondary: Icon(Icons.fmd_good_outlined),
|
||||
title: Text(AppLocalizations.of(context)!.showSarMarkersLabel),
|
||||
subtitle: Text(AppLocalizations.of(context)!.displaySarMarkersOnTheMainMap),
|
||||
value: drawingProvider.showSarMarkers,
|
||||
onChanged: (value) {
|
||||
drawingProvider.toggleSarMarkers();
|
||||
@@ -1353,8 +1353,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
Consumer<MapProvider>(
|
||||
builder: (context, mapProvider, child) => SwitchListTile(
|
||||
secondary: const Icon(Icons.timeline),
|
||||
title: const Text('Show all contact trails'),
|
||||
secondary: Icon(Icons.timeline),
|
||||
title: Text(AppLocalizations.of(context)!.showAllContactTrailsLabel),
|
||||
subtitle: const Text(
|
||||
'Display location trails for all contacts that have history',
|
||||
),
|
||||
@@ -1366,8 +1366,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
Consumer<MapProvider>(
|
||||
builder: (context, mapProvider, child) => SwitchListTile(
|
||||
secondary: const Icon(Icons.router_outlined),
|
||||
title: const Text('Hide repeaters on map'),
|
||||
secondary: Icon(Icons.router_outlined),
|
||||
title: Text(AppLocalizations.of(context)!.hideRepeatersOnMap),
|
||||
subtitle: const Text(
|
||||
'Hide repeater contacts from the main map view',
|
||||
),
|
||||
@@ -1397,16 +1397,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
_buildSettingsCard([
|
||||
ListTile(
|
||||
leading: const Icon(Icons.graphic_eq),
|
||||
title: const Text('Voice bitrate'),
|
||||
leading: Icon(Icons.graphic_eq),
|
||||
title: Text(AppLocalizations.of(context)!.voiceBitrate),
|
||||
subtitle: Text(_voiceBitrateSubtitle(_voiceBitrate)),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: _showVoiceBitrateDialog,
|
||||
),
|
||||
Consumer<AppProvider>(
|
||||
builder: (context, appProvider, child) => SwitchListTile(
|
||||
secondary: const Icon(Icons.tune),
|
||||
title: const Text('Band-pass filter voice'),
|
||||
secondary: Icon(Icons.tune),
|
||||
title: Text(AppLocalizations.of(context)!.bandpassFilterVoice),
|
||||
subtitle: const Text(
|
||||
'Keeps speech frequencies and cuts low/high noise',
|
||||
),
|
||||
@@ -1418,9 +1418,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
Consumer<AppProvider>(
|
||||
builder: (context, appProvider, child) => SwitchListTile(
|
||||
secondary: const Icon(Icons.compress),
|
||||
title: const Text('Voice compressor'),
|
||||
subtitle: const Text('Balances quiet and loud speech levels'),
|
||||
secondary: Icon(Icons.compress),
|
||||
title: Text(AppLocalizations.of(context)!.voiceCompressor),
|
||||
subtitle: Text(AppLocalizations.of(context)!.balancesQuietAndLoudSpeechLevels),
|
||||
value: appProvider.isVoiceCompressorEnabled,
|
||||
onChanged: (value) async {
|
||||
await appProvider.toggleVoiceCompressorEnabled(value);
|
||||
@@ -1429,9 +1429,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
Consumer<AppProvider>(
|
||||
builder: (context, appProvider, child) => SwitchListTile(
|
||||
secondary: const Icon(Icons.speed),
|
||||
title: const Text('Voice limiter'),
|
||||
subtitle: const Text('Prevents clipping peaks before encoding'),
|
||||
secondary: Icon(Icons.speed),
|
||||
title: Text(AppLocalizations.of(context)!.voiceLimiter),
|
||||
subtitle: Text(AppLocalizations.of(context)!.preventsClippingPeaksBeforeEncoding),
|
||||
value: appProvider.isVoiceLimiterEnabled,
|
||||
onChanged: (value) async {
|
||||
await appProvider.toggleVoiceLimiterEnabled(value);
|
||||
@@ -1440,9 +1440,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
Consumer<AppProvider>(
|
||||
builder: (context, appProvider, child) => SwitchListTile(
|
||||
secondary: const Icon(Icons.auto_fix_high),
|
||||
title: const Text('Mic auto gain'),
|
||||
subtitle: const Text('Lets the recorder adjust input level'),
|
||||
secondary: Icon(Icons.auto_fix_high),
|
||||
title: Text(AppLocalizations.of(context)!.micAutoGain),
|
||||
subtitle: Text(AppLocalizations.of(context)!.letsTheRecorderAdjustInputLevel),
|
||||
value: appProvider.isVoiceAutoGainEnabled,
|
||||
onChanged: (value) async {
|
||||
await appProvider.toggleVoiceAutoGainEnabled(value);
|
||||
@@ -1451,8 +1451,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
Consumer<AppProvider>(
|
||||
builder: (context, appProvider, child) => SwitchListTile(
|
||||
secondary: const Icon(Icons.hearing_disabled),
|
||||
title: const Text('Echo cancellation'),
|
||||
secondary: Icon(Icons.hearing_disabled),
|
||||
title: Text(AppLocalizations.of(context)!.echoCancellation),
|
||||
subtitle: const Text(
|
||||
'Uses recorder echo cancellation if available',
|
||||
),
|
||||
@@ -1464,8 +1464,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
Consumer<AppProvider>(
|
||||
builder: (context, appProvider, child) => SwitchListTile(
|
||||
secondary: const Icon(Icons.noise_control_off),
|
||||
title: const Text('Noise suppression'),
|
||||
secondary: Icon(Icons.noise_control_off),
|
||||
title: Text(AppLocalizations.of(context)!.noiseSuppression),
|
||||
subtitle: const Text(
|
||||
'Uses recorder noise suppression if available',
|
||||
),
|
||||
@@ -1477,8 +1477,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
Consumer<AppProvider>(
|
||||
builder: (context, appProvider, child) => SwitchListTile(
|
||||
secondary: const Icon(Icons.content_cut),
|
||||
title: const Text('Trim silence in voice messages'),
|
||||
secondary: Icon(Icons.content_cut),
|
||||
title: Text(AppLocalizations.of(context)!.trimSilenceInVoiceMessages),
|
||||
subtitle: const Text(
|
||||
'Removes long silent parts before sending voice',
|
||||
),
|
||||
@@ -1493,15 +1493,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_buildSectionHeader('Images'),
|
||||
_buildSettingsCard([
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_size_select_large),
|
||||
title: const Text('Max image size'),
|
||||
leading: Icon(Icons.photo_size_select_large),
|
||||
title: Text(AppLocalizations.of(context)!.maxImageSize),
|
||||
subtitle: Text('$_imageMaxSize×$_imageMaxSize px'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: _showImageMaxSizeDialog,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.tune),
|
||||
title: const Text('Image compression'),
|
||||
leading: Icon(Icons.tune),
|
||||
title: Text(AppLocalizations.of(context)!.imageCompression),
|
||||
subtitle: Text(
|
||||
'$_imageCompression / 90 (higher = smaller file)',
|
||||
),
|
||||
@@ -1519,8 +1519,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.invert_colors),
|
||||
title: const Text('Grayscale'),
|
||||
secondary: Icon(Icons.invert_colors),
|
||||
title: Text(AppLocalizations.of(context)!.grayscale),
|
||||
subtitle: const Text(
|
||||
'Converts image to grayscale for smaller file size',
|
||||
),
|
||||
@@ -1532,8 +1532,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.compress),
|
||||
title: const Text('Ultra mode'),
|
||||
secondary: Icon(Icons.compress),
|
||||
title: Text(AppLocalizations.of(context)!.ultraMode),
|
||||
subtitle: const Text(
|
||||
'Extra-aggressive compression with stronger AVIF settings',
|
||||
),
|
||||
@@ -1554,8 +1554,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_buildSectionHeader('Profiles'),
|
||||
_buildSettingsCard([
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.layers_outlined),
|
||||
title: const Text('Enable Profiles'),
|
||||
secondary: Icon(Icons.layers_outlined),
|
||||
title: Text(AppLocalizations.of(context)!.enableProfiles),
|
||||
subtitle: const Text(
|
||||
'Show profile management UI while keeping the hidden Default profile as the current workspace.',
|
||||
),
|
||||
@@ -1572,8 +1572,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
if (_profilesEnabled)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.folder_copy_outlined),
|
||||
title: const Text('Manage profiles'),
|
||||
leading: Icon(Icons.folder_copy_outlined),
|
||||
title: Text(AppLocalizations.of(context)!.manageProfiles),
|
||||
subtitle: Text(
|
||||
context.watch<ProfileManager>().activeProfileId ==
|
||||
ConfigProfile.defaultProfileId
|
||||
@@ -1594,7 +1594,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_buildSectionHeader('Templates & Help'),
|
||||
_buildSettingsCard([
|
||||
ListTile(
|
||||
leading: const Icon(Icons.location_searching),
|
||||
leading: Icon(Icons.location_searching),
|
||||
title: Text(AppLocalizations.of(context)!.sarTemplates),
|
||||
subtitle: Text(AppLocalizations.of(context)!.manageSarTemplates),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
@@ -1608,7 +1608,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.school),
|
||||
leading: Icon(Icons.school),
|
||||
title: Text(AppLocalizations.of(context)!.viewWelcomeTutorial),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () async {
|
||||
@@ -1629,8 +1629,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_buildSectionHeader(AppLocalizations.of(context)!.permissionsSection),
|
||||
_buildSettingsCard([
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.gps_fixed),
|
||||
title: const Text('Fast private GPS updates'),
|
||||
secondary: Icon(Icons.gps_fixed),
|
||||
title: Text(AppLocalizations.of(context)!.fastPrivateGpsUpdates),
|
||||
subtitle: const Text(
|
||||
'Use private zero-hop updates while moving significantly or while actively using map/messages.',
|
||||
),
|
||||
@@ -1638,8 +1638,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
onChanged: _setFastLocationUpdatesEnabled,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.straighten),
|
||||
title: const Text('Movement threshold'),
|
||||
leading: Icon(Icons.straighten),
|
||||
title: Text(AppLocalizations.of(context)!.movementThreshold),
|
||||
subtitle: Text(
|
||||
'${_fastLocationMovementThresholdMeters.toStringAsFixed(0)} m',
|
||||
),
|
||||
@@ -1647,14 +1647,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
onTap: _editFastLocationMovementThreshold,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.timer),
|
||||
title: const Text('Active-use update interval'),
|
||||
leading: Icon(Icons.timer),
|
||||
title: Text(AppLocalizations.of(context)!.activeuseUpdateInterval),
|
||||
subtitle: Text('$_fastLocationActiveCadenceSeconds s'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: _editFastLocationActiveCadence,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.location_on),
|
||||
leading: Icon(Icons.location_on),
|
||||
title: Text(AppLocalizations.of(context)!.locationPermission),
|
||||
subtitle: FutureBuilder<LocationPermission>(
|
||||
future: Geolocator.checkPermission(),
|
||||
@@ -1707,7 +1707,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_buildSectionHeader(AppLocalizations.of(context)!.about),
|
||||
_buildSettingsCard([
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info),
|
||||
leading: Icon(Icons.info),
|
||||
title: Text(AppLocalizations.of(context)!.appVersion),
|
||||
subtitle: Text(
|
||||
_packageInfo != null
|
||||
@@ -1717,12 +1717,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
onTap: _handleVersionTap,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.badge),
|
||||
leading: Icon(Icons.badge),
|
||||
title: Text(AppLocalizations.of(context)!.appName),
|
||||
subtitle: Text(_packageInfo?.appName ?? 'MeshCore SAR'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.description),
|
||||
leading: Icon(Icons.description),
|
||||
title: Text(AppLocalizations.of(context)!.aboutMeshCoreSar),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context)!.aboutDescription.split('\n\n')[0],
|
||||
@@ -1756,19 +1756,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_buildSectionHeader('Developer & Data'),
|
||||
_buildSettingsCard([
|
||||
ListTile(
|
||||
leading: const Icon(Icons.bug_report),
|
||||
leading: Icon(Icons.bug_report),
|
||||
title: Text(AppLocalizations.of(context)!.packageName),
|
||||
subtitle: Text(_packageInfo?.packageName ?? 'com.meshcore.sar'),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
padding: EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.sampleData,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
padding: EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.sampleDataDescription,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
@@ -1791,7 +1791,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.add_circle_outline),
|
||||
: Icon(Icons.add_circle_outline),
|
||||
label: Text(AppLocalizations.of(context)!.loadSampleData),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
@@ -1802,7 +1802,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _isLoadingSampleData ? null : _clearSampleData,
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
icon: Icon(Icons.delete_outline),
|
||||
label: Text(AppLocalizations.of(context)!.clearAllData),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.red,
|
||||
@@ -1908,8 +1908,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
onPressed: _isPreviewLoading
|
||||
? null
|
||||
: _selectPreviewImageFromGallery,
|
||||
icon: const Icon(Icons.photo_library_outlined),
|
||||
label: const Text('Select from gallery'),
|
||||
icon: Icon(Icons.photo_library_outlined),
|
||||
label: Text(AppLocalizations.of(context)!.selectFromGallery),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -2115,21 +2115,21 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: _voiceStatChip(
|
||||
label: 'Band-pass',
|
||||
label: AppLocalizations.of(context)!.bandpass,
|
||||
enabled: bandPassEnabled,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _voiceStatChip(
|
||||
label: 'Compressor',
|
||||
label: AppLocalizations.of(context)!.compressor,
|
||||
enabled: compressorEnabled,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _voiceStatChip(
|
||||
label: 'Limiter',
|
||||
label: AppLocalizations.of(context)!.limiter,
|
||||
enabled: limiterEnabled,
|
||||
),
|
||||
),
|
||||
@@ -2140,21 +2140,21 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: _voiceStatChip(
|
||||
label: 'Auto gain',
|
||||
label: AppLocalizations.of(context)!.autoGain,
|
||||
enabled: autoGainEnabled,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _voiceStatChip(
|
||||
label: 'Echo cancel',
|
||||
label: AppLocalizations.of(context)!.echoCancel,
|
||||
enabled: echoCancellationEnabled,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _voiceStatChip(
|
||||
label: 'Noise suppress',
|
||||
label: AppLocalizations.of(context)!.noiseSuppress,
|
||||
enabled: noiseSuppressionEnabled,
|
||||
),
|
||||
),
|
||||
@@ -2165,7 +2165,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: _voiceStatChip(
|
||||
label: 'Silence trim',
|
||||
label: AppLocalizations.of(context)!.silenceTrim,
|
||||
enabled: silenceTrimEnabled,
|
||||
),
|
||||
),
|
||||
@@ -2237,7 +2237,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
subtitle: Text(AppLocalizations.of(context)!.blueDarkTheme),
|
||||
value: AppThemeMode.dark,
|
||||
),
|
||||
const Divider(),
|
||||
Divider(),
|
||||
RadioListTile<AppThemeMode>(
|
||||
title: Row(
|
||||
children: [
|
||||
@@ -2301,7 +2301,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
value: AppThemeMode.sarNavyBlue,
|
||||
),
|
||||
const Divider(),
|
||||
Divider(),
|
||||
RadioListTile<AppThemeMode>(
|
||||
title: Text(AppLocalizations.of(context)!.autoSystem),
|
||||
subtitle: Text(
|
||||
@@ -2368,7 +2368,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Max image size'),
|
||||
title: Text(AppLocalizations.of(context)!.maxImageSize),
|
||||
content: SingleChildScrollView(
|
||||
child: RadioGroup<int>(
|
||||
groupValue: _imageMaxSize,
|
||||
@@ -2384,7 +2384,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
value: size,
|
||||
title: Text('$size×$size px'),
|
||||
subtitle: size == ImagePreferences.defaultMaxSize
|
||||
? const Text('Default')
|
||||
? Text(AppLocalizations.of(context)!.defaultValue)
|
||||
: null,
|
||||
),
|
||||
)
|
||||
@@ -2406,7 +2406,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Voice bitrate'),
|
||||
title: Text(AppLocalizations.of(context)!.voiceBitrate),
|
||||
content: SingleChildScrollView(
|
||||
child: RadioGroup<int>(
|
||||
groupValue: _voiceBitrate,
|
||||
@@ -2425,7 +2425,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
title: Text('$bitrate bps'),
|
||||
subtitle:
|
||||
bitrate == VoiceBitratePreferences.defaultBitrate
|
||||
? const Text('Default')
|
||||
? Text(AppLocalizations.of(context)!.defaultValue)
|
||||
: null,
|
||||
),
|
||||
)
|
||||
@@ -2464,16 +2464,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
'Version ${_packageInfo?.version ?? '1.0.0'}',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(height: 16),
|
||||
Text(AppLocalizations.of(context)!.aboutDescription),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.technologiesUsed,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(height: 8),
|
||||
Text(AppLocalizations.of(context)!.technologiesList),
|
||||
],
|
||||
),
|
||||
@@ -2486,7 +2486,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
icon: Icon(Icons.open_in_new),
|
||||
label: Text(AppLocalizations.of(context)!.moreInfo),
|
||||
),
|
||||
TextButton(
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
||||
import '../models/device_info.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../widgets/device/spectrum_scan_panel.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
class SpectrumScanScreen extends StatefulWidget {
|
||||
const SpectrumScanScreen({super.key});
|
||||
@@ -323,8 +324,8 @@ class _SpectrumScanScreenState extends State<SpectrumScanScreen> {
|
||||
final mergedCandidates = _mergeSectorCandidates(sectorCandidates);
|
||||
if (mergedCandidates.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Spectrum scan returned no candidate frequencies'),
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.spectrumScanReturnedNoCandidateFrequencies),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
@@ -383,7 +384,7 @@ class _SpectrumScanScreenState extends State<SpectrumScanScreen> {
|
||||
: possibleBracketFrequencies;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Spectrum Scan')),
|
||||
appBar: AppBar(title: Text(AppLocalizations.of(context)!.spectrumScan)),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
|
||||
@@ -331,7 +331,7 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
||||
if (_currentPage > 0)
|
||||
TextButton.icon(
|
||||
onPressed: _previousPage,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
icon: Icon(Icons.arrow_back),
|
||||
label: Text(l10n.wizardBack),
|
||||
)
|
||||
else
|
||||
@@ -510,7 +510,7 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
isConnected
|
||||
@@ -601,7 +601,7 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
||||
_selectedPreset.summary,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
SizedBox(height: 14),
|
||||
...[
|
||||
_FeatureItem(
|
||||
icon: Icons.rule,
|
||||
|
||||
Reference in New Issue
Block a user