From 5751cddaa1d228f89985fbc31156dcf57361870f Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 15 Feb 2026 11:45:48 -0800 Subject: [PATCH] Add SNRIndicator to AppBar and refactor BatteryIndicator layout --- lib/widgets/app_bar.dart | 10 +- lib/widgets/battery_indicator.dart | 64 ------------- lib/widgets/snr_indicator.dart | 142 +++++++++++++++++++++++++++++ pubspec.lock | 16 ++-- 4 files changed, 159 insertions(+), 73 deletions(-) diff --git a/lib/widgets/app_bar.dart b/lib/widgets/app_bar.dart index 1b2ed38..823da28 100644 --- a/lib/widgets/app_bar.dart +++ b/lib/widgets/app_bar.dart @@ -3,6 +3,8 @@ import 'package:meshcore_open/connector/meshcore_connector.dart'; import 'package:meshcore_open/widgets/battery_indicator.dart'; import 'package:provider/provider.dart'; +import 'snr_indicator.dart'; + class AppBarTitle extends StatelessWidget { final String title; final TextStyle? style; @@ -38,7 +40,13 @@ class AppBarTitle extends StatelessWidget { ], ), const SizedBox(width: 8), - BatteryIndicator(connector: connector), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + BatteryIndicator(connector: connector), + SNRIndicator(connector: connector), + ], + ), if (trailing != null) trailing!, ], ); diff --git a/lib/widgets/battery_indicator.dart b/lib/widgets/battery_indicator.dart index f203347..db05768 100644 --- a/lib/widgets/battery_indicator.dart +++ b/lib/widgets/battery_indicator.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:meshcore_open/widgets/snr_indicator.dart'; import '../connector/meshcore_connector.dart'; @@ -43,7 +42,6 @@ class _BatteryIndicatorState extends State { Widget build(BuildContext context) { final percent = widget.connector.batteryPercent; final millivolts = widget.connector.batteryMillivolts; - final directRepeaters = widget.connector.directRepeaters; if (millivolts == null) { return const SizedBox.shrink(); @@ -57,20 +55,6 @@ class _BatteryIndicatorState extends State { } final batteryUi = batteryUiForPercent(percent); - final directBestRepeaters = List.of(directRepeaters) - ..sort((a, b) { - final dateCompare = b.lastUpdated.compareTo(a.lastUpdated); - if (dateCompare != 0) return dateCompare; - return (b.snr).compareTo(a.snr); - }); - final directRepeater = directBestRepeaters.isEmpty - ? null - : directBestRepeaters.first; - - final snrUi = snrUiFromSNR( - directBestRepeaters.isNotEmpty ? directRepeater!.snr : null, - widget.connector.currentSf, - ); return InkWell( onTap: () { @@ -103,57 +87,9 @@ class _BatteryIndicatorState extends State { ), ], ), - const SizedBox(width: 8), - Padding( - padding: const EdgeInsets.only(top: 2), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(snrUi.icon, size: 18, color: snrUi.color), - Text( - snrUi.text, - style: TextStyle(fontSize: 12, color: snrUi.color), - ), - ], - ), - if (directRepeater != null) - Text( - '${directRepeaters.length}: ${directRepeater.pubkeyFirstByte.toRadixString(16).padLeft(2, '0')}: ${_formatLastUpdated(directRepeater.lastUpdated)}', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Colors.grey, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), ], ), ), ); } - - String _formatLastUpdated(DateTime lastSeen) { - final now = DateTime.now(); - final diff = now.difference(lastSeen); - - if (diff.isNegative || diff.inMinutes < 1) { - return "${diff.inSeconds}s"; - } - if (diff.inMinutes < 60) { - return "${diff.inMinutes}m"; - } - if (diff.inHours < 24) { - final hours = diff.inHours; - return hours == 1 ? "1h" : "${hours}hs"; - } - final days = diff.inDays; - return days == 1 ? "1d" : "${days}ds"; - } } diff --git a/lib/widgets/snr_indicator.dart b/lib/widgets/snr_indicator.dart index d6474d0..07660ac 100644 --- a/lib/widgets/snr_indicator.dart +++ b/lib/widgets/snr_indicator.dart @@ -1,4 +1,8 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; +import '../connector/meshcore_connector.dart'; +import '../l10n/l10n.dart'; class SNRUi { final IconData icon; @@ -56,3 +60,141 @@ SNRUi snrUiFromSNR(double? snr, int? spreadingFactor) { return SNRUi(icon, color, text); } + +class SNRIndicator extends StatefulWidget { + final MeshCoreConnector connector; + + const SNRIndicator({super.key, required this.connector}); + + @override + State createState() => _SNRIndicatorState(); +} + +class _SNRIndicatorState extends State { + @override + Widget build(BuildContext context) { + final directRepeaters = widget.connector.directRepeaters; + final directBestRepeaters = List.of(directRepeaters) + ..sort((a, b) => (b.snr).compareTo(a.snr)); + final directRepeater = directBestRepeaters.isEmpty + ? null + : directBestRepeaters.first; + + final snrUi = snrUiFromSNR( + directBestRepeaters.isNotEmpty ? directRepeater!.snr : null, + widget.connector.currentSf, + ); + + return InkWell( + onTap: () { + if (directRepeater != null) { + _showFullPathDialog(context, directBestRepeaters); + } + }, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(snrUi.icon, size: 18, color: snrUi.color), + Text( + snrUi.text, + style: TextStyle(fontSize: 12, color: snrUi.color), + ), + ], + ), + if (directRepeater != null) + Text( + '${directRepeaters.length}: ${directRepeater.pubkeyFirstByte.toRadixString(16).padLeft(2, '0')}: ${_formatLastUpdated(directRepeater.lastUpdated)}', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.grey, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ); + } + + String _formatLastUpdated(DateTime lastSeen) { + final now = DateTime.now(); + final diff = now.difference(lastSeen); + + if (diff.isNegative || diff.inMinutes < 1) { + return "${diff.inSeconds}s"; + } + if (diff.inMinutes < 60) { + return "${diff.inMinutes}m"; + } + if (diff.inHours < 24) { + final hours = diff.inHours; + return hours == 1 ? "1h" : "${hours}hs"; + } + final days = diff.inDays; + return days == 1 ? "1d" : "${days}ds"; + } + + void _showFullPathDialog( + BuildContext context, + List directBestRepeaters, + ) { + final l10n = context.l10n; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text("Nearby Repeaters"), + content: Expanded( + child: Scrollbar( + child: ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 4), + itemCount: directBestRepeaters.length, + separatorBuilder: (_, _) => const Divider(height: 1), + itemBuilder: (context, index) { + final repeater = directBestRepeaters[index]; + final snrUi = snrUiFromSNR( + repeater.snr, + widget.connector.currentSf, + ); + + final name = widget.connector.contacts + .where((c) => c.publicKey.first == repeater.pubkeyFirstByte) + .map((c) => c.name) + .firstOrNull; + + return Column( + children: [ + ListTile( + leading: Icon(snrUi.icon, color: snrUi.color), + title: Text( + name ?? + '${repeater.pubkeyFirstByte.toRadixString(16).padLeft(2, '0')}', + ), + subtitle: Text( + 'SNR: ${repeater.snr.toStringAsFixed(1)} dB\nLast seen: ${_formatLastUpdated(repeater.lastUpdated)}', + ), + ), + ], + ); + }, + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(l10n.common_close), + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 09e9301..f695838 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -497,18 +497,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -910,10 +910,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" timezone: dependency: transitive description: