mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-15 02:10:30 +00:00
Add stats pills and RFC009 date
This commit is contained in:
@@ -489,7 +489,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
CURRENT_PROJECT_VERSION = 71;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -511,7 +511,7 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
CURRENT_PROJECT_VERSION = 71;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
@@ -530,7 +530,7 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
CURRENT_PROJECT_VERSION = 71;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
@@ -547,7 +547,7 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
CURRENT_PROJECT_VERSION = 71;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
@@ -679,7 +679,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
CURRENT_PROJECT_VERSION = 71;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -702,7 +702,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
CURRENT_PROJECT_VERSION = 71;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>70</string>
|
||||
<string>71</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||
|
||||
@@ -5,22 +5,22 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000208">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000227">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.413276">
|
||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.41002">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: build_app" time="113.573989">
|
||||
<testcase classname="fastlane.lanes" name="2: build_app" time="107.00587">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="8345.856849">
|
||||
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="232.858645">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -58,10 +58,17 @@ class MessageBubble extends StatefulWidget {
|
||||
|
||||
class _MessageBubbleState extends State<MessageBubble> {
|
||||
bool _isExpanded = false;
|
||||
bool _showReceivedStats = false;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(MessageBubble oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.message.id != widget.message.id) {
|
||||
_isExpanded = false;
|
||||
_showReceivedStats = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Force rebuild when message properties change (especially recipient statuses)
|
||||
if (oldWidget.message.id == widget.message.id) {
|
||||
// Same message, but properties might have changed
|
||||
@@ -77,6 +84,15 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
});
|
||||
}
|
||||
|
||||
void _handleBubbleTap({required bool isSarMarker, required bool isDrawing}) {
|
||||
if (!widget.isCompact && !isSarMarker && !isDrawing) {
|
||||
setState(() {
|
||||
_showReceivedStats = !_showReceivedStats;
|
||||
});
|
||||
}
|
||||
widget.onTap?.call();
|
||||
}
|
||||
|
||||
Future<void> _retryFailedMessage(
|
||||
BuildContext context,
|
||||
Message failedMessage,
|
||||
@@ -611,7 +627,8 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
_techBadge(
|
||||
context,
|
||||
icon: Icons.message,
|
||||
label: widget.message.messageType.name.toUpperCase(),
|
||||
label: widget.message.messageType.name
|
||||
.toUpperCase(),
|
||||
),
|
||||
_techBadge(
|
||||
context,
|
||||
@@ -649,9 +666,7 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
label: 'RSSI',
|
||||
valueLabel: '$rssiDbm dBm',
|
||||
normalized:
|
||||
((rssiDbm.toDouble() +
|
||||
120.0) /
|
||||
70.0)
|
||||
((rssiDbm.toDouble() + 120.0) / 70.0)
|
||||
.clamp(0.0, 1.0),
|
||||
color: rssiDbm >= -80
|
||||
? Colors.green
|
||||
@@ -665,7 +680,10 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
context,
|
||||
label: 'SNR',
|
||||
valueLabel: '${snrDb.toStringAsFixed(1)} dB',
|
||||
normalized: ((snrDb + 20.0) / 40.0).clamp(0.0, 1.0),
|
||||
normalized: ((snrDb + 20.0) / 40.0).clamp(
|
||||
0.0,
|
||||
1.0,
|
||||
),
|
||||
color: snrDb >= 10
|
||||
? Colors.green
|
||||
: snrDb >= 0
|
||||
@@ -693,7 +711,8 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
_detailRow(
|
||||
context,
|
||||
label: l10n.expectedAckTag,
|
||||
value: widget.message.expectedAckTag!.toString(),
|
||||
value: widget.message.expectedAckTag!
|
||||
.toString(),
|
||||
),
|
||||
if (widget.message.roundTripTimeMs != null)
|
||||
_detailRow(
|
||||
@@ -739,7 +758,10 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
_detailRow(
|
||||
context,
|
||||
label: l10n.sender,
|
||||
value: senderName ?? widget.message.senderName ?? 'Unknown',
|
||||
value:
|
||||
senderName ??
|
||||
widget.message.senderName ??
|
||||
'Unknown',
|
||||
),
|
||||
if (senderPrefixHex != null)
|
||||
_detailRow(
|
||||
@@ -797,7 +819,9 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
_detailRow(
|
||||
context,
|
||||
label: l10n.complete,
|
||||
value: voiceSession.isComplete ? l10n.yes : l10n.no,
|
||||
value: voiceSession.isComplete
|
||||
? l10n.yes
|
||||
: l10n.no,
|
||||
),
|
||||
if (voiceTxEstimate > Duration.zero)
|
||||
_detailRow(
|
||||
@@ -819,7 +843,11 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
title: 'Image',
|
||||
child: Column(
|
||||
children: [
|
||||
_detailRow(context, label: l10n.envelope, value: 'IE1'),
|
||||
_detailRow(
|
||||
context,
|
||||
label: l10n.envelope,
|
||||
value: 'IE1',
|
||||
),
|
||||
_detailRow(
|
||||
context,
|
||||
label: 'Format',
|
||||
@@ -842,7 +870,9 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
_detailRow(
|
||||
context,
|
||||
label: l10n.complete,
|
||||
value: imageSession.isComplete ? l10n.yes : l10n.no,
|
||||
value: imageSession.isComplete
|
||||
? l10n.yes
|
||||
: l10n.no,
|
||||
),
|
||||
if (imageTxEstimate > Duration.zero)
|
||||
_detailRow(
|
||||
@@ -1043,7 +1073,10 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
);
|
||||
}
|
||||
|
||||
BlePacketLog? _findBestMatchingRxLog(List<BlePacketLog> logs, Message message) {
|
||||
BlePacketLog? _findBestMatchingRxLog(
|
||||
List<BlePacketLog> logs,
|
||||
Message message,
|
||||
) {
|
||||
if (message.pathLen < 0 || message.pathLen >= 255) return null;
|
||||
final expectedPayloadType = message.messageType == MessageType.channel
|
||||
? 0x05
|
||||
@@ -1465,6 +1498,55 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReceivedSignalStatus(
|
||||
BuildContext context,
|
||||
Message message, {
|
||||
required int? rssiDbm,
|
||||
required double? snrDb,
|
||||
}) {
|
||||
final hopLabel = message.pathLen == 0
|
||||
? 'Direct'
|
||||
: '${message.pathLen} hop${message.pathLen == 1 ? '' : 's'}';
|
||||
|
||||
return Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
_techChip(
|
||||
context,
|
||||
icon: Icons.alt_route,
|
||||
label: hopLabel,
|
||||
color: Colors.indigo,
|
||||
),
|
||||
if (rssiDbm != null || snrDb != null) ...[
|
||||
_techChip(
|
||||
context,
|
||||
icon: Icons.bolt,
|
||||
label: _linkQualityLabel(rssiDbm, snrDb),
|
||||
color: _linkQualityColor(_linkQualityLabel(rssiDbm, snrDb)),
|
||||
),
|
||||
if (rssiDbm != null)
|
||||
_signalCapsule(
|
||||
context,
|
||||
icon: Icons.network_cell,
|
||||
label: '$rssiDbm',
|
||||
filled: _rssiScore(rssiDbm),
|
||||
color: Colors.blueGrey,
|
||||
),
|
||||
if (snrDb != null)
|
||||
_signalCapsule(
|
||||
context,
|
||||
icon: Icons.graphic_eq,
|
||||
label: snrDb.toStringAsFixed(1),
|
||||
filled: _snrScore(snrDb),
|
||||
color: Colors.teal,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _techChip(
|
||||
BuildContext context, {
|
||||
required IconData icon,
|
||||
@@ -1585,6 +1667,19 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
final selfPublicKey = connectionProvider.deviceInfo.publicKey;
|
||||
final isOwnMessage =
|
||||
message.isSentMessage || message.isFromSelf(selfPublicKey);
|
||||
final matchedRxLog = !isOwnMessage
|
||||
? _findBestMatchingRxLog(
|
||||
connectionProvider.bleService.packetLogs,
|
||||
message,
|
||||
)
|
||||
: null;
|
||||
final snrDb =
|
||||
matchedRxLog?.logRxDataInfo?.snrDb ??
|
||||
(message.lastEchoSnrRaw != null
|
||||
? (message.lastEchoSnrRaw!.toSigned(8) / 4.0)
|
||||
: null);
|
||||
final rssiDbm =
|
||||
matchedRxLog?.logRxDataInfo?.rssiDbm ?? message.lastEchoRssiDbm;
|
||||
|
||||
// Look up contact information for rich display name
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
@@ -1660,7 +1755,10 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
: recipientDisplayName;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
onTap: () => _handleBubbleTap(
|
||||
isSarMarker: isSarMarker,
|
||||
isDrawing: message.isDrawing,
|
||||
),
|
||||
onLongPress: widget.isCompact ? null : () => _showMessageOptions(context),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
@@ -2065,6 +2163,19 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
else if (!message.isDrawing || widget.isCompact)
|
||||
Text(message.text, style: Theme.of(context).textTheme.bodyMedium),
|
||||
|
||||
if (!widget.isCompact &&
|
||||
!isSarMarker &&
|
||||
!message.isDrawing &&
|
||||
_showReceivedStats) ...[
|
||||
const SizedBox(height: 6),
|
||||
_buildReceivedSignalStatus(
|
||||
context,
|
||||
message,
|
||||
rssiDbm: rssiDbm,
|
||||
snrDb: snrDb,
|
||||
),
|
||||
],
|
||||
|
||||
// Delivery status for sent messages (skip in compact mode)
|
||||
if (message.isSentMessage && !widget.isCompact) ...[
|
||||
const SizedBox(height: 6),
|
||||
|
||||
Reference in New Issue
Block a user