mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Show values in sensor fields
This commit is contained in:
@@ -270,25 +270,10 @@ class AppProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _checkLowBatteryAlerts() async {
|
Future<void> _checkLowBatteryAlerts() async {
|
||||||
final recoveredIds = <String>{};
|
|
||||||
|
|
||||||
final deviceBattery = connectionProvider.deviceInfo.batteryPercent;
|
final deviceBattery = connectionProvider.deviceInfo.batteryPercent;
|
||||||
if (deviceBattery != null &&
|
if (deviceBattery != null &&
|
||||||
deviceBattery > _lowBatteryResetThresholdPercent) {
|
deviceBattery > _lowBatteryResetThresholdPercent) {
|
||||||
recoveredIds.add('device');
|
_lowBatteryNotifiedNodeIds.remove('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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connectionProvider.deviceInfo.isConnected && deviceBattery != null) {
|
if (connectionProvider.deviceInfo.isConnected && deviceBattery != null) {
|
||||||
@@ -302,19 +287,6 @@ class AppProvider with ChangeNotifier {
|
|||||||
isCurrentDevice: true,
|
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({
|
Future<void> _notifyLowBatteryIfNeeded({
|
||||||
|
|||||||
@@ -252,151 +252,48 @@ class _SensorsTabState extends State<SensorsTab> {
|
|||||||
publicKeyHex,
|
publicKeyHex,
|
||||||
option.key,
|
option.key,
|
||||||
);
|
);
|
||||||
return Padding(
|
return SensorMetricSelectorItem(
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
option: option,
|
||||||
child: Column(
|
visible: visible,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
span: span,
|
||||||
children: [
|
canMoveUp: index > 0,
|
||||||
Row(
|
canMoveDown: index < orderedOptions.length - 1,
|
||||||
children: [
|
onToggle: (value) {
|
||||||
Expanded(
|
sensorsProvider.toggleMetric(
|
||||||
child: FilterChip(
|
publicKeyHex,
|
||||||
selected: visible,
|
option.key,
|
||||||
label: Text(option.label),
|
value,
|
||||||
onSelected: (value) {
|
);
|
||||||
sensorsProvider.toggleMetric(
|
},
|
||||||
publicKeyHex,
|
onRename: () => _showMetricRenameDialog(
|
||||||
option.key,
|
context,
|
||||||
value,
|
publicKeyHex: publicKeyHex,
|
||||||
);
|
option: option,
|
||||||
},
|
sensorsProvider: sensorsProvider,
|
||||||
),
|
|
||||||
),
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
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 {
|
class _SensorCandidatePreview extends StatelessWidget {
|
||||||
final Contact contact;
|
final Contact contact;
|
||||||
|
|
||||||
|
|||||||
40
test/widgets/sensor_metric_selector_item_test.dart
Normal file
40
test/widgets/sensor_metric_selector_item_test.dart
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user