From 3664ae34cd32e9cf89b6474f277db33420c7132e Mon Sep 17 00:00:00 2001 From: ericz Date: Sun, 15 Mar 2026 11:42:46 +0100 Subject: [PATCH 01/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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) { From bdd7fc0cdd32c449cefb62536855e26887a8a4b9 Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Wed, 8 Apr 2026 14:56:34 -0700 Subject: [PATCH 19/41] remove unused macos path_provider_foundation added in #299 but appears not needed, flutter removes when building --- macos/Flutter/GeneratedPluginRegistrant.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 2428a77..ffc8c59 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,7 +9,6 @@ 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,7 +19,6 @@ 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 4879b136f8ba56245dcf655e28bc8a3f9fdf89b3 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Thu, 26 Mar 2026 22:28:01 -0700 Subject: [PATCH 20/41] 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 a8934f1..a436b46 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 8057f1f..4cda712 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -294,6 +294,7 @@ class _ChatScreenState extends State { tooltip: context.l10n.chat_pathManagement, onPressed: () => _showPathHistory(context), ), + const RadioStatsIconButton(), Consumer( builder: (context, connector, _) { return PopupMenuButton( @@ -366,7 +367,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 26516baf67becc1047c78707922101ec0e728712 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Tue, 31 Mar 2026 18:55:22 -0700 Subject: [PATCH 21/41] 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 a436b46..5f0ccdb 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 b5aa294fc196d133679b2073195db65080cced21 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 22/41] 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 32dc0fca22fbeeeee0956672e76524e655ae3294 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Thu, 26 Mar 2026 22:28:01 -0700 Subject: [PATCH 23/41] 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/l10n/app_es.arb | 3 +-- lib/l10n/app_hu.arb | 2 +- lib/l10n/app_ja.arb | 2 +- lib/l10n/app_ko.arb | 3 ++- lib/l10n/app_localizations_nl.dart | 6 ++++++ lib/l10n/app_sk.arb | 3 +-- lib/l10n/app_zh.arb | 2 +- lib/screens/channel_message_path_screen.dart | 2 +- 8 files changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 0372dff..5d98e4e 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -2088,7 +2088,6 @@ "translation_translateTo": "Traducir a {language}", "translation_translationOptions": "Opciones de traducción", "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_hu.arb b/lib/l10n/app_hu.arb index 68b3b11..2a1e717 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -2101,4 +2101,4 @@ "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_ja.arb b/lib/l10n/app_ja.arb index aef8fc0..e11adfe 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -2104,4 +2104,4 @@ "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 66ad1ed..06dc20c 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -2082,6 +2082,7 @@ }, "scanner_linuxPairingPinTitle": "블루투스 페어링 PIN", "scanner_linuxPairingHidePin": "PIN 숨기기", +<<<<<<< HEAD "scanner_linuxPairingShowPin": "PIN 보기", "scanner_linuxPairingPinPrompt": "{deviceName}의 PIN을 입력하세요 (해당하는 경우에만 입력).", "@translation_translateTo": { @@ -2101,4 +2102,4 @@ "scanner_linuxPairingPinPrompt": "{deviceName}에 대한 PIN을 입력하세요 (없으면 비워두세요).", "repeater_cliQuickClockSync": "시계 동기화", "repeater_cliQuickDiscovery": "이웃 발견하기" -} \ No newline at end of file +} diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 6b7bbe7..9ec0118 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2415,6 +2415,12 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_cliQuickDiscovery => 'Ontdek Buren'; + @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_sk.arb b/lib/l10n/app_sk.arb index 43e408f..50d42d2 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -2060,7 +2060,6 @@ "translation_translateTo": "Preložte do {language}", "translation_translationOptions": "Možnosti prekladania", "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_zh.arb b/lib/l10n/app_zh.arb index 2e19a8e..5dd5896 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -2068,4 +2068,4 @@ "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 53769d4..0eb2c22 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 > 50000 && + if (lastDistance + bestDistance > 70000 && candidates != null && candidates.isNotEmpty) { i--; From 637e08d22c6ccc21d0be1a02832df38434e64768 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Tue, 31 Mar 2026 18:55:22 -0700 Subject: [PATCH 24/41] Update ML timeout handling and adjust distance threshold for path hops --- lib/screens/channel_message_path_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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--; From c4f54efd77c4782a52fe63bff3fbefbc24c8ac51 Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Wed, 8 Apr 2026 08:22:13 -0700 Subject: [PATCH 25/41] 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 1177bc1..cc6d6ed 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 7accee3..40845e9 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 9ec0118..ae066a9 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 b6296a4..ed66e52 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 d1f66af..1aebdcf 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 cb2ae15..d8f38fe 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 07c1c01..c204c50 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 8745774..6b9ffb5 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 fc0abea..f6745c3 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 7beaaf4..64da058 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -1121,6 +1121,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 4cda712..a4ebc76 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -591,6 +591,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 754f8a6c621c59ce7af1315983a595767652670d Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Wed, 8 Apr 2026 08:31:37 -0700 Subject: [PATCH 26/41] 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 45cd8a56a3dcc89ac1b25708c60036d8fbbf628a Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Wed, 8 Apr 2026 08:37:50 -0700 Subject: [PATCH 27/41] 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 8386f262e18b61322ca093f9aea0464b854c0176 Mon Sep 17 00:00:00 2001 From: ericz Date: Sun, 15 Mar 2026 11:42:46 +0100 Subject: [PATCH 28/41] 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 ffc8c59..2428a77 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 @@ -19,6 +20,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 e4684b585a6870ee1db1cb5eb150ff2b63341b26 Mon Sep 17 00:00:00 2001 From: ericszimmermann Date: Sun, 15 Mar 2026 12:10:47 +0100 Subject: [PATCH 29/41] 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 7dcec5b4eed250bfe1915324240fd7e27de72a05 Mon Sep 17 00:00:00 2001 From: ericz Date: Sat, 28 Mar 2026 17:08:59 +0100 Subject: [PATCH 30/41] 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 f29960829662a28af0c06438ba173cf72b53d6eb Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Wed, 8 Apr 2026 10:01:45 -0700 Subject: [PATCH 31/41] 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 82e04e80908917fed70dd65bf7382c0143f149bd Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Mon, 9 Mar 2026 18:29:17 -0400 Subject: [PATCH 32/41] 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 c7b7deb0f6f7e1842b178a945ef441f7ec4928e3 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Wed, 11 Mar 2026 11:18:35 -0400 Subject: [PATCH 33/41] 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 5f0ccdb..b432277 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -104,6 +104,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; @@ -169,6 +185,7 @@ class MeshCoreConnector extends ChangeNotifier { int? _currentSf; int? _currentCr; bool? _clientRepeat; + MeshCoreRadioStateSnapshot? _rememberedNonRepeatRadioState; int? _firmwareVerCode; int _pathHashByteWidth = 1; CompanionRadioStats? _latestRadioStats; @@ -369,6 +386,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; @@ -380,6 +399,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; @@ -2278,6 +2301,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 20a993931465968d3e88050ba3af33d42368eccb Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Wed, 11 Mar 2026 11:19:53 -0400 Subject: [PATCH 34/41] 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 ea3b9609fc3c0cdcb114b2d3bbd963ce3d6993cc Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Sun, 15 Mar 2026 15:50:35 -0400 Subject: [PATCH 35/41] 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 69433b6d896a31d39e48dea09f9475353c81134a Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Wed, 8 Apr 2026 10:23:57 -0700 Subject: [PATCH 36/41] 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) { From 5354acb1d3af21606c539871f8b750898be1605d Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Thu, 9 Apr 2026 09:57:46 -0700 Subject: [PATCH 37/41] clean up after merge conflicts --- lib/helpers/gif_helper.dart | 2 +- lib/helpers/reaction_helper.dart | 2 +- lib/l10n/app_localizations_hu.dart | 2 +- lib/l10n/app_localizations_nl.dart | 2 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 -- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/helpers/gif_helper.dart b/lib/helpers/gif_helper.dart index 8dd187b..5b68e90 100644 --- a/lib/helpers/gif_helper.dart +++ b/lib/helpers/gif_helper.dart @@ -30,7 +30,7 @@ class GifHelper { ).firstMatch(trimmed); return pageMatch?.group(1); } - + /// Encode a GIF in a format that parseGif() can parse. static String encodeGif(String gifId) { return 'g:$gifId'; diff --git a/lib/helpers/reaction_helper.dart b/lib/helpers/reaction_helper.dart index 169b1a1..36118ca 100644 --- a/lib/helpers/reaction_helper.dart +++ b/lib/helpers/reaction_helper.dart @@ -109,7 +109,7 @@ class ReactionHelper { return ReactionInfo(targetHash: match.group(1)!, emoji: emoji); } - + /// Encode a reaction message that parseReaction() can parse. static String encodeReaction(String hash, String emojiIndex) { return 'r:$hash:$emojiIndex'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index bbf989e..5d305ee 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -3677,7 +3677,7 @@ class AppLocalizationsHu extends AppLocalizations { @override String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Adja meg a PIN kódot a $deviceName számára (hagyja üresen, ha nincs).'; + return 'Adja meg a(z) $deviceName PIN-kódját (hagyja üresen, ha nincs).'; } @override diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 37303a0..6fcad22 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2419,7 +2419,7 @@ class AppLocalizationsNl extends AppLocalizations { String get repeater_cliQuickDiscovery => 'Ontdek Buren'; @override - String get repeater_cliHelpAdvert => 'Verstuurt een advertentiepakket'; + String get repeater_cliHelpAdvert => 'Advertentie uitzenden'; @override String get repeater_cliHelpReboot => diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 2428a77..ffc8c59 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,7 +9,6 @@ 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,7 +19,6 @@ 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 8ba4bbfbc5d3d1a8aab94c4da8f730d24ab5cbe6 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Fri, 10 Apr 2026 14:25:53 -0400 Subject: [PATCH 38/41] add auto clock synchronization setting after repeater login Introduced a new setting for automatic clock synchronization after a successful repeater login. Added localization support for the new feature in multiple languages (Bulgarian, German, English, Spanish, French, Hungarian, Italian, Japanese, Korean, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, Chinese). Implemented storage service methods to manage the new setting. Updated the repeater settings screen to include a toggle for the new feature. Enhanced the repeater login dialog to trigger clock synchronization automatically if the setting is enabled. --- lib/l10n/app_bg.arb | 10 ++++++- lib/l10n/app_de.arb | 10 ++++++- lib/l10n/app_en.arb | 8 ++++++ lib/l10n/app_es.arb | 10 ++++++- lib/l10n/app_fr.arb | 10 ++++++- lib/l10n/app_hu.arb | 13 +++++++-- lib/l10n/app_it.arb | 10 ++++++- lib/l10n/app_ja.arb | 10 ++++++- lib/l10n/app_ko.arb | 10 ++++++- lib/l10n/app_localizations.dart | 12 ++++++++ lib/l10n/app_localizations_bg.dart | 8 ++++++ lib/l10n/app_localizations_de.dart | 8 ++++++ lib/l10n/app_localizations_en.dart | 7 +++++ lib/l10n/app_localizations_es.dart | 8 ++++++ lib/l10n/app_localizations_fr.dart | 8 ++++++ lib/l10n/app_localizations_hu.dart | 8 ++++++ lib/l10n/app_localizations_it.dart | 8 ++++++ lib/l10n/app_localizations_ja.dart | 7 +++++ lib/l10n/app_localizations_ko.dart | 7 +++++ lib/l10n/app_localizations_nl.dart | 8 ++++++ lib/l10n/app_localizations_pl.dart | 8 ++++++ lib/l10n/app_localizations_pt.dart | 8 ++++++ lib/l10n/app_localizations_ru.dart | 8 ++++++ lib/l10n/app_localizations_sk.dart | 8 ++++++ lib/l10n/app_localizations_sl.dart | 7 +++++ lib/l10n/app_localizations_sv.dart | 8 ++++++ lib/l10n/app_localizations_uk.dart | 7 +++++ lib/l10n/app_localizations_zh.dart | 6 ++++ lib/l10n/app_nl.arb | 10 ++++++- lib/l10n/app_pl.arb | 12 ++++++-- lib/l10n/app_pt.arb | 10 ++++++- lib/l10n/app_ru.arb | 12 ++++++-- lib/l10n/app_sk.arb | 10 ++++++- lib/l10n/app_sl.arb | 10 ++++++- lib/l10n/app_sv.arb | 12 ++++++-- lib/l10n/app_uk.arb | 12 ++++++-- lib/l10n/app_zh.arb | 10 ++++++- lib/screens/repeater_settings_screen.dart | 28 +++++++++++++++++++ lib/services/storage_service.dart | 34 +++++++++++++++++++++++ lib/widgets/repeater_login_dialog.dart | 23 +++++++++++++++ 40 files changed, 400 insertions(+), 23 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 567ed24..718bfd9 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -2061,5 +2061,13 @@ "scanner_linuxPairingHidePin": "Скриване на PIN кода", "scanner_linuxPairingShowPin": "Покажи PIN", "repeater_cliQuickClockSync": "Синхронизация на часовника", - "repeater_cliQuickDiscovery": "Открий Съседи" + "repeater_cliQuickDiscovery": "Открий Съседи", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLoginSubtitle": "Автоматично изпращайте съобщение \"синхронизиране на часовника\" след успешно влизане.", + "repeater_clockSyncAfterLogin": "Синхронизиране на часовника след влизане" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 2503372..54683d2 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -2089,5 +2089,13 @@ "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" + "repeater_cliQuickDiscovery": "Entdecke Nachbarn", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLogin": "Uhrzeit-Synchronisation nach dem Anmelden", + "repeater_clockSyncAfterLoginSubtitle": "Automatisch \"Uhrzeit-Synchronisierung\" nach erfolgreicher Anmeldung senden." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ffdf21d..1ac2357 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1116,6 +1116,14 @@ "repeater_neighborsSubtitle": "View zero hop neighbors.", "repeater_settings": "Settings", "repeater_settingsSubtitle": "Configure repeater parameters", + "repeater_clockSyncAfterLogin": "Clock sync after login", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "repeater_clockSyncAfterLoginSubtitle": "Automatically send \"clock sync\" after a successful login", + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, "repeater_statusTitle": "Repeater Status", "repeater_routingMode": "Routing mode", "repeater_autoUseSavedPath": "Auto (use saved path)", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 5d98e4e..6f95d81 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -2089,5 +2089,13 @@ "translation_translationOptions": "Opciones de traducción", "translation_systemLanguage": "Idioma del sistema", "repeater_cliQuickDiscovery": "Descubrir Vecinos", - "repeater_cliQuickClockSync": "Sincronización del reloj" + "repeater_cliQuickClockSync": "Sincronización del reloj", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLoginSubtitle": "Enviar automáticamente la función de \"sincronización de reloj\" después de un inicio de sesión exitoso.", + "repeater_clockSyncAfterLogin": "Sincronización del reloj después de iniciar sesión" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 00b861e..4b0497b 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -2061,5 +2061,13 @@ "scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si nécessaire).", "scanner_linuxPairingShowPin": "Afficher le code PIN", "repeater_cliQuickClockSync": "Synchronisation de l'horloge", - "repeater_cliQuickDiscovery": "Découvrir les voisins" + "repeater_cliQuickDiscovery": "Découvrir les voisins", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLoginSubtitle": "Envoyer automatiquement une notification \"synchronisation de l'heure\" après une connexion réussie.", + "repeater_clockSyncAfterLogin": "Synchronisation de l'horloge après la connexion" } diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index e7671a4..3553b18 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -2081,7 +2081,7 @@ } }, "scanner_linuxPairingShowPin": "Megjelenítse a PIN-kódot", - "scanner_linuxPairingPinPrompt": "Adja meg a PIN kódot a {deviceName} számára (hagyja üresen, ha nincs).", + "scanner_linuxPairingPinPrompt": "Adja meg a(z) {deviceName} PIN-kódját (hagyja üresen, ha nincs).", "scanner_linuxPairingHidePin": "Rejtse el a PIN-kódot", "scanner_linuxPairingPinTitle": "Bluetooth párosítási PIN", "@translation_translateTo": { @@ -2098,7 +2098,14 @@ "translation_translateTo": "Fordítás {language}-ra", "translation_translationOptions": "Fordítási lehetőségek", "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" + "repeater_cliQuickDiscovery": "Fedezd fel a szomszédokat", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLoginSubtitle": "Automatikusan küldje el a \"óra szinkronizálás\" üzenetet a sikeres bejelentkezés után.", + "repeater_clockSyncAfterLogin": "Óra szinkronizálás bejelentkezés után" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 6ebe4f9..d0e195e 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -2061,5 +2061,13 @@ "scanner_linuxPairingPinTitle": "PIN per l'accoppiamento Bluetooth", "scanner_linuxPairingHidePin": "Nascondi il PIN", "repeater_cliQuickClockSync": "Sincronizzazione dell'orologio", - "repeater_cliQuickDiscovery": "Scopri i Vicini" + "repeater_cliQuickDiscovery": "Scopri i Vicini", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLoginSubtitle": "Invia automaticamente il comando \"sincronizzazione dell'orologio\" dopo un login riuscito.", + "repeater_clockSyncAfterLogin": "Sincronizzazione dell'orologio dopo il login" } diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index bc38720..6f85116 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -2099,5 +2099,13 @@ "scanner_linuxPairingPinTitle": "Bluetooth ペアリング PIN", "scanner_linuxPairingPinPrompt": "{deviceName}のPINを入力してください(なしの場合は空欄のまま)。", "repeater_cliQuickClockSync": "クロック同期", - "repeater_cliQuickDiscovery": "近隣を発見する" + "repeater_cliQuickDiscovery": "近隣を発見する", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLogin": "ログイン後、時計の時刻を同期する", + "repeater_clockSyncAfterLoginSubtitle": "ログインが成功した場合、自動的に「時刻同期」を送信する。" } diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index f63badc..bd73847 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -2099,5 +2099,13 @@ "translation_translationOptions": "번역 옵션", "translation_systemLanguage": "시스템 언어", "repeater_cliQuickClockSync": "시계 동기화", - "repeater_cliQuickDiscovery": "이웃 발견하기" + "repeater_cliQuickDiscovery": "이웃 발견하기", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLogin": "로그인 후 시계 동기화", + "repeater_clockSyncAfterLoginSubtitle": "성공적인 로그인 후, 자동으로 \"시간 동기화\"를 전송합니다." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index bb390d5..efcbd0f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3675,6 +3675,18 @@ abstract class AppLocalizations { /// **'Configure repeater parameters'** String get repeater_settingsSubtitle; + /// Repeater setting: auto sync device clock after successful login + /// + /// In en, this message translates to: + /// **'Clock sync after login'** + String get repeater_clockSyncAfterLogin; + + /// Repeater setting subtitle: describes the clock sync after login behavior + /// + /// In en, this message translates to: + /// **'Automatically send \"clock sync\" after a successful login'** + String get repeater_clockSyncAfterLoginSubtitle; + /// No description provided for @repeater_statusTitle. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index bec54df..bb07229 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2056,6 +2056,14 @@ class AppLocalizationsBg extends AppLocalizations { String get repeater_settingsSubtitle => 'Конфигурирайте параметрите на репитера'; + @override + String get repeater_clockSyncAfterLogin => + 'Синхронизиране на часовника след влизане'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Автоматично изпращайте съобщение \"синхронизиране на часовника\" след успешно влизане.'; + @override String get repeater_statusTitle => 'Статус на повтарянето'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 078c9e9..49cf19a 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2052,6 +2052,14 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Repeater-parameter konfigurieren'; + @override + String get repeater_clockSyncAfterLogin => + 'Uhrzeit-Synchronisation nach dem Anmelden'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Automatisch \"Uhrzeit-Synchronisierung\" nach erfolgreicher Anmeldung senden.'; + @override String get repeater_statusTitle => 'Repeaterstatus'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index d7a79bd..e13934b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2014,6 +2014,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Configure repeater parameters'; + @override + String get repeater_clockSyncAfterLogin => 'Clock sync after login'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Automatically send \"clock sync\" after a successful login'; + @override String get repeater_statusTitle => 'Repeater Status'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 9a56c6d..ddb9b6e 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2050,6 +2050,14 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Configurar parámetros del repetidor'; + @override + String get repeater_clockSyncAfterLogin => + 'Sincronización del reloj después de iniciar sesión'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Enviar automáticamente la función de \"sincronización de reloj\" después de un inicio de sesión exitoso.'; + @override String get repeater_statusTitle => 'Estado del Repetidor'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 4ce4a75..fbe106d 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2062,6 +2062,14 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_settingsSubtitle => 'Configurer les paramètres du répéteur'; + @override + String get repeater_clockSyncAfterLogin => + 'Synchronisation de l\'horloge après la connexion'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Envoyer automatiquement une notification \"synchronisation de l\'heure\" après une connexion réussie.'; + @override String get repeater_statusTitle => 'État du répéteur'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 5d305ee..920efd8 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -2066,6 +2066,14 @@ class AppLocalizationsHu extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Állítsa be a repeater paramétereket'; + @override + String get repeater_clockSyncAfterLogin => + 'Óra szinkronizálás bejelentkezés után'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Automatikusan küldje el a \"óra szinkronizálás\" üzenetet a sikeres bejelentkezés után.'; + @override String get repeater_statusTitle => 'Adatkapcsolódás állapot'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 98cbfcb..b492d6a 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2053,6 +2053,14 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_settingsSubtitle => 'Configura i parametri del ripetitore'; + @override + String get repeater_clockSyncAfterLogin => + 'Sincronizzazione dell\'orologio dopo il login'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Invia automaticamente il comando \"sincronizzazione dell\'orologio\" dopo un login riuscito.'; + @override String get repeater_statusTitle => 'Stato del Ripetitore'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index fc59852..daebcba 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1965,6 +1965,13 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_settingsSubtitle => 'リピーターのパラメータを設定する'; + @override + String get repeater_clockSyncAfterLogin => 'ログイン後、時計の時刻を同期する'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'ログインが成功した場合、自動的に「時刻同期」を送信する。'; + @override String get repeater_statusTitle => '再送ステータス'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index b0d849b..605cc96 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1962,6 +1962,13 @@ class AppLocalizationsKo extends AppLocalizations { @override String get repeater_settingsSubtitle => '리피터 파라미터 설정'; + @override + String get repeater_clockSyncAfterLogin => '로그인 후 시계 동기화'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + '성공적인 로그인 후, 자동으로 \"시간 동기화\"를 전송합니다.'; + @override String get repeater_statusTitle => '반복 장치 상태'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 6fcad22..3d1644f 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2038,6 +2038,14 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Configureer repeaterparameters'; + @override + String get repeater_clockSyncAfterLogin => + 'Na het inloggen, klok synchroniseren'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Automatisch een \"klok synchroniseren\" bericht versturen na een succesvolle inlog.'; + @override String get repeater_statusTitle => 'Status repeater'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 8492702..f0006b1 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2066,6 +2066,14 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Skonfiguruj parametry przekaźnika'; + @override + String get repeater_clockSyncAfterLogin => + 'Synchronizacja zegara po zalogowaniu'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Automatycznie wysyłaj powiadomienie \"synchronizacja zegara\" po pomyślnym zalogowaniu.'; + @override String get repeater_statusTitle => 'Status przekaźnika'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index ce7c9e8..f4b7ffc 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2050,6 +2050,14 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Configurar parâmetros do repetidor'; + @override + String get repeater_clockSyncAfterLogin => + 'Sincronização do relógio após o login'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Enviar automaticamente a sincronização do \"relógio\" após um login bem-sucedido.'; + @override String get repeater_statusTitle => 'Status do Repetidor'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 4557885..dcd9d9c 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2054,6 +2054,14 @@ class AppLocalizationsRu extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Настройка параметров репитера'; + @override + String get repeater_clockSyncAfterLogin => + 'Синхронизация часов после входа в систему'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Автоматически отправлять сообщение \"синхронизация времени\" после успешной авторизации.'; + @override String get repeater_statusTitle => 'Статус репитера'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index b59d6d8..7323ddc 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2039,6 +2039,14 @@ class AppLocalizationsSk extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Konfigurujte parametre opakovača'; + @override + String get repeater_clockSyncAfterLogin => + 'Synchronizácia hodiniek po prihlávení'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Automaticky posielajte notifikáciu \"synchronizácia času\" po úspešnom prihládení.'; + @override String get repeater_statusTitle => 'Status opakého zboru'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index e5bf031..a374d4b 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2038,6 +2038,13 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_settingsSubtitle => 'Konfigurirajte parametre ponovitelja'; + @override + String get repeater_clockSyncAfterLogin => 'Sinhronizacija ure po prijavi'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Samodejno po uspešnem vstopu pošljite obvestilo o sinhronizaciji časa.'; + @override String get repeater_statusTitle => 'Status ponovitelja'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 731b846..6e2f563 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2025,6 +2025,14 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Konfigurera återspolarparametrar'; + @override + String get repeater_clockSyncAfterLogin => + 'Synkronisera klockan efter inloggning'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Automatiskt skicka \"klocksynkronisering\" efter en lyckad inloggning.'; + @override String get repeater_statusTitle => 'Återspelsstatus'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 1b5a800..dd189eb 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2050,6 +2050,13 @@ class AppLocalizationsUk extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Налаштувати параметри ретранслятора'; + @override + String get repeater_clockSyncAfterLogin => 'Синхронізація годин після входу'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Автоматично надсилати повідомлення \"синхронізація годин\" після успішного входу.'; + @override String get repeater_statusTitle => 'Статус ретранслятора'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index acadc58..b48b31d 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1923,6 +1923,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get repeater_settingsSubtitle => '配置转发节点参数'; + @override + String get repeater_clockSyncAfterLogin => '登录后,自动同步时钟'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => '在成功登录后,自动发送“时钟同步”指令。'; + @override String get repeater_statusTitle => '转发节点状态'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 490c269..abfd5e7 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -2061,5 +2061,13 @@ "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" + "repeater_cliQuickClockSync": "Kloksynchronisatie", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLoginSubtitle": "Automatisch een \"klok synchroniseren\" bericht versturen na een succesvolle inlog.", + "repeater_clockSyncAfterLogin": "Na het inloggen, klok synchroniseren" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 928bd0d..e626708 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -2099,5 +2099,13 @@ "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 + "repeater_cliQuickDiscovery": "Odkryj Sąsiadów", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLogin": "Synchronizacja zegara po zalogowaniu", + "repeater_clockSyncAfterLoginSubtitle": "Automatycznie wysyłaj powiadomienie \"synchronizacja zegara\" po pomyślnym zalogowaniu." +} diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 8693a28..bacc1ca 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -2061,5 +2061,13 @@ "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" + "repeater_cliQuickDiscovery": "Descobrir Vizinhos", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLoginSubtitle": "Enviar automaticamente a sincronização do \"relógio\" após um login bem-sucedido.", + "repeater_clockSyncAfterLogin": "Sincronização do relógio após o login" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index bdb9bee..e4dad42 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1301,5 +1301,13 @@ "scanner_linuxPairingHidePin": "Скрыть PIN", "scanner_linuxPairingPinTitle": "PIN‑код сопряжения Bluetooth", "repeater_cliQuickDiscovery": "Обнаружить Соседей", - "repeater_cliQuickClockSync": "Синхронизация часов" -} \ No newline at end of file + "repeater_cliQuickClockSync": "Синхронизация часов", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLogin": "Синхронизация часов после входа в систему", + "repeater_clockSyncAfterLoginSubtitle": "Автоматически отправлять сообщение \"синхронизация времени\" после успешной авторизации." +} diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 50d42d2..937bacb 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -2061,5 +2061,13 @@ "translation_translationOptions": "Možnosti prekladania", "translation_systemLanguage": "Jazyk systému", "repeater_cliQuickClockSync": "Synchronizácia hodin", - "repeater_cliQuickDiscovery": "Objaviť susedov" + "repeater_cliQuickDiscovery": "Objaviť susedov", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLogin": "Synchronizácia hodiniek po prihlávení", + "repeater_clockSyncAfterLoginSubtitle": "Automaticky posielajte notifikáciu \"synchronizácia času\" po úspešnom prihládení." } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 6fc76a8..58d0f9e 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -2061,5 +2061,13 @@ "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" + "repeater_cliQuickClockSync": "Usklajevanje ure", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLoginSubtitle": "Samodejno po uspešnem vstopu pošljite obvestilo o sinhronizaciji časa.", + "repeater_clockSyncAfterLogin": "Sinhronizacija ure po prijavi" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index eb7de6c..59b27a4 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -2061,5 +2061,13 @@ "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 + "repeater_cliQuickClockSync": "Synkronisera klocka", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLoginSubtitle": "Automatiskt skicka \"klocksynkronisering\" efter en lyckad inloggning.", + "repeater_clockSyncAfterLogin": "Synkronisera klockan efter inloggning" +} diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 3fac32b..c19f3bd 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -2061,5 +2061,13 @@ "scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).", "scanner_linuxPairingHidePin": "Приховати PIN", "repeater_cliQuickClockSync": "Синхронізація годинника", - "repeater_cliQuickDiscovery": "Відкрити сусідів" -} \ No newline at end of file + "repeater_cliQuickDiscovery": "Відкрити сусідів", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLoginSubtitle": "Автоматично надсилати повідомлення \"синхронізація годин\" після успішного входу.", + "repeater_clockSyncAfterLogin": "Синхронізація годин після входу" +} diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index f054f79..3fbfc39 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -2066,5 +2066,13 @@ "translation_translationOptions": "翻译选项", "translation_systemLanguage": "系统语言", "repeater_cliQuickDiscovery": "发现邻居", - "repeater_cliQuickClockSync": "同步时钟" + "repeater_cliQuickClockSync": "同步时钟", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLogin": "登录后,自动同步时钟", + "repeater_clockSyncAfterLoginSubtitle": "在成功登录后,自动发送“时钟同步”指令。" } diff --git a/lib/screens/repeater_settings_screen.dart b/lib/screens/repeater_settings_screen.dart index 6375e0b..d0236bb 100644 --- a/lib/screens/repeater_settings_screen.dart +++ b/lib/screens/repeater_settings_screen.dart @@ -8,6 +8,7 @@ import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../services/app_debug_log_service.dart'; import '../services/repeater_command_service.dart'; +import '../services/storage_service.dart'; import '../widgets/path_management_dialog.dart'; class RepeaterSettingsScreen extends StatefulWidget { @@ -25,6 +26,8 @@ class RepeaterSettingsScreen extends StatefulWidget { } class _RepeaterSettingsScreenState extends State { + final StorageService _storage = StorageService(); + bool _isLoading = false; bool _hasChanges = false; bool _refreshingBasic = false; @@ -59,6 +62,7 @@ class _RepeaterSettingsScreenState extends State { bool _repeatEnabled = true; bool _allowReadOnly = true; bool _privacyMode = false; + bool _autoClockSyncAfterLogin = false; // Advertisement settings bool _advertEnable = true; @@ -566,6 +570,15 @@ class _RepeaterSettingsScreenState extends State { _lonController.text = widget.repeater.longitude?.toString() ?? ''; } }); + + final autoClockSync = await _storage + .getRepeaterAutoClockSyncAfterLoginEnabled( + widget.repeater.publicKeyHex, + ); + if (!mounted) return; + setState(() { + _autoClockSyncAfterLogin = autoClockSync; + }); } Future _saveSettings() async { @@ -1139,6 +1152,21 @@ class _RepeaterSettingsScreenState extends State { onRefresh: _refreshAllowReadOnly, refreshTooltip: l10n.repeater_refreshGuestAccess, ), + SwitchListTile( + title: Text(l10n.repeater_clockSyncAfterLogin), + subtitle: Text(l10n.repeater_clockSyncAfterLoginSubtitle), + value: _autoClockSyncAfterLogin, + onChanged: (value) async { + setState(() { + _autoClockSyncAfterLogin = value; + }); + await _storage.setRepeaterAutoClockSyncAfterLoginEnabled( + widget.repeater.publicKeyHex, + value, + ); + }, + contentPadding: EdgeInsets.zero, + ), // Privacy mode - hidden until fully implemented // _buildFeatureToggleRow( // title: l10n.repeater_privacyMode, diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index a86c1f6..0c78c59 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -7,8 +7,42 @@ class StorageService { static const String _pathHistoryPrefix = 'path_history_'; static const String _pendingMessagesKey = 'pending_messages'; static const String _repeaterPasswordsKey = 'repeater_passwords'; + static const String _repeaterAutoClockSyncAfterLoginKey = + 'repeater_auto_clock_sync_after_login'; static const String _deliveryObservationsKey = 'delivery_observations'; + Future> _loadRepeaterAutoClockSyncAfterLogin() async { + final prefs = PrefsManager.instance; + final jsonStr = prefs.getString(_repeaterAutoClockSyncAfterLoginKey); + + if (jsonStr == null) return {}; + + try { + final json = jsonDecode(jsonStr) as Map; + return json.map((key, value) => MapEntry(key, value == true)); + } catch (e) { + return {}; + } + } + + Future getRepeaterAutoClockSyncAfterLoginEnabled( + String repeaterPubKeyHex, + ) async { + final settings = await _loadRepeaterAutoClockSyncAfterLogin(); + return settings[repeaterPubKeyHex] ?? false; + } + + Future setRepeaterAutoClockSyncAfterLoginEnabled( + String repeaterPubKeyHex, + bool enabled, + ) async { + final prefs = PrefsManager.instance; + final settings = await _loadRepeaterAutoClockSyncAfterLogin(); + settings[repeaterPubKeyHex] = enabled; + final jsonStr = jsonEncode(settings); + await prefs.setString(_repeaterAutoClockSyncAfterLoginKey, jsonStr); + } + Future savePathHistory( String contactPubKeyHex, ContactPathHistory history, diff --git a/lib/widgets/repeater_login_dialog.dart b/lib/widgets/repeater_login_dialog.dart index 48bb6ac..521cfd2 100644 --- a/lib/widgets/repeater_login_dialog.dart +++ b/lib/widgets/repeater_login_dialog.dart @@ -187,6 +187,29 @@ class _RepeaterLoginDialogState extends State { await _storage.removeRepeaterPassword(widget.repeater.publicKeyHex); } + final autoClockSync = await _storage + .getRepeaterAutoClockSyncAfterLoginEnabled( + widget.repeater.publicKeyHex, + ); + if (autoClockSync) { + try { + final timestampSeconds = + DateTime.now().millisecondsSinceEpoch ~/ 1000; + await _connector.sendFrame( + buildSendCliCommandFrame( + repeater.publicKey, + 'clock sync', + timestampSeconds: timestampSeconds, + ), + ); + } catch (e) { + appLogger.warn( + 'Auto clock sync failed for ${repeater.name}: $e', + tag: 'RepeaterLogin', + ); + } + } + if (mounted) { Navigator.pop(context, password); Future.microtask(() => widget.onLogin(password)); From 7dc162d9683be55e2be8abf3cac57847c40d6f78 Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Fri, 10 Apr 2026 14:14:31 -0700 Subject: [PATCH 39/41] temp translations fix --- lib/l10n/app_en.arb | 7 +- lib/l10n/app_localizations.dart | 22 ++- lib/l10n/app_localizations_bg.dart | 9 ++ lib/l10n/app_localizations_de.dart | 9 ++ lib/l10n/app_localizations_en.dart | 13 +- lib/l10n/app_localizations_es.dart | 9 ++ lib/l10n/app_localizations_fr.dart | 9 ++ lib/l10n/app_localizations_hu.dart | 9 ++ lib/l10n/app_localizations_it.dart | 9 ++ lib/l10n/app_localizations_ja.dart | 9 ++ lib/l10n/app_localizations_ko.dart | 9 ++ lib/l10n/app_localizations_nl.dart | 9 ++ lib/l10n/app_localizations_pl.dart | 9 ++ lib/l10n/app_localizations_pt.dart | 9 ++ lib/l10n/app_localizations_ru.dart | 9 ++ lib/l10n/app_localizations_sk.dart | 9 ++ lib/l10n/app_localizations_sl.dart | 9 ++ lib/l10n/app_localizations_sv.dart | 9 ++ lib/l10n/app_localizations_uk.dart | 9 ++ lib/l10n/app_localizations_zh.dart | 9 ++ lib/screens/contacts_screen.dart | 17 ++- lib/screens/map_screen.dart | 12 +- lib/screens/repeater_hub_screen.dart | 196 +++++++++++++------------ lib/widgets/repeater_login_dialog.dart | 17 ++- lib/widgets/room_login_dialog.dart | 15 +- untranslated.json | 85 ++++++++--- 26 files changed, 403 insertions(+), 134 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ffdf21d..f104480 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1038,8 +1038,8 @@ "login_enterPassword": "Enter password", "login_savePassword": "Save password", "login_savePasswordSubtitle": "Password will be stored securely on this device", - "login_repeaterDescription": "Enter the repeater password to access settings and status.", - "login_roomDescription": "Enter the room password to access settings and status.", + "login_repeaterDescription": "Enter the repeater password for guest or admin access.", + "login_roomDescription": "Enter the room password for guest or admin access.", "login_routing": "Routing", "login_routingMode": "Routing mode", "login_autoUseSavedPath": "Auto (use saved path)", @@ -1105,7 +1105,10 @@ "path_setPath": "Set Path", "repeater_management": "Repeater Management", "room_management": "Room Server Management", + "repeater_guest": "Repeater Information", + "room_guest": "Room Server Information", "repeater_managementTools": "Management Tools", + "repeater_guestTools": "Guest Tools", "repeater_status": "Status", "repeater_statusSubtitle": "View repeater status, stats, and neighbors", "repeater_telemetry": "Telemetry", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index bb390d5..3b89d69 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3438,13 +3438,13 @@ abstract class AppLocalizations { /// No description provided for @login_repeaterDescription. /// /// In en, this message translates to: - /// **'Enter the repeater password to access settings and status.'** + /// **'Enter the repeater password for guest or admin access.'** String get login_repeaterDescription; /// No description provided for @login_roomDescription. /// /// In en, this message translates to: - /// **'Enter the room password to access settings and status.'** + /// **'Enter the room password for guest or admin access.'** String get login_roomDescription; /// No description provided for @login_routing. @@ -3609,12 +3609,30 @@ abstract class AppLocalizations { /// **'Room Server Management'** String get room_management; + /// No description provided for @repeater_guest. + /// + /// In en, this message translates to: + /// **'Repeater Information'** + String get repeater_guest; + + /// No description provided for @room_guest. + /// + /// In en, this message translates to: + /// **'Room Server Information'** + String get room_guest; + /// No description provided for @repeater_managementTools. /// /// In en, this message translates to: /// **'Management Tools'** String get repeater_managementTools; + /// No description provided for @repeater_guestTools. + /// + /// In en, this message translates to: + /// **'Guest Tools'** + String get repeater_guestTools; + /// No description provided for @repeater_status. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index bec54df..ec12a78 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2019,9 +2019,18 @@ class AppLocalizationsBg extends AppLocalizations { @override String get room_management => 'Управление на сървъра за стая'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Инструменти за управление'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Статус'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 078c9e9..4d3aa4b 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2017,9 +2017,18 @@ class AppLocalizationsDe extends AppLocalizations { @override String get room_management => 'Raum-Server-Verwaltung'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Verwaltungs-Tools'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Status'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index d7a79bd..73a9308 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1871,11 +1871,11 @@ class AppLocalizationsEn extends AppLocalizations { @override String get login_repeaterDescription => - 'Enter the repeater password to access settings and status.'; + 'Enter the repeater password for guest or admin access.'; @override String get login_roomDescription => - 'Enter the room password to access settings and status.'; + 'Enter the room password for guest or admin access.'; @override String get login_routing => 'Routing'; @@ -1979,9 +1979,18 @@ class AppLocalizationsEn extends AppLocalizations { @override String get room_management => 'Room Server Management'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Management Tools'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Status'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 9a56c6d..0dfde3f 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2015,9 +2015,18 @@ class AppLocalizationsEs extends AppLocalizations { @override String get room_management => 'Administración del Servidor de Habitación'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Herramientas de Gestión'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Estado'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 4ce4a75..6b57723 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2026,9 +2026,18 @@ class AppLocalizationsFr extends AppLocalizations { @override String get room_management => 'Administrattion Room Server'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Outils de Gestion'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'État'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 5d305ee..052a3c7 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -2030,9 +2030,18 @@ class AppLocalizationsHu extends AppLocalizations { @override String get room_management => 'Szoba-szerver kezelés'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Menedzsmentes eszközök'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Állapot'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 98cbfcb..6f0d5ea 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2016,9 +2016,18 @@ class AppLocalizationsIt extends AppLocalizations { @override String get room_management => 'Gestione del Server di Camera'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Strumenti di Gestione'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Stato'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index fc59852..9ffc8ab 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1932,9 +1932,18 @@ class AppLocalizationsJa extends AppLocalizations { @override String get room_management => 'ルームサーバーの管理'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => '管理ツール'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'ステータス'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index b0d849b..9142657 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1929,9 +1929,18 @@ class AppLocalizationsKo extends AppLocalizations { @override String get room_management => '방 서버 관리'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => '관리 도구'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => '상태'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 6fcad22..b191919 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2003,9 +2003,18 @@ class AppLocalizationsNl extends AppLocalizations { @override String get room_management => 'Beheer Server Kamer'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Beheerfuncties'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Status'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 8492702..ce9549d 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2031,9 +2031,18 @@ class AppLocalizationsPl extends AppLocalizations { @override String get room_management => 'Zarządzanie Serwerem Pokoju'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Narzędzia Zarządzania'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Status'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index ce7c9e8..7679d27 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2015,9 +2015,18 @@ class AppLocalizationsPt extends AppLocalizations { @override String get room_management => 'Gerenciamento de Servidor de Sala'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Ferramentas de Gerenciamento'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Status'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 4557885..493eeb4 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2019,9 +2019,18 @@ class AppLocalizationsRu extends AppLocalizations { @override String get room_management => 'Управление сервером комнат'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Инструменты управления'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Статус'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index b59d6d8..e8104fa 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2004,9 +2004,18 @@ class AppLocalizationsSk extends AppLocalizations { @override String get room_management => 'Správa servera miestnosti'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Nástroje na správu'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Status'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index e5bf031..cfef170 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2001,9 +2001,18 @@ class AppLocalizationsSl extends AppLocalizations { @override String get room_management => 'Upravljanje stremlišča'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Upravne orodje'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Status'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 731b846..d3ca0bf 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1990,9 +1990,18 @@ class AppLocalizationsSv extends AppLocalizations { @override String get room_management => 'Rumserverhantering'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Administrationsverktyg'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Status'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 1b5a800..fe568e7 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2014,9 +2014,18 @@ class AppLocalizationsUk extends AppLocalizations { @override String get room_management => 'Адміністрування сервера кімнати'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Інструменти керування'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Статус'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index acadc58..e496cd0 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1890,9 +1890,18 @@ class AppLocalizationsZh extends AppLocalizations { @override String get room_management => '房间服务器管理'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => '管理工具'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => '状态'; diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 46e2be6..50c47dc 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -963,13 +963,16 @@ class _ContactsScreenState extends State context: context, builder: (context) => RepeaterLoginDialog( repeater: repeater, - onLogin: (password) { + onLogin: (password, isAdmin) { // Navigate to repeater hub screen after successful login Navigator.push( context, MaterialPageRoute( - builder: (context) => - RepeaterHubScreen(repeater: repeater, password: password), + builder: (context) => RepeaterHubScreen( + repeater: repeater, + password: password, + isAdmin: isAdmin, + ), ), ); }, @@ -986,14 +989,18 @@ class _ContactsScreenState extends State context: context, builder: (context) => RoomLoginDialog( room: room, - onLogin: (password) { + onLogin: (password, isAdmin) { context.read().markContactRead(room.publicKeyHex); Navigator.push( context, MaterialPageRoute( builder: (context) => destination == RoomLoginDestination.management - ? RepeaterHubScreen(repeater: room, password: password) + ? RepeaterHubScreen( + repeater: room, + password: password, + isAdmin: isAdmin, + ) : ChatScreen(contact: room), ), ); diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index f2d09f3..de801ee 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -1366,13 +1366,16 @@ class _MapScreenState extends State { context: context, builder: (context) => RepeaterLoginDialog( repeater: repeater, - onLogin: (password) { + onLogin: (password, isAdmin) { // Navigate to repeater hub screen after successful login Navigator.push( context, MaterialPageRoute( - builder: (context) => - RepeaterHubScreen(repeater: repeater, password: password), + builder: (context) => RepeaterHubScreen( + repeater: repeater, + password: password, + isAdmin: isAdmin, + ), ), ); }, @@ -1385,7 +1388,8 @@ class _MapScreenState extends State { context: context, builder: (context) => RoomLoginDialog( room: room, - onLogin: (password) { + // onLogin(password, isAdmin) isAdmin not used for room caht screen + onLogin: (password, _) { // Navigate to chat screen after successful login context.read().markContactRead(room.publicKeyHex); Navigator.push( diff --git a/lib/screens/repeater_hub_screen.dart b/lib/screens/repeater_hub_screen.dart index 8a14253..0dc141c 100644 --- a/lib/screens/repeater_hub_screen.dart +++ b/lib/screens/repeater_hub_screen.dart @@ -13,11 +13,13 @@ import 'neighbors_screen.dart'; class RepeaterHubScreen extends StatelessWidget { final Contact repeater; final String password; + final bool isAdmin; const RepeaterHubScreen({ super.key, required this.repeater, required this.password, + required this.isAdmin, }); @override @@ -33,11 +35,18 @@ class RepeaterHubScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text( - repeater.type == advTypeRepeater - ? l10n.repeater_management - : l10n.room_management, - ), + if (isAdmin) + Text( + repeater.type == advTypeRepeater + ? l10n.repeater_management + : l10n.room_management, + ), + if (!isAdmin) + Text( + repeater.type == advTypeRepeater + ? l10n.repeater_guest + : l10n.room_guest, + ), Text( repeater.name, style: const TextStyle( @@ -113,64 +122,67 @@ class RepeaterHubScreen extends StatelessWidget { ), ), const SizedBox(height: 24), - Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon(Icons.battery_full), - const SizedBox(width: 10), - Expanded( - child: Text( - l10n.appSettings_batteryChemistry, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, + if (isAdmin) + Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.battery_full), + const SizedBox(width: 10), + Expanded( + child: Text( + l10n.appSettings_batteryChemistry, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), ), ), - ), - ], - ), - const SizedBox(height: 12), - DropdownButtonFormField( - initialValue: chemistry, - isExpanded: true, - decoration: const InputDecoration( - border: UnderlineInputBorder(), - isDense: true, + ], ), - onChanged: (value) { - if (value == null) return; - settingsService.setBatteryChemistryForRepeater( - repeater.publicKeyHex, - value, - ); - }, - items: [ - DropdownMenuItem( - value: 'nmc', - child: Text(l10n.appSettings_batteryNmc), + const SizedBox(height: 12), + DropdownButtonFormField( + initialValue: chemistry, + isExpanded: true, + decoration: const InputDecoration( + border: UnderlineInputBorder(), + isDense: true, ), - DropdownMenuItem( - value: 'lifepo4', - child: Text(l10n.appSettings_batteryLifepo4), - ), - DropdownMenuItem( - value: 'lipo', - child: Text(l10n.appSettings_batteryLipo), - ), - ], - ), - ], + onChanged: (value) { + if (value == null) return; + settingsService.setBatteryChemistryForRepeater( + repeater.publicKeyHex, + value, + ); + }, + items: [ + DropdownMenuItem( + value: 'nmc', + child: Text(l10n.appSettings_batteryNmc), + ), + DropdownMenuItem( + value: 'lifepo4', + child: Text(l10n.appSettings_batteryLifepo4), + ), + DropdownMenuItem( + value: 'lipo', + child: Text(l10n.appSettings_batteryLipo), + ), + ], + ), + ], + ), ), ), - ), const SizedBox(height: 24), Text( - l10n.repeater_managementTools, + isAdmin + ? l10n.repeater_managementTools + : l10n.repeater_guestTools, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), @@ -210,26 +222,27 @@ class RepeaterHubScreen extends StatelessWidget { ); }, ), - const SizedBox(height: 12), + if (isAdmin) const SizedBox(height: 12), // CLI button - _buildManagementCard( - context, - icon: Icons.terminal, - title: l10n.repeater_cli, - subtitle: l10n.repeater_cliSubtitle, - color: Colors.green, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => RepeaterCliScreen( - repeater: repeater, - password: password, + if (isAdmin) + _buildManagementCard( + context, + icon: Icons.terminal, + title: l10n.repeater_cli, + subtitle: l10n.repeater_cliSubtitle, + color: Colors.green, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RepeaterCliScreen( + repeater: repeater, + password: password, + ), ), - ), - ); - }, - ), + ); + }, + ), const SizedBox(height: 12), // Neighbors button _buildManagementCard( @@ -248,26 +261,27 @@ class RepeaterHubScreen extends StatelessWidget { ); }, ), - const SizedBox(height: 12), + if (isAdmin) const SizedBox(height: 12), // Settings button - _buildManagementCard( - context, - icon: Icons.settings, - title: l10n.repeater_settings, - subtitle: l10n.repeater_settingsSubtitle, - color: Colors.deepOrange, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => RepeaterSettingsScreen( - repeater: repeater, - password: password, + if (isAdmin) + _buildManagementCard( + context, + icon: Icons.settings, + title: l10n.repeater_settings, + subtitle: l10n.repeater_settingsSubtitle, + color: Colors.deepOrange, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RepeaterSettingsScreen( + repeater: repeater, + password: password, + ), ), - ), - ); - }, - ), + ); + }, + ), ], ), ), diff --git a/lib/widgets/repeater_login_dialog.dart b/lib/widgets/repeater_login_dialog.dart index 48bb6ac..f4db904 100644 --- a/lib/widgets/repeater_login_dialog.dart +++ b/lib/widgets/repeater_login_dialog.dart @@ -14,7 +14,7 @@ import 'path_management_dialog.dart'; class RepeaterLoginDialog extends StatefulWidget { final Contact repeater; - final Function(String password) onLogin; + final Function(String password, bool isAdmin) onLogin; const RepeaterLoginDialog({ super.key, @@ -119,6 +119,7 @@ class _RepeaterLoginDialogState extends State { : '${selection.hopCount} hops'; appLogger.info('Login routing: $selectionLabel', tag: 'RepeaterLogin'); bool? loginResult; + bool isAdmin = false; for (int attempt = 0; attempt < _maxAttempts; attempt++) { if (!mounted) return; setState(() { @@ -131,7 +132,7 @@ class _RepeaterLoginDialogState extends State { ); await _connector.sendFrame(loginFrame); - loginResult = await _awaitLoginResponse(timeout); + (loginResult, isAdmin) = await _awaitLoginResponse(timeout); if (loginResult == true) { appLogger.info( 'Login succeeded for ${repeater.name}', @@ -189,7 +190,7 @@ class _RepeaterLoginDialogState extends State { if (mounted) { Navigator.pop(context, password); - Future.microtask(() => widget.onLogin(password)); + Future.microtask(() => widget.onLogin(password, isAdmin)); } } catch (e) { final repeater = _resolveRepeater(_connector); @@ -206,17 +207,21 @@ class _RepeaterLoginDialogState extends State { } } - Future _awaitLoginResponse(Duration timeout) async { + // _awaitLoginResponse returns a record of bool, for success and if the client is an admin + Future<(bool?, bool)> _awaitLoginResponse(Duration timeout) async { final completer = Completer(); Timer? timer; StreamSubscription? subscription; final targetPrefix = widget.repeater.publicKey.sublist(0, 6); - + bool isAdmin = false; subscription = _connector.receivedFrames.listen((frame) { if (frame.isEmpty) return; final code = frame[0]; if (code != pushCodeLoginSuccess && code != pushCodeLoginFail) return; if (frame.length < 8) return; + // NOTE: a bug in the repeater firmware only ever sends 1 or 0 back, not the + // expected client permissions + isAdmin = (frame[1] == 1); final prefix = frame.sublist(2, 8); if (!listEquals(prefix, targetPrefix)) return; @@ -235,7 +240,7 @@ class _RepeaterLoginDialogState extends State { final result = await completer.future; timer.cancel(); await subscription.cancel(); - return result; + return (result, isAdmin); } @override diff --git a/lib/widgets/room_login_dialog.dart b/lib/widgets/room_login_dialog.dart index 3a923fe..2206227 100644 --- a/lib/widgets/room_login_dialog.dart +++ b/lib/widgets/room_login_dialog.dart @@ -14,7 +14,7 @@ import 'path_management_dialog.dart'; class RoomLoginDialog extends StatefulWidget { final Contact room; - final Function(String password) onLogin; + final Function(String password, bool isAdmin) onLogin; const RoomLoginDialog({super.key, required this.room, required this.onLogin}); @@ -114,6 +114,7 @@ class _RoomLoginDialogState extends State { : '${selection.hopCount} hops'; appLogger.info('Login routing: $selectionLabel', tag: 'RoomLogin'); bool? loginResult; + bool isAdmin = false; for (int attempt = 0; attempt < _maxAttempts; attempt++) { if (!mounted) return; setState(() { @@ -126,7 +127,7 @@ class _RoomLoginDialogState extends State { ); await _connector.sendFrame(loginFrame); - loginResult = await _awaitLoginResponse(timeout); + (loginResult, isAdmin) = await _awaitLoginResponse(timeout); if (loginResult == true) { appLogger.info('Login succeeded for ${room.name}', tag: 'RoomLogin'); break; @@ -166,7 +167,7 @@ class _RoomLoginDialogState extends State { if (mounted) { Navigator.pop(context, password); - Future.microtask(() => widget.onLogin(password)); + Future.microtask(() => widget.onLogin(password, isAdmin)); } } catch (e) { final room = _resolveRepeater(_connector); @@ -185,16 +186,20 @@ class _RoomLoginDialogState extends State { } } - Future _awaitLoginResponse(Duration timeout) async { + Future<(bool?, bool)> _awaitLoginResponse(Duration timeout) async { final completer = Completer(); Timer? timer; StreamSubscription? subscription; final targetPrefix = widget.room.publicKey.sublist(0, 6); + bool isAdmin = false; subscription = _connector.receivedFrames.listen((frame) { if (frame.isEmpty) return; final code = frame[0]; if (code != pushCodeLoginSuccess && code != pushCodeLoginFail) return; + // NOTE: a bug in the repeater firmware only ever sends 1 or 0 back, not the + // expected client permissions + isAdmin = (frame[1] == 1); if (frame.length < 8) return; final prefix = frame.sublist(2, 8); if (!listEquals(prefix, targetPrefix)) return; @@ -214,7 +219,7 @@ class _RoomLoginDialogState extends State { final result = await completer.future; timer.cancel(); await subscription.cancel(); - return result; + return (result, isAdmin); } @override diff --git a/untranslated.json b/untranslated.json index 1ebd9bc..2b4bbbc 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,69 +1,120 @@ { "bg": [ - "chat_sendMessage" + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" ], "de": [ - "chat_sendMessage" + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" ], "es": [ - "chat_sendMessage" + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" ], "fr": [ - "chat_sendMessage" + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" ], "hu": [ - "chat_sendMessage" + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" ], "it": [ - "chat_sendMessage" + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" ], "ja": [ - "chat_sendMessage" + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" ], "ko": [ - "chat_sendMessage" + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" ], "nl": [ - "chat_sendMessage" + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" ], "pl": [ - "chat_sendMessage" + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" ], "pt": [ - "chat_sendMessage" + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" ], "ru": [ - "chat_sendMessage" + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" ], "sk": [ - "chat_sendMessage" + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" ], "sl": [ - "chat_sendMessage" + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" ], "sv": [ - "chat_sendMessage" + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" ], "uk": [ - "chat_sendMessage" + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" ], "zh": [ - "chat_sendMessage" + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" ] } From add4731d05341a484a9b7b1df9e4d1488ab4c111 Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Fri, 10 Apr 2026 15:11:44 -0700 Subject: [PATCH 40/41] fix: settings dialog lists switched to using RadioListTile instead of ListTile to be more accessible --- lib/screens/app_settings_screen.dart | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index cd7fb67..9195602 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -1065,25 +1065,25 @@ class AppSettingsScreen extends StatelessWidget { children: [ Text(context.l10n.appSettings_showNodesDiscoveredWithin), const SizedBox(height: 16), - ListTile( + RadioListTile( title: Text(context.l10n.appSettings_allTime), - leading: Radio(value: 0), + value: 0, ), - ListTile( + RadioListTile( title: Text(context.l10n.appSettings_lastHour), - leading: Radio(value: 1), + value: 1, ), - ListTile( + RadioListTile( title: Text(context.l10n.appSettings_last6Hours), - leading: Radio(value: 6), + value: 6, ), - ListTile( + RadioListTile( title: Text(context.l10n.appSettings_last24Hours), - leading: Radio(value: 24), + value: 24, ), - ListTile( + RadioListTile( title: Text(context.l10n.appSettings_lastWeek), - leading: Radio(value: 168), + value: 168, ), ], ), @@ -1117,13 +1117,13 @@ class AppSettingsScreen extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - ListTile( + RadioListTile( title: Text(context.l10n.appSettings_unitsMetric), - leading: const Radio(value: UnitSystem.metric), + value: UnitSystem.metric, ), - ListTile( + RadioListTile( title: Text(context.l10n.appSettings_unitsImperial), - leading: const Radio(value: UnitSystem.imperial), + value: UnitSystem.imperial, ), ], ), From aa2d0f19274985f5c970f58e3ab6e537a5c69b82 Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Tue, 14 Apr 2026 11:59:18 -0700 Subject: [PATCH 41/41] clear toast on tap this adds a generator showDismissibleSnackBar which by default allows tapping to clear snack bar toasts. all SnackBar properties are still available and the all callers should now use showDismissibleSnackBar() instead of calling ScaffoldMessenger.of(context).showSnackBar(SnackBar()) --- lib/helpers/link_handler.dart | 19 +- lib/helpers/snack_bar_builder.dart | 56 ++++++ lib/screens/app_debug_log_screen.dart | 6 +- lib/screens/app_settings_screen.dart | 110 ++++++----- lib/screens/ble_debug_log_screen.dart | 8 +- lib/screens/channel_chat_screen.dart | 30 +-- lib/screens/channels_screen.dart | 187 ++++++++----------- lib/screens/chat_screen.dart | 75 ++++---- lib/screens/community_qr_scanner_screen.dart | 30 ++- lib/screens/contacts_screen.dart | 105 +++++------ lib/screens/discovery_screen.dart | 6 +- lib/screens/map_cache_screen.dart | 20 +- lib/screens/map_screen.dart | 16 +- lib/screens/neighbors_screen.dart | 28 ++- lib/screens/repeater_cli_screen.dart | 6 +- lib/screens/repeater_settings_screen.dart | 58 +++--- lib/screens/repeater_status_screen.dart | 21 +-- lib/screens/scanner_screen.dart | 10 +- lib/screens/settings_screen.dart | 90 +++++---- lib/screens/tcp_screen.dart | 7 +- lib/screens/telemetry_screen.dart | 28 ++- lib/screens/usb_screen.dart | 10 +- lib/widgets/path_management_dialog.dart | 59 +++--- lib/widgets/path_selection_dialog.dart | 25 ++- lib/widgets/room_login_dialog.dart | 10 +- 25 files changed, 526 insertions(+), 494 deletions(-) create mode 100644 lib/helpers/snack_bar_builder.dart diff --git a/lib/helpers/link_handler.dart b/lib/helpers/link_handler.dart index b931ca1..c2eae29 100644 --- a/lib/helpers/link_handler.dart +++ b/lib/helpers/link_handler.dart @@ -3,6 +3,7 @@ import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:url_launcher/url_launcher.dart'; import '../l10n/l10n.dart'; import '../utils/platform_info.dart'; +import '../helpers/snack_bar_builder.dart'; class LinkHandler { static TextStyle defaultLinkStyle(BuildContext context, TextStyle base) { @@ -93,21 +94,19 @@ class LinkHandler { final uri = Uri.parse(url); if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.chat_couldNotOpenLink(url)), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.chat_couldNotOpenLink(url)), + backgroundColor: Colors.red, ); } } } catch (e) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.chat_invalidLink), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.chat_invalidLink), + backgroundColor: Colors.red, ); } } diff --git a/lib/helpers/snack_bar_builder.dart b/lib/helpers/snack_bar_builder.dart new file mode 100644 index 0000000..d7409b6 --- /dev/null +++ b/lib/helpers/snack_bar_builder.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; + +// showDismissibleSnackBar shows a [SnackBar] with tap to dismiss +// all other properties are default and optional +void showDismissibleSnackBar( + BuildContext context, { + Key? key, + required Widget content, + Color? backgroundColor, + double? elevation, + EdgeInsetsGeometry? margin, + EdgeInsetsGeometry? padding, + double? width, + ShapeBorder? shape, + HitTestBehavior? hitTestBehavior, + SnackBarBehavior? behavior, + SnackBarAction? action, + double? actionOverflowThreshold, + bool? showCloseIcon, + Color? closeIconColor, + Duration? duration, + bool? persist, + Animation? animation, + void Function()? onVisible, + DismissDirection? dismissDirection, + Clip? clipBehavior, +}) { + final messenger = ScaffoldMessenger.of(context); + messenger.showSnackBar( + SnackBar( + key: key, + content: GestureDetector( + onTap: () => messenger.hideCurrentSnackBar(), + child: content, + ), + backgroundColor: backgroundColor, + elevation: elevation, + margin: margin, + padding: padding, + width: width, + shape: shape, + hitTestBehavior: hitTestBehavior, + behavior: behavior, + action: action, + actionOverflowThreshold: actionOverflowThreshold, + showCloseIcon: showCloseIcon, + closeIconColor: closeIconColor, + duration: duration ?? const Duration(seconds: 4), + persist: persist, + animation: animation, + onVisible: onVisible, + dismissDirection: dismissDirection ?? DismissDirection.down, + clipBehavior: clipBehavior ?? Clip.hardEdge, + ), + ); +} diff --git a/lib/screens/app_debug_log_screen.dart b/lib/screens/app_debug_log_screen.dart index 4877038..ca6a6bf 100644 --- a/lib/screens/app_debug_log_screen.dart +++ b/lib/screens/app_debug_log_screen.dart @@ -5,6 +5,7 @@ import 'package:provider/provider.dart'; import '../l10n/l10n.dart'; import '../services/app_debug_log_service.dart'; import '../widgets/adaptive_app_bar_title.dart'; +import '../helpers/snack_bar_builder.dart'; class AppDebugLogScreen extends StatelessWidget { const AppDebugLogScreen({super.key}); @@ -34,8 +35,9 @@ class AppDebugLogScreen extends StatelessWidget { .join('\n'); await Clipboard.setData(ClipboardData(text: text)); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.debugLog_copied)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.debugLog_copied), ); } : null, diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index cd7fb67..f4cab7f 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -10,6 +10,7 @@ import '../services/app_settings_service.dart'; import '../services/notification_service.dart'; import '../services/translation_service.dart'; import '../widgets/adaptive_app_bar_title.dart'; +import '../helpers/snack_bar_builder.dart'; import 'map_cache_screen.dart'; class AppSettingsScreen extends StatelessWidget { @@ -151,13 +152,12 @@ class AppSettingsScreen extends StatelessWidget { .requestPermissions(); if (!granted) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.appSettings_notificationPermissionDenied, - ), - duration: const Duration(seconds: 2), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.appSettings_notificationPermissionDenied, ), + duration: const Duration(seconds: 2), ); } return; @@ -166,15 +166,14 @@ class AppSettingsScreen extends StatelessWidget { await settingsService.setNotificationsEnabled(value); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - value - ? context.l10n.appSettings_notificationsEnabled - : context.l10n.appSettings_notificationsDisabled, - ), - duration: const Duration(seconds: 2), + showDismissibleSnackBar( + context, + content: Text( + value + ? context.l10n.appSettings_notificationsEnabled + : context.l10n.appSettings_notificationsDisabled, ), + duration: const Duration(seconds: 2), ); } }, @@ -301,15 +300,14 @@ class AppSettingsScreen extends StatelessWidget { value: settingsService.settings.clearPathOnMaxRetry, onChanged: (value) { settingsService.setClearPathOnMaxRetry(value); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - value - ? context.l10n.appSettings_pathsWillBeCleared - : context.l10n.appSettings_pathsWillNotBeCleared, - ), - duration: const Duration(seconds: 2), + showDismissibleSnackBar( + context, + content: Text( + value + ? context.l10n.appSettings_pathsWillBeCleared + : context.l10n.appSettings_pathsWillNotBeCleared, ), + duration: const Duration(seconds: 2), ); }, ), @@ -329,15 +327,14 @@ class AppSettingsScreen extends StatelessWidget { value: settingsService.settings.autoRouteRotationEnabled, onChanged: (value) { settingsService.setAutoRouteRotationEnabled(value); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - value - ? context.l10n.appSettings_autoRouteRotationEnabled - : context.l10n.appSettings_autoRouteRotationDisabled, - ), - duration: const Duration(seconds: 2), + showDismissibleSnackBar( + context, + content: Text( + value + ? context.l10n.appSettings_autoRouteRotationEnabled + : context.l10n.appSettings_autoRouteRotationDisabled, ), + duration: const Duration(seconds: 2), ); }, ), @@ -1164,8 +1161,9 @@ class AppSettingsScreen extends StatelessWidget { String? id, }) async { if (sourceUrl.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.translation_enterUrlFirst)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.translation_enterUrlFirst), ); return; } @@ -1176,22 +1174,23 @@ class AppSettingsScreen extends StatelessWidget { id: id, ); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.translation_modelDownloaded)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.translation_modelDownloaded), ); await settingsService.setTranslationEnabled(true); } on TranslationDownloadCancelled { if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.translation_downloadStopped)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.translation_downloadStopped), ); } catch (error) { if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.translation_downloadFailed(error.toString()), - ), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.translation_downloadFailed(error.toString()), ), ); } @@ -1236,16 +1235,16 @@ class AppSettingsScreen extends StatelessWidget { try { await translationService.removeModel(model); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - // TODO: l10n - content: Text('Deleted ${translationModelFriendlyName(model)}.'), - ), + showDismissibleSnackBar( + context, + // TODO: l10n + content: Text('Deleted ${translationModelFriendlyName(model)}.'), ); } catch (error) { if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Delete failed: $error')), + showDismissibleSnackBar( + context, + content: Text('Delete failed: $error'), ); // TODO: l10n } } @@ -1279,15 +1278,14 @@ class AppSettingsScreen extends StatelessWidget { onChanged: (value) async { await settingsService.setAppDebugLogEnabled(value); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - value - ? context.l10n.appSettings_appDebugLoggingEnabled - : context.l10n.appSettings_appDebugLoggingDisabled, - ), - duration: const Duration(seconds: 2), + showDismissibleSnackBar( + context, + content: Text( + value + ? context.l10n.appSettings_appDebugLoggingEnabled + : context.l10n.appSettings_appDebugLoggingDisabled, ), + duration: const Duration(seconds: 2), ); }, ), diff --git a/lib/screens/ble_debug_log_screen.dart b/lib/screens/ble_debug_log_screen.dart index 1009bc4..6d18697 100644 --- a/lib/screens/ble_debug_log_screen.dart +++ b/lib/screens/ble_debug_log_screen.dart @@ -5,6 +5,7 @@ import '../l10n/l10n.dart'; import '../services/ble_debug_log_service.dart'; import '../connector/meshcore_protocol.dart'; import '../widgets/adaptive_app_bar_title.dart'; +import '../helpers/snack_bar_builder.dart'; enum _BleLogView { frames, rawLogRx } @@ -52,10 +53,9 @@ class _BleDebugLogScreenState extends State { .join('\n'); await Clipboard.setData(ClipboardData(text: text)); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.debugLog_bleCopied), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.debugLog_bleCopied), ); } : null, diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 64da058..e5b5f67 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -14,6 +14,7 @@ import '../connector/meshcore_protocol.dart'; import '../helpers/gif_helper.dart'; import '../helpers/reaction_helper.dart'; import '../helpers/utf8_length_limiter.dart'; +import '../helpers/snack_bar_builder.dart'; import '../l10n/l10n.dart'; import '../models/channel.dart'; import '../models/channel_message.dart'; @@ -144,11 +145,10 @@ class _ChannelChatScreenState extends State { Future _scrollToMessage(String messageId) async { final key = _messageKeys[messageId]; if (key == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.chat_originalMessageNotFound), - duration: const Duration(seconds: 2), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.chat_originalMessageNotFound), + duration: const Duration(seconds: 2), ); return; } @@ -1151,9 +1151,10 @@ class _ChannelChatScreenState extends State { final now = DateTime.now(); if (_lastChannelSendAt != null && now.difference(_lastChannelSendAt!) < const Duration(seconds: 1)) { - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(context.l10n.chat_sendCooldown))); + content: Text(context.l10n.chat_sendCooldown), + ); return; } _lastChannelSendAt = now; @@ -1195,8 +1196,9 @@ class _ChannelChatScreenState extends State { final maxBytes = maxChannelMessageBytes(connector.selfName); if (utf8.encode(messageText).length > maxBytes) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))), + showDismissibleSnackBar( + context, + content: Text(context.l10n.chat_messageTooLong(maxBytes)), ); return; } @@ -1323,17 +1325,19 @@ class _ChannelChatScreenState extends State { void _copyMessageText(String text) { Clipboard.setData(ClipboardData(text: text)); - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageCopied))); + content: Text(context.l10n.chat_messageCopied), + ); } Future _deleteMessage(ChannelMessage message) async { await context.read().deleteChannelMessage(message); if (!mounted) return; - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageDeleted))); + content: Text(context.l10n.chat_messageDeleted), + ); } String _formatPathPrefixes(Uint8List pathBytes) { diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 51d2453..44c7a69 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -24,6 +24,7 @@ import '../widgets/empty_state.dart'; import '../widgets/qr_code_display.dart'; import '../widgets/quick_switch_bar.dart'; import '../widgets/unread_badge.dart'; +import '../helpers/snack_bar_builder.dart'; import 'channel_chat_screen.dart'; import 'community_qr_scanner_screen.dart'; import 'contacts_screen.dart'; @@ -809,15 +810,12 @@ class _ChannelsScreenState extends State onPressed: () async { final name = nameController.text.trim(); if (name.isEmpty) { - ScaffoldMessenger.of( - dialogContext, - ).showSnackBar( - SnackBar( - content: Text( - dialogContext - .l10n - .channels_enterChannelName, - ), + showDismissibleSnackBar( + context, + content: Text( + dialogContext + .l10n + .channels_enterChannelName, ), ); return; @@ -837,13 +835,10 @@ class _ChannelsScreenState extends State nextIndex, ); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.channels_channelAdded( - name, - ), - ), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.channels_channelAdded(name), ), ); } @@ -897,15 +892,12 @@ class _ChannelsScreenState extends State final name = nameController.text.trim(); final pskHex = pskController.text.trim(); if (name.isEmpty) { - ScaffoldMessenger.of( - dialogContext, - ).showSnackBar( - SnackBar( - content: Text( - dialogContext - .l10n - .channels_enterChannelName, - ), + showDismissibleSnackBar( + context, + content: Text( + dialogContext + .l10n + .channels_enterChannelName, ), ); return; @@ -914,15 +906,12 @@ class _ChannelsScreenState extends State try { psk = Channel.parsePskHex(pskHex); } on FormatException { - ScaffoldMessenger.of( - dialogContext, - ).showSnackBar( - SnackBar( - content: Text( - dialogContext - .l10n - .channels_pskMustBe32Hex, - ), + showDismissibleSnackBar( + context, + content: Text( + dialogContext + .l10n + .channels_pskMustBe32Hex, ), ); return; @@ -930,13 +919,10 @@ class _ChannelsScreenState extends State Navigator.pop(dialogContext); connector.setChannel(nextIndex, name, psk); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.channels_channelAdded( - name, - ), - ), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.channels_channelAdded(name), ), ); } @@ -967,11 +953,10 @@ class _ChannelsScreenState extends State Navigator.pop(dialogContext); connector.setChannel(nextIndex, 'Public', psk); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.channels_publicChannelAdded, - ), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.channels_publicChannelAdded, ), ); } @@ -1097,15 +1082,12 @@ class _ChannelsScreenState extends State onPressed: () async { var hashtag = hashtagController.text.trim(); if (hashtag.isEmpty) { - ScaffoldMessenger.of( - dialogContext, - ).showSnackBar( - SnackBar( - content: Text( - dialogContext - .l10n - .channels_enterChannelName, - ), + showDismissibleSnackBar( + context, + content: Text( + dialogContext + .l10n + .channels_enterChannelName, ), ); return; @@ -1125,15 +1107,12 @@ class _ChannelsScreenState extends State } else { // Community hashtag - HMAC derivation from community secret if (selectedCommunity == null) { - ScaffoldMessenger.of( + showDismissibleSnackBar( dialogContext, - ).showSnackBar( - SnackBar( - content: Text( - dialogContext - .l10n - .community_selectCommunity, - ), + content: Text( + dialogContext + .l10n + .community_selectCommunity, ), ); return; @@ -1159,12 +1138,11 @@ class _ChannelsScreenState extends State psk, ); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.channels_channelAdded( - channelName, - ), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.channels_channelAdded( + channelName, ), ), ); @@ -1259,13 +1237,10 @@ class _ChannelsScreenState extends State onPressed: () async { final name = nameController.text.trim(); if (name.isEmpty) { - ScaffoldMessenger.of( - dialogContext, - ).showSnackBar( - SnackBar( - content: Text( - dialogContext.l10n.community_enterName, - ), + showDismissibleSnackBar( + context, + content: Text( + dialogContext.l10n.community_enterName, ), ); return; @@ -1301,11 +1276,10 @@ class _ChannelsScreenState extends State _loadCommunities(); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.community_created(name), - ), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.community_created(name), ), ); @@ -1494,10 +1468,9 @@ class _ChannelsScreenState extends State try { psk = Channel.parsePskHex(pskHex); } on FormatException { - ScaffoldMessenger.of(dialogContext).showSnackBar( - SnackBar( - content: Text(dialogContext.l10n.channels_pskMustBe32Hex), - ), + showDismissibleSnackBar( + dialogContext, + content: Text(dialogContext.l10n.channels_pskMustBe32Hex), ); return; } @@ -1510,16 +1483,16 @@ class _ChannelsScreenState extends State smazEnabled, ); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.channels_channelUpdated(name)), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.channels_channelUpdated(name)), ); } catch (e, st) { debugPrint(st.toString()); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to update channel: $e')), + showDismissibleSnackBar( + context, + content: Text('Failed to update channel: $e'), ); } }, @@ -1559,21 +1532,19 @@ class _ChannelsScreenState extends State if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.channels_channelDeleted(channel.name), - ), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.channels_channelDeleted(channel.name), ), ); } catch (e, st) { if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.channels_channelDeleteFailed(channel.name), - ), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.channels_channelDeleteFailed(channel.name), ), ); @@ -1594,8 +1565,9 @@ class _ChannelsScreenState extends State void _addPublicChannel(BuildContext context, MeshCoreConnector connector) { final psk = Channel.parsePskHex(Channel.publicChannelPsk); connector.setChannel(0, 'Public', psk); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.channels_publicChannelAdded)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.channels_publicChannelAdded), ); } @@ -1810,12 +1782,9 @@ class _ChannelsScreenState extends State _loadCommunities(); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.community_deleted(community.name), - ), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.community_deleted(community.name)), ); } }, diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index a4ebc76..2aee61c 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -43,6 +43,7 @@ import '../widgets/radio_stats_entry.dart'; import '../widgets/translated_message_content.dart'; import '../utils/app_logger.dart'; import '../l10n/l10n.dart'; +import '../helpers/snack_bar_builder.dart'; import 'telemetry_screen.dart'; class ChatScreen extends StatefulWidget { @@ -633,9 +634,10 @@ class _ChatScreenState extends State { final now = DateTime.now(); if (_lastTextSendAt != null && now.difference(_lastTextSendAt!) < const Duration(seconds: 1)) { - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(context.l10n.chat_sendCooldown))); + content: Text(context.l10n.chat_sendCooldown), + ); return; } _lastTextSendAt = now; @@ -671,8 +673,9 @@ class _ChatScreenState extends State { } final maxBytes = maxContactMessageBytes(); if (utf8.encode(outgoingText).length > maxBytes) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))), + showDismissibleSnackBar( + context, + content: Text(context.l10n.chat_messageTooLong(maxBytes)), ); return; } @@ -860,15 +863,12 @@ class _ChatScreenState extends State { _showFullPathDialog(context, path.pathBytes), onTap: () async { if (path.pathBytes.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context - .l10n - .chat_pathDetailsNotAvailable, - ), - duration: const Duration(seconds: 2), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.chat_pathDetailsNotAvailable, ), + duration: const Duration(seconds: 2), ); return; } @@ -952,11 +952,10 @@ class _ChatScreenState extends State { _resolveContact(connector), ); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.chat_pathCleared), - duration: const Duration(seconds: 2), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.chat_pathCleared), + duration: const Duration(seconds: 2), ); Navigator.pop(context); }, @@ -982,11 +981,10 @@ class _ChatScreenState extends State { pathLen: -1, ); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.chat_floodModeEnabled), - duration: const Duration(seconds: 2), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.chat_floodModeEnabled), + duration: const Duration(seconds: 2), ); Navigator.pop(context); }, @@ -1020,11 +1018,10 @@ class _ChatScreenState extends State { void _showFullPathDialog(BuildContext context, List pathBytes) { if (pathBytes.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.chat_pathDetailsNotAvailable), - duration: const Duration(seconds: 2), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.chat_pathDetailsNotAvailable), + duration: const Duration(seconds: 2), ); return; } @@ -1137,11 +1134,10 @@ class _ChatScreenState extends State { : (verified ? context.l10n.chat_pathDeviceConfirmed : context.l10n.chat_pathDeviceNotConfirmed); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.chat_pathSetHops(hopCount, status)), - duration: const Duration(seconds: 3), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.chat_pathSetHops(hopCount, status)), + duration: const Duration(seconds: 3), ); } @@ -1490,26 +1486,29 @@ class _ChatScreenState extends State { void _copyMessageText(String text) { Clipboard.setData(ClipboardData(text: text)); - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageCopied))); + content: Text(context.l10n.chat_messageCopied), + ); } Future _deleteMessage(Message message) async { await context.read().deleteMessage(message); if (!mounted) return; - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageDeleted))); + content: Text(context.l10n.chat_messageDeleted), + ); } void _retryMessage(Message message) { final connector = Provider.of(context, listen: false); // Retry using the contact's current path override setting connector.sendMessage(_resolveContact(connector), message.text); - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(context.l10n.chat_retryingMessage))); + content: Text(context.l10n.chat_retryingMessage), + ); } void _showEmojiPicker(Message message, Contact senderContact) { diff --git a/lib/screens/community_qr_scanner_screen.dart b/lib/screens/community_qr_scanner_screen.dart index 6852dfa..6b71715 100644 --- a/lib/screens/community_qr_scanner_screen.dart +++ b/lib/screens/community_qr_scanner_screen.dart @@ -8,6 +8,7 @@ import '../models/community.dart'; import '../storage/community_store.dart'; import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/qr_scanner_widget.dart'; +import '../helpers/snack_bar_builder.dart'; /// Screen for scanning community QR codes to join communities. /// @@ -76,11 +77,10 @@ class _CommunityQrScannerScreenState extends State { } } catch (e) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.community_invalidQrCode), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.community_invalidQrCode), + backgroundColor: Colors.red, ); } } finally { @@ -93,12 +93,11 @@ class _CommunityQrScannerScreenState extends State { } void _showInvalidQrError(BuildContext context) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.community_invalidQrCode), - backgroundColor: Colors.orange, - duration: const Duration(seconds: 2), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.community_invalidQrCode), + backgroundColor: Colors.orange, + duration: const Duration(seconds: 2), ); } @@ -229,11 +228,10 @@ class _CommunityQrScannerScreenState extends State { } if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.community_joined(community.name)), - backgroundColor: Colors.green, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.community_joined(community.name)), + backgroundColor: Colors.green, ); // Return to previous screen diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 46e2be6..5a6f359 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -27,6 +27,7 @@ import '../widgets/quick_switch_bar.dart'; import '../widgets/repeater_login_dialog.dart'; import '../widgets/room_login_dialog.dart'; import '../widgets/unread_badge.dart'; +import '../helpers/snack_bar_builder.dart'; import 'channels_screen.dart'; import 'chat_screen.dart'; import 'discovery_screen.dart'; @@ -150,9 +151,10 @@ class _ContactsScreenState extends State } void _showGroupsUnavailableMessage(BuildContext context) { - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(context.l10n.common_loading))); + content: Text(context.l10n.common_loading), + ); } void _setupFrameListener() { @@ -169,10 +171,9 @@ class _ContactsScreenState extends State // Validate packet has expected minimum size (98+ bytes per protocol) if (advertPacket.length < 98) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_invalidAdvertFormat), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_invalidAdvertFormat), ); } _pendingOperations.remove(ContactOperationType.export); @@ -187,24 +188,23 @@ class _ContactsScreenState extends State if (!mounted) return; if (_pendingOperations.contains(ContactOperationType.import)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_contactImported)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactImported), ); } if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_zeroHopContactAdvertSent), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_zeroHopContactAdvertSent), ); } if (_pendingOperations.contains(ContactOperationType.export)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_contactAdvertCopied), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactAdvertCopied), ); } @@ -216,25 +216,22 @@ class _ContactsScreenState extends State if (!mounted) return; if (_pendingOperations.contains(ContactOperationType.import)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_contactImportFailed), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactImportFailed), ); } if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_zeroHopContactAdvertFailed), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_zeroHopContactAdvertFailed), ); } if (_pendingOperations.contains(ContactOperationType.export)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_contactAdvertCopyFailed), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactAdvertCopyFailed), ); } @@ -271,8 +268,9 @@ class _ContactsScreenState extends State final clipboardData = await Clipboard.getData('text/plain'); if (clipboardData == null || clipboardData.text == null) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_clipboardEmpty)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_clipboardEmpty), ); } return; @@ -280,8 +278,9 @@ class _ContactsScreenState extends State final text = clipboardData.text!.trim(); if (!text.startsWith('meshcore://')) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_invalidAdvertFormat), ); } return; @@ -294,8 +293,9 @@ class _ContactsScreenState extends State connector.importContact(importContactFrame); } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_invalidAdvertFormat), ); } } @@ -330,10 +330,9 @@ class _ContactsScreenState extends State ), onTap: () => { connector.sendSelfAdvert(flood: false), - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.settings_advertisementSent), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.settings_advertisementSent), ), }, ), @@ -347,10 +346,9 @@ class _ContactsScreenState extends State ), onTap: () => { connector.sendSelfAdvert(flood: true), - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.settings_advertisementSent), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.settings_advertisementSent), ), }, ), @@ -1146,19 +1144,17 @@ class _ContactsScreenState extends State onPressed: () async { final name = nameController.text.trim(); if (name.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_groupNameRequired), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_groupNameRequired), ); return; } if (name.toLowerCase() == contactsAllGroupsValue.toLowerCase()) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_groupNameReserved), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_groupNameReserved), ); return; } @@ -1167,11 +1163,10 @@ class _ContactsScreenState extends State return g.name.toLowerCase() == name.toLowerCase(); }); if (exists) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.contacts_groupAlreadyExists(name), - ), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.contacts_groupAlreadyExists(name), ), ); return; diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart index 3f9d965..f9f0e07 100644 --- a/lib/screens/discovery_screen.dart +++ b/lib/screens/discovery_screen.dart @@ -12,6 +12,7 @@ import '../utils/contact_search.dart'; import '../utils/platform_info.dart'; import '../widgets/app_bar.dart'; import '../widgets/list_filter_widget.dart'; +import '../helpers/snack_bar_builder.dart'; enum DiscoverySortOption { lastSeen, name, type } @@ -234,8 +235,9 @@ class _DiscoveryScreenState extends State { final hexString = pubKeyToHex(contact.rawPacket!); Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactAdvertCopied), ); break; case 'delete_contact': diff --git a/lib/screens/map_cache_screen.dart b/lib/screens/map_cache_screen.dart index 1391660..1eb59a8 100644 --- a/lib/screens/map_cache_screen.dart +++ b/lib/screens/map_cache_screen.dart @@ -8,6 +8,7 @@ import '../l10n/l10n.dart'; import '../services/app_settings_service.dart'; import '../services/map_tile_cache_service.dart'; import '../widgets/adaptive_app_bar_title.dart'; +import '../helpers/snack_bar_builder.dart'; class MapCacheScreen extends StatefulWidget { const MapCacheScreen({super.key}); @@ -112,15 +113,17 @@ class _MapCacheScreenState extends State { Future _startDownload() async { final bounds = _selectedBounds; if (bounds == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.mapCache_selectAreaFirst)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.mapCache_selectAreaFirst), ); return; } if (_estimatedTiles == 0) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.mapCache_noTilesToDownload)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.mapCache_noTilesToDownload), ); return; } @@ -182,9 +185,7 @@ class _MapCacheScreenState extends State { result.failed, ) : context.l10n.mapCache_cachedTiles(result.downloaded); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); + showDismissibleSnackBar(context, content: Text(message)); } Future _clearCache() async { @@ -210,8 +211,9 @@ class _MapCacheScreenState extends State { final cacheService = context.read(); await cacheService.clearCache(); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.mapCache_offlineCacheCleared)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.mapCache_offlineCacheCleared), ); } diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index f2d09f3..daf49d9 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -29,6 +29,7 @@ import 'chat_screen.dart'; import 'contacts_screen.dart'; import '../widgets/repeater_login_dialog.dart'; import '../widgets/room_login_dialog.dart'; +import '../helpers/snack_bar_builder.dart'; import 'repeater_hub_screen.dart'; import 'settings_screen.dart'; import 'line_of_sight_map_screen.dart'; @@ -1659,7 +1660,10 @@ class _MapScreenState extends State { ); await connector.refreshDeviceInfo(); if (!mounted) return; - messenger.showSnackBar(SnackBar(content: Text(successMsg))); + showDismissibleSnackBar( + messenger.context, + content: Text(successMsg), + ); }, ), ListTile( @@ -1681,8 +1685,9 @@ class _MapScreenState extends State { required String flags, }) async { if (!connector.isConnected) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.map_connectToShareMarkers)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.map_connectToShareMarkers), ); return; } @@ -2271,8 +2276,9 @@ class _MapScreenState extends State { _points.clear(); _polylines.clear(); }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.map_pathTraceCancelled)), + showDismissibleSnackBar( + context, + content: Text(l10n.map_pathTraceCancelled), ); }, tooltip: l10n.common_cancel, diff --git a/lib/screens/neighbors_screen.dart b/lib/screens/neighbors_screen.dart index 7286eb0..77559d4 100644 --- a/lib/screens/neighbors_screen.dart +++ b/lib/screens/neighbors_screen.dart @@ -11,6 +11,7 @@ import '../connector/meshcore_protocol.dart'; import '../services/repeater_command_service.dart'; import '../widgets/path_management_dialog.dart'; import '../widgets/snr_indicator.dart'; +import '../helpers/snack_bar_builder.dart'; class NeighborsScreen extends StatefulWidget { final Contact repeater; @@ -163,11 +164,10 @@ class _NeighborsScreenState extends State { _neighborCount = neighborCount; }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.neighbors_receivedData), - backgroundColor: Colors.green, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.neighbors_receivedData), + backgroundColor: Colors.green, ); _statusTimeout?.cancel(); if (!mounted) return; @@ -224,11 +224,10 @@ class _NeighborsScreenState extends State { _isLoading = false; _isLoaded = false; }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.neighbors_requestTimedOut), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.neighbors_requestTimedOut), + backgroundColor: Colors.red, ); _recordStatusResult(false); }); @@ -239,11 +238,10 @@ class _NeighborsScreenState extends State { _isLoaded = false; }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.neighbors_errorLoading(e.toString())), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.neighbors_errorLoading(e.toString())), + backgroundColor: Colors.red, ); } } diff --git a/lib/screens/repeater_cli_screen.dart b/lib/screens/repeater_cli_screen.dart index 5f76828..5e9a462 100644 --- a/lib/screens/repeater_cli_screen.dart +++ b/lib/screens/repeater_cli_screen.dart @@ -9,6 +9,7 @@ import '../connector/meshcore_protocol.dart'; import '../widgets/debug_frame_viewer.dart'; import '../services/repeater_command_service.dart'; import '../widgets/path_management_dialog.dart'; +import '../helpers/snack_bar_builder.dart'; class RepeaterCliScreen extends StatefulWidget { final Contact repeater; @@ -336,8 +337,9 @@ class _RepeaterCliScreenState extends State { if (_commandController.text.trim().isNotEmpty) { _sendCommand(showDebug: true); } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.repeater_enterCommandFirst)), + showDismissibleSnackBar( + context, + content: Text(l10n.repeater_enterCommandFirst), ); } }, diff --git a/lib/screens/repeater_settings_screen.dart b/lib/screens/repeater_settings_screen.dart index d0236bb..6d0b4e6 100644 --- a/lib/screens/repeater_settings_screen.dart +++ b/lib/screens/repeater_settings_screen.dart @@ -10,6 +10,7 @@ import '../services/app_debug_log_service.dart'; import '../services/repeater_command_service.dart'; import '../services/storage_service.dart'; import '../widgets/path_management_dialog.dart'; +import '../helpers/snack_bar_builder.dart'; class RepeaterSettingsScreen extends StatefulWidget { final Contact repeater; @@ -468,18 +469,16 @@ class _RepeaterSettingsScreenState extends State { if (mounted) { if (successCount > 0) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.repeater_refreshed(label)), - backgroundColor: Colors.green, - ), + showDismissibleSnackBar( + context, + content: Text(l10n.repeater_refreshed(label)), + backgroundColor: Colors.green, ); } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.repeater_errorRefreshing(label)), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(l10n.repeater_errorRefreshing(label)), + backgroundColor: Colors.red, ); } @@ -666,11 +665,10 @@ class _RepeaterSettingsScreenState extends State { }); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.repeater_settingsSaved), - backgroundColor: Colors.green, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.repeater_settingsSaved), + backgroundColor: Colors.green, ); } } catch (e) { @@ -679,13 +677,12 @@ class _RepeaterSettingsScreenState extends State { }); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.repeater_errorSavingSettings(e.toString()), - ), - backgroundColor: Colors.red, + showDismissibleSnackBar( + context, + content: Text( + context.l10n.repeater_errorSavingSettings(e.toString()), ), + backgroundColor: Colors.red, ); } } @@ -1429,9 +1426,10 @@ class _RepeaterSettingsScreenState extends State { if (command == 'erase') { if (mounted) { - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(l10n.repeater_eraseSerialOnly))); + content: Text(l10n.repeater_eraseSerialOnly), + ); } return; } @@ -1453,17 +1451,17 @@ class _RepeaterSettingsScreenState extends State { await connector.sendFrame(frame); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.repeater_commandSent(command))), + showDismissibleSnackBar( + context, + content: Text(l10n.repeater_commandSent(command)), ); } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.repeater_errorSendingCommand(e.toString())), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(l10n.repeater_errorSendingCommand(e.toString())), + backgroundColor: Colors.red, ); } } diff --git a/lib/screens/repeater_status_screen.dart b/lib/screens/repeater_status_screen.dart index f938419..720c32a 100644 --- a/lib/screens/repeater_status_screen.dart +++ b/lib/screens/repeater_status_screen.dart @@ -12,6 +12,7 @@ import '../services/app_settings_service.dart'; import '../services/repeater_command_service.dart'; import '../utils/battery_utils.dart'; import '../widgets/path_management_dialog.dart'; +import '../helpers/snack_bar_builder.dart'; class RepeaterStatusScreen extends StatefulWidget { final Contact repeater; @@ -309,11 +310,10 @@ class _RepeaterStatusScreenState extends State { setState(() { _isLoading = false; }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.repeater_statusRequestTimeout), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.repeater_statusRequestTimeout), + backgroundColor: Colors.red, ); _recordStatusResult(false); }); @@ -323,13 +323,10 @@ class _RepeaterStatusScreenState extends State { _isLoading = false; }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.repeater_errorLoadingStatus(e.toString()), - ), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.repeater_errorLoadingStatus(e.toString())), + backgroundColor: Colors.red, ); } _recordStatusResult(false); diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 17f26ea..a503ec0 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -10,6 +10,7 @@ import '../services/linux_ble_error_classifier.dart'; import '../utils/app_logger.dart'; import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/device_tile.dart'; +import '../helpers/snack_bar_builder.dart'; import 'contacts_screen.dart'; import 'tcp_screen.dart'; import 'usb_screen.dart'; @@ -317,11 +318,10 @@ class _ScannerScreenState extends State { return; } if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.scanner_connectionFailed(e.toString())), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.scanner_connectionFailed(e.toString())), + backgroundColor: Colors.red, ); } } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index e9b73f8..47b9b9c 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -11,6 +11,7 @@ import '../l10n/l10n.dart'; import '../models/radio_settings.dart'; import '../services/app_debug_log_service.dart'; import '../widgets/app_bar.dart'; +import '../helpers/snack_bar_builder.dart'; import 'app_settings_screen.dart'; import 'app_debug_log_screen.dart'; import 'ble_debug_log_screen.dart'; @@ -513,8 +514,9 @@ class _SettingsScreenState extends State { await connector.setNodeName(controller.text); await connector.refreshDeviceInfo(); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_nodeNameUpdated)), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_nodeNameUpdated), ); }, child: Text(l10n.common_save), @@ -628,10 +630,9 @@ class _SettingsScreenState extends State { final interval = int.tryParse(intervalText); if (interval == null || interval < 60 || interval >= 86400) { if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.settings_locationIntervalInvalid), - ), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_locationIntervalInvalid), ); return; } @@ -639,8 +640,9 @@ class _SettingsScreenState extends State { await connector.setCustomVar("gps_interval:$interval"); await connector.refreshDeviceInfo(); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_locationUpdated)), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_locationUpdated), ); } @@ -660,15 +662,17 @@ class _SettingsScreenState extends State { : currentLon; if (lat == null || lon == null) { if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_locationBothRequired)), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_locationBothRequired), ); return; } if (lat < -90 || lat > 90 || lon < -180 || lon > 180) { if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_locationInvalid)), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_locationInvalid), ); return; } @@ -676,8 +680,9 @@ class _SettingsScreenState extends State { await connector.setNodeLocation(lat: lat, lon: lon); await connector.refreshDeviceInfo(); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_locationUpdated)), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_locationUpdated), ); }, child: Text(l10n.common_save), @@ -691,9 +696,10 @@ class _SettingsScreenState extends State { void _syncTime(BuildContext context, MeshCoreConnector connector) { final l10n = context.l10n; connector.syncTime(); - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(l10n.settings_timeSynchronized))); + content: Text(l10n.settings_timeSynchronized), + ); } void _confirmReboot(BuildContext context, MeshCoreConnector connector) { @@ -758,23 +764,27 @@ class _SettingsScreenState extends State { if (!mounted) return; switch (result) { case gpxExportSuccess: - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportSuccess))); + content: Text(l10n.settings_gpxExportSuccess), + ); case gpxExportNoContacts: - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_gpxExportNoContacts)), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_gpxExportNoContacts), ); break; case gpxExportNotAvailable: - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_gpxExportNotAvailable)), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_gpxExportNotAvailable), ); break; case gpxExportFailed: - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportError))); + content: Text(l10n.settings_gpxExportError), + ); break; } } @@ -1077,8 +1087,9 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) { ); await connector.refreshDeviceInfo(); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_telemetryModeUpdated)), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_telemetryModeUpdated), ); }, child: Text(l10n.common_save), @@ -1410,18 +1421,18 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { final txPower = int.tryParse(_txPowerController.text); if (freqMHz == null || freqMHz < 300 || freqMHz > 2500) { - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(l10n.settings_frequencyInvalid))); + content: Text(l10n.settings_frequencyInvalid), + ); return; } final maxTxPower = widget.connector.maxTxPower ?? 22; if (txPower == null || txPower < 0 || txPower > maxTxPower) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'), - ), + showDismissibleSnackBar( + context, + content: Text('${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'), ); return; } @@ -1441,8 +1452,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { if (knownRepeat) { const validRepeatFreqsKHz = {433000, 869000, 918000}; if (_clientRepeat && !validRepeatFreqsKHz.contains(freqHz)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_clientRepeatFreqWarning)), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_clientRepeatFreqWarning), ); return; } @@ -1472,14 +1484,16 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { if (!mounted) return; _logRadioSettingsState('Radio settings saved successfully'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_radioSettingsUpdated)), + showDismissibleSnackBar( + context, + 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()))), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_error(e.toString())), ); } Navigator.pop(context); diff --git a/lib/screens/tcp_screen.dart b/lib/screens/tcp_screen.dart index 11ab80a..3bd1b0b 100644 --- a/lib/screens/tcp_screen.dart +++ b/lib/screens/tcp_screen.dart @@ -8,6 +8,7 @@ import '../l10n/l10n.dart'; import '../services/app_settings_service.dart'; import '../utils/platform_info.dart'; import '../widgets/adaptive_app_bar_title.dart'; +import '../helpers/snack_bar_builder.dart'; import 'contacts_screen.dart'; import 'usb_screen.dart'; @@ -270,8 +271,10 @@ class _TcpScreenState extends State { void _showError(String message) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message), backgroundColor: Colors.red), + showDismissibleSnackBar( + context, + content: Text(message), + backgroundColor: Colors.red, ); } diff --git a/lib/screens/telemetry_screen.dart b/lib/screens/telemetry_screen.dart index 66911dc..47593a3 100644 --- a/lib/screens/telemetry_screen.dart +++ b/lib/screens/telemetry_screen.dart @@ -14,6 +14,7 @@ import '../utils/app_logger.dart'; import '../widgets/path_management_dialog.dart'; import '../helpers/cayenne_lpp.dart'; import '../utils/battery_utils.dart'; +import '../helpers/snack_bar_builder.dart'; class TelemetryScreen extends StatefulWidget { final Contact contact; @@ -86,11 +87,10 @@ class _TelemetryScreenState extends State { _isLoading = false; _isLoaded = false; }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.telemetry_requestTimeout), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.telemetry_requestTimeout), + backgroundColor: Colors.red, ); _recordTelemetryResult(false); }); @@ -137,11 +137,10 @@ class _TelemetryScreenState extends State { _parsedTelemetry = parsedTelemetry; }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.telemetry_receivedData), - backgroundColor: Colors.green, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.telemetry_receivedData), + backgroundColor: Colors.green, ); _statusTimeout?.cancel(); if (!mounted) return; @@ -182,11 +181,10 @@ class _TelemetryScreenState extends State { _isLoaded = false; }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.telemetry_errorLoading(e.toString())), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.telemetry_errorLoading(e.toString())), + backgroundColor: Colors.red, ); } } diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index 2f2713a..6b8fe9d 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -10,6 +10,7 @@ import '../utils/app_logger.dart'; import '../utils/platform_info.dart'; import '../utils/usb_port_labels.dart'; import '../widgets/adaptive_app_bar_title.dart'; +import '../helpers/snack_bar_builder.dart'; import 'contacts_screen.dart'; import 'scanner_screen.dart'; import 'tcp_screen.dart'; @@ -383,11 +384,10 @@ class _UsbScreenState extends State { void _showError(Object error) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(_friendlyErrorMessage(error)), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(_friendlyErrorMessage(error)), + backgroundColor: Colors.red, ); } diff --git a/lib/widgets/path_management_dialog.dart b/lib/widgets/path_management_dialog.dart index 4e91a69..094805a 100644 --- a/lib/widgets/path_management_dialog.dart +++ b/lib/widgets/path_management_dialog.dart @@ -11,6 +11,7 @@ import '../l10n/l10n.dart'; import '../models/contact.dart'; import '../helpers/path_helper.dart'; import '../services/path_history_service.dart'; +import '../helpers/snack_bar_builder.dart'; import 'path_selection_dialog.dart'; class PathManagementDialog { @@ -65,11 +66,10 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { void _showFullPathDialog(BuildContext context, List pathBytes) { final l10n = context.l10n; if (pathBytes.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.chat_pathDetailsNotAvailable), - duration: const Duration(seconds: 2), - ), + showDismissibleSnackBar( + context, + content: Text(l10n.chat_pathDetailsNotAvailable), + duration: const Duration(seconds: 2), ); return; } @@ -159,11 +159,10 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { ); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.chat_hopsCount(result.length)), - duration: const Duration(seconds: 2), - ), + showDismissibleSnackBar( + context, + content: Text(l10n.chat_hopsCount(result.length)), + duration: const Duration(seconds: 2), ); } } @@ -337,13 +336,12 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { _showFullPathDialog(context, path.pathBytes), onTap: () async { if (path.pathBytes.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - l10n.chat_pathDetailsNotAvailable, - ), - duration: const Duration(seconds: 2), + showDismissibleSnackBar( + context, + content: Text( + l10n.chat_pathDetailsNotAvailable, ), + duration: const Duration(seconds: 2), ); return; } @@ -361,13 +359,12 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { if (!context.mounted) return; Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - l10n.path_usingHopsPath(path.hopCount), - ), - duration: const Duration(seconds: 2), + showDismissibleSnackBar( + context, + content: Text( + l10n.path_usingHopsPath(path.hopCount), ), + duration: const Duration(seconds: 2), ); }, ), @@ -459,11 +456,10 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { onTap: () async { await connector.clearContactPath(currentContact); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.chat_pathCleared), - duration: const Duration(seconds: 2), - ), + showDismissibleSnackBar( + context, + content: Text(l10n.chat_pathCleared), + duration: const Duration(seconds: 2), ); Navigator.pop(context); }, @@ -489,11 +485,10 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { pathLen: -1, ); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.chat_floodModeEnabled), - duration: const Duration(seconds: 2), - ), + showDismissibleSnackBar( + context, + content: Text(l10n.chat_floodModeEnabled), + duration: const Duration(seconds: 2), ); Navigator.pop(context); }, diff --git a/lib/widgets/path_selection_dialog.dart b/lib/widgets/path_selection_dialog.dart index b1733fc..7a890ec 100644 --- a/lib/widgets/path_selection_dialog.dart +++ b/lib/widgets/path_selection_dialog.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:meshcore_open/connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; +import '../helpers/snack_bar_builder.dart'; class PathSelectionDialog extends StatefulWidget { final List availableContacts; @@ -138,26 +139,22 @@ class _PathSelectionDialogState extends State { // Show error for invalid prefixes if (invalidPrefixes.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - l10n.path_invalidHexPrefixes(invalidPrefixes.join(", ")), - ), - duration: const Duration(seconds: 3), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(l10n.path_invalidHexPrefixes(invalidPrefixes.join(", "))), + duration: const Duration(seconds: 3), + backgroundColor: Colors.red, ); return; } // Check max path length (64 hops) if (pathBytesList.length > 64) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.path_tooLong), - duration: const Duration(seconds: 3), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(l10n.path_tooLong), + duration: const Duration(seconds: 3), + backgroundColor: Colors.red, ); return; } diff --git a/lib/widgets/room_login_dialog.dart b/lib/widgets/room_login_dialog.dart index 3a923fe..d4028a3 100644 --- a/lib/widgets/room_login_dialog.dart +++ b/lib/widgets/room_login_dialog.dart @@ -10,6 +10,7 @@ import '../services/storage_service.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../utils/app_logger.dart'; +import '../helpers/snack_bar_builder.dart'; import 'path_management_dialog.dart'; class RoomLoginDialog extends StatefulWidget { @@ -175,11 +176,10 @@ class _RoomLoginDialogState extends State { setState(() { _isLoggingIn = false; }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.login_failed(e.toString())), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.login_failed(e.toString())), + backgroundColor: Colors.red, ); } }