From 3664ae34cd32e9cf89b6474f277db33420c7132e Mon Sep 17 00:00:00 2001 From: ericz Date: Sun, 15 Mar 2026 11:42:46 +0100 Subject: [PATCH 01/18] reimplement location aware snr-indikator after alpha7 --- lib/utils/contact_search.dart | 55 +++++++++++++++++++ lib/widgets/snr_indicator.dart | 24 ++++++-- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/lib/utils/contact_search.dart b/lib/utils/contact_search.dart index 7a82c53..8aa75b0 100644 --- a/lib/utils/contact_search.dart +++ b/lib/utils/contact_search.dart @@ -1,3 +1,6 @@ +import 'package:latlong2/latlong.dart'; + +import '../connector/meshcore_protocol.dart'; import '../models/contact.dart'; export 'contact_filter_types.dart'; @@ -43,3 +46,55 @@ String? _extractHexPrefix(String query) { if (!RegExp(r'^[0-9a-f]+$').hasMatch(cleaned)) return null; return cleaned; } + +Contact? getRepeaterPrefixMatchNearLocation( + List contacts, + int pubkeyFirstByte, { + LatLng? searchPoint, + bool preferFavorites = false, +}) { + final candidates = contacts + .where( + (c) => + c.publicKey.isNotEmpty && + c.publicKey.first == pubkeyFirstByte && + (c.type == advTypeRepeater || c.type == advTypeRoom), + ) + .toList(); + + if (candidates.isEmpty) return null; + + candidates.sort((a, b) { + if (preferFavorites) { + final favA = a.isFavorite ? 1 : 0; + final favB = b.isFavorite ? 1 : 0; + final favCompare = favB.compareTo(favA); + if (favCompare != 0) return favCompare; + } + + final seenCompare = b.lastSeen.compareTo(a.lastSeen); + if (seenCompare != 0) return seenCompare; + + return a.publicKeyHex.compareTo(b.publicKeyHex); + }); + + if (searchPoint == null) { + return candidates.first; + } + + final distance = Distance(); + Contact best = candidates.first; + var bestDistance = double.infinity; + + for (final c in candidates) { + if (c.hasLocation) { + final d = distance(searchPoint, LatLng(c.latitude!, c.longitude!)); + if (d < bestDistance) { + bestDistance = d; + best = c; + } + } + } + + return best; +} diff --git a/lib/widgets/snr_indicator.dart b/lib/widgets/snr_indicator.dart index 30956e2..cf3c275 100644 --- a/lib/widgets/snr_indicator.dart +++ b/lib/widgets/snr_indicator.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:latlong2/latlong.dart'; + import '../connector/meshcore_connector.dart'; +import '../utils/contact_search.dart'; import '../l10n/l10n.dart'; import 'signal_ui.dart'; @@ -158,10 +161,23 @@ class _SNRIndicatorState extends State { widget.connector.currentSf, ); final allContacts = widget.connector.allContacts; - final name = allContacts - .where((c) => c.publicKey.first == repeater.pubkeyFirstByte) - .map((c) => c.name) - .firstOrNull; + + final selfLat = widget.connector.selfLatitude; + final selfLon = widget.connector.selfLongitude; + + LatLng? selfPoint; + if (selfLat != null && selfLon != null) { + selfPoint = LatLng(selfLat, selfLon); + } + + final contact = getRepeaterPrefixMatchNearLocation( + allContacts, + repeater.pubkeyFirstByte, + searchPoint: selfPoint, + preferFavorites: true, + ); + + final name = contact?.name; return Column( children: [ diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d2ea57e..4084d9b 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import flutter_blue_plus_darwin import flutter_local_notifications import mobile_scanner import package_info_plus +import path_provider_foundation import share_plus import shared_preferences_foundation import sqflite_darwin @@ -20,6 +21,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) From 0ef2194fb02a95cabf39516205329abb7c482285 Mon Sep 17 00:00:00 2001 From: ericszimmermann Date: Sun, 15 Mar 2026 12:10:47 +0100 Subject: [PATCH 02/18] codex suggested fix: explicit check if contact location is not null Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/utils/contact_search.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/contact_search.dart b/lib/utils/contact_search.dart index 8aa75b0..6a708e8 100644 --- a/lib/utils/contact_search.dart +++ b/lib/utils/contact_search.dart @@ -87,7 +87,7 @@ Contact? getRepeaterPrefixMatchNearLocation( var bestDistance = double.infinity; for (final c in candidates) { - if (c.hasLocation) { + if (c.hasLocation && c.latitude != null && c.longitude != null) { final d = distance(searchPoint, LatLng(c.latitude!, c.longitude!)); if (d < bestDistance) { bestDistance = d; From f9cb0c80a5544534187271fc71f977f15fd2d48c Mon Sep 17 00:00:00 2001 From: n-kam <96840503+n-kam@users.noreply.github.com> Date: Fri, 27 Mar 2026 01:39:52 +0300 Subject: [PATCH 03/18] make unread badge max out at 9999+ not 99+ --- lib/widgets/unread_badge.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/unread_badge.dart b/lib/widgets/unread_badge.dart index 37db11a..424cb6f 100644 --- a/lib/widgets/unread_badge.dart +++ b/lib/widgets/unread_badge.dart @@ -7,7 +7,7 @@ class UnreadBadge extends StatelessWidget { @override Widget build(BuildContext context) { - final display = count > 99 ? '99+' : count.toString(); + final display = count > 9999 ? '9999+' : count.toString(); return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( From d0e3767db6774bcaffcaafbf75466f2523a045bb Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Thu, 26 Mar 2026 22:28:01 -0700 Subject: [PATCH 04/18] Refactor contact handling and other improvments (#317) * Refactor contact filtering and improve localization strings; enhance path trace handling * Add localization for new CLI commands and update existing strings * Enhance contact handling and UI updates across multiple screens add unfiltered contact access and improve last seen resolution * Add polling interval configuration and improve contact handling * Reorder command constants for better organization and clarity * Refactor contact handling by removing unnecessary mapping and improving clarity across multiple screens * Moved RadioStatsIconButton in chat screen for improved UI consistency * Added indicators to AppBar for channels * Ignore contacts with self public key in contact handling * Simplify path removal logic and clean up unused imports in path management dialog * Enhance path hop resolution by adding distance checks to improve candidate selection accuracy * Remove unnecessary reset of radio stats poll reference count in polling interval setter --- lib/connector/meshcore_connector.dart | 45 ++++++++++-- lib/connector/meshcore_protocol.dart | 4 +- lib/l10n/app_bg.arb | 6 +- lib/l10n/app_de.arb | 6 +- lib/l10n/app_en.arb | 10 ++- lib/l10n/app_es.arb | 6 +- lib/l10n/app_fr.arb | 6 +- lib/l10n/app_hu.arb | 4 +- lib/l10n/app_it.arb | 6 +- lib/l10n/app_ja.arb | 4 +- lib/l10n/app_ko.arb | 4 +- lib/l10n/app_localizations.dart | 12 ++++ lib/l10n/app_localizations_bg.dart | 6 ++ lib/l10n/app_localizations_de.dart | 6 ++ lib/l10n/app_localizations_en.dart | 6 ++ lib/l10n/app_localizations_es.dart | 6 ++ lib/l10n/app_localizations_fr.dart | 6 ++ lib/l10n/app_localizations_hu.dart | 6 ++ lib/l10n/app_localizations_it.dart | 6 ++ lib/l10n/app_localizations_ja.dart | 6 ++ lib/l10n/app_localizations_ko.dart | 6 ++ lib/l10n/app_localizations_nl.dart | 6 ++ lib/l10n/app_localizations_pl.dart | 6 ++ lib/l10n/app_localizations_pt.dart | 6 ++ lib/l10n/app_localizations_ru.dart | 6 ++ lib/l10n/app_localizations_sk.dart | 6 ++ lib/l10n/app_localizations_sl.dart | 6 ++ lib/l10n/app_localizations_sv.dart | 6 ++ lib/l10n/app_localizations_uk.dart | 6 ++ lib/l10n/app_localizations_zh.dart | 6 ++ lib/l10n/app_nl.arb | 6 +- lib/l10n/app_pl.arb | 6 +- lib/l10n/app_pt.arb | 6 +- lib/l10n/app_ru.arb | 6 +- lib/l10n/app_sk.arb | 6 +- lib/l10n/app_sl.arb | 6 +- lib/l10n/app_sv.arb | 6 +- lib/l10n/app_uk.arb | 6 +- lib/l10n/app_zh.arb | 6 +- lib/screens/channel_message_path_screen.dart | 18 ++++- lib/screens/channels_screen.dart | 2 +- lib/screens/chat_screen.dart | 2 +- lib/screens/companion_radio_stats_screen.dart | 2 + lib/screens/contacts_screen.dart | 19 ++--- lib/screens/discovery_screen.dart | 62 +++++++++++++++-- lib/screens/map_screen.dart | 69 +++++++++++++------ lib/screens/path_trace_map.dart | 50 +++++++++++--- lib/screens/repeater_cli_screen.dart | 8 ++- lib/utils/gpx_export.dart | 34 ++++++--- lib/widgets/repeater_login_dialog.dart | 2 +- lib/widgets/room_login_dialog.dart | 2 +- 51 files changed, 440 insertions(+), 105 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index b99ecf7..93c5dcd 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -193,6 +193,7 @@ class MeshCoreConnector extends ChangeNotifier { static const int _contactMsgBackoffFallbackMs = 5000; static const int _contactMsgBackoffMinMs = 500; static const int _contactMsgBackoffMaxMs = 15000; + int _pollingInterval = 30; bool _batteryRequested = false; bool _awaitingSelfInfo = false; bool _hasReceivedDeviceInfo = false; @@ -323,8 +324,14 @@ class MeshCoreConnector extends ChangeNotifier { List get allContacts => List.unmodifiable([ ..._contacts, - ..._discoveredContacts.where((c) => !c.isActive), + ..._discoveredContacts.where( + (c) => !c.isActive && c.publicKeyHex != selfPublicKeyHex, + ), ]); + + List get allContactsUnfiltered => + List.unmodifiable([..._contacts, ..._discoveredContacts]); + List get discoveredContacts { return List.unmodifiable(_discoveredContacts); } @@ -2242,9 +2249,18 @@ class MeshCoreConnector extends ChangeNotifier { _batteryPollTimer = null; } + void setPollingInterval(int i) { + _pollingInterval = i.clamp(1, 60); + if (isConnected) { + _startRadioStatsPolling(); + } + } + void _startRadioStatsPolling() { _radioStatsPollTimer?.cancel(); - _radioStatsPollTimer = Timer.periodic(const Duration(seconds: 1), (_) { + _radioStatsPollTimer = Timer.periodic(Duration(seconds: _pollingInterval), ( + _, + ) { if (!isConnected) { _stopRadioStatsPolling(); return; @@ -2369,6 +2385,18 @@ class MeshCoreConnector extends ChangeNotifier { }); } + Contact getFromDiscovered(Contact contact) { + final tmp = _discoveredContacts.firstWhere( + (c) => c.publicKeyHex == contact.publicKeyHex, + orElse: () => contact, + ); + return contact.copyWith( + rawPacket: tmp.rawPacket, + latitude: tmp.latitude, + longitude: tmp.longitude, + ); + } + Future getContacts({int? since, bool preserveExisting = false}) async { if (!isConnected) return; @@ -3735,8 +3763,17 @@ class MeshCoreConnector extends ChangeNotifier { } void _handleContact(Uint8List frame, {bool isContact = true}) { - final contact = Contact.fromFrame(frame); - if (contact != null) { + final contactTmp = Contact.fromFrame(frame); + if (contactTmp != null) { + if (listEquals(contactTmp.publicKey, _selfPublicKey)) { + appLogger.info( + 'Ignoring contact with self public key: ${contactTmp.name}', + tag: 'Connector', + ); + removeContact(contactTmp); + return; + } + final contact = getFromDiscovered(contactTmp); _handleDiscovery(contact, frame, noNotify: true, addActive: true); if (contact.type == advTypeRepeater) { diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index b42e3e5..396d78b 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -202,15 +202,15 @@ const int cmdGetChannel = 31; const int cmdSetChannel = 32; const int cmdSendTracePath = 36; const int cmdSetOtherParams = 38; -const int cmdSendAnonReq = 57; const int cmdSendTelemetryReq = 39; const int cmdGetCustomVar = 40; const int cmdSetCustomVar = 41; const int cmdSendBinaryReq = 50; +const int cmdGetStats = 56; +const int cmdSendAnonReq = 57; const int cmdSetAutoAddConfig = 58; const int cmdGetAutoAddConfig = 59; const int cmdSetPathHashMode = 61; -const int cmdGetStats = 56; // Text message types const int txtTypePlain = 0; diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 0f5145d..09bde69 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -2017,5 +2017,7 @@ "scanner_linuxPairingHidePin": "Скрий ПИН", "scanner_linuxPairingShowPin": "Покажи PIN", "scanner_linuxPairingPinTitle": "PIN код за сдвояване на Bluetooth", - "scanner_linuxPairingPinPrompt": "Въведете ПИН за {deviceName} (оставете празно, ако няма)." -} + "scanner_linuxPairingPinPrompt": "Въведете ПИН за {deviceName} (оставете празно, ако няма).", + "repeater_cliQuickClockSync": "Синхронизация на часовника", + "repeater_cliQuickDiscovery": "Открий Съседи" +} \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index c156a44..08dbe85 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -2045,5 +2045,7 @@ "scanner_linuxPairingShowPin": "PIN anzeigen", "scanner_linuxPairingHidePin": "PIN ausblenden", "scanner_linuxPairingPinTitle": "Bluetooth-Paarungs-PIN", - "scanner_linuxPairingPinPrompt": "Geben Sie die PIN für {deviceName} ein (leer lassen, falls keine)." -} + "scanner_linuxPairingPinPrompt": "Geben Sie die PIN für {deviceName} ein (leer lassen, falls keine).", + "repeater_cliQuickClockSync": "Uhr Synchronisieren", + "repeater_cliQuickDiscovery": "Entdecke Nachbarn" +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d8d73ab..bccb472 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -303,8 +303,12 @@ "path_routeWeight": "{weight}/{max}", "@path_routeWeight": { "placeholders": { - "weight": { "type": "String" }, - "max": { "type": "String" } + "weight": { + "type": "String" + }, + "max": { + "type": "String" + } } }, "appSettings_battery": "Battery", @@ -1333,6 +1337,8 @@ "repeater_cliQuickVersion": "Version", "repeater_cliQuickAdvertise": "Advertise", "repeater_cliQuickClock": "Clock", + "repeater_cliQuickClockSync": "Clock Sync", + "repeater_cliQuickDiscovery": "Discover Neighbors", "repeater_cliHelpAdvert": "Sends an advertisement packet", "repeater_cliHelpReboot": "Reboots the device. (note, you'll prob get 'Timeout' which is normal)", "repeater_cliHelpClock": "Displays current time per device's clock.", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 245f732..7fcd72b 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -2045,5 +2045,7 @@ "scanner_linuxPairingShowPin": "Mostrar PIN", "scanner_linuxPairingPinTitle": "PIN de emparejamiento Bluetooth", "scanner_linuxPairingHidePin": "Ocultar PIN", - "scanner_linuxPairingPinPrompt": "Introduzca el PIN para {deviceName} (déjelo en blanco si no hay ninguno)." -} + "scanner_linuxPairingPinPrompt": "Introduzca el PIN para {deviceName} (déjelo en blanco si no hay ninguno).", + "repeater_cliQuickDiscovery": "Descubrir Vecinos", + "repeater_cliQuickClockSync": "Sincronización del reloj" +} \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 21b231a..c78380a 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -2017,5 +2017,7 @@ "scanner_linuxPairingShowPin": "Afficher le code PIN", "scanner_linuxPairingHidePin": "Masquer le code PIN", "scanner_linuxPairingPinTitle": "Code PIN d’appairage Bluetooth", - "scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si aucun)." -} + "scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si aucun).", + "repeater_cliQuickClockSync": "Synchronisation de l'horloge", + "repeater_cliQuickDiscovery": "Découvrir les voisins" +} \ No newline at end of file diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index dc96020..77081d2 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -2055,5 +2055,7 @@ "scanner_linuxPairingHidePin": "PIN elrejtése", "scanner_linuxPairingShowPin": "PIN megjelenítése", "scanner_linuxPairingPinTitle": "Bluetooth párosítási PIN", - "scanner_linuxPairingPinPrompt": "Adja meg a(z) {deviceName} PIN-kódját (hagyja üresen, ha nincs)." + "scanner_linuxPairingPinPrompt": "Adja meg a(z) {deviceName} PIN-kódját (hagyja üresen, ha nincs).", + "repeater_cliQuickClockSync": "Óra szinkronizálás", + "repeater_cliQuickDiscovery": "Fedezd fel a szomszédokat" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 13a9602..e101fea 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -2017,5 +2017,7 @@ "scanner_linuxPairingShowPin": "Mostra PIN", "scanner_linuxPairingHidePin": "Nascondi PIN", "scanner_linuxPairingPinTitle": "PIN di associazione Bluetooth", - "scanner_linuxPairingPinPrompt": "Inserisci il PIN per {deviceName} (lascia vuoto se non ce n'è)." -} + "scanner_linuxPairingPinPrompt": "Inserisci il PIN per {deviceName} (lascia vuoto se non ce n'è).", + "repeater_cliQuickClockSync": "Sincronizzazione dell'orologio", + "repeater_cliQuickDiscovery": "Scopri i Vicini" +} \ No newline at end of file diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index adb4eea..d991c7a 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -2055,5 +2055,7 @@ "scanner_linuxPairingShowPin": "PINを表示", "scanner_linuxPairingHidePin": "PINを非表示", "scanner_linuxPairingPinTitle": "Bluetooth ペアリング PIN", - "scanner_linuxPairingPinPrompt": "{deviceName}のPINを入力してください(なしの場合は空欄のまま)。" + "scanner_linuxPairingPinPrompt": "{deviceName}のPINを入力してください(なしの場合は空欄のまま)。", + "repeater_cliQuickClockSync": "クロック同期", + "repeater_cliQuickDiscovery": "近隣を発見する" } diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 6bccc19..5ce2eed 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -2055,5 +2055,7 @@ "scanner_linuxPairingShowPin": "PIN 표시", "scanner_linuxPairingPinTitle": "블루투스 페어링 PIN", "scanner_linuxPairingHidePin": "PIN 숨기기", - "scanner_linuxPairingPinPrompt": "{deviceName}에 대한 PIN을 입력하세요 (없으면 비워두세요)." + "scanner_linuxPairingPinPrompt": "{deviceName}에 대한 PIN을 입력하세요 (없으면 비워두세요).", + "repeater_cliQuickClockSync": "시계 동기화", + "repeater_cliQuickDiscovery": "이웃 발견하기" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index db787b3..fe2d1ea 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4322,6 +4322,18 @@ abstract class AppLocalizations { /// **'Clock'** String get repeater_cliQuickClock; + /// No description provided for @repeater_cliQuickClockSync. + /// + /// In en, this message translates to: + /// **'Clock Sync'** + String get repeater_cliQuickClockSync; + + /// No description provided for @repeater_cliQuickDiscovery. + /// + /// In en, this message translates to: + /// **'Discover Neighbors'** + String get repeater_cliQuickDiscovery; + /// No description provided for @repeater_cliHelpAdvert. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 2909278..fc3003c 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2429,6 +2429,12 @@ class AppLocalizationsBg extends AppLocalizations { @override String get repeater_cliQuickClock => 'Часовник'; + @override + String get repeater_cliQuickClockSync => 'Синхронизация на часовника'; + + @override + String get repeater_cliQuickDiscovery => 'Открий Съседи'; + @override String get repeater_cliHelpAdvert => 'Изпраща рекламен пакет'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 4afefde..e451072 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2429,6 +2429,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliQuickClock => 'Uhr'; + @override + String get repeater_cliQuickClockSync => 'Uhr Synchronisieren'; + + @override + String get repeater_cliQuickDiscovery => 'Entdecke Nachbarn'; + @override String get repeater_cliHelpAdvert => 'Sendet eine Ankündigung'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index a420a55..59dae81 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2379,6 +2379,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get repeater_cliQuickClock => 'Clock'; + @override + String get repeater_cliQuickClockSync => 'Clock Sync'; + + @override + String get repeater_cliQuickDiscovery => 'Discover Neighbors'; + @override String get repeater_cliHelpAdvert => 'Sends an advertisement packet'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 93a8bc9..4404a61 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2423,6 +2423,12 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliQuickClock => 'Reloj'; + @override + String get repeater_cliQuickClockSync => 'Sincronización del reloj'; + + @override + String get repeater_cliQuickDiscovery => 'Descubrir Vecinos'; + @override String get repeater_cliHelpAdvert => 'Envía un paquete de publicidad'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 9912542..505262f 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2442,6 +2442,12 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_cliQuickClock => 'Horloge'; + @override + String get repeater_cliQuickClockSync => 'Synchronisation de l\'horloge'; + + @override + String get repeater_cliQuickDiscovery => 'Découvrir les voisins'; + @override String get repeater_cliHelpAdvert => 'Envoie un paquet d\'annonce'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index dc6374a..66c1085 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -2437,6 +2437,12 @@ class AppLocalizationsHu extends AppLocalizations { @override String get repeater_cliQuickClock => 'óra'; + @override + String get repeater_cliQuickClockSync => 'Óra szinkronizálás'; + + @override + String get repeater_cliQuickDiscovery => 'Fedezd fel a szomszédokat'; + @override String get repeater_cliHelpAdvert => 'Elküldi egy hirdetési csomagot'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 3fc5e56..9e7a990 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2426,6 +2426,12 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliQuickClock => 'Orologio'; + @override + String get repeater_cliQuickClockSync => 'Sincronizzazione dell\'orologio'; + + @override + String get repeater_cliQuickDiscovery => 'Scopri i Vicini'; + @override String get repeater_cliHelpAdvert => 'Invia un pacchetto pubblicitario'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 03d70d4..d632b38 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -2322,6 +2322,12 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliQuickClock => '時計'; + @override + String get repeater_cliQuickClockSync => 'クロック同期'; + + @override + String get repeater_cliQuickDiscovery => '近隣を発見する'; + @override String get repeater_cliHelpAdvert => '広告用資料を送る'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 5e5925f..10620eb 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -2319,6 +2319,12 @@ class AppLocalizationsKo extends AppLocalizations { @override String get repeater_cliQuickClock => '시계'; + @override + String get repeater_cliQuickClockSync => '시계 동기화'; + + @override + String get repeater_cliQuickDiscovery => '이웃 발견하기'; + @override String get repeater_cliHelpAdvert => '광고 패킷을 발송'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index bcb2d5d..fe784e9 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2409,6 +2409,12 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_cliQuickClock => 'Tijd'; + @override + String get repeater_cliQuickClockSync => 'Kloksynchronisatie'; + + @override + String get repeater_cliQuickDiscovery => 'Ontdek Buren'; + @override String get repeater_cliHelpAdvert => 'Verstuurt een advertentiepakket'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 5c66761..89adf74 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2436,6 +2436,12 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_cliQuickClock => 'Godzina'; + @override + String get repeater_cliQuickClockSync => 'Synchronizacja zegara'; + + @override + String get repeater_cliQuickDiscovery => 'Odkryj Sąsiadów'; + @override String get repeater_cliHelpAdvert => 'Wysyła pakiet rozgłoszeniowy'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 98c72f5..1b347c4 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2423,6 +2423,12 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_cliQuickClock => 'Relógio'; + @override + String get repeater_cliQuickClockSync => 'Sincronização do Relógio'; + + @override + String get repeater_cliQuickDiscovery => 'Descobrir Vizinhos'; + @override String get repeater_cliHelpAdvert => 'Envia um pacote de anúncios'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 4184641..ff6fb71 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2427,6 +2427,12 @@ class AppLocalizationsRu extends AppLocalizations { @override String get repeater_cliQuickClock => 'Время'; + @override + String get repeater_cliQuickClockSync => 'Синхронизация часов'; + + @override + String get repeater_cliQuickDiscovery => 'Обнаружить Соседей'; + @override String get repeater_cliHelpAdvert => 'Отправляет пакет анонсирования'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 59f46bd..37b3757 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2406,6 +2406,12 @@ class AppLocalizationsSk extends AppLocalizations { @override String get repeater_cliQuickClock => 'Hodiny'; + @override + String get repeater_cliQuickClockSync => 'Synchronizácia hodin'; + + @override + String get repeater_cliQuickDiscovery => 'Objaviť susedov'; + @override String get repeater_cliHelpAdvert => 'Odosiela reklamnú balíček.'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 171353c..536894e 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2409,6 +2409,12 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_cliQuickClock => 'Ura'; + @override + String get repeater_cliQuickClockSync => 'Usklajevanje ure'; + + @override + String get repeater_cliQuickDiscovery => 'Odkrijte sosede'; + @override String get repeater_cliHelpAdvert => 'Pošlje paket oglasov'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 6a776d7..2e20f2a 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2394,6 +2394,12 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_cliQuickClock => 'Klocka'; + @override + String get repeater_cliQuickClockSync => 'Synkronisera klocka'; + + @override + String get repeater_cliQuickDiscovery => 'Upptäck grannar'; + @override String get repeater_cliHelpAdvert => 'Skickar ett annonspaket'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 9ebead2..2335f37 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2427,6 +2427,12 @@ class AppLocalizationsUk extends AppLocalizations { @override String get repeater_cliQuickClock => 'Годинник'; + @override + String get repeater_cliQuickClockSync => 'Синхронізація годинника'; + + @override + String get repeater_cliQuickDiscovery => 'Відкрити сусідів'; + @override String get repeater_cliHelpAdvert => 'Надсилає пакет оголошення'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 6d3a856..8672ac8 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2277,6 +2277,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get repeater_cliQuickClock => '时钟'; + @override + String get repeater_cliQuickClockSync => '同步时钟'; + + @override + String get repeater_cliQuickDiscovery => '发现邻居'; + @override String get repeater_cliHelpAdvert => '发送广播包'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 9f164fd..013b074 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -2017,5 +2017,7 @@ "scanner_linuxPairingShowPin": "Toon PIN", "scanner_linuxPairingHidePin": "PIN verbergen", "scanner_linuxPairingPinPrompt": "Voer PIN in voor {deviceName} (laat leeg als er geen is).", - "scanner_linuxPairingPinTitle": "Bluetooth‑koppelings‑PIN" -} + "scanner_linuxPairingPinTitle": "Bluetooth‑koppelings‑PIN", + "repeater_cliQuickDiscovery": "Ontdek Buren", + "repeater_cliQuickClockSync": "Kloksynchronisatie" +} \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 87b4754..c499b83 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -2055,5 +2055,7 @@ "scanner_linuxPairingShowPin": "Pokaż PIN", "scanner_linuxPairingHidePin": "Ukryj PIN", "scanner_linuxPairingPinPrompt": "Wprowadź kod PIN dla {deviceName} (pozostaw puste, jeśli brak).", - "scanner_linuxPairingPinTitle": "Kod PIN parowania Bluetooth" -} + "scanner_linuxPairingPinTitle": "Kod PIN parowania Bluetooth", + "repeater_cliQuickClockSync": "Synchronizacja zegara", + "repeater_cliQuickDiscovery": "Odkryj Sąsiadów" +} \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index eb87a15..d492fbd 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -2017,5 +2017,7 @@ "scanner_linuxPairingShowPin": "Mostrar PIN", "scanner_linuxPairingHidePin": "Ocultar PIN", "scanner_linuxPairingPinPrompt": "Insira o PIN para {deviceName} (deixe em branco se não houver).", - "scanner_linuxPairingPinTitle": "PIN de emparelhamento Bluetooth" -} + "scanner_linuxPairingPinTitle": "PIN de emparelhamento Bluetooth", + "repeater_cliQuickClockSync": "Sincronização do Relógio", + "repeater_cliQuickDiscovery": "Descobrir Vizinhos" +} \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index c9493a0..18fa20e 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1257,5 +1257,7 @@ "scanner_linuxPairingShowPin": "Показать PIN", "scanner_linuxPairingPinPrompt": "Введите PIN‑код для {deviceName} (оставьте пустым, если нет).", "scanner_linuxPairingHidePin": "Скрыть PIN", - "scanner_linuxPairingPinTitle": "PIN‑код сопряжения Bluetooth" -} + "scanner_linuxPairingPinTitle": "PIN‑код сопряжения Bluetooth", + "repeater_cliQuickDiscovery": "Обнаружить Соседей", + "repeater_cliQuickClockSync": "Синхронизация часов" +} \ No newline at end of file diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 5a7aa6d..12b5081 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -2017,5 +2017,7 @@ "scanner_linuxPairingPinPrompt": "Zadajte PIN pre {deviceName} (ak nie je, nechajte prázdne).", "scanner_linuxPairingShowPin": "Zobraziť PIN", "scanner_linuxPairingHidePin": "Skryť PIN", - "scanner_linuxPairingPinTitle": "Bluetooth párovací PIN" -} + "scanner_linuxPairingPinTitle": "Bluetooth párovací PIN", + "repeater_cliQuickClockSync": "Synchronizácia hodin", + "repeater_cliQuickDiscovery": "Objaviť susedov" +} \ No newline at end of file diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 9adb387..8de6586 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -2017,5 +2017,7 @@ "scanner_linuxPairingShowPin": "Prikaži PIN", "scanner_linuxPairingHidePin": "Skrij PIN", "scanner_linuxPairingPinPrompt": "Vnesite PIN za {deviceName} (pustite prazno, če ga ni).", - "scanner_linuxPairingPinTitle": "Bluetooth PIN za seznanjanje" -} + "scanner_linuxPairingPinTitle": "Bluetooth PIN za seznanjanje", + "repeater_cliQuickDiscovery": "Odkrijte sosede", + "repeater_cliQuickClockSync": "Usklajevanje ure" +} \ No newline at end of file diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index e4ace3e..91743db 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -2017,5 +2017,7 @@ "scanner_linuxPairingShowPin": "Visa PIN", "scanner_linuxPairingPinTitle": "Bluetooth‑parnings‑PIN", "scanner_linuxPairingPinPrompt": "Ange PIN för {deviceName} (lämna tomt om ingen).", - "scanner_linuxPairingHidePin": "Dölj PIN" -} + "scanner_linuxPairingHidePin": "Dölj PIN", + "repeater_cliQuickDiscovery": "Upptäck grannar", + "repeater_cliQuickClockSync": "Synkronisera klocka" +} \ No newline at end of file diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 8e27da1..b8cabed 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -2017,5 +2017,7 @@ "scanner_linuxPairingPinTitle": "PIN‑код спарювання Bluetooth", "scanner_linuxPairingShowPin": "Показати PIN", "scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).", - "scanner_linuxPairingHidePin": "Приховати PIN" -} + "scanner_linuxPairingHidePin": "Приховати PIN", + "repeater_cliQuickClockSync": "Синхронізація годинника", + "repeater_cliQuickDiscovery": "Відкрити сусідів" +} \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index cd7b44d..9d23c8e 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -2022,5 +2022,7 @@ "scanner_linuxPairingShowPin": "显示 PIN码", "scanner_linuxPairingPinPrompt": "输入 {deviceName} 的 PIN(如果没有,请留空)。", "scanner_linuxPairingPinTitle": "蓝牙配对 PIN", - "scanner_linuxPairingHidePin": "隐藏 PIN" -} + "scanner_linuxPairingHidePin": "隐藏 PIN", + "repeater_cliQuickDiscovery": "发现邻居", + "repeater_cliQuickClockSync": "同步时钟" +} \ No newline at end of file diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 94b8eee..0eb2c22 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -822,7 +822,8 @@ List<_PathHop> _buildPathHops( ) { if (pathBytes.isEmpty) return const []; final candidatesByPrefix = >{}; - for (final contact in connector.allContacts) { + final allContacts = connector.allContacts; + for (final contact in allContacts) { if (contact.publicKey.isEmpty) continue; if (contact.type != advTypeRepeater && contact.type != advTypeRoom) { continue; @@ -839,7 +840,8 @@ List<_PathHop> _buildPathHops( : null; var previousPosition = startPoint; final distance = Distance(); - + var lastDistance = 0.0; + var bestDistance = 0.0; final hops = <_PathHop>[]; for (var i = 0; i < pathBytes.length; i++) { final searchPoint = i == 0 ? startPoint : previousPosition; @@ -848,7 +850,7 @@ List<_PathHop> _buildPathHops( if (candidates != null && candidates.isNotEmpty) { var bestIndex = 0; if (searchPoint != null) { - var bestDistance = double.infinity; + bestDistance = double.infinity; for (var j = 0; j < candidates.length; j++) { final candidate = candidates[j]; if (!candidate.hasLocation || @@ -876,6 +878,16 @@ List<_PathHop> _buildPathHops( if (resolvedPosition != null) { previousPosition = resolvedPosition; } + // If the best candidate is much farther than the previous hop, it's likely not the correct match. + if (lastDistance + bestDistance > 70000 && + candidates != null && + candidates.isNotEmpty) { + i--; + lastDistance = bestDistance; + continue; + } + lastDistance = bestDistance; + hops.add( _PathHop( index: i + 1, diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index d67d03d..51d2453 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -127,7 +127,7 @@ class _ChannelsScreenState extends State canPop: allowBack, child: Scaffold( appBar: AppBar( - title: AppBarTitle(context.l10n.channels_title, indicators: false), + title: AppBarTitle(context.l10n.channels_title), centerTitle: true, automaticallyImplyLeading: false, actions: [ diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index aecdc81..ab7082c 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -290,6 +290,7 @@ class _ChatScreenState extends State { tooltip: context.l10n.chat_pathManagement, onPressed: () => _showPathHistory(context), ), + const RadioStatsIconButton(), Consumer( builder: (context, connector, _) { return PopupMenuButton( @@ -362,7 +363,6 @@ class _ChatScreenState extends State { ); }, ), - const RadioStatsIconButton(), ], ), body: Consumer( diff --git a/lib/screens/companion_radio_stats_screen.dart b/lib/screens/companion_radio_stats_screen.dart index 01fb64d..9c37676 100644 --- a/lib/screens/companion_radio_stats_screen.dart +++ b/lib/screens/companion_radio_stats_screen.dart @@ -24,6 +24,7 @@ class _CompanionRadioStatsScreenState extends State { final c = context.read(); _connector = c; c.acquireRadioStatsPolling(); + c.setPollingInterval(1); c.radioStatsNotifier.addListener(_onStatsUpdate); } @@ -44,6 +45,7 @@ class _CompanionRadioStatsScreenState extends State { void dispose() { _connector?.radioStatsNotifier.removeListener(_onStatsUpdate); _connector?.releaseRadioStatsPolling(); + _connector?.setPollingInterval(30); super.dispose(); } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index d5b01f2..62a380b 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1240,9 +1240,7 @@ class _ContactsScreenState extends State if (isRepeater) ...[ ListTile( leading: const Icon(Icons.radar, color: Colors.green), - title: contact.pathBytesForDisplay.isNotEmpty - ? Text(context.l10n.contacts_pathTrace) - : Text(context.l10n.contacts_ping), + title: Text(context.l10n.contacts_ping), onTap: () { final hw = context .read() @@ -1251,11 +1249,8 @@ class _ContactsScreenState extends State context, MaterialPageRoute( builder: (context) => PathTraceMapScreen( - title: contact.pathBytesForDisplay.isNotEmpty - ? context.l10n.contacts_repeaterPathTrace - : context.l10n.contacts_repeaterPing, - path: contact.pathBytesForDisplay, - flipPathAround: true, + title: context.l10n.contacts_repeaterPing, + path: Uint8List.fromList([contact.publicKey.first]), targetContact: contact, pathHashByteWidth: hw, ), @@ -1274,9 +1269,7 @@ class _ContactsScreenState extends State ] else if (isRoom) ...[ ListTile( leading: const Icon(Icons.radar, color: Colors.green), - title: contact.pathLength > 0 - ? Text(context.l10n.contacts_pathTrace) - : Text(context.l10n.contacts_ping), + title: Text(context.l10n.contacts_pathTrace), onTap: () { final hw = context .read() @@ -1288,7 +1281,9 @@ class _ContactsScreenState extends State title: contact.pathBytesForDisplay.isNotEmpty ? context.l10n.contacts_roomPathTrace : context.l10n.contacts_roomPing, - path: contact.pathBytesForDisplay, + path: contact.pathBytesForDisplay.isNotEmpty + ? contact.pathBytesForDisplay + : Uint8List.fromList([contact.publicKey.first]), flipPathAround: contact.pathBytesForDisplay.isNotEmpty, targetContact: contact, pathHashByteWidth: hw, diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart index 4e7c6e8..3f9d965 100644 --- a/lib/screens/discovery_screen.dart +++ b/lib/screens/discovery_screen.dart @@ -38,6 +38,13 @@ class _DiscoveryScreenState extends State { super.dispose(); } + DateTime _resolveLastSeen(Contact contact) { + if (contact.type != advTypeChat) return contact.lastSeen; + return contact.lastMessageAt.isAfter(contact.lastSeen) + ? contact.lastMessageAt + : contact.lastSeen; + } + @override Widget build(BuildContext context) { final l10n = context.l10n; @@ -108,11 +115,56 @@ class _DiscoveryScreenState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, ), - trailing: Text( - _formatLastSeen(context, contact.lastSeen), - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], + // Clamp text scaling in trailing section to prevent overflow while + // maintaining accessibility. Primary content (title/subtitle) scales normally. + trailing: MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear( + MediaQuery.textScalerOf( + context, + ).scale(1.0).clamp(1.0, 1.3), + ), + ), + child: SizedBox( + width: 120, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + _formatLastSeen( + context, + _resolveLastSeen(contact), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.right, + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (contact.hasLocation) + Icon( + Icons.location_on, + size: 14, + color: Colors.grey[400], + ), + if (contact.rawPacket != null) + const SizedBox(width: 2), + if (contact.rawPacket != null) + Icon( + Icons.cell_tower, + size: 14, + color: Colors.grey[400], + ), + ], + ), + ], + ), ), ), onTap: () { diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 9616d47..f2d09f3 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -64,6 +64,7 @@ class _MapScreenState extends State { bool _hasInitializedMap = false; bool _removedMarkersLoaded = false; final List _pathTrace = []; + final List _pathTraceContacts = []; final List _points = []; final List _polylines = []; bool _legendExpanded = false; @@ -488,7 +489,7 @@ class _MapScreenState extends State { ), ), ), - if (!_isBuildingPathTrace) + if (!settings.mapShowOverlaps) ..._buildGuessedMarker( guessedLocations, showLabels: _showNodeLabels, @@ -788,17 +789,26 @@ class _MapScreenState extends State { final markers = []; for (final guess in guessed) { + if (guess.contact.type == advTypeChat && _isBuildingPathTrace) { + continue; + } + final color = _getNodeColor(guess.contact.type); final marker = Marker( point: guess.position, width: 35, height: 35, child: GestureDetector( - onTap: () => _showNodeInfo( - context, - guess.contact, - guessedPosition: guess.position, - ), + onLongPress: () => _isBuildingPathTrace + ? _showNodeInfo(context, guess.contact) + : null, + onTap: () => _isBuildingPathTrace + ? _addToPath(context, guess.contact, position: guess.position) + : _showNodeInfo( + context, + guess.contact, + guessedPosition: guess.position, + ), child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( @@ -870,23 +880,29 @@ class _MapScreenState extends State { addContact = true; } - final hasOverlap = contacts - .where( - (c) => - c.publicKeyHex != contact.publicKeyHex && - c.publicKey.first == contact.publicKey.first && - (c.type == advTypeRepeater || c.type == advTypeRoom) && - (contact.type == advTypeRepeater || - contact.type == advTypeRoom), - ) - .firstOrNull; - - if (hasOverlap == null && - settings.mapShowOverlaps && - !_isBuildingPathTrace) { + if (contact.type == advTypeChat && _isBuildingPathTrace) { addContact = false; } + if (settings.mapShowOverlaps) { + final hasOverlap = contacts + .where( + (c) => + c.publicKeyHex != contact.publicKeyHex && + c.publicKey.first == contact.publicKey.first && + (c.type == advTypeRepeater || c.type == advTypeRoom) && + (contact.type == advTypeRepeater || + contact.type == advTypeRoom), + ) + .firstOrNull; + + if (hasOverlap == null && + settings.mapShowOverlaps && + !_isBuildingPathTrace) { + addContact = false; + } + } + if (addContact) { filtered.add(contact); } @@ -2121,12 +2137,18 @@ class _MapScreenState extends State { } } - void _addToPath(BuildContext context, Contact contact) { + void _addToPath(BuildContext context, Contact contact, {LatLng? position}) { setState(() { _pathTrace.add( contact.publicKey[0], ); // Add first 16 bytes of public key to path trace - _points.add(LatLng(contact.latitude!, contact.longitude!)); + _pathTraceContacts.add( + contact.copyWith( + latitude: position?.latitude ?? contact.latitude, + longitude: position?.longitude ?? contact.longitude, + ), + ); // Add contact to path trace contacts + _points.add(position ?? LatLng(contact.latitude!, contact.longitude!)); }); } @@ -2134,6 +2156,7 @@ class _MapScreenState extends State { setState(() { _isBuildingPathTrace = true; _pathTrace.clear(); + _pathTraceContacts.clear(); _points.clear(); _polylines.clear(); _points.add(position); @@ -2142,6 +2165,7 @@ class _MapScreenState extends State { void _removePath() { setState(() { + _pathTraceContacts.removeLast(); _pathTrace.removeLast(); // Remove last node from path trace _points.removeLast(); // Remove last point from points list _polylines.clear(); // Clear polylines @@ -2201,6 +2225,7 @@ class _MapScreenState extends State { title: l10n.contacts_pathTrace, path: Uint8List.fromList(_pathTrace), pathHashByteWidth: hashW, + pathContacts: _pathTraceContacts, ), ), ); diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 5b02931..7f3b4eb 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -56,6 +56,7 @@ class PathTraceMapScreen extends StatefulWidget { final bool reversePathAround; final Contact? targetContact; final int pathHashByteWidth; + final List? pathContacts; const PathTraceMapScreen({ super.key, @@ -66,6 +67,7 @@ class PathTraceMapScreen extends StatefulWidget { this.reversePathAround = false, this.targetContact, this.pathHashByteWidth = pathHashSize, + this.pathContacts, }); @override @@ -74,6 +76,8 @@ class PathTraceMapScreen extends StatefulWidget { class _PathTraceMapScreenState extends State { static const double _labelZoomThreshold = 8.5; + //miles to meters conversion for filtering out repeaters that are too far from the last known GPS hop to be a likely match, to avoid false matches that throw off the inferred positions of other hops in the path + static const double _maxRepeaterMatchDistanceMeters = 40 * 1609.344; StreamSubscription? _frameSubscription; Timer? _timeoutTimer; @@ -266,17 +270,43 @@ class _PathTraceMapScreenState extends State { .toList(); Map pathContacts = {}; - final contacts = connector.allContacts; - contacts.where((c) => c.type != advTypeChat).forEach((repeater) { - for (var repeaterData in pathData) { - if (listEquals( - repeater.publicKey.sublist(0, 1), - Uint8List.fromList([repeaterData]), - )) { - pathContacts[repeaterData] = repeater; + Contact lastContact = Contact( + path: Uint8List(0), + pathLength: 0, + publicKey: connector.selfPublicKey ?? Uint8List(0), + name: context.l10n.pathTrace_you, + type: advTypeChat, + latitude: connector.selfLatitude, + longitude: connector.selfLongitude, + lastSeen: DateTime.now(), + ); + if (widget.pathContacts != null) { + pathContacts = {for (var c in widget.pathContacts!) c.publicKey[0]: c}; + } else { + final contacts = connector.allContactsUnfiltered; + contacts.where((c) => c.type != advTypeChat).forEach((repeater) { + if (lastContact.latitude != null && + lastContact.longitude != null && + repeater.hasLocation && + lastContact.hasLocation && + Distance().distance( + LatLng(lastContact.latitude!, lastContact.longitude!), + LatLng(repeater.latitude!, repeater.longitude!), + ) > + _maxRepeaterMatchDistanceMeters) { + return; //skip reapeaters that are far away from the last one with known GPS, to avoid false matches } - } - }); + for (var repeaterData in pathData) { + if (listEquals( + repeater.publicKey.sublist(0, 1), + Uint8List.fromList([repeaterData]), + )) { + pathContacts[repeaterData] = repeater; + lastContact = repeater; + } + } + }); + } // For hops with no GPS contact, infer position from other contacts // with known GPS that share the same last-hop byte. diff --git a/lib/screens/repeater_cli_screen.dart b/lib/screens/repeater_cli_screen.dart index 52d92aa..5f76828 100644 --- a/lib/screens/repeater_cli_screen.dart +++ b/lib/screens/repeater_cli_screen.dart @@ -35,13 +35,15 @@ class _RepeaterCliScreenState extends State { // Common commands for quick access late final List> _quickCommands = [ + {'labelKey': 'advertise', 'command': 'advert'}, {'labelKey': 'getName', 'command': 'get name'}, {'labelKey': 'getRadio', 'command': 'get radio'}, {'labelKey': 'getTx', 'command': 'get tx'}, + {'labelKey': 'discovery', 'command': 'discover.neighbors'}, {'labelKey': 'neighbors', 'command': 'neighbors'}, {'labelKey': 'version', 'command': 'ver'}, - {'labelKey': 'advertise', 'command': 'advert'}, {'labelKey': 'clock', 'command': 'clock'}, + {'labelKey': 'clock sync', 'command': 'clock sync'}, ]; @override @@ -407,6 +409,10 @@ class _RepeaterCliScreenState extends State { return l10n.repeater_cliQuickAdvertise; case 'clock': return l10n.repeater_cliQuickClock; + case 'clock sync': + return l10n.repeater_cliQuickClockSync; + case 'discovery': + return l10n.repeater_cliQuickDiscovery; default: return key; } diff --git a/lib/utils/gpx_export.dart b/lib/utils/gpx_export.dart index b0165bd..296cc3a 100644 --- a/lib/utils/gpx_export.dart +++ b/lib/utils/gpx_export.dart @@ -14,12 +14,13 @@ class ContactExport { final double lon; final String desc; final double? ele; - + final String url; ContactExport({ required this.name, required this.lat, required this.lon, required this.desc, + required this.url, this.ele, }); } @@ -40,6 +41,7 @@ class GpxExport { String name, double lat, double lon, + String url, String desc, [ double? ele, ]) { @@ -50,55 +52,66 @@ class GpxExport { lon: lon, desc: desc.trim(), ele: ele, + url: url, ), ); } void addRepeaters() { - final contacts = _connector.contacts - .where((c) => c.type == advTypeRepeater || c.type == advTypeRoom) - .toList(); + final contacts = _connector.allContacts.where( + (c) => c.type == advTypeRepeater || c.type == advTypeRoom, + ); for (var contact in contacts) { if (contact.latitude == null || contact.longitude == null) { continue; } + final url = contact.rawPacket != null + ? "meshcore://${pubKeyToHex(contact.rawPacket!)}" + : ""; _addContact( contact.name, contact.latitude!, contact.longitude!, "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", + url, ); } } void addContacts() { - final contacts = _connector.contacts - .where((c) => c.type == advTypeChat) - .toList(); + final contacts = _connector.allContacts.where((c) => c.type == advTypeChat); for (var contact in contacts) { if (contact.latitude == null || contact.longitude == null) { continue; } + final url = contact.rawPacket != null + ? "meshcore://${pubKeyToHex(contact.rawPacket!)}" + : ""; _addContact( contact.name, contact.latitude!, contact.longitude!, "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", + url, ); } } void addAll() { - final contacts = _connector.contacts; - for (var contact in contacts.toList()) { + final contacts = _connector.allContacts; + for (var contact in contacts) { if (contact.latitude == null || contact.longitude == null) { continue; } + final url = contact.rawPacket != null + ? "meshcore://${pubKeyToHex(contact.rawPacket!)}" + : ""; _addContact( contact.name, contact.latitude ?? 0.0, contact.longitude ?? 0.0, "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", + url, ); } } @@ -138,6 +151,9 @@ class GpxExport { ele: c.ele, name: c.name, desc: c.desc, + extensions: { + "meshcore": {"url": c.url}, + }, ), ) .toList(); diff --git a/lib/widgets/repeater_login_dialog.dart b/lib/widgets/repeater_login_dialog.dart index ce6c2b7..48bb6ac 100644 --- a/lib/widgets/repeater_login_dialog.dart +++ b/lib/widgets/repeater_login_dialog.dart @@ -113,7 +113,7 @@ class _RepeaterLoginDialogState extends State { messageBytes: responseBytes, ); final timeoutSeconds = (timeoutMs / 1000).ceil(); - final timeout = Duration(milliseconds: timeoutMs); + final timeout = Duration(milliseconds: timeoutMs + 2000); final selectionLabel = selection.useFlood ? 'flood' : '${selection.hopCount} hops'; diff --git a/lib/widgets/room_login_dialog.dart b/lib/widgets/room_login_dialog.dart index 91d2c8c..3a923fe 100644 --- a/lib/widgets/room_login_dialog.dart +++ b/lib/widgets/room_login_dialog.dart @@ -108,7 +108,7 @@ class _RoomLoginDialogState extends State { messageBytes: responseBytes, ); final timeoutSeconds = (timeoutMs / 1000).ceil(); - final timeout = Duration(milliseconds: timeoutMs); + final timeout = Duration(milliseconds: timeoutMs + 2000); final selectionLabel = selection.useFlood ? 'flood' : '${selection.hopCount} hops'; From 6b6d9caeeb5d3370aded165c934619e6db46d614 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Mon, 9 Mar 2026 18:29:17 -0400 Subject: [PATCH 05/18] Reapply "Fixed Preset on offgrid repeat toggle enhancemet #183" This reverts commit 758619bbaa6ce5895c7146bbfc3b89054e759527. --- lib/screens/settings_screen.dart | 55 +++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index d9e0d20..a0dedac 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1088,6 +1088,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { LoRaCodingRate _codingRate = LoRaCodingRate.cr4_5; final _txPowerController = TextEditingController(text: '20'); bool _clientRepeat = false; + int? _selectedPresetIndex; @override void initState() { @@ -1139,6 +1140,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { } _clientRepeat = widget.connector.clientRepeat ?? false; + _selectedPresetIndex = _findMatchingPresetIndex(); } @override @@ -1158,6 +1160,55 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { }); } + int? _findMatchingPresetIndex() { + final freqMHz = double.tryParse(_frequencyController.text); + final txPower = int.tryParse(_txPowerController.text); + if (freqMHz == null || txPower == null) return null; + + const epsilon = 0.001; + for (var i = 0; i < RadioSettings.presets.length; i++) { + final preset = RadioSettings.presets[i].$2; + if ((preset.frequencyMHz - freqMHz).abs() < epsilon && + preset.bandwidth == _bandwidth && + preset.spreadingFactor == _spreadingFactor && + preset.codingRate == _codingRate && + preset.txPowerDbm == txPower) { + return i; + } + } + return null; + } + + double _offGridFrequencyForBaseFrequency(double baseFrequencyMHz) { + if (baseFrequencyMHz < 500) return 433.0; + if (baseFrequencyMHz < 900) return 869.0; + return 918.0; + } + + double _normalFrequencyForBand(double frequencyMHz) { + if (frequencyMHz < 500) return 433.650; + if (frequencyMHz < 900) return 869.432; + return 915.8; + } + + void _handleClientRepeatChanged(bool enabled) { + setState(() { + _clientRepeat = enabled; + + final baseFrequencyMHz = _selectedPresetIndex != null + ? RadioSettings.presets[_selectedPresetIndex!].$2.frequencyMHz + : (double.tryParse(_frequencyController.text) ?? 915.0); + + final nextFrequencyMHz = enabled + ? _offGridFrequencyForBaseFrequency(baseFrequencyMHz) + : (_selectedPresetIndex != null + ? RadioSettings.presets[_selectedPresetIndex!].$2.frequencyMHz + : _normalFrequencyForBand(baseFrequencyMHz)); + + _frequencyController.text = nextFrequencyMHz.toStringAsFixed(3); + }); + } + Future _saveSettings() async { final l10n = context.l10n; final freqMHz = double.tryParse(_frequencyController.text); @@ -1250,6 +1301,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { crossAxisAlignment: CrossAxisAlignment.start, children: [ DropdownButtonFormField( + initialValue: _selectedPresetIndex, decoration: InputDecoration( labelText: l10n.settings_presets, border: const OutlineInputBorder(), @@ -1263,6 +1315,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ], onChanged: (index) { if (index != null) { + _selectedPresetIndex = index; _applyPreset(RadioSettings.presets[index].$2); } }, @@ -1345,7 +1398,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { title: Text(l10n.settings_clientRepeat), subtitle: Text(l10n.settings_clientRepeatSubtitle), value: _clientRepeat, - onChanged: (value) => setState(() => _clientRepeat = value), + onChanged: _handleClientRepeatChanged, contentPadding: EdgeInsets.zero, ), ], From c9145c99d346548c2553f8b06de4f56c7aca0d86 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Wed, 11 Mar 2026 11:18:35 -0400 Subject: [PATCH 06/18] fix(settings): preserve preset across off-grid repeat --- lib/connector/meshcore_connector.dart | 24 ++ lib/screens/settings_screen.dart | 383 +++++++++++++++++++++++--- 2 files changed, 374 insertions(+), 33 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 93c5dcd..280b7e9 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -102,6 +102,22 @@ class RepeaterBatterySnapshot { }); } +class MeshCoreRadioStateSnapshot { + final int freqHz; + final int bwHz; + final int sf; + final int cr; + final int txPowerDbm; + + const MeshCoreRadioStateSnapshot({ + required this.freqHz, + required this.bwHz, + required this.sf, + required this.cr, + required this.txPowerDbm, + }); +} + class MeshCoreConnector extends ChangeNotifier { // Message windowing to limit memory usage static const int _messageWindowSize = 200; @@ -167,6 +183,7 @@ class MeshCoreConnector extends ChangeNotifier { int? _currentSf; int? _currentCr; bool? _clientRepeat; + MeshCoreRadioStateSnapshot? _rememberedNonRepeatRadioState; int? _firmwareVerCode; int _pathHashByteWidth = 1; CompanionRadioStats? _latestRadioStats; @@ -366,6 +383,8 @@ class MeshCoreConnector extends ChangeNotifier { int? get currentBwHz => _currentBwHz; int? get currentSf => _currentSf; int? get currentCr => _currentCr; + MeshCoreRadioStateSnapshot? get rememberedNonRepeatRadioState => + _rememberedNonRepeatRadioState; bool? get autoAddUsers => _autoAddUsers; bool? get autoAddRepeaters => _autoAddRepeaters; bool? get autoAddRoomServers => _autoAddRoomServers; @@ -377,6 +396,10 @@ class MeshCoreConnector extends ChangeNotifier { int get advertLocationPolicy => _advertLocPolicy; int get multiAcks => _multiAcks; bool? get clientRepeat => _clientRepeat; + void rememberNonRepeatRadioState(MeshCoreRadioStateSnapshot snapshot) { + _rememberedNonRepeatRadioState = snapshot; + } + int? get firmwareVerCode => _firmwareVerCode; Map? get currentCustomVars => _currentCustomVars; int? get batteryMillivolts => _batteryMillivolts; @@ -2152,6 +2175,7 @@ class MeshCoreConnector extends ChangeNotifier { _selfLatitude = null; _selfLongitude = null; _clientRepeat = null; + _rememberedNonRepeatRadioState = null; _firmwareVerCode = null; _batteryMillivolts = null; _repeaterBatterySnapshots.clear(); diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index a0dedac..c90827b 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -8,6 +8,7 @@ import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; import '../models/radio_settings.dart'; +import '../services/app_debug_log_service.dart'; import '../widgets/app_bar.dart'; import 'app_settings_screen.dart'; import 'app_debug_log_screen.dart'; @@ -1089,6 +1090,10 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { final _txPowerController = TextEditingController(text: '20'); bool _clientRepeat = false; int? _selectedPresetIndex; + _RadioSettingsSnapshot? _lastNonRepeatSnapshot; + + AppDebugLogService get _appLog => + Provider.of(context, listen: false); @override void initState() { @@ -1141,6 +1146,23 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { _clientRepeat = widget.connector.clientRepeat ?? false; _selectedPresetIndex = _findMatchingPresetIndex(); + _lastNonRepeatSnapshot = _currentSnapshot(); + if (_clientRepeat) { + _lastNonRepeatSnapshot = + _sessionRememberedNonRepeatSnapshot() ?? + _inferNonRepeatSnapshotForRepeatEnabled(); + _selectedPresetIndex = _findMatchingPresetIndexForSnapshot( + _lastNonRepeatSnapshot!, + ); + } else { + _lastNonRepeatSnapshot = + _sessionRememberedNonRepeatSnapshot() ?? + _nonRepeatSnapshotForCurrentSelection(); + } + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + _logRadioSettingsState('Dialog initialized'); + }); } @override @@ -1150,35 +1172,60 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { super.dispose(); } - void _applyPreset(RadioSettings preset) { + void _applyPreset(int index) { setState(() { - _frequencyController.text = preset.frequencyMHz.toString(); - _bandwidth = preset.bandwidth; - _spreadingFactor = preset.spreadingFactor; - _codingRate = preset.codingRate; - _txPowerController.text = preset.txPowerDbm.toString(); + _applyPresetState(index); }); + _logRadioSettingsState( + 'Applied preset ${RadioSettings.presets[index].$1} (#$index)', + ); } int? _findMatchingPresetIndex() { - final freqMHz = double.tryParse(_frequencyController.text); - final txPower = int.tryParse(_txPowerController.text); - if (freqMHz == null || txPower == null) return null; + return _findMatchingPresetIndexForSnapshot(_currentSnapshot()); + } + int? _findMatchingPresetIndexForSnapshot(_RadioSettingsSnapshot snapshot) { const epsilon = 0.001; - for (var i = 0; i < RadioSettings.presets.length; i++) { + for (final i in _visiblePresetIndexes()) { final preset = RadioSettings.presets[i].$2; - if ((preset.frequencyMHz - freqMHz).abs() < epsilon && - preset.bandwidth == _bandwidth && - preset.spreadingFactor == _spreadingFactor && - preset.codingRate == _codingRate && - preset.txPowerDbm == txPower) { + if ((preset.frequencyMHz - snapshot.frequencyMHz).abs() < epsilon && + preset.bandwidth == snapshot.bandwidth && + preset.spreadingFactor == snapshot.spreadingFactor && + preset.codingRate == snapshot.codingRate && + preset.txPowerDbm == snapshot.txPowerDbm) { return i; } } return null; } + Iterable _visiblePresetIndexes() sync* { + for (var i = 0; i < RadioSettings.presets.length; i++) { + if (_isOffGridPresetIndex(i)) { + continue; + } + yield i; + } + } + + _RadioSettingsSnapshot _currentSnapshot() { + final frequencyMHz = double.tryParse(_frequencyController.text) ?? 915.0; + final txPowerDbm = int.tryParse(_txPowerController.text) ?? 20; + return _RadioSettingsSnapshot( + frequencyMHz: frequencyMHz, + bandwidth: _bandwidth, + spreadingFactor: _spreadingFactor, + codingRate: _codingRate, + txPowerDbm: txPowerDbm, + ); + } + + bool _isOffGridPresetIndex(int? index) { + if (index == null) return false; + return RadioSettings.presets[index].$1.startsWith('Off-Grid '); + } + double _offGridFrequencyForBaseFrequency(double baseFrequencyMHz) { if (baseFrequencyMHz < 500) return 433.0; if (baseFrequencyMHz < 900) return 869.0; @@ -1191,22 +1238,182 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { return 915.8; } + _RadioSettingsSnapshot _fallbackNonRepeatSnapshot( + double currentFrequencyMHz, + ) { + return _RadioSettingsSnapshot( + frequencyMHz: _normalFrequencyForBand(currentFrequencyMHz), + bandwidth: _bandwidth, + spreadingFactor: _spreadingFactor, + codingRate: _codingRate, + txPowerDbm: int.tryParse(_txPowerController.text) ?? 20, + ); + } + + _RadioSettingsSnapshot _nonRepeatSnapshotForCurrentSelection() { + final current = _currentSnapshot(); + if (!_isOffGridPresetIndex(_selectedPresetIndex)) { + return current; + } + return _fallbackNonRepeatSnapshot(current.frequencyMHz); + } + + _RadioSettingsSnapshot? _sessionRememberedNonRepeatSnapshot() { + final snapshot = widget.connector.rememberedNonRepeatRadioState; + if (snapshot == null) { + return null; + } + + final bandwidth = LoRaBandwidth.values + .where((bw) => bw.hz == snapshot.bwHz) + .firstOrNull; + final spreadingFactor = LoRaSpreadingFactor.values + .where((sf) => sf.value == snapshot.sf) + .firstOrNull; + final codingRate = LoRaCodingRate.values + .where((cr) => cr.value == _toUiCodingRate(snapshot.cr)) + .firstOrNull; + + if (bandwidth == null || spreadingFactor == null || codingRate == null) { + return null; + } + + return _RadioSettingsSnapshot( + frequencyMHz: snapshot.freqHz / 1000.0, + bandwidth: bandwidth, + spreadingFactor: spreadingFactor, + codingRate: codingRate, + txPowerDbm: snapshot.txPowerDbm, + ); + } + + _RadioSettingsSnapshot _inferNonRepeatSnapshotForRepeatEnabled() { + final current = _currentSnapshot(); + const epsilon = 0.001; + for (final i in _visiblePresetIndexes()) { + final preset = RadioSettings.presets[i].$2; + final offGridFrequencyMHz = _offGridFrequencyForBaseFrequency( + preset.frequencyMHz, + ); + if ((offGridFrequencyMHz - current.frequencyMHz).abs() < epsilon && + preset.bandwidth == current.bandwidth && + preset.spreadingFactor == current.spreadingFactor && + preset.codingRate == current.codingRate && + preset.txPowerDbm == current.txPowerDbm) { + return _RadioSettingsSnapshot( + frequencyMHz: preset.frequencyMHz, + bandwidth: preset.bandwidth, + spreadingFactor: preset.spreadingFactor, + codingRate: preset.codingRate, + txPowerDbm: preset.txPowerDbm, + ); + } + } + return _fallbackNonRepeatSnapshot(current.frequencyMHz); + } + + void _applySnapshot(_RadioSettingsSnapshot snapshot) { + _frequencyController.text = snapshot.frequencyMHz.toStringAsFixed(3); + _bandwidth = snapshot.bandwidth; + _spreadingFactor = snapshot.spreadingFactor; + _codingRate = snapshot.codingRate; + _txPowerController.text = snapshot.txPowerDbm.toString(); + } + + void _applyPresetState(int index) { + final preset = RadioSettings.presets[index].$2; + final baseSnapshot = _RadioSettingsSnapshot( + frequencyMHz: preset.frequencyMHz, + bandwidth: preset.bandwidth, + spreadingFactor: preset.spreadingFactor, + codingRate: preset.codingRate, + txPowerDbm: preset.txPowerDbm, + ); + final frequencyMHz = _clientRepeat + ? _offGridFrequencyForBaseFrequency(baseSnapshot.frequencyMHz) + : baseSnapshot.frequencyMHz; + _frequencyController.text = frequencyMHz.toString(); + _bandwidth = preset.bandwidth; + _spreadingFactor = preset.spreadingFactor; + _codingRate = preset.codingRate; + _txPowerController.text = preset.txPowerDbm.toString(); + _selectedPresetIndex = index; + _lastNonRepeatSnapshot = baseSnapshot; + } + + void _syncPresetSelection() { + final previousPresetIndex = _selectedPresetIndex; + final previousLastNonRepeat = _lastNonRepeatSnapshot; + if (_clientRepeat) { + final baseSnapshot = + previousLastNonRepeat ?? _inferNonRepeatSnapshotForRepeatEnabled(); + if (_bandwidth != baseSnapshot.bandwidth || + _spreadingFactor != baseSnapshot.spreadingFactor || + _codingRate != baseSnapshot.codingRate || + (int.tryParse(_txPowerController.text) ?? 20) != + baseSnapshot.txPowerDbm) { + _lastNonRepeatSnapshot = _RadioSettingsSnapshot( + frequencyMHz: baseSnapshot.frequencyMHz, + bandwidth: _bandwidth, + spreadingFactor: _spreadingFactor, + codingRate: _codingRate, + txPowerDbm: int.tryParse(_txPowerController.text) ?? 20, + ); + } + _selectedPresetIndex = _findMatchingPresetIndexForSnapshot( + _lastNonRepeatSnapshot ?? baseSnapshot, + ); + if (previousPresetIndex != _selectedPresetIndex || + previousLastNonRepeat != _lastNonRepeatSnapshot) { + _logRadioSettingsState( + 'Preset match updated while repeat enabled: ${_presetLabel(previousPresetIndex)} -> ${_presetLabel(_selectedPresetIndex)}', + ); + } + return; + } + _lastNonRepeatSnapshot = _nonRepeatSnapshotForCurrentSelection(); + _selectedPresetIndex = _findMatchingPresetIndexForSnapshot( + _lastNonRepeatSnapshot!, + ); + if (previousPresetIndex != _selectedPresetIndex || + previousLastNonRepeat != _lastNonRepeatSnapshot) { + _logRadioSettingsState( + 'Preset sync updated state from ${_presetLabel(previousPresetIndex)} to ${_presetLabel(_selectedPresetIndex)}', + ); + } + } + + void _handleManualSettingsChanged(String source) { + _logRadioSettingsState('Manual settings edit: $source'); + setState(_syncPresetSelection); + } + void _handleClientRepeatChanged(bool enabled) { + _logRadioSettingsState( + 'Off-grid repeat toggle requested: $_clientRepeat -> $enabled', + ); setState(() { - _clientRepeat = enabled; + final currentSnapshot = _currentSnapshot(); + if (enabled) { + if (!_clientRepeat) { + _syncPresetSelection(); + } + final baseSnapshot = _lastNonRepeatSnapshot ?? currentSnapshot; + _clientRepeat = true; + _frequencyController.text = _offGridFrequencyForBaseFrequency( + baseSnapshot.frequencyMHz, + ).toStringAsFixed(3); + return; + } - final baseFrequencyMHz = _selectedPresetIndex != null - ? RadioSettings.presets[_selectedPresetIndex!].$2.frequencyMHz - : (double.tryParse(_frequencyController.text) ?? 915.0); - - final nextFrequencyMHz = enabled - ? _offGridFrequencyForBaseFrequency(baseFrequencyMHz) - : (_selectedPresetIndex != null - ? RadioSettings.presets[_selectedPresetIndex!].$2.frequencyMHz - : _normalFrequencyForBand(baseFrequencyMHz)); - - _frequencyController.text = nextFrequencyMHz.toStringAsFixed(3); + _clientRepeat = false; + _applySnapshot( + _lastNonRepeatSnapshot ?? + _fallbackNonRepeatSnapshot(currentSnapshot.frequencyMHz), + ); + _syncPresetSelection(); }); + _logRadioSettingsState('Off-grid repeat toggle applied'); } Future _saveSettings() async { @@ -1254,6 +1461,24 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { } try { + final rememberedSnapshot = _clientRepeat + ? _lastNonRepeatSnapshot + : _currentSnapshot(); + if (rememberedSnapshot != null) { + widget.connector.rememberNonRepeatRadioState( + MeshCoreRadioStateSnapshot( + freqHz: (rememberedSnapshot.frequencyMHz * 1000).round(), + bwHz: rememberedSnapshot.bandwidth.hz, + sf: rememberedSnapshot.spreadingFactor.value, + cr: _toDeviceCodingRate( + rememberedSnapshot.codingRate.value, + widget.connector.currentCr, + ), + txPowerDbm: rememberedSnapshot.txPowerDbm, + ), + ); + } + _logRadioSettingsState('Saving radio settings'); await widget.connector.sendFrame( buildSetRadioParamsFrame( freqHz, @@ -1268,10 +1493,12 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { if (!mounted) return; Navigator.pop(context); + _logRadioSettingsState('Radio settings saved successfully'); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l10n.settings_radioSettingsUpdated)), ); } catch (e) { + _appLog.warn('Radio settings save failed: $e', tag: 'RadioSettings'); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l10n.settings_error(e.toString()))), @@ -1290,6 +1517,39 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { return uiCr; } + String _presetLabel(int? index) { + if (index == null) { + return 'custom'; + } + return '${RadioSettings.presets[index].$1} (#$index)'; + } + + String _formatSnapshot(_RadioSettingsSnapshot? snapshot) { + if (snapshot == null) { + return 'null'; + } + return '${snapshot.frequencyMHz.toStringAsFixed(3)}MHz/' + '${snapshot.bandwidth.label}/' + '${snapshot.spreadingFactor.label}/' + '${snapshot.codingRate.label}/' + '${snapshot.txPowerDbm}dBm'; + } + + void _logRadioSettingsState(String message) { + _appLog.info( + '$message | ' + 'freq=${_frequencyController.text}MHz ' + 'bw=${_bandwidth.label} ' + 'sf=${_spreadingFactor.label} ' + 'cr=${_codingRate.label} ' + 'tx=${_txPowerController.text}dBm ' + 'repeat=$_clientRepeat ' + 'preset=${_presetLabel(_selectedPresetIndex)} ' + 'lastNonRepeat=${_formatSnapshot(_lastNonRepeatSnapshot)}', + tag: 'RadioSettings', + ); + } + @override Widget build(BuildContext context) { final l10n = context.l10n; @@ -1301,13 +1561,14 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { crossAxisAlignment: CrossAxisAlignment.start, children: [ DropdownButtonFormField( + key: ValueKey(_selectedPresetIndex), initialValue: _selectedPresetIndex, decoration: InputDecoration( labelText: l10n.settings_presets, border: const OutlineInputBorder(), ), items: [ - for (var i = 0; i < RadioSettings.presets.length; i++) + for (final i in _visiblePresetIndexes()) DropdownMenuItem( value: i, child: Text(RadioSettings.presets[i].$1), @@ -1315,14 +1576,14 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ], onChanged: (index) { if (index != null) { - _selectedPresetIndex = index; - _applyPreset(RadioSettings.presets[index].$2); + _applyPreset(index); } }, ), const SizedBox(height: 16), TextField( controller: _frequencyController, + onChanged: (_) => _handleManualSettingsChanged('frequency'), decoration: InputDecoration( labelText: l10n.settings_frequency, border: const OutlineInputBorder(), @@ -1345,7 +1606,13 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ) .toList(), onChanged: (value) { - if (value != null) setState(() => _bandwidth = value); + if (value != null) { + setState(() { + _bandwidth = value; + _syncPresetSelection(); + }); + _logRadioSettingsState('Manual settings edit: bandwidth'); + } }, ), const SizedBox(height: 16), @@ -1361,7 +1628,15 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ) .toList(), onChanged: (value) { - if (value != null) setState(() => _spreadingFactor = value); + if (value != null) { + setState(() { + _spreadingFactor = value; + _syncPresetSelection(); + }); + _logRadioSettingsState( + 'Manual settings edit: spreading factor', + ); + } }, ), const SizedBox(height: 16), @@ -1377,12 +1652,19 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ) .toList(), onChanged: (value) { - if (value != null) setState(() => _codingRate = value); + if (value != null) { + setState(() { + _codingRate = value; + _syncPresetSelection(); + }); + _logRadioSettingsState('Manual settings edit: coding rate'); + } }, ), const SizedBox(height: 16), TextField( controller: _txPowerController, + onChanged: (_) => _handleManualSettingsChanged('tx power'), decoration: InputDecoration( labelText: l10n.settings_txPower, border: const OutlineInputBorder(), @@ -1415,3 +1697,38 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ); } } + +class _RadioSettingsSnapshot { + final double frequencyMHz; + final LoRaBandwidth bandwidth; + final LoRaSpreadingFactor spreadingFactor; + final LoRaCodingRate codingRate; + final int txPowerDbm; + + const _RadioSettingsSnapshot({ + required this.frequencyMHz, + required this.bandwidth, + required this.spreadingFactor, + required this.codingRate, + required this.txPowerDbm, + }); + + @override + bool operator ==(Object other) { + return other is _RadioSettingsSnapshot && + frequencyMHz == other.frequencyMHz && + bandwidth == other.bandwidth && + spreadingFactor == other.spreadingFactor && + codingRate == other.codingRate && + txPowerDbm == other.txPowerDbm; + } + + @override + int get hashCode => Object.hash( + frequencyMHz, + bandwidth, + spreadingFactor, + codingRate, + txPowerDbm, + ); +} From 36697c6e617adf151f5969bdcc6b2e34ca8f7a33 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Wed, 11 Mar 2026 11:19:53 -0400 Subject: [PATCH 07/18] fix(settings): scope repeat preset memory to saved state --- lib/screens/settings_screen.dart | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index c90827b..44019dd 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1155,9 +1155,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { _lastNonRepeatSnapshot!, ); } else { - _lastNonRepeatSnapshot = - _sessionRememberedNonRepeatSnapshot() ?? - _nonRepeatSnapshotForCurrentSelection(); + _lastNonRepeatSnapshot = _nonRepeatSnapshotForCurrentSelection(); } WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; @@ -1461,6 +1459,18 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { } try { + _logRadioSettingsState('Saving radio settings'); + await widget.connector.sendFrame( + buildSetRadioParamsFrame( + freqHz, + bwHz, + sf, + cr, + clientRepeat: knownRepeat ? _clientRepeat : null, + ), + ); + await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower)); + await widget.connector.refreshDeviceInfo(); final rememberedSnapshot = _clientRepeat ? _lastNonRepeatSnapshot : _currentSnapshot(); @@ -1478,18 +1488,6 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ), ); } - _logRadioSettingsState('Saving radio settings'); - await widget.connector.sendFrame( - buildSetRadioParamsFrame( - freqHz, - bwHz, - sf, - cr, - clientRepeat: knownRepeat ? _clientRepeat : null, - ), - ); - await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower)); - await widget.connector.refreshDeviceInfo(); if (!mounted) return; Navigator.pop(context); From 1e9508d4016c2ccb2bbf1d0a04fb23a5e0ae5c75 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Sun, 15 Mar 2026 15:50:35 -0400 Subject: [PATCH 08/18] fix(settings): use integer Hz comparison, unify snapshot conversion, gate debug logging - Replace floating-point epsilon frequency comparison with integer Hz - Add frequencyHz getter and fromMeshCoreSnapshot/toMeshCoreSnapshot conversion methods on _RadioSettingsSnapshot - Move _toUiCodingRate/_toDeviceCodingRate to documented top-level functions - Gate _logRadioSettingsState behind kDebugMode - Use integer Hz in == and hashCode for _RadioSettingsSnapshot Addresses code review findings on preset/off-grid repeat toggle PR. --- lib/screens/settings_screen.dart | 119 +++++++++++++++++-------------- 1 file changed, 64 insertions(+), 55 deletions(-) diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 44019dd..e7d61ee 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:meshcore_open/utils/gpx_export.dart'; import 'package:meshcore_open/widgets/elements_ui.dart'; @@ -15,6 +16,21 @@ import 'app_debug_log_screen.dart'; import 'ble_debug_log_screen.dart'; import '../widgets/radio_stats_entry.dart'; +/// Convert device coding-rate value (1-4 on some firmware, 5-8 on others) +/// to the UI enum range (always 5-8). +int _toUiCodingRate(int deviceCr) { + return deviceCr <= 4 ? deviceCr + 4 : deviceCr; +} + +/// Convert UI coding-rate value (5-8) back to firmware encoding. +/// Uses the current device CR to detect which encoding the firmware expects. +int _toDeviceCodingRate(int uiCr, int? deviceCr) { + if (deviceCr != null && deviceCr <= 4) { + return uiCr - 4; + } + return uiCr; +} + class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @@ -1184,10 +1200,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { } int? _findMatchingPresetIndexForSnapshot(_RadioSettingsSnapshot snapshot) { - const epsilon = 0.001; for (final i in _visiblePresetIndexes()) { final preset = RadioSettings.presets[i].$2; - if ((preset.frequencyMHz - snapshot.frequencyMHz).abs() < epsilon && + if (preset.frequencyHz == snapshot.frequencyHz && preset.bandwidth == snapshot.bandwidth && preset.spreadingFactor == snapshot.spreadingFactor && preset.codingRate == snapshot.codingRate && @@ -1258,42 +1273,18 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { _RadioSettingsSnapshot? _sessionRememberedNonRepeatSnapshot() { final snapshot = widget.connector.rememberedNonRepeatRadioState; - if (snapshot == null) { - return null; - } - - final bandwidth = LoRaBandwidth.values - .where((bw) => bw.hz == snapshot.bwHz) - .firstOrNull; - final spreadingFactor = LoRaSpreadingFactor.values - .where((sf) => sf.value == snapshot.sf) - .firstOrNull; - final codingRate = LoRaCodingRate.values - .where((cr) => cr.value == _toUiCodingRate(snapshot.cr)) - .firstOrNull; - - if (bandwidth == null || spreadingFactor == null || codingRate == null) { - return null; - } - - return _RadioSettingsSnapshot( - frequencyMHz: snapshot.freqHz / 1000.0, - bandwidth: bandwidth, - spreadingFactor: spreadingFactor, - codingRate: codingRate, - txPowerDbm: snapshot.txPowerDbm, - ); + if (snapshot == null) return null; + return _RadioSettingsSnapshot.fromMeshCoreSnapshot(snapshot); } _RadioSettingsSnapshot _inferNonRepeatSnapshotForRepeatEnabled() { final current = _currentSnapshot(); - const epsilon = 0.001; for (final i in _visiblePresetIndexes()) { final preset = RadioSettings.presets[i].$2; - final offGridFrequencyMHz = _offGridFrequencyForBaseFrequency( - preset.frequencyMHz, - ); - if ((offGridFrequencyMHz - current.frequencyMHz).abs() < epsilon && + final offGridFreqHz = + (_offGridFrequencyForBaseFrequency(preset.frequencyMHz) * 1000) + .round(); + if (offGridFreqHz == current.frequencyHz && preset.bandwidth == current.bandwidth && preset.spreadingFactor == current.spreadingFactor && preset.codingRate == current.codingRate && @@ -1476,16 +1467,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { : _currentSnapshot(); if (rememberedSnapshot != null) { widget.connector.rememberNonRepeatRadioState( - MeshCoreRadioStateSnapshot( - freqHz: (rememberedSnapshot.frequencyMHz * 1000).round(), - bwHz: rememberedSnapshot.bandwidth.hz, - sf: rememberedSnapshot.spreadingFactor.value, - cr: _toDeviceCodingRate( - rememberedSnapshot.codingRate.value, - widget.connector.currentCr, - ), - txPowerDbm: rememberedSnapshot.txPowerDbm, - ), + rememberedSnapshot.toMeshCoreSnapshot(widget.connector.currentCr), ); } @@ -1504,17 +1486,6 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { } } - int _toUiCodingRate(int deviceCr) { - return deviceCr <= 4 ? deviceCr + 4 : deviceCr; - } - - int _toDeviceCodingRate(int uiCr, int? deviceCr) { - if (deviceCr != null && deviceCr <= 4) { - return uiCr - 4; - } - return uiCr; - } - String _presetLabel(int? index) { if (index == null) { return 'custom'; @@ -1534,6 +1505,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { } void _logRadioSettingsState(String message) { + if (!kDebugMode) return; _appLog.info( '$message | ' 'freq=${_frequencyController.text}MHz ' @@ -1711,10 +1683,47 @@ class _RadioSettingsSnapshot { required this.txPowerDbm, }); + /// Frequency in integer Hz — avoids floating-point comparison issues. + int get frequencyHz => (frequencyMHz * 1000).round(); + + /// Convert from the connector's raw-int snapshot to UI-enum snapshot. + static _RadioSettingsSnapshot? fromMeshCoreSnapshot( + MeshCoreRadioStateSnapshot snapshot, + ) { + final bw = LoRaBandwidth.values + .where((b) => b.hz == snapshot.bwHz) + .firstOrNull; + final sf = LoRaSpreadingFactor.values + .where((s) => s.value == snapshot.sf) + .firstOrNull; + final cr = LoRaCodingRate.values + .where((c) => c.value == _toUiCodingRate(snapshot.cr)) + .firstOrNull; + if (bw == null || sf == null || cr == null) return null; + return _RadioSettingsSnapshot( + frequencyMHz: snapshot.freqHz / 1000.0, + bandwidth: bw, + spreadingFactor: sf, + codingRate: cr, + txPowerDbm: snapshot.txPowerDbm, + ); + } + + /// Convert back to the connector's raw-int snapshot. + MeshCoreRadioStateSnapshot toMeshCoreSnapshot(int? deviceCr) { + return MeshCoreRadioStateSnapshot( + freqHz: frequencyHz, + bwHz: bandwidth.hz, + sf: spreadingFactor.value, + cr: _toDeviceCodingRate(codingRate.value, deviceCr), + txPowerDbm: txPowerDbm, + ); + } + @override bool operator ==(Object other) { return other is _RadioSettingsSnapshot && - frequencyMHz == other.frequencyMHz && + frequencyHz == other.frequencyHz && bandwidth == other.bandwidth && spreadingFactor == other.spreadingFactor && codingRate == other.codingRate && @@ -1723,7 +1732,7 @@ class _RadioSettingsSnapshot { @override int get hashCode => Object.hash( - frequencyMHz, + frequencyHz, bandwidth, spreadingFactor, codingRate, From d1e45fc2ba82ef2274ae73acacb2865dc432b8a2 Mon Sep 17 00:00:00 2001 From: ericz Date: Sat, 28 Mar 2026 17:08:59 +0100 Subject: [PATCH 09/18] moved _getRepeaterPrefixMatchNearLocation since I don't need the function anywhere else anymore. --- lib/utils/contact_search.dart | 55 -------------------------- lib/widgets/snr_indicator.dart | 70 ++++++++++++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 58 deletions(-) diff --git a/lib/utils/contact_search.dart b/lib/utils/contact_search.dart index 6a708e8..7a82c53 100644 --- a/lib/utils/contact_search.dart +++ b/lib/utils/contact_search.dart @@ -1,6 +1,3 @@ -import 'package:latlong2/latlong.dart'; - -import '../connector/meshcore_protocol.dart'; import '../models/contact.dart'; export 'contact_filter_types.dart'; @@ -46,55 +43,3 @@ String? _extractHexPrefix(String query) { if (!RegExp(r'^[0-9a-f]+$').hasMatch(cleaned)) return null; return cleaned; } - -Contact? getRepeaterPrefixMatchNearLocation( - List contacts, - int pubkeyFirstByte, { - LatLng? searchPoint, - bool preferFavorites = false, -}) { - final candidates = contacts - .where( - (c) => - c.publicKey.isNotEmpty && - c.publicKey.first == pubkeyFirstByte && - (c.type == advTypeRepeater || c.type == advTypeRoom), - ) - .toList(); - - if (candidates.isEmpty) return null; - - candidates.sort((a, b) { - if (preferFavorites) { - final favA = a.isFavorite ? 1 : 0; - final favB = b.isFavorite ? 1 : 0; - final favCompare = favB.compareTo(favA); - if (favCompare != 0) return favCompare; - } - - final seenCompare = b.lastSeen.compareTo(a.lastSeen); - if (seenCompare != 0) return seenCompare; - - return a.publicKeyHex.compareTo(b.publicKeyHex); - }); - - if (searchPoint == null) { - return candidates.first; - } - - final distance = Distance(); - Contact best = candidates.first; - var bestDistance = double.infinity; - - for (final c in candidates) { - if (c.hasLocation && c.latitude != null && c.longitude != null) { - final d = distance(searchPoint, LatLng(c.latitude!, c.longitude!)); - if (d < bestDistance) { - bestDistance = d; - best = c; - } - } - } - - return best; -} diff --git a/lib/widgets/snr_indicator.dart b/lib/widgets/snr_indicator.dart index cf3c275..99f2053 100644 --- a/lib/widgets/snr_indicator.dart +++ b/lib/widgets/snr_indicator.dart @@ -2,10 +2,63 @@ import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; import '../connector/meshcore_connector.dart'; -import '../utils/contact_search.dart'; +import '../connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; +import '../models/contact.dart'; import 'signal_ui.dart'; +Contact? _getRepeaterPrefixMatchNearLocation( + List contacts, + int pubkeyFirstByte, { + LatLng? searchPoint, + bool preferFavorites = false, +}) { + final candidates = contacts + .where( + (c) => + c.publicKey.isNotEmpty && + c.publicKey.first == pubkeyFirstByte && + (c.type == advTypeRepeater || c.type == advTypeRoom), + ) + .toList(); + + if (candidates.isEmpty) return null; + + candidates.sort((a, b) { + if (preferFavorites) { + final favA = a.isFavorite ? 1 : 0; + final favB = b.isFavorite ? 1 : 0; + final favCompare = favB.compareTo(favA); + if (favCompare != 0) return favCompare; + } + + final seenCompare = b.lastSeen.compareTo(a.lastSeen); + if (seenCompare != 0) return seenCompare; + + return a.publicKeyHex.compareTo(b.publicKeyHex); + }); + + if (searchPoint == null) { + return candidates.first; + } + + final distance = Distance(); + Contact best = candidates.first; + var bestDistance = double.infinity; + + for (final c in candidates) { + if (c.hasLocation && c.latitude != null && c.longitude != null) { + final d = distance(searchPoint, LatLng(c.latitude!, c.longitude!)); + if (d < bestDistance) { + bestDistance = d; + best = c; + } + } + } + + return best; +} + class SNRUi { final IconData icon; final Color color; @@ -67,6 +120,15 @@ class SNRIndicator extends StatefulWidget { } class _SNRIndicatorState extends State { + bool _isValidSelfLocation(double lat, double lon) { + const double epsilon = 1e-6; + return (lat.abs() > epsilon || lon.abs() > epsilon) && + lat >= -90.0 && + lat <= 90.0 && + lon >= -180.0 && + lon <= 180.0; + } + @override Widget build(BuildContext context) { final directRepeaters = widget.connector.directRepeaters; @@ -166,11 +228,13 @@ class _SNRIndicatorState extends State { final selfLon = widget.connector.selfLongitude; LatLng? selfPoint; - if (selfLat != null && selfLon != null) { + if (selfLat != null && + selfLon != null && + _isValidSelfLocation(selfLat, selfLon)) { selfPoint = LatLng(selfLat, selfLon); } - final contact = getRepeaterPrefixMatchNearLocation( + final contact = _getRepeaterPrefixMatchNearLocation( allContacts, repeater.pubkeyFirstByte, searchPoint: selfPoint, From 817c60a15516f6bde34acc1791e00287e2d8c465 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Tue, 31 Mar 2026 18:55:22 -0700 Subject: [PATCH 10/18] Update ML timeout handling and adjust distance threshold for path hops --- lib/connector/meshcore_connector.dart | 4 +++- lib/screens/channel_message_path_screen.dart | 2 +- lib/screens/neighbors_screen.dart | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 93c5dcd..a44ba37 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -3753,7 +3753,9 @@ class MeshCoreConnector extends ChangeNotifier { if (mlTimeout != null) { if (pathLength < 0) { // Flood: trust ML, only enforce firmware formula as floor - return mlTimeout.clamp(physicsMin, mlTimeout); + if (mlTimeout < physicsMin) { + return physicsMin; + } } return mlTimeout.clamp(physicsMin, physicsMax); } diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 0eb2c22..53769d4 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -879,7 +879,7 @@ List<_PathHop> _buildPathHops( previousPosition = resolvedPosition; } // If the best candidate is much farther than the previous hop, it's likely not the correct match. - if (lastDistance + bestDistance > 70000 && + if (lastDistance + bestDistance > 50000 && candidates != null && candidates.isNotEmpty) { i--; diff --git a/lib/screens/neighbors_screen.dart b/lib/screens/neighbors_screen.dart index f4c1673..7286eb0 100644 --- a/lib/screens/neighbors_screen.dart +++ b/lib/screens/neighbors_screen.dart @@ -142,7 +142,7 @@ class _NeighborsScreenState extends State { void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) { final buffer = BufferReader(frame); - final contacts = connector.allContacts; + final contacts = connector.allContactsUnfiltered; try { final neighborCount = buffer.readUInt16LE(); final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE()); From 77566b0fe128f7ea55ee6c3e8d8f460dc701a4b6 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Thu, 26 Mar 2026 22:28:01 -0700 Subject: [PATCH 11/18] Refactor contact handling and other improvments (#317) * Refactor contact filtering and improve localization strings; enhance path trace handling * Add localization for new CLI commands and update existing strings * Enhance contact handling and UI updates across multiple screens add unfiltered contact access and improve last seen resolution * Add polling interval configuration and improve contact handling * Reorder command constants for better organization and clarity * Refactor contact handling by removing unnecessary mapping and improving clarity across multiple screens * Moved RadioStatsIconButton in chat screen for improved UI consistency * Added indicators to AppBar for channels * Ignore contacts with self public key in contact handling * Simplify path removal logic and clean up unused imports in path management dialog * Enhance path hop resolution by adding distance checks to improve candidate selection accuracy * Remove unnecessary reset of radio stats poll reference count in polling interval setter --- lib/connector/meshcore_connector.dart | 45 ++++++++++-- lib/connector/meshcore_protocol.dart | 4 +- lib/l10n/app_bg.arb | 8 ++- lib/l10n/app_de.arb | 9 ++- lib/l10n/app_en.arb | 10 ++- lib/l10n/app_es.arb | 7 +- lib/l10n/app_fr.arb | 8 ++- lib/l10n/app_hu.arb | 7 +- lib/l10n/app_it.arb | 9 ++- lib/l10n/app_ja.arb | 10 ++- lib/l10n/app_ko.arb | 7 +- lib/l10n/app_localizations.dart | 12 ++++ lib/l10n/app_localizations_bg.dart | 6 ++ lib/l10n/app_localizations_de.dart | 6 ++ lib/l10n/app_localizations_en.dart | 6 ++ lib/l10n/app_localizations_es.dart | 6 ++ lib/l10n/app_localizations_fr.dart | 6 ++ lib/l10n/app_localizations_hu.dart | 6 ++ lib/l10n/app_localizations_it.dart | 6 ++ lib/l10n/app_localizations_ja.dart | 6 ++ lib/l10n/app_localizations_ko.dart | 6 ++ lib/l10n/app_localizations_nl.dart | 8 ++- lib/l10n/app_localizations_pl.dart | 6 ++ lib/l10n/app_localizations_pt.dart | 6 ++ lib/l10n/app_localizations_ru.dart | 6 ++ lib/l10n/app_localizations_sk.dart | 6 ++ lib/l10n/app_localizations_sl.dart | 6 ++ lib/l10n/app_localizations_sv.dart | 6 ++ lib/l10n/app_localizations_uk.dart | 6 ++ lib/l10n/app_localizations_zh.dart | 6 ++ lib/l10n/app_nl.arb | 9 ++- lib/l10n/app_pl.arb | 10 ++- lib/l10n/app_pt.arb | 9 ++- lib/l10n/app_ru.arb | 10 ++- lib/l10n/app_sk.arb | 7 +- lib/l10n/app_sl.arb | 9 ++- lib/l10n/app_sv.arb | 10 ++- lib/l10n/app_uk.arb | 10 ++- lib/l10n/app_zh.arb | 7 +- lib/screens/channel_message_path_screen.dart | 18 ++++- lib/screens/channels_screen.dart | 2 +- lib/screens/chat_screen.dart | 2 +- lib/screens/companion_radio_stats_screen.dart | 2 + lib/screens/contacts_screen.dart | 19 ++--- lib/screens/discovery_screen.dart | 62 +++++++++++++++-- lib/screens/map_screen.dart | 69 +++++++++++++------ lib/screens/path_trace_map.dart | 50 +++++++++++--- lib/screens/repeater_cli_screen.dart | 8 ++- lib/utils/gpx_export.dart | 34 ++++++--- lib/widgets/repeater_login_dialog.dart | 2 +- lib/widgets/room_login_dialog.dart | 2 +- 51 files changed, 488 insertions(+), 109 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index c804340..228fc39 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -196,6 +196,7 @@ class MeshCoreConnector extends ChangeNotifier { static const int _contactMsgBackoffFallbackMs = 5000; static const int _contactMsgBackoffMinMs = 500; static const int _contactMsgBackoffMaxMs = 15000; + int _pollingInterval = 30; bool _batteryRequested = false; bool _awaitingSelfInfo = false; bool _hasReceivedDeviceInfo = false; @@ -326,8 +327,14 @@ class MeshCoreConnector extends ChangeNotifier { List get allContacts => List.unmodifiable([ ..._contacts, - ..._discoveredContacts.where((c) => !c.isActive), + ..._discoveredContacts.where( + (c) => !c.isActive && c.publicKeyHex != selfPublicKeyHex, + ), ]); + + List get allContactsUnfiltered => + List.unmodifiable([..._contacts, ..._discoveredContacts]); + List get discoveredContacts { return List.unmodifiable(_discoveredContacts); } @@ -2368,9 +2375,18 @@ class MeshCoreConnector extends ChangeNotifier { _batteryPollTimer = null; } + void setPollingInterval(int i) { + _pollingInterval = i.clamp(1, 60); + if (isConnected) { + _startRadioStatsPolling(); + } + } + void _startRadioStatsPolling() { _radioStatsPollTimer?.cancel(); - _radioStatsPollTimer = Timer.periodic(const Duration(seconds: 1), (_) { + _radioStatsPollTimer = Timer.periodic(Duration(seconds: _pollingInterval), ( + _, + ) { if (!isConnected) { _stopRadioStatsPolling(); return; @@ -2495,6 +2511,18 @@ class MeshCoreConnector extends ChangeNotifier { }); } + Contact getFromDiscovered(Contact contact) { + final tmp = _discoveredContacts.firstWhere( + (c) => c.publicKeyHex == contact.publicKeyHex, + orElse: () => contact, + ); + return contact.copyWith( + rawPacket: tmp.rawPacket, + latitude: tmp.latitude, + longitude: tmp.longitude, + ); + } + Future getContacts({int? since, bool preserveExisting = false}) async { if (!isConnected) return; @@ -3885,8 +3913,17 @@ class MeshCoreConnector extends ChangeNotifier { } void _handleContact(Uint8List frame, {bool isContact = true}) { - final contact = Contact.fromFrame(frame); - if (contact != null) { + final contactTmp = Contact.fromFrame(frame); + if (contactTmp != null) { + if (listEquals(contactTmp.publicKey, _selfPublicKey)) { + appLogger.info( + 'Ignoring contact with self public key: ${contactTmp.name}', + tag: 'Connector', + ); + removeContact(contactTmp); + return; + } + final contact = getFromDiscovered(contactTmp); _handleDiscovery(contact, frame, noNotify: true, addActive: true); if (contact.type == advTypeRepeater) { diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index b42e3e5..396d78b 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -202,15 +202,15 @@ const int cmdGetChannel = 31; const int cmdSetChannel = 32; const int cmdSendTracePath = 36; const int cmdSetOtherParams = 38; -const int cmdSendAnonReq = 57; const int cmdSendTelemetryReq = 39; const int cmdGetCustomVar = 40; const int cmdSetCustomVar = 41; const int cmdSendBinaryReq = 50; +const int cmdGetStats = 56; +const int cmdSendAnonReq = 57; const int cmdSetAutoAddConfig = 58; const int cmdGetAutoAddConfig = 59; const int cmdSetPathHashMode = 61; -const int cmdGetStats = 56; // Text message types const int txtTypePlain = 0; diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 13e9de7..cd822e3 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -2059,5 +2059,9 @@ "translation_composerEnabledHint": "Съобщенията ще бъдат преведени, преди да бъдат изпратени.", "translation_translateTo": "Превеждане на {language}", "translation_translationOptions": "Опции за превод", - "translation_systemLanguage": "Език на системата" -} + "translation_systemLanguage": "Език на системата", + "scanner_linuxPairingPinTitle": "PIN код за сдвояване на Bluetooth", + "scanner_linuxPairingPinPrompt": "Въведете ПИН за {deviceName} (оставете празно, ако няма).", + "repeater_cliQuickClockSync": "Синхронизация на часовника", + "repeater_cliQuickDiscovery": "Открий Съседи" +} \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 62badce..10af5da 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -2087,5 +2087,10 @@ "translation_composerDisabledHint": "Nachrichten in der ursprünglichen, getippten Sprache senden.", "translation_translateTo": "Übersetzen Sie auf {language}", "translation_translationOptions": "Übersetzungsmöglichkeiten", - "translation_systemLanguage": "Sprache des Systems" -} + "translation_systemLanguage": "Sprache des Systems", + "scanner_linuxPairingHidePin": "PIN ausblenden", + "scanner_linuxPairingPinTitle": "Bluetooth-Paarungs-PIN", + "scanner_linuxPairingPinPrompt": "Geben Sie die PIN für {deviceName} ein (leer lassen, falls keine).", + "repeater_cliQuickClockSync": "Uhr Synchronisieren", + "repeater_cliQuickDiscovery": "Entdecke Nachbarn" +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0617553..b703630 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -303,8 +303,12 @@ "path_routeWeight": "{weight}/{max}", "@path_routeWeight": { "placeholders": { - "weight": { "type": "String" }, - "max": { "type": "String" } + "weight": { + "type": "String" + }, + "max": { + "type": "String" + } } }, "appSettings_battery": "Battery", @@ -1333,6 +1337,8 @@ "repeater_cliQuickVersion": "Version", "repeater_cliQuickAdvertise": "Advertise", "repeater_cliQuickClock": "Clock", + "repeater_cliQuickClockSync": "Clock Sync", + "repeater_cliQuickDiscovery": "Discover Neighbors", "repeater_cliHelpAdvert": "Sends an advertisement packet", "repeater_cliHelpReboot": "Reboots the device. (note, you'll prob get 'Timeout' which is normal)", "repeater_cliHelpClock": "Displays current time per device's clock.", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 4d465bb..0372dff 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -2087,5 +2087,8 @@ "translation_translateBeforeSending": "Traducir antes de enviar", "translation_translateTo": "Traducir a {language}", "translation_translationOptions": "Opciones de traducción", - "translation_systemLanguage": "Idioma del sistema" -} + "translation_systemLanguage": "Idioma del sistema", + "scanner_linuxPairingPinPrompt": "Introduzca el PIN para {deviceName} (déjelo en blanco si no hay ninguno).", + "repeater_cliQuickDiscovery": "Descubrir Vecinos", + "repeater_cliQuickClockSync": "Sincronización del reloj" +} \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 16e1d3d..d74c358 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -2059,5 +2059,9 @@ "translation_messageTranslation": "Traduction du message", "translation_translateTo": "Traduire en {language}", "translation_translationOptions": "Options de traduction", - "translation_systemLanguage": "Langue du système" -} + "translation_systemLanguage": "Langue du système", + "scanner_linuxPairingPinTitle": "Code PIN d’appairage Bluetooth", + "scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si aucun).", + "repeater_cliQuickClockSync": "Synchronisation de l'horloge", + "repeater_cliQuickDiscovery": "Découvrir les voisins" +} \ No newline at end of file diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index cf42e1b..68b3b11 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -2097,5 +2097,8 @@ "translation_composerDisabledHint": "Küldj üzeneteket az eredeti, nyomtatott nyelven.", "translation_translateTo": "Fordítás {language}-ra", "translation_translationOptions": "Fordítási lehetőségek", - "translation_systemLanguage": "Rendszer nyelvé" -} + "translation_systemLanguage": "Rendszer nyelvé", + "scanner_linuxPairingPinPrompt": "Adja meg a(z) {deviceName} PIN-kódját (hagyja üresen, ha nincs).", + "repeater_cliQuickClockSync": "Óra szinkronizálás", + "repeater_cliQuickDiscovery": "Fedezd fel a szomszédokat" +} \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index b9676bb..9b539a0 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -2059,5 +2059,10 @@ "translation_composerEnabledHint": "I messaggi verranno tradotti prima di essere inviati.", "translation_translateTo": "Tradurre in {language}", "translation_translationOptions": "Opzioni di traduzione", - "translation_systemLanguage": "Lingua del sistema" -} + "translation_systemLanguage": "Lingua del sistema", + "scanner_linuxPairingHidePin": "Nascondi PIN", + "scanner_linuxPairingPinTitle": "PIN di associazione Bluetooth", + "scanner_linuxPairingPinPrompt": "Inserisci il PIN per {deviceName} (lascia vuoto se non ce n'è).", + "repeater_cliQuickClockSync": "Sincronizzazione dell'orologio", + "repeater_cliQuickDiscovery": "Scopri i Vicini" +} \ No newline at end of file diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 6a9c975..aef8fc0 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -2097,5 +2097,11 @@ "translation_composerDisabledHint": "元のタイプされた言語でメッセージを送信してください。", "translation_translateTo": "{language} への翻訳", "translation_translationOptions": "翻訳の選択肢", - "translation_systemLanguage": "システム言語" -} + "translation_systemLanguage": "システム言語", + "scanner_linuxPairingShowPin": "PINを表示", + "scanner_linuxPairingHidePin": "PINを非表示", + "scanner_linuxPairingPinTitle": "Bluetooth ペアリング PIN", + "scanner_linuxPairingPinPrompt": "{deviceName}のPINを入力してください(なしの場合は空欄のまま)。", + "repeater_cliQuickClockSync": "クロック同期", + "repeater_cliQuickDiscovery": "近隣を発見する" +} \ No newline at end of file diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 2050e3b..66ad1ed 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -2097,5 +2097,8 @@ "translation_composerDisabledHint": "원래 작성된 언어로 메시지를 보내세요.", "translation_translateTo": "{language} 번역", "translation_translationOptions": "번역 옵션", - "translation_systemLanguage": "시스템 언어" -} + "translation_systemLanguage": "시스템 언어", + "scanner_linuxPairingPinPrompt": "{deviceName}에 대한 PIN을 입력하세요 (없으면 비워두세요).", + "repeater_cliQuickClockSync": "시계 동기화", + "repeater_cliQuickDiscovery": "이웃 발견하기" +} \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e2bd2f3..408a243 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4322,6 +4322,18 @@ abstract class AppLocalizations { /// **'Clock'** String get repeater_cliQuickClock; + /// No description provided for @repeater_cliQuickClockSync. + /// + /// In en, this message translates to: + /// **'Clock Sync'** + String get repeater_cliQuickClockSync; + + /// No description provided for @repeater_cliQuickDiscovery. + /// + /// In en, this message translates to: + /// **'Discover Neighbors'** + String get repeater_cliQuickDiscovery; + /// No description provided for @repeater_cliHelpAdvert. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 283860e..8a43322 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2429,6 +2429,12 @@ class AppLocalizationsBg extends AppLocalizations { @override String get repeater_cliQuickClock => 'Часовник'; + @override + String get repeater_cliQuickClockSync => 'Синхронизация на часовника'; + + @override + String get repeater_cliQuickDiscovery => 'Открий Съседи'; + @override String get repeater_cliHelpAdvert => 'Изпраща рекламен пакет'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index e29ae9e..1177bc1 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2429,6 +2429,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliQuickClock => 'Uhr'; + @override + String get repeater_cliQuickClockSync => 'Uhr Synchronisieren'; + + @override + String get repeater_cliQuickDiscovery => 'Entdecke Nachbarn'; + @override String get repeater_cliHelpAdvert => 'Sendet eine Ankündigung'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 877e11d..9104f8b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2379,6 +2379,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get repeater_cliQuickClock => 'Clock'; + @override + String get repeater_cliQuickClockSync => 'Clock Sync'; + + @override + String get repeater_cliQuickDiscovery => 'Discover Neighbors'; + @override String get repeater_cliHelpAdvert => 'Sends an advertisement packet'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index c963902..cc3b714 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2423,6 +2423,12 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliQuickClock => 'Reloj'; + @override + String get repeater_cliQuickClockSync => 'Sincronización del reloj'; + + @override + String get repeater_cliQuickDiscovery => 'Descubrir Vecinos'; + @override String get repeater_cliHelpAdvert => 'Envía un paquete de publicidad'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index eea88f5..402e373 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2442,6 +2442,12 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_cliQuickClock => 'Horloge'; + @override + String get repeater_cliQuickClockSync => 'Synchronisation de l\'horloge'; + + @override + String get repeater_cliQuickDiscovery => 'Découvrir les voisins'; + @override String get repeater_cliHelpAdvert => 'Envoie un paquet d\'annonce'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 5e36e94..204e21b 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -2437,6 +2437,12 @@ class AppLocalizationsHu extends AppLocalizations { @override String get repeater_cliQuickClock => 'óra'; + @override + String get repeater_cliQuickClockSync => 'Óra szinkronizálás'; + + @override + String get repeater_cliQuickDiscovery => 'Fedezd fel a szomszédokat'; + @override String get repeater_cliHelpAdvert => 'Elküldi egy hirdetési csomagot'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index bb9e0d2..936ecc1 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2426,6 +2426,12 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliQuickClock => 'Orologio'; + @override + String get repeater_cliQuickClockSync => 'Sincronizzazione dell\'orologio'; + + @override + String get repeater_cliQuickDiscovery => 'Scopri i Vicini'; + @override String get repeater_cliHelpAdvert => 'Invia un pacchetto pubblicitario'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 5151ab8..7accee3 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -2322,6 +2322,12 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliQuickClock => '時計'; + @override + String get repeater_cliQuickClockSync => 'クロック同期'; + + @override + String get repeater_cliQuickDiscovery => '近隣を発見する'; + @override String get repeater_cliHelpAdvert => '広告用資料を送る'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index be64545..06d7db6 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -2319,6 +2319,12 @@ class AppLocalizationsKo extends AppLocalizations { @override String get repeater_cliQuickClock => '시계'; + @override + String get repeater_cliQuickClockSync => '시계 동기화'; + + @override + String get repeater_cliQuickDiscovery => '이웃 발견하기'; + @override String get repeater_cliHelpAdvert => '광고 패킷을 발송'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 86809df..6b7bbe7 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2410,7 +2410,13 @@ class AppLocalizationsNl extends AppLocalizations { String get repeater_cliQuickClock => 'Tijd opvragen'; @override - String get repeater_cliHelpAdvert => 'Advertentie uitzenden'; + String get repeater_cliQuickClockSync => 'Kloksynchronisatie'; + + @override + String get repeater_cliQuickDiscovery => 'Ontdek Buren'; + + @override + String get repeater_cliHelpAdvert => 'Verstuurt een advertentiepakket'; @override String get repeater_cliHelpReboot => diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 8952815..b6296a4 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2435,6 +2435,12 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_cliQuickClock => 'Godzina'; + @override + String get repeater_cliQuickClockSync => 'Synchronizacja zegara'; + + @override + String get repeater_cliQuickDiscovery => 'Odkryj Sąsiadów'; + @override String get repeater_cliHelpAdvert => 'Wysyła pakiet rozgłoszeniowy'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 43dc27a..d1f66af 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2423,6 +2423,12 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_cliQuickClock => 'Relógio'; + @override + String get repeater_cliQuickClockSync => 'Sincronização do Relógio'; + + @override + String get repeater_cliQuickDiscovery => 'Descobrir Vizinhos'; + @override String get repeater_cliHelpAdvert => 'Envia um pacote de anúncios'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 703d80d..cb2ae15 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2427,6 +2427,12 @@ class AppLocalizationsRu extends AppLocalizations { @override String get repeater_cliQuickClock => 'Время'; + @override + String get repeater_cliQuickClockSync => 'Синхронизация часов'; + + @override + String get repeater_cliQuickDiscovery => 'Обнаружить Соседей'; + @override String get repeater_cliHelpAdvert => 'Отправляет пакет анонсирования'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 980657d..8ddea4b 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2406,6 +2406,12 @@ class AppLocalizationsSk extends AppLocalizations { @override String get repeater_cliQuickClock => 'Hodiny'; + @override + String get repeater_cliQuickClockSync => 'Synchronizácia hodin'; + + @override + String get repeater_cliQuickDiscovery => 'Objaviť susedov'; + @override String get repeater_cliHelpAdvert => 'Odosiela reklamnú balíček.'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index ad2a278..07c1c01 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2409,6 +2409,12 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_cliQuickClock => 'Ura'; + @override + String get repeater_cliQuickClockSync => 'Usklajevanje ure'; + + @override + String get repeater_cliQuickDiscovery => 'Odkrijte sosede'; + @override String get repeater_cliHelpAdvert => 'Pošlje paket oglasov'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index cc590c2..8745774 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2394,6 +2394,12 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_cliQuickClock => 'Klocka'; + @override + String get repeater_cliQuickClockSync => 'Synkronisera klocka'; + + @override + String get repeater_cliQuickDiscovery => 'Upptäck grannar'; + @override String get repeater_cliHelpAdvert => 'Skickar ett annonspaket'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index dd7bf63..fc0abea 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2427,6 +2427,12 @@ class AppLocalizationsUk extends AppLocalizations { @override String get repeater_cliQuickClock => 'Годинник'; + @override + String get repeater_cliQuickClockSync => 'Синхронізація годинника'; + + @override + String get repeater_cliQuickDiscovery => 'Відкрити сусідів'; + @override String get repeater_cliHelpAdvert => 'Надсилає пакет оголошення'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 8910dcd..f9ff709 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2277,6 +2277,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get repeater_cliQuickClock => '时钟'; + @override + String get repeater_cliQuickClockSync => '同步时钟'; + + @override + String get repeater_cliQuickDiscovery => '发现邻居'; + @override String get repeater_cliHelpAdvert => '发送广播包'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index cb1a11c..ac3ddca 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -2059,5 +2059,10 @@ "translation_messageTranslation": "Berichtvertaling", "translation_translationOptions": "Opties voor vertaling", "translation_systemLanguage": "Taal van het systeem", - "translation_translateTo": "Vertalen naar {language}" -} + "translation_translateTo": "Vertalen naar {language}", + "scanner_linuxPairingHidePin": "PIN verbergen", + "scanner_linuxPairingPinPrompt": "Voer PIN in voor {deviceName} (laat leeg als er geen is).", + "scanner_linuxPairingPinTitle": "Bluetooth‑koppelings‑PIN", + "repeater_cliQuickDiscovery": "Ontdek Buren", + "repeater_cliQuickClockSync": "Kloksynchronisatie" +} \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index aa3049f..cf530af 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -2097,5 +2097,11 @@ "translation_messageTranslation": "Tłumaczenie wiadomości", "translation_translationOptions": "Opcje tłumaczenia", "translation_systemLanguage": "Język systemu", - "translation_translateTo": "Tłumacz na {language}" -} + "translation_translateTo": "Tłumacz na {language}", + "scanner_linuxPairingShowPin": "Pokaż PIN", + "scanner_linuxPairingHidePin": "Ukryj PIN", + "scanner_linuxPairingPinPrompt": "Wprowadź kod PIN dla {deviceName} (pozostaw puste, jeśli brak).", + "scanner_linuxPairingPinTitle": "Kod PIN parowania Bluetooth", + "repeater_cliQuickClockSync": "Synchronizacja zegara", + "repeater_cliQuickDiscovery": "Odkryj Sąsiadów" +} \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index c667cb0..f88c5e0 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -2059,5 +2059,10 @@ "translation_composerDisabledHint": "Envie mensagens no idioma original, conforme digitado.", "translation_translateTo": "Traduzir para {language}", "translation_translationOptions": "Opções de tradução", - "translation_systemLanguage": "Idioma do sistema" -} + "translation_systemLanguage": "Idioma do sistema", + "scanner_linuxPairingHidePin": "Ocultar PIN", + "scanner_linuxPairingPinPrompt": "Insira o PIN para {deviceName} (deixe em branco se não houver).", + "scanner_linuxPairingPinTitle": "PIN de emparelhamento Bluetooth", + "repeater_cliQuickClockSync": "Sincronização do Relógio", + "repeater_cliQuickDiscovery": "Descobrir Vizinhos" +} \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 730cfc9..13eac22 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1299,5 +1299,11 @@ "translation_composerDisabledHint": "Отправляйте сообщения на языке, в котором они были изначально набраны.", "translation_translateTo": "Перевести на {language}", "translation_translationOptions": "Варианты перевода", - "translation_systemLanguage": "Язык системы" -} + "translation_systemLanguage": "Язык системы", + "scanner_linuxPairingShowPin": "Показать PIN", + "scanner_linuxPairingPinPrompt": "Введите PIN‑код для {deviceName} (оставьте пустым, если нет).", + "scanner_linuxPairingHidePin": "Скрыть PIN", + "scanner_linuxPairingPinTitle": "PIN‑код сопряжения Bluetooth", + "repeater_cliQuickDiscovery": "Обнаружить Соседей", + "repeater_cliQuickClockSync": "Синхронизация часов" +} \ No newline at end of file diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index cf99ca8..43e408f 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -2059,5 +2059,8 @@ "translation_messageTranslation": "Preklad textu", "translation_translateTo": "Preložte do {language}", "translation_translationOptions": "Možnosti prekladania", - "translation_systemLanguage": "Jazyk systému" -} + "translation_systemLanguage": "Jazyk systému", + "scanner_linuxPairingPinTitle": "Bluetooth párovací PIN", + "repeater_cliQuickClockSync": "Synchronizácia hodin", + "repeater_cliQuickDiscovery": "Objaviť susedov" +} \ No newline at end of file diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 0c29a86..3ef08b1 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -2059,5 +2059,10 @@ "translation_messageTranslation": "Prevod sporočila", "translation_translateTo": "Prevesti v {language}", "translation_translationOptions": "Možnosti prevoda", - "translation_systemLanguage": "Jezik sistema" -} + "translation_systemLanguage": "Jezik sistema", + "scanner_linuxPairingHidePin": "Skrij PIN", + "scanner_linuxPairingPinPrompt": "Vnesite PIN za {deviceName} (pustite prazno, če ga ni).", + "scanner_linuxPairingPinTitle": "Bluetooth PIN za seznanjanje", + "repeater_cliQuickDiscovery": "Odkrijte sosede", + "repeater_cliQuickClockSync": "Usklajevanje ure" +} \ No newline at end of file diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 3232888..9f317db 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -2059,5 +2059,11 @@ "translation_messageTranslation": "Meddelandets översättning", "translation_translateTo": "Översätt till {language}", "translation_translationOptions": "Översättningsalternativ", - "translation_systemLanguage": "Språk för systemet" -} + "translation_systemLanguage": "Språk för systemet", + "scanner_linuxPairingShowPin": "Visa PIN", + "scanner_linuxPairingPinTitle": "Bluetooth‑parnings‑PIN", + "scanner_linuxPairingPinPrompt": "Ange PIN för {deviceName} (lämna tomt om ingen).", + "scanner_linuxPairingHidePin": "Dölj PIN", + "repeater_cliQuickDiscovery": "Upptäck grannar", + "repeater_cliQuickClockSync": "Synkronisera klocka" +} \ No newline at end of file diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index ddab576..a0cce7e 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -2059,5 +2059,11 @@ "translation_translateBeforeSending": "Перекладіть перед відправкою", "translation_translateTo": "Перекласти на {language}", "translation_translationOptions": "Варіанти перекладу", - "translation_systemLanguage": "Мова системи" -} + "translation_systemLanguage": "Мова системи", + "scanner_linuxPairingPinTitle": "PIN‑код спарювання Bluetooth", + "scanner_linuxPairingShowPin": "Показати PIN", + "scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).", + "scanner_linuxPairingHidePin": "Приховати PIN", + "repeater_cliQuickClockSync": "Синхронізація годинника", + "repeater_cliQuickDiscovery": "Відкрити сусідів" +} \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 766be44..2e19a8e 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -2064,5 +2064,8 @@ "translation_translateBeforeSending": "在发送前进行翻译", "translation_translateTo": "翻译成 {language}", "translation_translationOptions": "翻译选项", - "translation_systemLanguage": "系统语言" -} + "translation_systemLanguage": "系统语言", + "scanner_linuxPairingHidePin": "隐藏 PIN", + "repeater_cliQuickDiscovery": "发现邻居", + "repeater_cliQuickClockSync": "同步时钟" +} \ No newline at end of file diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 94b8eee..0eb2c22 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -822,7 +822,8 @@ List<_PathHop> _buildPathHops( ) { if (pathBytes.isEmpty) return const []; final candidatesByPrefix = >{}; - for (final contact in connector.allContacts) { + final allContacts = connector.allContacts; + for (final contact in allContacts) { if (contact.publicKey.isEmpty) continue; if (contact.type != advTypeRepeater && contact.type != advTypeRoom) { continue; @@ -839,7 +840,8 @@ List<_PathHop> _buildPathHops( : null; var previousPosition = startPoint; final distance = Distance(); - + var lastDistance = 0.0; + var bestDistance = 0.0; final hops = <_PathHop>[]; for (var i = 0; i < pathBytes.length; i++) { final searchPoint = i == 0 ? startPoint : previousPosition; @@ -848,7 +850,7 @@ List<_PathHop> _buildPathHops( if (candidates != null && candidates.isNotEmpty) { var bestIndex = 0; if (searchPoint != null) { - var bestDistance = double.infinity; + bestDistance = double.infinity; for (var j = 0; j < candidates.length; j++) { final candidate = candidates[j]; if (!candidate.hasLocation || @@ -876,6 +878,16 @@ List<_PathHop> _buildPathHops( if (resolvedPosition != null) { previousPosition = resolvedPosition; } + // If the best candidate is much farther than the previous hop, it's likely not the correct match. + if (lastDistance + bestDistance > 70000 && + candidates != null && + candidates.isNotEmpty) { + i--; + lastDistance = bestDistance; + continue; + } + lastDistance = bestDistance; + hops.add( _PathHop( index: i + 1, diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index d67d03d..51d2453 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -127,7 +127,7 @@ class _ChannelsScreenState extends State canPop: allowBack, child: Scaffold( appBar: AppBar( - title: AppBarTitle(context.l10n.channels_title, indicators: false), + title: AppBarTitle(context.l10n.channels_title), centerTitle: true, automaticallyImplyLeading: false, actions: [ diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 372e3e7..082374f 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -293,6 +293,7 @@ class _ChatScreenState extends State { tooltip: context.l10n.chat_pathManagement, onPressed: () => _showPathHistory(context), ), + const RadioStatsIconButton(), Consumer( builder: (context, connector, _) { return PopupMenuButton( @@ -365,7 +366,6 @@ class _ChatScreenState extends State { ); }, ), - const RadioStatsIconButton(), ], ), body: Consumer( diff --git a/lib/screens/companion_radio_stats_screen.dart b/lib/screens/companion_radio_stats_screen.dart index 01fb64d..9c37676 100644 --- a/lib/screens/companion_radio_stats_screen.dart +++ b/lib/screens/companion_radio_stats_screen.dart @@ -24,6 +24,7 @@ class _CompanionRadioStatsScreenState extends State { final c = context.read(); _connector = c; c.acquireRadioStatsPolling(); + c.setPollingInterval(1); c.radioStatsNotifier.addListener(_onStatsUpdate); } @@ -44,6 +45,7 @@ class _CompanionRadioStatsScreenState extends State { void dispose() { _connector?.radioStatsNotifier.removeListener(_onStatsUpdate); _connector?.releaseRadioStatsPolling(); + _connector?.setPollingInterval(30); super.dispose(); } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index d5b01f2..62a380b 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1240,9 +1240,7 @@ class _ContactsScreenState extends State if (isRepeater) ...[ ListTile( leading: const Icon(Icons.radar, color: Colors.green), - title: contact.pathBytesForDisplay.isNotEmpty - ? Text(context.l10n.contacts_pathTrace) - : Text(context.l10n.contacts_ping), + title: Text(context.l10n.contacts_ping), onTap: () { final hw = context .read() @@ -1251,11 +1249,8 @@ class _ContactsScreenState extends State context, MaterialPageRoute( builder: (context) => PathTraceMapScreen( - title: contact.pathBytesForDisplay.isNotEmpty - ? context.l10n.contacts_repeaterPathTrace - : context.l10n.contacts_repeaterPing, - path: contact.pathBytesForDisplay, - flipPathAround: true, + title: context.l10n.contacts_repeaterPing, + path: Uint8List.fromList([contact.publicKey.first]), targetContact: contact, pathHashByteWidth: hw, ), @@ -1274,9 +1269,7 @@ class _ContactsScreenState extends State ] else if (isRoom) ...[ ListTile( leading: const Icon(Icons.radar, color: Colors.green), - title: contact.pathLength > 0 - ? Text(context.l10n.contacts_pathTrace) - : Text(context.l10n.contacts_ping), + title: Text(context.l10n.contacts_pathTrace), onTap: () { final hw = context .read() @@ -1288,7 +1281,9 @@ class _ContactsScreenState extends State title: contact.pathBytesForDisplay.isNotEmpty ? context.l10n.contacts_roomPathTrace : context.l10n.contacts_roomPing, - path: contact.pathBytesForDisplay, + path: contact.pathBytesForDisplay.isNotEmpty + ? contact.pathBytesForDisplay + : Uint8List.fromList([contact.publicKey.first]), flipPathAround: contact.pathBytesForDisplay.isNotEmpty, targetContact: contact, pathHashByteWidth: hw, diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart index 4e7c6e8..3f9d965 100644 --- a/lib/screens/discovery_screen.dart +++ b/lib/screens/discovery_screen.dart @@ -38,6 +38,13 @@ class _DiscoveryScreenState extends State { super.dispose(); } + DateTime _resolveLastSeen(Contact contact) { + if (contact.type != advTypeChat) return contact.lastSeen; + return contact.lastMessageAt.isAfter(contact.lastSeen) + ? contact.lastMessageAt + : contact.lastSeen; + } + @override Widget build(BuildContext context) { final l10n = context.l10n; @@ -108,11 +115,56 @@ class _DiscoveryScreenState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, ), - trailing: Text( - _formatLastSeen(context, contact.lastSeen), - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], + // Clamp text scaling in trailing section to prevent overflow while + // maintaining accessibility. Primary content (title/subtitle) scales normally. + trailing: MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear( + MediaQuery.textScalerOf( + context, + ).scale(1.0).clamp(1.0, 1.3), + ), + ), + child: SizedBox( + width: 120, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + _formatLastSeen( + context, + _resolveLastSeen(contact), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.right, + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (contact.hasLocation) + Icon( + Icons.location_on, + size: 14, + color: Colors.grey[400], + ), + if (contact.rawPacket != null) + const SizedBox(width: 2), + if (contact.rawPacket != null) + Icon( + Icons.cell_tower, + size: 14, + color: Colors.grey[400], + ), + ], + ), + ], + ), ), ), onTap: () { diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 9616d47..f2d09f3 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -64,6 +64,7 @@ class _MapScreenState extends State { bool _hasInitializedMap = false; bool _removedMarkersLoaded = false; final List _pathTrace = []; + final List _pathTraceContacts = []; final List _points = []; final List _polylines = []; bool _legendExpanded = false; @@ -488,7 +489,7 @@ class _MapScreenState extends State { ), ), ), - if (!_isBuildingPathTrace) + if (!settings.mapShowOverlaps) ..._buildGuessedMarker( guessedLocations, showLabels: _showNodeLabels, @@ -788,17 +789,26 @@ class _MapScreenState extends State { final markers = []; for (final guess in guessed) { + if (guess.contact.type == advTypeChat && _isBuildingPathTrace) { + continue; + } + final color = _getNodeColor(guess.contact.type); final marker = Marker( point: guess.position, width: 35, height: 35, child: GestureDetector( - onTap: () => _showNodeInfo( - context, - guess.contact, - guessedPosition: guess.position, - ), + onLongPress: () => _isBuildingPathTrace + ? _showNodeInfo(context, guess.contact) + : null, + onTap: () => _isBuildingPathTrace + ? _addToPath(context, guess.contact, position: guess.position) + : _showNodeInfo( + context, + guess.contact, + guessedPosition: guess.position, + ), child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( @@ -870,23 +880,29 @@ class _MapScreenState extends State { addContact = true; } - final hasOverlap = contacts - .where( - (c) => - c.publicKeyHex != contact.publicKeyHex && - c.publicKey.first == contact.publicKey.first && - (c.type == advTypeRepeater || c.type == advTypeRoom) && - (contact.type == advTypeRepeater || - contact.type == advTypeRoom), - ) - .firstOrNull; - - if (hasOverlap == null && - settings.mapShowOverlaps && - !_isBuildingPathTrace) { + if (contact.type == advTypeChat && _isBuildingPathTrace) { addContact = false; } + if (settings.mapShowOverlaps) { + final hasOverlap = contacts + .where( + (c) => + c.publicKeyHex != contact.publicKeyHex && + c.publicKey.first == contact.publicKey.first && + (c.type == advTypeRepeater || c.type == advTypeRoom) && + (contact.type == advTypeRepeater || + contact.type == advTypeRoom), + ) + .firstOrNull; + + if (hasOverlap == null && + settings.mapShowOverlaps && + !_isBuildingPathTrace) { + addContact = false; + } + } + if (addContact) { filtered.add(contact); } @@ -2121,12 +2137,18 @@ class _MapScreenState extends State { } } - void _addToPath(BuildContext context, Contact contact) { + void _addToPath(BuildContext context, Contact contact, {LatLng? position}) { setState(() { _pathTrace.add( contact.publicKey[0], ); // Add first 16 bytes of public key to path trace - _points.add(LatLng(contact.latitude!, contact.longitude!)); + _pathTraceContacts.add( + contact.copyWith( + latitude: position?.latitude ?? contact.latitude, + longitude: position?.longitude ?? contact.longitude, + ), + ); // Add contact to path trace contacts + _points.add(position ?? LatLng(contact.latitude!, contact.longitude!)); }); } @@ -2134,6 +2156,7 @@ class _MapScreenState extends State { setState(() { _isBuildingPathTrace = true; _pathTrace.clear(); + _pathTraceContacts.clear(); _points.clear(); _polylines.clear(); _points.add(position); @@ -2142,6 +2165,7 @@ class _MapScreenState extends State { void _removePath() { setState(() { + _pathTraceContacts.removeLast(); _pathTrace.removeLast(); // Remove last node from path trace _points.removeLast(); // Remove last point from points list _polylines.clear(); // Clear polylines @@ -2201,6 +2225,7 @@ class _MapScreenState extends State { title: l10n.contacts_pathTrace, path: Uint8List.fromList(_pathTrace), pathHashByteWidth: hashW, + pathContacts: _pathTraceContacts, ), ), ); diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 5b02931..7f3b4eb 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -56,6 +56,7 @@ class PathTraceMapScreen extends StatefulWidget { final bool reversePathAround; final Contact? targetContact; final int pathHashByteWidth; + final List? pathContacts; const PathTraceMapScreen({ super.key, @@ -66,6 +67,7 @@ class PathTraceMapScreen extends StatefulWidget { this.reversePathAround = false, this.targetContact, this.pathHashByteWidth = pathHashSize, + this.pathContacts, }); @override @@ -74,6 +76,8 @@ class PathTraceMapScreen extends StatefulWidget { class _PathTraceMapScreenState extends State { static const double _labelZoomThreshold = 8.5; + //miles to meters conversion for filtering out repeaters that are too far from the last known GPS hop to be a likely match, to avoid false matches that throw off the inferred positions of other hops in the path + static const double _maxRepeaterMatchDistanceMeters = 40 * 1609.344; StreamSubscription? _frameSubscription; Timer? _timeoutTimer; @@ -266,17 +270,43 @@ class _PathTraceMapScreenState extends State { .toList(); Map pathContacts = {}; - final contacts = connector.allContacts; - contacts.where((c) => c.type != advTypeChat).forEach((repeater) { - for (var repeaterData in pathData) { - if (listEquals( - repeater.publicKey.sublist(0, 1), - Uint8List.fromList([repeaterData]), - )) { - pathContacts[repeaterData] = repeater; + Contact lastContact = Contact( + path: Uint8List(0), + pathLength: 0, + publicKey: connector.selfPublicKey ?? Uint8List(0), + name: context.l10n.pathTrace_you, + type: advTypeChat, + latitude: connector.selfLatitude, + longitude: connector.selfLongitude, + lastSeen: DateTime.now(), + ); + if (widget.pathContacts != null) { + pathContacts = {for (var c in widget.pathContacts!) c.publicKey[0]: c}; + } else { + final contacts = connector.allContactsUnfiltered; + contacts.where((c) => c.type != advTypeChat).forEach((repeater) { + if (lastContact.latitude != null && + lastContact.longitude != null && + repeater.hasLocation && + lastContact.hasLocation && + Distance().distance( + LatLng(lastContact.latitude!, lastContact.longitude!), + LatLng(repeater.latitude!, repeater.longitude!), + ) > + _maxRepeaterMatchDistanceMeters) { + return; //skip reapeaters that are far away from the last one with known GPS, to avoid false matches } - } - }); + for (var repeaterData in pathData) { + if (listEquals( + repeater.publicKey.sublist(0, 1), + Uint8List.fromList([repeaterData]), + )) { + pathContacts[repeaterData] = repeater; + lastContact = repeater; + } + } + }); + } // For hops with no GPS contact, infer position from other contacts // with known GPS that share the same last-hop byte. diff --git a/lib/screens/repeater_cli_screen.dart b/lib/screens/repeater_cli_screen.dart index 52d92aa..5f76828 100644 --- a/lib/screens/repeater_cli_screen.dart +++ b/lib/screens/repeater_cli_screen.dart @@ -35,13 +35,15 @@ class _RepeaterCliScreenState extends State { // Common commands for quick access late final List> _quickCommands = [ + {'labelKey': 'advertise', 'command': 'advert'}, {'labelKey': 'getName', 'command': 'get name'}, {'labelKey': 'getRadio', 'command': 'get radio'}, {'labelKey': 'getTx', 'command': 'get tx'}, + {'labelKey': 'discovery', 'command': 'discover.neighbors'}, {'labelKey': 'neighbors', 'command': 'neighbors'}, {'labelKey': 'version', 'command': 'ver'}, - {'labelKey': 'advertise', 'command': 'advert'}, {'labelKey': 'clock', 'command': 'clock'}, + {'labelKey': 'clock sync', 'command': 'clock sync'}, ]; @override @@ -407,6 +409,10 @@ class _RepeaterCliScreenState extends State { return l10n.repeater_cliQuickAdvertise; case 'clock': return l10n.repeater_cliQuickClock; + case 'clock sync': + return l10n.repeater_cliQuickClockSync; + case 'discovery': + return l10n.repeater_cliQuickDiscovery; default: return key; } diff --git a/lib/utils/gpx_export.dart b/lib/utils/gpx_export.dart index b0165bd..296cc3a 100644 --- a/lib/utils/gpx_export.dart +++ b/lib/utils/gpx_export.dart @@ -14,12 +14,13 @@ class ContactExport { final double lon; final String desc; final double? ele; - + final String url; ContactExport({ required this.name, required this.lat, required this.lon, required this.desc, + required this.url, this.ele, }); } @@ -40,6 +41,7 @@ class GpxExport { String name, double lat, double lon, + String url, String desc, [ double? ele, ]) { @@ -50,55 +52,66 @@ class GpxExport { lon: lon, desc: desc.trim(), ele: ele, + url: url, ), ); } void addRepeaters() { - final contacts = _connector.contacts - .where((c) => c.type == advTypeRepeater || c.type == advTypeRoom) - .toList(); + final contacts = _connector.allContacts.where( + (c) => c.type == advTypeRepeater || c.type == advTypeRoom, + ); for (var contact in contacts) { if (contact.latitude == null || contact.longitude == null) { continue; } + final url = contact.rawPacket != null + ? "meshcore://${pubKeyToHex(contact.rawPacket!)}" + : ""; _addContact( contact.name, contact.latitude!, contact.longitude!, "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", + url, ); } } void addContacts() { - final contacts = _connector.contacts - .where((c) => c.type == advTypeChat) - .toList(); + final contacts = _connector.allContacts.where((c) => c.type == advTypeChat); for (var contact in contacts) { if (contact.latitude == null || contact.longitude == null) { continue; } + final url = contact.rawPacket != null + ? "meshcore://${pubKeyToHex(contact.rawPacket!)}" + : ""; _addContact( contact.name, contact.latitude!, contact.longitude!, "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", + url, ); } } void addAll() { - final contacts = _connector.contacts; - for (var contact in contacts.toList()) { + final contacts = _connector.allContacts; + for (var contact in contacts) { if (contact.latitude == null || contact.longitude == null) { continue; } + final url = contact.rawPacket != null + ? "meshcore://${pubKeyToHex(contact.rawPacket!)}" + : ""; _addContact( contact.name, contact.latitude ?? 0.0, contact.longitude ?? 0.0, "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", + url, ); } } @@ -138,6 +151,9 @@ class GpxExport { ele: c.ele, name: c.name, desc: c.desc, + extensions: { + "meshcore": {"url": c.url}, + }, ), ) .toList(); diff --git a/lib/widgets/repeater_login_dialog.dart b/lib/widgets/repeater_login_dialog.dart index ce6c2b7..48bb6ac 100644 --- a/lib/widgets/repeater_login_dialog.dart +++ b/lib/widgets/repeater_login_dialog.dart @@ -113,7 +113,7 @@ class _RepeaterLoginDialogState extends State { messageBytes: responseBytes, ); final timeoutSeconds = (timeoutMs / 1000).ceil(); - final timeout = Duration(milliseconds: timeoutMs); + final timeout = Duration(milliseconds: timeoutMs + 2000); final selectionLabel = selection.useFlood ? 'flood' : '${selection.hopCount} hops'; diff --git a/lib/widgets/room_login_dialog.dart b/lib/widgets/room_login_dialog.dart index 91d2c8c..3a923fe 100644 --- a/lib/widgets/room_login_dialog.dart +++ b/lib/widgets/room_login_dialog.dart @@ -108,7 +108,7 @@ class _RoomLoginDialogState extends State { messageBytes: responseBytes, ); final timeoutSeconds = (timeoutMs / 1000).ceil(); - final timeout = Duration(milliseconds: timeoutMs); + final timeout = Duration(milliseconds: timeoutMs + 2000); final selectionLabel = selection.useFlood ? 'flood' : '${selection.hopCount} hops'; From 36d4a10396f96c35ab4e49a2b8649942481ca52a Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Tue, 31 Mar 2026 18:55:22 -0700 Subject: [PATCH 12/18] Update ML timeout handling and adjust distance threshold for path hops --- lib/connector/meshcore_connector.dart | 4 +++- lib/screens/channel_message_path_screen.dart | 2 +- lib/screens/neighbors_screen.dart | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 228fc39..108ccc7 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -3903,7 +3903,9 @@ class MeshCoreConnector extends ChangeNotifier { if (mlTimeout != null) { if (pathLength < 0) { // Flood: trust ML, only enforce firmware formula as floor - return mlTimeout.clamp(physicsMin, mlTimeout); + if (mlTimeout < physicsMin) { + return physicsMin; + } } return mlTimeout.clamp(physicsMin, physicsMax); } diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 0eb2c22..53769d4 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -879,7 +879,7 @@ List<_PathHop> _buildPathHops( previousPosition = resolvedPosition; } // If the best candidate is much farther than the previous hop, it's likely not the correct match. - if (lastDistance + bestDistance > 70000 && + if (lastDistance + bestDistance > 50000 && candidates != null && candidates.isNotEmpty) { i--; diff --git a/lib/screens/neighbors_screen.dart b/lib/screens/neighbors_screen.dart index f4c1673..7286eb0 100644 --- a/lib/screens/neighbors_screen.dart +++ b/lib/screens/neighbors_screen.dart @@ -142,7 +142,7 @@ class _NeighborsScreenState extends State { void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) { final buffer = BufferReader(frame); - final contacts = connector.allContacts; + final contacts = connector.allContactsUnfiltered; try { final neighborCount = buffer.readUInt16LE(); final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE()); From 457b44de3a92e44593697c683f18d965fcf061b2 Mon Sep 17 00:00:00 2001 From: n-kam <96840503+n-kam@users.noreply.github.com> Date: Fri, 27 Mar 2026 01:39:52 +0300 Subject: [PATCH 13/18] make unread badge max out at 9999+ not 99+ --- lib/widgets/unread_badge.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/unread_badge.dart b/lib/widgets/unread_badge.dart index 37db11a..424cb6f 100644 --- a/lib/widgets/unread_badge.dart +++ b/lib/widgets/unread_badge.dart @@ -7,7 +7,7 @@ class UnreadBadge extends StatelessWidget { @override Widget build(BuildContext context) { - final display = count > 99 ? '99+' : count.toString(); + final display = count > 9999 ? '9999+' : count.toString(); return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( From b570539a2ded0a7c25c447531be6e187c6e1f8dc Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Wed, 8 Apr 2026 08:22:13 -0700 Subject: [PATCH 14/18] add tooltip to send message buttons --- lib/l10n/app_en.arb | 9 ++++ lib/l10n/app_localizations.dart | 18 ++++--- lib/l10n/app_localizations_bg.dart | 13 ++++-- lib/l10n/app_localizations_de.dart | 13 ++++-- lib/l10n/app_localizations_en.dart | 13 ++++-- lib/l10n/app_localizations_es.dart | 13 ++++-- lib/l10n/app_localizations_fr.dart | 13 ++++-- lib/l10n/app_localizations_hu.dart | 13 ++++-- lib/l10n/app_localizations_it.dart | 13 ++++-- lib/l10n/app_localizations_ja.dart | 13 ++++-- lib/l10n/app_localizations_ko.dart | 13 ++++-- lib/l10n/app_localizations_nl.dart | 13 ++++-- lib/l10n/app_localizations_pl.dart | 13 ++++-- lib/l10n/app_localizations_pt.dart | 13 ++++-- lib/l10n/app_localizations_ru.dart | 13 ++++-- lib/l10n/app_localizations_sk.dart | 13 ++++-- lib/l10n/app_localizations_sl.dart | 13 ++++-- lib/l10n/app_localizations_sv.dart | 13 ++++-- lib/l10n/app_localizations_uk.dart | 13 ++++-- lib/l10n/app_localizations_zh.dart | 13 ++++-- lib/screens/channel_chat_screen.dart | 1 + lib/screens/chat_screen.dart | 3 ++ untranslated.json | 70 +++++++++++++++++++++++++++- 23 files changed, 238 insertions(+), 97 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b703630..ffdf21d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -607,6 +607,15 @@ "channels_enterHashtag": "Enter hashtag", "channels_hashtagHint": "e.g. #team", "chat_noMessages": "No messages yet", + "chat_sendMessage": "Send message", + "chat_sendMessageTo": "Send message to {name}", + "@chat_sendMessageTo": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "chat_sendMessageToStart": "Send a message to get started", "chat_originalMessageNotFound": "Original message not found", "chat_replyingTo": "Replying to {name}", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 408a243..bb390d5 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2296,6 +2296,18 @@ abstract class AppLocalizations { /// **'No messages yet'** String get chat_noMessages; + /// No description provided for @chat_sendMessage. + /// + /// In en, this message translates to: + /// **'Send message'** + String get chat_sendMessage; + + /// No description provided for @chat_sendMessageTo. + /// + /// In en, this message translates to: + /// **'Send a message to {contactName}'** + String chat_sendMessageTo(String contactName); + /// No description provided for @chat_sendMessageToStart. /// /// In en, this message translates to: @@ -2326,12 +2338,6 @@ abstract class AppLocalizations { /// **'Location'** String get chat_location; - /// No description provided for @chat_sendMessageTo. - /// - /// In en, this message translates to: - /// **'Send a message to {contactName}'** - String chat_sendMessageTo(String contactName); - /// No description provided for @chat_typeMessage. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 8a43322..bec54df 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1239,6 +1239,14 @@ class AppLocalizationsBg extends AppLocalizations { @override String get chat_noMessages => 'Няма съобщения.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Изпрати съобщение на $contactName'; + } + @override String get chat_sendMessageToStart => 'Изпрати съобщение, за да започнеш.'; @@ -1258,11 +1266,6 @@ class AppLocalizationsBg extends AppLocalizations { @override String get chat_location => 'Местоположение'; - @override - String chat_sendMessageTo(String contactName) { - return 'Изпрати съобщение на $contactName'; - } - @override String get chat_typeMessage => 'Въведете съобщение...'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index d4fd3ad..078c9e9 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1238,6 +1238,14 @@ class AppLocalizationsDe extends AppLocalizations { @override String get chat_noMessages => 'Noch keine Nachrichten.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Sende eine Nachricht an $contactName'; + } + @override String get chat_sendMessageToStart => 'Eine Nachricht senden, um anzufangen.'; @@ -1257,11 +1265,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get chat_location => 'Ort'; - @override - String chat_sendMessageTo(String contactName) { - return 'Sende eine Nachricht an $contactName'; - } - @override String get chat_typeMessage => 'Eine Nachricht eingeben...'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 9104f8b..d7a79bd 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1213,6 +1213,14 @@ class AppLocalizationsEn extends AppLocalizations { @override String get chat_noMessages => 'No messages yet'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Send a message to $contactName'; + } + @override String get chat_sendMessageToStart => 'Send a message to get started'; @@ -1232,11 +1240,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get chat_location => 'Location'; - @override - String chat_sendMessageTo(String contactName) { - return 'Send a message to $contactName'; - } - @override String get chat_typeMessage => 'Type a message...'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index cc3b714..9a56c6d 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1238,6 +1238,14 @@ class AppLocalizationsEs extends AppLocalizations { @override String get chat_noMessages => 'Aún no hay mensajes'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Enviar un mensaje a $contactName'; + } + @override String get chat_sendMessageToStart => 'Enviar un mensaje para comenzar'; @@ -1257,11 +1265,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get chat_location => 'Ubicación'; - @override - String chat_sendMessageTo(String contactName) { - return 'Enviar un mensaje a $contactName'; - } - @override String get chat_typeMessage => 'Escribe un mensaje...'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 402e373..4ce4a75 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1243,6 +1243,14 @@ class AppLocalizationsFr extends AppLocalizations { @override String get chat_noMessages => 'Aucun message pour le moment.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Envoyer un message à $contactName'; + } + @override String get chat_sendMessageToStart => 'Envoyer un message pour commencer'; @@ -1262,11 +1270,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get chat_location => 'Emplacement'; - @override - String chat_sendMessageTo(String contactName) { - return 'Envoyer un message à $contactName'; - } - @override String get chat_typeMessage => 'Saisir un message...'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 204e21b..bbf989e 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -1246,6 +1246,14 @@ class AppLocalizationsHu extends AppLocalizations { @override String get chat_noMessages => 'Még nincs üzenet.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Küldj üzenetet $contactName-nek'; + } + @override String get chat_sendMessageToStart => 'Küldj egy üzenetet, hogy elindulj!'; @@ -1265,11 +1273,6 @@ class AppLocalizationsHu extends AppLocalizations { @override String get chat_location => 'Helyszín'; - @override - String chat_sendMessageTo(String contactName) { - return 'Küldj üzenetet $contactName-nek'; - } - @override String get chat_typeMessage => 'Írjon üzenetet...'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 936ecc1..98cbfcb 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1239,6 +1239,14 @@ class AppLocalizationsIt extends AppLocalizations { @override String get chat_noMessages => 'Nessun messaggio ancora'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Invia un messaggio a $contactName'; + } + @override String get chat_sendMessageToStart => 'Invia un messaggio per iniziare'; @@ -1258,11 +1266,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get chat_location => 'Posizione'; - @override - String chat_sendMessageTo(String contactName) { - return 'Invia un messaggio a $contactName'; - } - @override String get chat_typeMessage => 'Digita un messaggio...'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 13c8444..fc59852 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1179,6 +1179,14 @@ class AppLocalizationsJa extends AppLocalizations { @override String get chat_noMessages => 'まだメッセージは届いていません'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return '$contactName へのメッセージを送信する'; + } + @override String get chat_sendMessageToStart => '開始するためにメッセージを送信してください'; @@ -1198,11 +1206,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get chat_location => '場所'; - @override - String chat_sendMessageTo(String contactName) { - return '$contactName へのメッセージを送信する'; - } - @override String get chat_typeMessage => 'メッセージを入力してください…'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 06d7db6..b0d849b 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1174,6 +1174,14 @@ class AppLocalizationsKo extends AppLocalizations { @override String get chat_noMessages => '아직 메시지가 없습니다.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return '$contactName에게 메시지를 보내'; + } + @override String get chat_sendMessageToStart => '시작하려면 메시지를 보내세요.'; @@ -1193,11 +1201,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get chat_location => '위치'; - @override - String chat_sendMessageTo(String contactName) { - return '$contactName에게 메시지를 보내'; - } - @override String get chat_typeMessage => '메시지를 입력하세요...'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index bbeb25f..6fcad22 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1227,6 +1227,14 @@ class AppLocalizationsNl extends AppLocalizations { @override String get chat_noMessages => 'Nog geen berichten.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Verstuur een bericht naar $contactName'; + } + @override String get chat_sendMessageToStart => 'Een bericht sturen om te beginnen'; @@ -1246,11 +1254,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get chat_location => 'Locatie'; - @override - String chat_sendMessageTo(String contactName) { - return 'Verstuur een bericht naar $contactName'; - } - @override String get chat_typeMessage => 'Type een bericht...'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 9c6d85b..8492702 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1247,6 +1247,14 @@ class AppLocalizationsPl extends AppLocalizations { @override String get chat_noMessages => 'Brak jeszcze wiadomości'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Wyślij wiadomość do $contactName'; + } + @override String get chat_sendMessageToStart => 'Wyślij wiadomość, aby rozpocząć.'; @@ -1267,11 +1275,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get chat_location => 'Lokalizacja'; - @override - String chat_sendMessageTo(String contactName) { - return 'Wyślij wiadomość do $contactName'; - } - @override String get chat_typeMessage => 'Wpisz wiadomość...'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index e5f3848..ce7c9e8 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1238,6 +1238,14 @@ class AppLocalizationsPt extends AppLocalizations { @override String get chat_noMessages => 'Ainda não existem mensagens.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Enviar uma mensagem para $contactName'; + } + @override String get chat_sendMessageToStart => 'Enviar uma mensagem para começar'; @@ -1257,11 +1265,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get chat_location => 'Localização'; - @override - String chat_sendMessageTo(String contactName) { - return 'Enviar uma mensagem para $contactName'; - } - @override String get chat_typeMessage => 'Digite uma mensagem...'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 425b3ff..4557885 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1238,6 +1238,14 @@ class AppLocalizationsRu extends AppLocalizations { @override String get chat_noMessages => 'Сообщений пока нет'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Отправить сообщение $contactName'; + } + @override String get chat_sendMessageToStart => 'Отправьте сообщение, чтобы начать'; @@ -1257,11 +1265,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get chat_location => 'Местоположение'; - @override - String chat_sendMessageTo(String contactName) { - return 'Отправить сообщение $contactName'; - } - @override String get chat_typeMessage => 'Напишите сообщение...'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 8ddea4b..b59d6d8 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1226,6 +1226,14 @@ class AppLocalizationsSk extends AppLocalizations { @override String get chat_noMessages => 'Zatiaľ žiadne správy.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Pošli správu $contactName'; + } + @override String get chat_sendMessageToStart => 'Pošlite správu na začiatok'; @@ -1245,11 +1253,6 @@ class AppLocalizationsSk extends AppLocalizations { @override String get chat_location => 'Lokalita'; - @override - String chat_sendMessageTo(String contactName) { - return 'Pošli správu $contactName'; - } - @override String get chat_typeMessage => 'Napište správu...'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 0a5b540..e5bf031 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1224,6 +1224,14 @@ class AppLocalizationsSl extends AppLocalizations { @override String get chat_noMessages => 'Še ni sporočil.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Pošlji sporočilo $contactName'; + } + @override String get chat_sendMessageToStart => 'Pošlji sporočilo za začetek.'; @@ -1244,11 +1252,6 @@ class AppLocalizationsSl extends AppLocalizations { @override String get chat_location => 'Lokacija'; - @override - String chat_sendMessageTo(String contactName) { - return 'Pošlji sporočilo $contactName'; - } - @override String get chat_typeMessage => 'Vnesi sporočilo...'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index b87111c..731b846 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1217,6 +1217,14 @@ class AppLocalizationsSv extends AppLocalizations { @override String get chat_noMessages => 'Inga meddelanden ännu'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Skicka ett meddelande till $contactName'; + } + @override String get chat_sendMessageToStart => 'Skicka ett meddelande för att komma igång'; @@ -1238,11 +1246,6 @@ class AppLocalizationsSv extends AppLocalizations { @override String get chat_location => 'Plats'; - @override - String chat_sendMessageTo(String contactName) { - return 'Skicka ett meddelande till $contactName'; - } - @override String get chat_typeMessage => 'Skriv ett meddelande...'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 608f436..1b5a800 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1230,6 +1230,14 @@ class AppLocalizationsUk extends AppLocalizations { @override String get chat_noMessages => 'Поки немає повідомлень.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Надіслати повідомлення $contactName'; + } + @override String get chat_sendMessageToStart => 'Надішліть повідомлення, щоб почати'; @@ -1250,11 +1258,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get chat_location => 'Розташування'; - @override - String chat_sendMessageTo(String contactName) { - return 'Надіслати повідомлення $contactName'; - } - @override String get chat_typeMessage => 'Введіть повідомлення...'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index f9ff709..acadc58 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1161,6 +1161,14 @@ class AppLocalizationsZh extends AppLocalizations { @override String get chat_noMessages => '暂无消息'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return '发送消息给 $contactName'; + } + @override String get chat_sendMessageToStart => '发送消息开始对话'; @@ -1180,11 +1188,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get chat_location => '位置'; - @override - String chat_sendMessageTo(String contactName) { - return '发送消息给 $contactName'; - } - @override String get chat_typeMessage => '输入消息...'; diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 628ae1c..1ca0ee9 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -1126,6 +1126,7 @@ class _ChannelChatScreenState extends State { const SizedBox(width: 8), IconButton( icon: const Icon(Icons.send), + tooltip: context.l10n.chat_sendMessage, onPressed: _sendMessage, color: Theme.of(context).colorScheme.primary, ), diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 082374f..b0f0c0c 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -590,6 +590,9 @@ class _ChatScreenState extends State { const SizedBox(width: 8), IconButton.filled( icon: const Icon(Icons.send), + tooltip: context.l10n.chat_sendMessageTo( + _resolveContact(connector).name, + ), onPressed: () => _sendMessage(connector), ), ], diff --git a/untranslated.json b/untranslated.json index 9e26dfe..1ebd9bc 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1 +1,69 @@ -{} \ No newline at end of file +{ + "bg": [ + "chat_sendMessage" + ], + + "de": [ + "chat_sendMessage" + ], + + "es": [ + "chat_sendMessage" + ], + + "fr": [ + "chat_sendMessage" + ], + + "hu": [ + "chat_sendMessage" + ], + + "it": [ + "chat_sendMessage" + ], + + "ja": [ + "chat_sendMessage" + ], + + "ko": [ + "chat_sendMessage" + ], + + "nl": [ + "chat_sendMessage" + ], + + "pl": [ + "chat_sendMessage" + ], + + "pt": [ + "chat_sendMessage" + ], + + "ru": [ + "chat_sendMessage" + ], + + "sk": [ + "chat_sendMessage" + ], + + "sl": [ + "chat_sendMessage" + ], + + "sv": [ + "chat_sendMessage" + ], + + "uk": [ + "chat_sendMessage" + ], + + "zh": [ + "chat_sendMessage" + ] +} From 5fe6738f25e605128239b1b8716270d59ed701be Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Wed, 8 Apr 2026 08:31:37 -0700 Subject: [PATCH 15/18] add fvm directory and rc file to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 779856c..88295e7 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,9 @@ migrate_working_dir/ pubspec.lock /build/ /coverage/ +# fvm project files +.fvm/ +.fvmrc # Symbolication related app.*.symbols From 9e46f8b44c2dd67f77b9de60a82109639601fe34 Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Wed, 8 Apr 2026 08:37:50 -0700 Subject: [PATCH 16/18] add jni to generated plugins linux and windows were missing jni which was being added on fresh builds from dev --- linux/flutter/generated_plugins.cmake | 1 + windows/flutter/generated_plugins.cmake | 1 + 2 files changed, 2 insertions(+) diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 379e36f..93e4682 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flserial + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index f02857f..533a171 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flserial flutter_local_notifications_windows + jni ) set(PLUGIN_BUNDLED_LIBRARIES) From e4cfbb57b435a956c8e23bec8eaf406ac738dd36 Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Wed, 8 Apr 2026 10:01:45 -0700 Subject: [PATCH 17/18] use l10n strings for discovered menu item --- lib/screens/contacts_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 62a380b..46e2be6 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -394,7 +394,7 @@ class _ContactsScreenState extends State children: [ const Icon(Icons.person_add_rounded), const SizedBox(width: 8), - Text("Discovered Contacts"), + Text(context.l10n.discoveredContacts_Title), ], ), onTap: () => Navigator.push( From 9d20be1c062368f31026ce95a4ae13dd24c88525 Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Wed, 8 Apr 2026 10:23:57 -0700 Subject: [PATCH 18/18] small clean up from PR #275 just removes extraneous assignment to _lastNonRepeatSnapshot and moves the Navigator pop to after all uses of the context in _RadioSettingsDialog --- lib/screens/settings_screen.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index e7d61ee..e9b73f8 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1162,7 +1162,6 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { _clientRepeat = widget.connector.clientRepeat ?? false; _selectedPresetIndex = _findMatchingPresetIndex(); - _lastNonRepeatSnapshot = _currentSnapshot(); if (_clientRepeat) { _lastNonRepeatSnapshot = _sessionRememberedNonRepeatSnapshot() ?? @@ -1472,7 +1471,6 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { } if (!mounted) return; - Navigator.pop(context); _logRadioSettingsState('Radio settings saved successfully'); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l10n.settings_radioSettingsUpdated)), @@ -1484,6 +1482,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { SnackBar(content: Text(l10n.settings_error(e.toString()))), ); } + Navigator.pop(context); } String _presetLabel(int? index) {