diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index f6a8587..03efc9f 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -270,25 +270,10 @@ class AppProvider with ChangeNotifier { } Future _checkLowBatteryAlerts() async { - final recoveredIds = {}; - final deviceBattery = connectionProvider.deviceInfo.batteryPercent; if (deviceBattery != null && deviceBattery > _lowBatteryResetThresholdPercent) { - recoveredIds.add('device'); - } - - for (final contact in contactsProvider.contacts) { - if (contact.isChannel) continue; - final battery = contact.displayBattery; - if (battery == null) continue; - if (battery > _lowBatteryResetThresholdPercent) { - recoveredIds.add(contact.publicKeyHex); - } - } - - if (recoveredIds.isNotEmpty) { - _lowBatteryNotifiedNodeIds.removeAll(recoveredIds); + _lowBatteryNotifiedNodeIds.remove('device'); } if (connectionProvider.deviceInfo.isConnected && deviceBattery != null) { @@ -302,19 +287,6 @@ class AppProvider with ChangeNotifier { isCurrentDevice: true, ); } - - for (final contact in contactsProvider.contacts) { - if (contact.isChannel) continue; - final battery = contact.displayBattery; - if (battery == null) continue; - - await _notifyLowBatteryIfNeeded( - nodeId: contact.publicKeyHex, - nodeName: contact.displayName, - batteryPercent: battery, - isCurrentDevice: false, - ); - } } Future _notifyLowBatteryIfNeeded({ diff --git a/lib/screens/sensors_tab.dart b/lib/screens/sensors_tab.dart index 35fddab..ba5eebd 100644 --- a/lib/screens/sensors_tab.dart +++ b/lib/screens/sensors_tab.dart @@ -252,151 +252,48 @@ class _SensorsTabState extends State { publicKeyHex, option.key, ); - return Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: FilterChip( - selected: visible, - label: Text(option.label), - onSelected: (value) { - sensorsProvider.toggleMetric( - publicKeyHex, - option.key, - value, - ); - }, - ), - ), - const SizedBox(width: 8), - IconButton( - tooltip: 'Rename', - onPressed: () => _showMetricRenameDialog( - context, - publicKeyHex: publicKeyHex, - option: option, - sensorsProvider: sensorsProvider, - ), - icon: const Icon(Icons.edit_outlined), - ), - ], - ), - if (option.valuePreview != null || - option.channel != null) - Padding( - padding: const EdgeInsets.only(top: 8), - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 8, - runSpacing: 6, - children: [ - if (option.valuePreview != null) - Text( - option.valuePreview!, - key: ValueKey( - 'sensor_selector_value_${option.key}', - ), - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w600, - ), - ), - if (option.channel != null) - Container( - key: ValueKey( - 'sensor_selector_channel_${option.key}', - ), - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 3, - ), - decoration: BoxDecoration( - color: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(999), - ), - child: Text( - 'ch${option.channel}', - style: Theme.of(context) - .textTheme - .labelSmall - ?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w800, - ), - ), - ), - ], - ), - ), - const SizedBox(height: 8), - Align( - alignment: Alignment.centerRight, - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 8, - children: [ - IconButton( - tooltip: 'Move up', - onPressed: index > 0 - ? () => sensorsProvider.moveMetric( - publicKeyHex, - availableFieldKeys: orderedFieldKeys, - oldIndex: index, - newIndex: index - 1, - ) - : null, - icon: const Icon(Icons.arrow_upward), - ), - IconButton( - tooltip: 'Move down', - onPressed: index < orderedOptions.length - 1 - ? () => sensorsProvider.moveMetric( - publicKeyHex, - availableFieldKeys: orderedFieldKeys, - oldIndex: index, - newIndex: index + 1, - ) - : null, - icon: const Icon(Icons.arrow_downward), - ), - SegmentedButton( - segments: const [ - ButtonSegment( - value: 1, - label: Text('1x'), - ), - ButtonSegment( - value: 2, - label: Text('2x'), - ), - ], - selected: {span}, - onSelectionChanged: (selection) { - sensorsProvider.setFieldSpan( - publicKeyHex, - option.key, - selection.first, - ); - }, - ), - ], - ), - ), - ], + return SensorMetricSelectorItem( + option: option, + visible: visible, + span: span, + canMoveUp: index > 0, + canMoveDown: index < orderedOptions.length - 1, + onToggle: (value) { + sensorsProvider.toggleMetric( + publicKeyHex, + option.key, + value, + ); + }, + onRename: () => _showMetricRenameDialog( + context, + publicKeyHex: publicKeyHex, + option: option, + sensorsProvider: sensorsProvider, ), + onMoveUp: index > 0 + ? () => sensorsProvider.moveMetric( + publicKeyHex, + availableFieldKeys: orderedFieldKeys, + oldIndex: index, + newIndex: index - 1, + ) + : null, + onMoveDown: index < orderedOptions.length - 1 + ? () => sensorsProvider.moveMetric( + publicKeyHex, + availableFieldKeys: orderedFieldKeys, + oldIndex: index, + newIndex: index + 1, + ) + : null, + onSpanChanged: (selection) { + sensorsProvider.setFieldSpan( + publicKeyHex, + option.key, + selection, + ); + }, ); }), ], @@ -542,6 +439,132 @@ class _SensorsTabState extends State { } } +class SensorMetricSelectorItem extends StatelessWidget { + final SensorMetricOption option; + final bool visible; + final int span; + final bool canMoveUp; + final bool canMoveDown; + final ValueChanged onToggle; + final VoidCallback onRename; + final VoidCallback? onMoveUp; + final VoidCallback? onMoveDown; + final ValueChanged onSpanChanged; + + const SensorMetricSelectorItem({ + super.key, + required this.option, + required this.visible, + required this.span, + required this.canMoveUp, + required this.canMoveDown, + required this.onToggle, + required this.onRename, + this.onMoveUp, + this.onMoveDown, + required this.onSpanChanged, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: FilterChip( + selected: visible, + label: Text(option.label), + onSelected: onToggle, + ), + ), + const SizedBox(width: 8), + IconButton( + tooltip: 'Rename', + onPressed: onRename, + icon: const Icon(Icons.edit_outlined), + ), + ], + ), + if (option.valuePreview != null || option.channel != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 8, + runSpacing: 6, + children: [ + if (option.valuePreview != null) + Text( + option.valuePreview!, + key: ValueKey('sensor_selector_value_${option.key}'), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w600, + ), + ), + if (option.channel != null) + Container( + key: ValueKey('sensor_selector_channel_${option.key}'), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 3, + ), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(999), + ), + child: Text( + 'ch${option.channel}', + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w800, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 8), + Align( + alignment: Alignment.centerRight, + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 8, + children: [ + IconButton( + tooltip: 'Move up', + onPressed: canMoveUp ? onMoveUp : null, + icon: const Icon(Icons.arrow_upward), + ), + IconButton( + tooltip: 'Move down', + onPressed: canMoveDown ? onMoveDown : null, + icon: const Icon(Icons.arrow_downward), + ), + SegmentedButton( + segments: const [ + ButtonSegment(value: 1, label: Text('1x')), + ButtonSegment(value: 2, label: Text('2x')), + ], + selected: {span}, + onSelectionChanged: (selection) { + onSpanChanged(selection.first); + }, + ), + ], + ), + ), + ], + ), + ); + } +} + class _SensorCandidatePreview extends StatelessWidget { final Contact contact; diff --git a/test/widgets/sensor_metric_selector_item_test.dart b/test/widgets/sensor_metric_selector_item_test.dart new file mode 100644 index 0000000..7e7b3d4 --- /dev/null +++ b/test/widgets/sensor_metric_selector_item_test.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_sar_app/screens/sensors_tab.dart'; +import 'package:meshcore_sar_app/widgets/sensors/sensor_telemetry_card.dart'; + +void main() { + testWidgets('renders selector previews and channel badges', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SensorMetricSelectorItem( + option: const SensorMetricOption( + key: 'extra:illuminance_2', + label: 'Illuminance (ch 2)', + defaultLabel: 'Illuminance', + channel: 2, + valuePreview: '500 lx', + ), + visible: true, + span: 1, + canMoveUp: true, + canMoveDown: true, + onToggle: (_) {}, + onRename: () {}, + onMoveUp: () {}, + onMoveDown: () {}, + onSpanChanged: (_) {}, + ), + ), + ), + ); + + expect(find.text('500 lx'), findsOneWidget); + expect( + find.byKey(const ValueKey('sensor_selector_channel_extra:illuminance_2')), + findsOneWidget, + ); + expect(find.text('ch2'), findsOneWidget); + }); +}