mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
fix: Restore telemetry refresh behavior
ref:
This commit is contained in:
@@ -513,7 +513,6 @@ class ContactTile extends StatelessWidget {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
showDragHandle: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (sheetContext) => _ContactActionSheet(
|
||||
contact: contact,
|
||||
@@ -1252,10 +1251,21 @@ class _ContactActionSheetState extends State<_ContactActionSheet> {
|
||||
child: Material(
|
||||
color: colorScheme.surface,
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.fromLTRB(16, 8, 16, 16 + bottomInset),
|
||||
padding: EdgeInsets.fromLTRB(16, 12, 16, 16 + bottomInset),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 44,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.outlineVariant,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -1363,17 +1373,26 @@ class _ContactActionSheetState extends State<_ContactActionSheet> {
|
||||
const SizedBox(height: 20),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final columnCount = widget.primaryActions.length <= 1
|
||||
? 1
|
||||
: widget.primaryActions.length == 2
|
||||
? 2
|
||||
: 3;
|
||||
const spacing = 12.0;
|
||||
const minTileWidth = 104.0;
|
||||
final actionCount = widget.primaryActions.length;
|
||||
final maxColumnsByWidth =
|
||||
((constraints.maxWidth + spacing) /
|
||||
(minTileWidth + spacing))
|
||||
.floor()
|
||||
.clamp(1, 4);
|
||||
var columnCount = actionCount.clamp(1, maxColumnsByWidth);
|
||||
if (actionCount > 3 &&
|
||||
columnCount > 2 &&
|
||||
actionCount % columnCount == 1) {
|
||||
columnCount -= 1;
|
||||
}
|
||||
final itemWidth =
|
||||
(constraints.maxWidth - (12 * (columnCount - 1))) /
|
||||
(constraints.maxWidth - (spacing * (columnCount - 1))) /
|
||||
columnCount;
|
||||
return Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
spacing: spacing,
|
||||
runSpacing: spacing,
|
||||
children: [
|
||||
for (final action in widget.primaryActions)
|
||||
SizedBox(
|
||||
|
||||
@@ -87,6 +87,9 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
||||
return lastSeenCompare;
|
||||
}
|
||||
} else if (_sortMode == _RecipientSortMode.activity) {
|
||||
if (a.isFavourite != b.isFavourite) {
|
||||
return a.isFavourite ? -1 : 1;
|
||||
}
|
||||
final unreadCompare = _unreadFor(b).compareTo(_unreadFor(a));
|
||||
if (unreadCompare != 0) {
|
||||
return unreadCompare;
|
||||
@@ -147,6 +150,30 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
||||
}
|
||||
}
|
||||
|
||||
String _sectionDescription(AppLocalizations l10n, String type) {
|
||||
switch (type) {
|
||||
case 'channel':
|
||||
return 'Broadcast lanes for nearby mesh traffic';
|
||||
case 'room':
|
||||
return 'Shared spaces for ongoing team coordination';
|
||||
case 'contact':
|
||||
return 'Direct people and devices you can reach';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
String _channelSubtitle(BuildContext context, Contact channel) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
if (channel.isPublicChannel) {
|
||||
return l10n.broadcastToAllNearby;
|
||||
}
|
||||
|
||||
final channelIdx = channel.publicKey.length > 1 ? channel.publicKey[1] : 0;
|
||||
final shortKey = channel.publicKeyShort.toUpperCase();
|
||||
return '${l10n.channel} $channelIdx • $shortKey';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
@@ -194,21 +221,45 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.selectRecipient,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.selectRecipient,
|
||||
style: Theme.of(context).textTheme.titleLarge
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Pick a channel, room, or direct contact.',
|
||||
style: Theme.of(context).textTheme.bodyMedium
|
||||
?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
IconButton.filledTonal(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: l10n.close,
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(
|
||||
color: colorScheme.outlineVariant.withValues(
|
||||
alpha: 0.18,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: l10n.close,
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -278,9 +329,7 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
||||
type: 'channel',
|
||||
contact: channel,
|
||||
title: channel.getLocalizedDisplayName(context),
|
||||
subtitle: channel.isPublicChannel
|
||||
? l10n.broadcastToAllNearby
|
||||
: '${l10n.channel} ${channel.publicKey[1]}',
|
||||
subtitle: _channelSubtitle(context, channel),
|
||||
unreadCount: _unreadFor(channel),
|
||||
isSelected: _isSelected('channel', channel),
|
||||
onTap: () {
|
||||
@@ -511,6 +560,13 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_sectionDescription(AppLocalizations.of(context)!, type),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (children.isEmpty)
|
||||
Padding(
|
||||
@@ -685,14 +741,42 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.2,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleSmall
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (contact.isPublicChannel) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: accentColor.withValues(alpha: 0.10),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
'Public',
|
||||
style: Theme.of(context).textTheme.labelSmall
|
||||
?.copyWith(
|
||||
color: accentColor,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_map/flutter_map.dart' as flutter_map;
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
@@ -547,12 +548,15 @@ SensorMetricCardData? _buildOptionPreviewCardData(
|
||||
fieldKey: fieldKey,
|
||||
icon: Icons.explore_outlined,
|
||||
label: label,
|
||||
value: previewValue,
|
||||
value: degrees == null
|
||||
? previewValue
|
||||
: '${_formatPreviewNumber(degrees, maxFractionDigits: 0)}°',
|
||||
secondaryValue: degrees == null
|
||||
? null
|
||||
: _previewFormatCardinalDirection(degrees),
|
||||
accent: const Color(0xFF8A5A44),
|
||||
channel: metricKey.channel,
|
||||
directionDegrees: degrees,
|
||||
);
|
||||
case 'unixtime':
|
||||
return SensorMetricCardData(
|
||||
@@ -1007,6 +1011,13 @@ String _formatPreviewNumber(num value, {int maxFractionDigits = 2}) {
|
||||
? math.min(maxFractionDigits, 1)
|
||||
: maxFractionDigits;
|
||||
final text = value.toStringAsFixed(digits);
|
||||
return _trimFractionZeros(text);
|
||||
}
|
||||
|
||||
String _trimFractionZeros(String text) {
|
||||
if (!text.contains('.')) {
|
||||
return text;
|
||||
}
|
||||
return text.replaceFirst(RegExp(r'\.?0+$'), '');
|
||||
}
|
||||
|
||||
@@ -1030,6 +1041,8 @@ class SensorTelemetryCard extends StatelessWidget {
|
||||
final Future<void> Function()? onRefresh;
|
||||
final VoidCallback? onCustomize;
|
||||
final Future<void> Function(Contact contact)? onShowMetHistory;
|
||||
final Future<void> Function()? onMoveUp;
|
||||
final Future<void> Function()? onMoveDown;
|
||||
final EdgeInsetsGeometry margin;
|
||||
final String emptyMetricsMessage;
|
||||
final Map<String, String> labelOverrides;
|
||||
@@ -1045,6 +1058,8 @@ class SensorTelemetryCard extends StatelessWidget {
|
||||
this.onRefresh,
|
||||
this.onCustomize,
|
||||
this.onShowMetHistory,
|
||||
this.onMoveUp,
|
||||
this.onMoveDown,
|
||||
this.margin = const EdgeInsets.only(bottom: 16),
|
||||
this.emptyMetricsMessage =
|
||||
'All fields are hidden. Use Visible fields to choose what to show.',
|
||||
@@ -1055,6 +1070,9 @@ class SensorTelemetryCard extends StatelessWidget {
|
||||
onRefresh != null ||
|
||||
onCustomize != null ||
|
||||
onRemove != null ||
|
||||
onMoveUp != null ||
|
||||
onMoveDown != null ||
|
||||
_rawTelemetryHex(contact?.telemetry) != null ||
|
||||
(contact != null &&
|
||||
onShowMetHistory != null &&
|
||||
supportsBTHomeMetHistory(contact));
|
||||
@@ -1136,21 +1154,21 @@ class SensorTelemetryCard extends StatelessWidget {
|
||||
),
|
||||
if (state == SensorRefreshState.refreshing)
|
||||
_InlineStateMeta(
|
||||
label: l10n.refreshing,
|
||||
color: Color(0xFF266AC2),
|
||||
spinning: true,
|
||||
tooltip: l10n.refreshing,
|
||||
),
|
||||
if (state == SensorRefreshState.success)
|
||||
const _InlineStateMeta(
|
||||
label: 'Updated',
|
||||
color: Color(0xFF218B63),
|
||||
icon: Icons.check_circle,
|
||||
tooltip: 'Updated',
|
||||
),
|
||||
if (state == SensorRefreshState.unavailable)
|
||||
_InlineStateMeta(
|
||||
label: l10n.unavailable,
|
||||
color: Color(0xFFB13B55),
|
||||
icon: Icons.error_outline,
|
||||
tooltip: l10n.unavailable,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1163,6 +1181,26 @@ class SensorTelemetryCard extends StatelessWidget {
|
||||
onSelected: (value) async {
|
||||
if (value == 'refresh' && onRefresh != null) {
|
||||
await onRefresh!();
|
||||
} else if (value == 'move_up' && onMoveUp != null) {
|
||||
await onMoveUp!();
|
||||
} else if (value == 'move_down' && onMoveDown != null) {
|
||||
await onMoveDown!();
|
||||
} else if (value == 'copy_raw') {
|
||||
final rawTelemetry = _rawTelemetryHex(
|
||||
contact?.telemetry,
|
||||
);
|
||||
if (rawTelemetry != null && context.mounted) {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(text: rawTelemetry),
|
||||
);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Raw response copied'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (value == 'remove' && onRemove != null) {
|
||||
await onRemove!();
|
||||
} else if (value == 'customize' && onCustomize != null) {
|
||||
@@ -1183,6 +1221,30 @@ class SensorTelemetryCard extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
if (onMoveUp != null) {
|
||||
items.add(
|
||||
const PopupMenuItem<String>(
|
||||
value: 'move_up',
|
||||
child: Text('Move up'),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (onMoveDown != null) {
|
||||
items.add(
|
||||
const PopupMenuItem<String>(
|
||||
value: 'move_down',
|
||||
child: Text('Move down'),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_rawTelemetryHex(contact?.telemetry) != null) {
|
||||
items.add(
|
||||
const PopupMenuItem<String>(
|
||||
value: 'copy_raw',
|
||||
child: Text('Copy raw response'),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (onCustomize != null) {
|
||||
items.add(
|
||||
PopupMenuItem<String>(
|
||||
@@ -1811,10 +1873,11 @@ class SensorTelemetryCard extends StatelessWidget {
|
||||
fieldKey: _extraFieldKey(rawKey),
|
||||
icon: Icons.explore_outlined,
|
||||
label: label,
|
||||
value: '${_formatNumber(degrees, maxFractionDigits: 0)} deg',
|
||||
value: '${_formatNumber(degrees, maxFractionDigits: 0)}°',
|
||||
secondaryValue: _formatCardinalDirection(degrees),
|
||||
accent: const Color(0xFF8A5A44),
|
||||
channel: metricKey.channel,
|
||||
directionDegrees: degrees,
|
||||
);
|
||||
|
||||
case 'rotation':
|
||||
@@ -2054,7 +2117,7 @@ class SensorTelemetryCard extends StatelessWidget {
|
||||
? math.min(maxFractionDigits, 1)
|
||||
: maxFractionDigits;
|
||||
final text = value.toStringAsFixed(digits);
|
||||
return text.replaceFirst(RegExp(r'\.?0+$'), '');
|
||||
return _trimFractionZeros(text);
|
||||
}
|
||||
|
||||
_Vector3? _asVector3(dynamic value) {
|
||||
@@ -2118,31 +2181,28 @@ class SensorTelemetryCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _InlineStateMeta extends StatelessWidget {
|
||||
final String label;
|
||||
final Color color;
|
||||
final IconData? icon;
|
||||
final bool spinning;
|
||||
final String? tooltip;
|
||||
|
||||
const _InlineStateMeta({
|
||||
required this.label,
|
||||
required this.color,
|
||||
this.icon,
|
||||
this.spinning = false,
|
||||
this.tooltip,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
final badge = Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.10),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (spinning)
|
||||
SizedBox(
|
||||
child: spinning
|
||||
? SizedBox(
|
||||
width: 11,
|
||||
height: 11,
|
||||
child: CircularProgressIndicator(
|
||||
@@ -2150,18 +2210,20 @@ class _InlineStateMeta extends StatelessWidget {
|
||||
valueColor: AlwaysStoppedAnimation<Color>(color),
|
||||
),
|
||||
)
|
||||
else if (icon != null)
|
||||
Icon(icon, size: 11, color: color),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
: Icon(
|
||||
icon ?? Icons.info_outline,
|
||||
size: 11,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (tooltip == null || tooltip!.isEmpty) {
|
||||
return badge;
|
||||
}
|
||||
|
||||
return Tooltip(
|
||||
message: tooltip!,
|
||||
child: badge,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2292,7 +2354,40 @@ class SensorMetricTile extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
border: Border.all(color: data.accent.withValues(alpha: 0.14)),
|
||||
),
|
||||
child: data.mapLocation == null || !allowMapPreview
|
||||
child: data.directionDegrees != null
|
||||
? Stack(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_DirectionDial(
|
||||
accent: data.accent,
|
||||
degrees: data.directionDegrees!,
|
||||
cardinal: data.secondaryValue ?? '',
|
||||
compact: true,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _MetricText(data: data, keyPrefix: keyPrefix),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (data.channel != null)
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Text(
|
||||
'ch${data.channel}',
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: data.accent.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: data.mapLocation == null || !allowMapPreview
|
||||
? Stack(
|
||||
children: [
|
||||
Row(
|
||||
@@ -2456,6 +2551,10 @@ class _MetricText extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (data.directionDegrees != null) {
|
||||
return _DirectionMetricText(data: data);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -2495,12 +2594,138 @@ class _MetricText extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _DirectionMetricText extends StatelessWidget {
|
||||
final SensorMetricCardData data;
|
||||
|
||||
const _DirectionMetricText({required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final directionLabel = data.secondaryValue ?? '';
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
data.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: data.accent,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
data.value,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
if (directionLabel.isNotEmpty)
|
||||
Text(
|
||||
directionLabel,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: data.accent.withValues(alpha: 0.9),
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DirectionDial extends StatelessWidget {
|
||||
final Color accent;
|
||||
final double degrees;
|
||||
final String cardinal;
|
||||
final bool compact;
|
||||
|
||||
const _DirectionDial({
|
||||
required this.accent,
|
||||
required this.degrees,
|
||||
required this.cardinal,
|
||||
this.compact = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final normalized = ((degrees % 360) + 360) % 360;
|
||||
final size = compact ? 34.0 : 44.0;
|
||||
final innerSize = compact ? 24.0 : 30.0;
|
||||
final arrowSize = compact ? 14.0 : 16.0;
|
||||
final northTop = compact ? 1.0 : 3.0;
|
||||
|
||||
return SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: accent.withValues(alpha: 0.08),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: accent.withValues(alpha: 0.2)),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: innerSize,
|
||||
height: innerSize,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: accent.withValues(alpha: 0.2)),
|
||||
),
|
||||
),
|
||||
Transform.rotate(
|
||||
angle: normalized * math.pi / 180,
|
||||
child: Icon(
|
||||
Icons.navigation_rounded,
|
||||
size: arrowSize,
|
||||
color: accent,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: northTop,
|
||||
child: Text(
|
||||
'N',
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: accent.withValues(alpha: 0.65),
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: compact ? 7.5 : 9,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SensorMetricCardData {
|
||||
final String fieldKey;
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String value;
|
||||
final String? secondaryValue;
|
||||
final double? directionDegrees;
|
||||
final Color accent;
|
||||
final bool wide;
|
||||
final LatLng? mapLocation;
|
||||
@@ -2512,6 +2737,7 @@ class SensorMetricCardData {
|
||||
required this.label,
|
||||
required this.value,
|
||||
this.secondaryValue,
|
||||
this.directionDegrees,
|
||||
required this.accent,
|
||||
this.wide = false,
|
||||
this.mapLocation,
|
||||
@@ -2543,13 +2769,20 @@ class _RgbColor {
|
||||
}
|
||||
|
||||
const String _telemetrySourceChannelPrefix = '__source_channel:';
|
||||
const String _rawTelemetryHexKey = '__raw_lpp_hex';
|
||||
|
||||
String _extraFieldKey(String label) {
|
||||
return 'extra:$label';
|
||||
}
|
||||
|
||||
bool _isTelemetryMetadataKey(String key) {
|
||||
return key.startsWith(_telemetrySourceChannelPrefix);
|
||||
return key.startsWith(_telemetrySourceChannelPrefix) ||
|
||||
key == _rawTelemetryHexKey;
|
||||
}
|
||||
|
||||
String? _rawTelemetryHex(ContactTelemetry? telemetry) {
|
||||
final value = telemetry?.extraSensorData?[_rawTelemetryHexKey];
|
||||
return value is String && value.trim().isNotEmpty ? value : null;
|
||||
}
|
||||
|
||||
String _telemetrySourceChannelKey(String fieldKey) {
|
||||
|
||||
Reference in New Issue
Block a user