Show values in sensor fields

This commit is contained in:
Janez T
2026-03-15 10:22:53 +01:00
parent df993cd176
commit 650f3c66aa
3 changed files with 208 additions and 173 deletions

View File

@@ -270,25 +270,10 @@ class AppProvider with ChangeNotifier {
}
Future<void> _checkLowBatteryAlerts() async {
final recoveredIds = <String>{};
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<void> _notifyLowBatteryIfNeeded({

View File

@@ -252,151 +252,48 @@ class _SensorsTabState extends State<SensorsTab> {
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<int>(
segments: const [
ButtonSegment<int>(
value: 1,
label: Text('1x'),
),
ButtonSegment<int>(
value: 2,
label: Text('2x'),
),
],
selected: <int>{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<SensorsTab> {
}
}
class SensorMetricSelectorItem extends StatelessWidget {
final SensorMetricOption option;
final bool visible;
final int span;
final bool canMoveUp;
final bool canMoveDown;
final ValueChanged<bool> onToggle;
final VoidCallback onRename;
final VoidCallback? onMoveUp;
final VoidCallback? onMoveDown;
final ValueChanged<int> 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<int>(
segments: const [
ButtonSegment<int>(value: 1, label: Text('1x')),
ButtonSegment<int>(value: 2, label: Text('2x')),
],
selected: <int>{span},
onSelectionChanged: (selection) {
onSpanChanged(selection.first);
},
),
],
),
),
],
),
);
}
}
class _SensorCandidatePreview extends StatelessWidget {
final Contact contact;

View File

@@ -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);
});
}