import 'package:flutter/material.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; import 'signal_ui.dart'; class SNRUi { final IconData icon; final Color color; final String text; const SNRUi(this.icon, this.color, this.text); } List getSNRfromSF(int spreadingFactor) { switch (spreadingFactor) { case 7: return [4.0, -2.0, -4.0, -6.0]; case 8: return [4.0, -4.0, -6.0, -8.0]; case 9: return [4.0, -6.0, -8.0, -10.0]; case 10: return [4.0, -8.0, -10.0, -13.0]; case 11: return [4.0, -10.0, -12.5, -15.0]; case 12: return [4.0, -12.5, -15.0, -18.0]; default: return []; // Or throw Exception('Invalid SF: $spreadingFactor'); } } SNRUi snrUiFromSNR(double? snr, int? spreadingFactor) { if (snr == null || spreadingFactor == null || spreadingFactor < 7 || spreadingFactor > 12) { return const SNRUi(Icons.signal_cellular_off, Colors.grey, '—'); } final snrLevels = getSNRfromSF(spreadingFactor); String text = '${snr.toStringAsFixed(1)} dB'; final tier = snr >= snrLevels[0] ? 0 : snr >= snrLevels[1] ? 1 : snr >= snrLevels[2] ? 2 : snr >= snrLevels[3] ? 3 : 4; final signalUi = signalUiForStrengthTier(tier); return SNRUi(signalUi.icon, signalUi.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.ranking).compareTo(a.ranking)); final directRepeater = directBestRepeaters.isEmpty ? null : directBestRepeaters.first; final snrUi = snrUiFromSNR( directBestRepeaters.isNotEmpty ? directRepeater!.snr : null, widget.connector.currentSf, ); return ConstrainedBox( constraints: const BoxConstraints(minWidth: 40, minHeight: 40), child: InkWell( onTap: directRepeater != null ? () => _showFullPathDialog(context, directBestRepeaters) : null, borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), child: 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) { return "0s"; } if (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}h"; } final days = diff.inDays; return "${days}d"; } void _showFullPathDialog( BuildContext context, List directBestRepeaters, ) { final l10n = context.l10n; showDialog( context: context, builder: (context) => AlertDialog( title: Text(l10n.snrIndicator_nearByRepeaters), content: SizedBox( width: double.maxFinite, child: Scrollbar( child: ListView.separated( shrinkWrap: true, 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\n${l10n.snrIndicator_lastSeen}: ${_formatLastUpdated(repeater.lastUpdated)}', ), ), ], ); }, ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text(l10n.common_close), ), ], ), ); } }