From ede3142d40ed66cadf05c611468241576aebfbb1 Mon Sep 17 00:00:00 2001 From: 446564 Date: Fri, 30 Jan 2026 11:05:57 -0800 Subject: [PATCH] allow disable repeater adverts Adds checkbox to disable adverts and flood adverts Also updates flood avert range to new max of 168 hours --- lib/screens/repeater_settings_screen.dart | 241 ++++++++++++++++------ 1 file changed, 178 insertions(+), 63 deletions(-) diff --git a/lib/screens/repeater_settings_screen.dart b/lib/screens/repeater_settings_screen.dart index c757404..6c1e85f 100644 --- a/lib/screens/repeater_settings_screen.dart +++ b/lib/screens/repeater_settings_screen.dart @@ -41,7 +41,8 @@ class _RepeaterSettingsScreenState extends State { // Basic settings final TextEditingController _nameController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); - final TextEditingController _guestPasswordController = TextEditingController(); + final TextEditingController _guestPasswordController = + TextEditingController(); // Radio settings final TextEditingController _freqController = TextEditingController(); @@ -60,7 +61,9 @@ class _RepeaterSettingsScreenState extends State { bool _privacyMode = false; // Advertisement settings + bool _advertEnable = true; int _advertInterval = 120; // minutes/2 + bool _floodAdvertEnable = true; int _floodAdvertInterval = 12; // hours int _privAdvertInterval = 60; // minutes @@ -146,7 +149,10 @@ class _RepeaterSettingsScreenState extends State { if (_fetchedSettings.isEmpty) return; final appLog = Provider.of(context, listen: false); - appLog.info('Updating UI with keys: ${_fetchedSettings.keys.toList()}', tag: 'RadioSettings'); + appLog.info( + 'Updating UI with keys: ${_fetchedSettings.keys.toList()}', + tag: 'RadioSettings', + ); setState(() { // Update name @@ -161,7 +167,10 @@ class _RepeaterSettingsScreenState extends State { final radioStr = _fetchedSettings['radio']!; appLog.info('Raw radio string: "$radioStr"', tag: 'RadioSettings'); final parts = radioStr.split(','); - appLog.info('Split into ${parts.length} parts: $parts', tag: 'RadioSettings'); + appLog.info( + 'Split into ${parts.length} parts: $parts', + tag: 'RadioSettings', + ); if (parts.isNotEmpty) { final freqText = parts[0].replaceAll(RegExp(r'[^0-9.]'), '').trim(); @@ -193,7 +202,10 @@ class _RepeaterSettingsScreenState extends State { appLog.info('CR text: "$crText"', tag: 'RadioSettings'); _codingRate = int.tryParse(crText) ?? _codingRate; } - appLog.info('Final values: freq=${_freqController.text}, bw=$_bandwidth, sf=$_spreadingFactor, cr=$_codingRate', tag: 'RadioSettings'); + appLog.info( + 'Final values: freq=${_freqController.text}, bw=$_bandwidth, sf=$_spreadingFactor, cr=$_codingRate', + tag: 'RadioSettings', + ); } if (_fetchedSettings.containsKey('tx')) { @@ -207,11 +219,17 @@ class _RepeaterSettingsScreenState extends State { } if (_fetchedSettings.containsKey('lat')) { - appLog.info('Setting lat to: "${_fetchedSettings['lat']}"', tag: 'RadioSettings'); + appLog.info( + 'Setting lat to: "${_fetchedSettings['lat']}"', + tag: 'RadioSettings', + ); _latController.text = _fetchedSettings['lat']!; } if (_fetchedSettings.containsKey('lon')) { - appLog.info('Setting lon to: "${_fetchedSettings['lon']}"', tag: 'RadioSettings'); + appLog.info( + 'Setting lon to: "${_fetchedSettings['lon']}"', + tag: 'RadioSettings', + ); _lonController.text = _fetchedSettings['lon']!; } @@ -268,7 +286,10 @@ class _RepeaterSettingsScreenState extends State { void _applySettingResponse(String command, String response) { final appLog = Provider.of(context, listen: false); - appLog.info('Command: "$command", Raw response: "$response"', tag: 'RadioSettings'); + appLog.info( + 'Command: "$command", Raw response: "$response"', + tag: 'RadioSettings', + ); final value = _extractCliValue(response); appLog.info('Extracted value: "$value"', tag: 'RadioSettings'); if (value == null) return; @@ -280,7 +301,10 @@ class _RepeaterSettingsScreenState extends State { // Validate response content matches expected format for the command // This prevents mismatched responses over LoRa where order isn't guaranteed if (!_validateResponseForCommand(key, value)) { - appLog.warn('Response "$value" does not match expected format for "$key", ignoring', tag: 'RadioSettings'); + appLog.warn( + 'Response "$value" does not match expected format for "$key", ignoring', + tag: 'RadioSettings', + ); return; } @@ -311,7 +335,9 @@ class _RepeaterSettingsScreenState extends State { // Must have at least 3 commas and start with a frequency-like number final parts = value.split(','); if (parts.length < 4) return false; - final freq = double.tryParse(parts[0].replaceAll(RegExp(r'[^0-9.]'), '')); + final freq = double.tryParse( + parts[0].replaceAll(RegExp(r'[^0-9.]'), ''), + ); // Frequency should be in reasonable LoRa range (300-2500 MHz) return freq != null && freq >= 300 && freq <= 2500; @@ -339,7 +365,16 @@ class _RepeaterSettingsScreenState extends State { case 'privacy': // Boolean values: on/off/true/false/1/0/enabled/disabled final lower = value.toLowerCase().trim(); - return ['on', 'off', 'true', 'false', '1', '0', 'enabled', 'disabled'].contains(lower); + return [ + 'on', + 'off', + 'true', + 'false', + '1', + '0', + 'enabled', + 'disabled', + ].contains(lower); case 'advert.interval': case 'flood.advert.interval': @@ -354,7 +389,8 @@ class _RepeaterSettingsScreenState extends State { if (value.isEmpty) return false; // If it has 3+ commas and looks like numbers, probably radio data final commaCount = ','.allMatches(value).length; - if (commaCount >= 3 && RegExp(r'^[\d.,\s]+$').hasMatch(value)) return false; + if (commaCount >= 3 && RegExp(r'^[\d.,\s]+$').hasMatch(value)) + return false; return true; default: @@ -551,7 +587,9 @@ class _RepeaterSettingsScreenState extends State { final freqMHz = double.tryParse(_freqController.text); if (freqMHz != null) { final bwKHz = _bandwidth! / 1000; - commands.add('set radio ${freqMHz.toStringAsFixed(1)} $bwKHz $_spreadingFactor $_codingRate'); + commands.add( + 'set radio ${freqMHz.toStringAsFixed(1)} $bwKHz $_spreadingFactor $_codingRate', + ); } } @@ -590,7 +628,9 @@ class _RepeaterSettingsScreenState extends State { timestampSeconds: timestampSeconds, ); await connector.sendFrame(frame); - await Future.delayed(const Duration(milliseconds: 200)); // Delay between commands + await Future.delayed( + const Duration(milliseconds: 200), + ); // Delay between commands } setState(() { @@ -614,7 +654,9 @@ class _RepeaterSettingsScreenState extends State { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(context.l10n.repeater_errorSavingSettings(e.toString())), + content: Text( + context.l10n.repeater_errorSavingSettings(e.toString()), + ), backgroundColor: Colors.red, ), ); @@ -699,7 +741,10 @@ class _RepeaterSettingsScreenState extends State { Text(l10n.repeater_settingsTitle), Text( repeater.name, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + ), ), ], ), @@ -723,12 +768,20 @@ class _RepeaterSettingsScreenState extends State { value: 'auto', child: Row( children: [ - Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null), + Icon( + Icons.auto_mode, + size: 20, + color: !isFloodMode + ? Theme.of(context).primaryColor + : null, + ), const SizedBox(width: 8), Text( l10n.repeater_autoUseSavedPath, style: TextStyle( - fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal, + fontWeight: !isFloodMode + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -738,12 +791,20 @@ class _RepeaterSettingsScreenState extends State { value: 'flood', child: Row( children: [ - Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null), + Icon( + Icons.waves, + size: 20, + color: isFloodMode + ? Theme.of(context).primaryColor + : null, + ), const SizedBox(width: 8), Text( l10n.repeater_forceFloodMode, style: TextStyle( - fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal, + fontWeight: isFloodMode + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -754,7 +815,8 @@ class _RepeaterSettingsScreenState extends State { IconButton( icon: const Icon(Icons.timeline), tooltip: l10n.repeater_pathManagement, - onPressed: () => PathManagementDialog.show(context, contact: repeater), + onPressed: () => + PathManagementDialog.show(context, contact: repeater), ), if (_hasChanges) TextButton.icon( @@ -865,7 +927,9 @@ class _RepeaterSettingsScreenState extends State { border: const OutlineInputBorder(), suffixText: 'MHz', ), - keyboardType: const TextInputType.numberWithOptions(decimal: true), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), onChanged: (_) => _markChanged(), ), const SizedBox(height: 16), @@ -923,10 +987,7 @@ class _RepeaterSettingsScreenState extends State { border: const OutlineInputBorder(), ), items: _spreadingFactorOptions.map((sf) { - return DropdownMenuItem( - value: sf, - child: Text('SF$sf'), - ); + return DropdownMenuItem(value: sf, child: Text('SF$sf')); }).toList(), onChanged: (value) { if (value != null) { @@ -945,10 +1006,7 @@ class _RepeaterSettingsScreenState extends State { border: const OutlineInputBorder(), ), items: _codingRateOptions.map((cr) { - return DropdownMenuItem( - value: cr, - child: Text('4/$cr'), - ); + return DropdownMenuItem(value: cr, child: Text('4/$cr')); }).toList(), onChanged: (value) { if (value != null) { @@ -988,7 +1046,10 @@ class _RepeaterSettingsScreenState extends State { helperText: l10n.repeater_latitudeHelper, border: const OutlineInputBorder(), ), - keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + signed: true, + ), onChanged: (_) => _markChanged(), ), const SizedBox(height: 16), @@ -999,7 +1060,10 @@ class _RepeaterSettingsScreenState extends State { helperText: l10n.repeater_longitudeHelper, border: const OutlineInputBorder(), ), - keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + signed: true, + ), onChanged: (_) => _markChanged(), ), ], @@ -1018,11 +1082,17 @@ class _RepeaterSettingsScreenState extends State { children: [ Row( children: [ - Icon(Icons.toggle_on, color: Theme.of(context).textTheme.headlineSmall?.color), + Icon( + Icons.toggle_on, + color: Theme.of(context).textTheme.headlineSmall?.color, + ), const SizedBox(width: 8), Text( l10n.repeater_features, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), ], ), @@ -1102,7 +1172,7 @@ class _RepeaterSettingsScreenState extends State { width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), - ) + ) : const Icon(Icons.refresh, size: 20), onPressed: isRefreshing ? null : onRefresh, tooltip: refreshTooltip, @@ -1130,40 +1200,72 @@ class _RepeaterSettingsScreenState extends State { const Divider(), ListTile( title: Text(l10n.repeater_localAdvertInterval), - subtitle: Text(l10n.repeater_localAdvertIntervalMinutes(_advertInterval)), - trailing: Text(l10n.repeater_localAdvertIntervalMinutes(_advertInterval)), + subtitle: Text( + l10n.repeater_localAdvertIntervalMinutes(_advertInterval), + ), + trailing: Checkbox( + value: _advertEnable, + onChanged: (value) { + setState(() { + _advertInterval = value! ? 60 : 0; + _advertEnable = value; + }); + _markChanged(); + }, + ), ), Slider( - value: _advertInterval.toDouble(), + value: _advertInterval == 0 + ? 60.toDouble() + : _advertInterval.toDouble(), min: 60, max: 240, divisions: 18, label: l10n.repeater_localAdvertIntervalMinutes(_advertInterval), - onChanged: (value) { - setState(() { - _advertInterval = value.toInt(); - }); - _markChanged(); - }, + onChanged: _advertEnable + ? (value) { + setState(() { + _advertInterval = value.toInt(); + }); + _markChanged(); + } + : null, ), const SizedBox(height: 16), ListTile( title: Text(l10n.repeater_floodAdvertInterval), - subtitle: Text(l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval)), - trailing: Text(l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval)), + subtitle: Text( + l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval), + ), + trailing: Checkbox( + value: _floodAdvertEnable, + onChanged: (value) { + setState(() { + _floodAdvertInterval = value! ? 3 : 0; + _floodAdvertEnable = value; + }); + _markChanged(); + }, + ), ), Slider( - value: _floodAdvertInterval.toDouble(), + value: _floodAdvertInterval == 0 + ? 3.toDouble() + : _floodAdvertInterval.toDouble(), min: 3, - max: 48, - divisions: 45, - label: l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval), - onChanged: (value) { - setState(() { - _floodAdvertInterval = value.toInt(); - }); - _markChanged(); - }, + max: 168, + divisions: 165, + label: l10n.repeater_floodAdvertIntervalHours( + _floodAdvertInterval, + ), + onChanged: _floodAdvertEnable + ? (value) { + setState(() { + _floodAdvertInterval = value.toInt(); + }); + _markChanged(); + } + : null, ), // Encrypted advertisement interval - hidden until privacy mode is implemented // if (_privacyMode) ...[ @@ -1220,10 +1322,15 @@ class _RepeaterSettingsScreenState extends State { const Divider(), ListTile( leading: Icon(Icons.refresh, color: colorScheme.onErrorContainer), - title: Text(l10n.repeater_rebootRepeater, style: TextStyle(color: colorScheme.onErrorContainer)), + title: Text( + l10n.repeater_rebootRepeater, + style: TextStyle(color: colorScheme.onErrorContainer), + ), subtitle: Text( l10n.repeater_rebootRepeaterSubtitle, - style: TextStyle(color: colorScheme.onErrorContainer.withValues(alpha: 0.8)), + style: TextStyle( + color: colorScheme.onErrorContainer.withValues(alpha: 0.8), + ), ), onTap: () => _confirmAction( l10n.repeater_rebootRepeater, @@ -1246,11 +1353,19 @@ class _RepeaterSettingsScreenState extends State { // ), // ), ListTile( - leading: Icon(Icons.delete_forever, color: colorScheme.onErrorContainer), - title: Text(l10n.repeater_eraseFileSystem, style: TextStyle(color: colorScheme.onErrorContainer)), + leading: Icon( + Icons.delete_forever, + color: colorScheme.onErrorContainer, + ), + title: Text( + l10n.repeater_eraseFileSystem, + style: TextStyle(color: colorScheme.onErrorContainer), + ), subtitle: Text( l10n.repeater_eraseFileSystemSubtitle, - style: TextStyle(color: colorScheme.onErrorContainer.withValues(alpha: 0.8)), + style: TextStyle( + color: colorScheme.onErrorContainer.withValues(alpha: 0.8), + ), ), onTap: () => _confirmAction( l10n.repeater_eraseFileSystem, @@ -1272,9 +1387,9 @@ class _RepeaterSettingsScreenState extends State { if (command == 'erase') { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.repeater_eraseSerialOnly)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.repeater_eraseSerialOnly))); } return; }