mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-04-20 22:13:48 +00:00
Add SNRIndicator to AppBar and refactor BatteryIndicator layout
This commit is contained in:
parent
fa5a0932ee
commit
5751cddaa1
4 changed files with 159 additions and 73 deletions
|
|
@ -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!,
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<BatteryIndicator> {
|
|||
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<BatteryIndicator> {
|
|||
}
|
||||
|
||||
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<BatteryIndicator> {
|
|||
),
|
||||
],
|
||||
),
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SNRIndicator> createState() => _SNRIndicatorState();
|
||||
}
|
||||
|
||||
class _SNRIndicatorState extends State<SNRIndicator> {
|
||||
@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<DirectRepeater> 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
pubspec.lock
16
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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue