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 7a82c53a..8aa75b0d 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 30956e22..cf3c275f 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 d2ea57e9..4084d9b8 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 8aa75b0d..6a708e89 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 37db11ac..424cb6f5 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 b99ecf75..93c5dcd7 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 b42e3e5c..396d78b3 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 0f5145d9..09bde693 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 c156a444..08dbe854 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 d8d73ab9..bccb472b 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 245f7323..7fcd72b0 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 21b231af..c78380ae 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 dc96020e..77081d29 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 13a96024..e101fea0 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 adb4eea3..d991c7aa 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 6bccc197..5ce2eed0 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 db787b3a..fe2d1ead 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 2909278b..fc3003cf 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 4afefde3..e451072d 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 a420a55d..59dae81d 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 93a8bc9f..4404a61e 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 99125422..505262fc 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 dc6374a7..66c10858 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 3fc5e560..9e7a9904 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 03d70d47..d632b38f 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 5e5925f0..10620ebf 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 bcb2d5d5..fe784e9e 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 5c66761d..89adf74b 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 98c72f5c..1b347c47 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 41846415..ff6fb716 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 59f46bdc..37b37570 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 171353c4..536894ee 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 6a776d76..2e20f2a0 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 9ebead21..2335f378 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 6d3a856b..8672ac83 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 9f164fd9..013b074d 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 87b4754c..c499b83e 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 eb87a15f..d492fbd2 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 c9493a0f..18fa20ec 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 5a7aa6df..12b50815 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 9adb3878..8de65861 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 e4ace3e6..91743db0 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 8e27da1f..b8cabed9 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 cd7b44dd..9d23c8ef 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 94b8eeec..0eb2c220 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 d67d03da..51d24533 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 aecdc817..ab7082c3 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 01fb64d8..9c376769 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 d5b01f27..62a380bf 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 4e7c6e8e..3f9d9655 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 9616d472..f2d09f35 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 5b029313..7f3b4eb5 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 52d92aac..5f76828e 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 b0165bdd..296cc3ae 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 ce6c2b7f..48bb6ac9 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 91d2c8c8..3a923fe8 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 d9e0d209..a0dedac1 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 93c5dcd7..280b7e9d 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 a0dedac1..c90827b5 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 c90827b5..44019dd8 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 44019dd8..e7d61ee7 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 6a708e89..7a82c53a 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 cf3c275f..99f20539 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 93c5dcd7..a44ba373 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 0eb2c220..53769d40 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 f4c16734..7286eb03 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 c804340a..228fc39d 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 b42e3e5c..396d78b3 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 13e9de7e..cd822e3e 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 62badcef..10af5dac 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 06175532..b703630e 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 4d465bb9..0372dffc 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 16e1d3db..d74c3588 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 cf42e1b0..68b3b11a 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 b9676bbd..9b539a05 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 6a9c975c..aef8fc05 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 2050e3b2..66ad1ed9 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 e2bd2f3f..408a2436 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 283860e1..8a433224 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 e29ae9e9..1177bc12 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 877e11d1..9104f8b7 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 c9639028..cc3b7140 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 eea88f51..402e3738 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 5e36e94a..204e21ba 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 bb9e0d25..936ecc16 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 5151ab84..7accee3e 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 be645456..06d7db63 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 86809df5..6b7bbe79 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 89528158..b6296a4d 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 43dc27a3..d1f66af2 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 703d80dd..cb2ae158 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 980657df..8ddea4b7 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 ad2a2788..07c1c01f 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 cc590c29..87457744 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 dd7bf634..fc0abea7 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 8910dcd1..f9ff7099 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 cb1a11c0..ac3ddca3 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 aa3049f3..cf530af8 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 c667cb07..f88c5e0d 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 730cfc92..13eac229 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 cf99ca8a..43e408fd 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 0c29a862..3ef08b19 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 3232888e..9f317dba 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 ddab5769..a0cce7e9 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 766be449..2e19a8ee 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 94b8eeec..0eb2c220 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 d67d03da..51d24533 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 372e3e7c..082374f0 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 01fb64d8..9c376769 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 d5b01f27..62a380bf 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 4e7c6e8e..3f9d9655 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 9616d472..f2d09f35 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 5b029313..7f3b4eb5 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 52d92aac..5f76828e 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 b0165bdd..296cc3ae 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 ce6c2b7f..48bb6ac9 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 91d2c8c8..3a923fe8 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 228fc39d..108ccc70 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 0eb2c220..53769d40 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 f4c16734..7286eb03 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 37db11ac..424cb6f5 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 b703630e..ffdf21de 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 408a2436..bb390d5b 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 8a433224..bec54dfd 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 d4fd3ad7..078c9e95 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 9104f8b7..d7a79bd2 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 cc3b7140..9a56c6df 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 402e3738..4ce4a753 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 204e21ba..bbf989e0 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 936ecc16..98cbfcb8 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 13c84442..fc59852b 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 06d7db63..b0d849b6 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 bbeb25fe..6fcad22b 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 9c6d85b7..84927023 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 e5f38482..ce7c9e8c 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 425b3ff2..4557885e 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 8ddea4b7..b59d6d80 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 0a5b5400..e5bf031c 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 b87111c9..731b8467 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 608f4368..1b5a8007 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 f9ff7099..acadc58a 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 628ae1cc..1ca0ee9c 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 082374f0..b0f0c0c4 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 9e26dfee..1ebd9bc5 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 779856c5..88295e7c 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 379e36fa..93e46829 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 f02857f4..533a1712 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 62a380bf..46e2be6a 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 e7d61ee7..e9b73f83 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 2428a778..ffc8c590 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 a8934f10..a436b469 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 b42e3e5c..396d78b3 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 13e9de7e..cd822e3e 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 62badcef..10af5dac 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 06175532..b703630e 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 4d465bb9..0372dffc 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 16e1d3db..d74c3588 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 cf42e1b0..68b3b11a 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 b9676bbd..9b539a05 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 6a9c975c..aef8fc05 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 2050e3b2..66ad1ed9 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 e2bd2f3f..408a2436 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 283860e1..8a433224 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 e29ae9e9..1177bc12 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 877e11d1..9104f8b7 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 c9639028..cc3b7140 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 eea88f51..402e3738 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 5e36e94a..204e21ba 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 bb9e0d25..936ecc16 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 5151ab84..7accee3e 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 be645456..06d7db63 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 86809df5..6b7bbe79 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 89528158..b6296a4d 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 43dc27a3..d1f66af2 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 703d80dd..cb2ae158 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 980657df..8ddea4b7 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 ad2a2788..07c1c01f 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 cc590c29..87457744 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 dd7bf634..fc0abea7 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 8910dcd1..f9ff7099 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 cb1a11c0..ac3ddca3 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 aa3049f3..cf530af8 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 c667cb07..f88c5e0d 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 730cfc92..13eac229 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 cf99ca8a..43e408fd 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 0c29a862..3ef08b19 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 3232888e..9f317dba 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 ddab5769..a0cce7e9 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 766be449..2e19a8ee 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 94b8eeec..0eb2c220 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 d67d03da..51d24533 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 8057f1f5..4cda7127 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 01fb64d8..9c376769 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 d5b01f27..62a380bf 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 4e7c6e8e..3f9d9655 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 9616d472..f2d09f35 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 5b029313..7f3b4eb5 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 52d92aac..5f76828e 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 b0165bdd..296cc3ae 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 ce6c2b7f..48bb6ac9 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 91d2c8c8..3a923fe8 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 a436b469..5f0ccdb6 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 0eb2c220..53769d40 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 f4c16734..7286eb03 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 37db11ac..424cb6f5 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 0372dffc..5d98e4ef 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 68b3b11a..2a1e7171 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 aef8fc05..e11adfe8 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 66ad1ed9..06dc20c0 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 6b7bbe79..9ec0118b 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 43e408fd..50d42d29 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 2e19a8ee..5dd58962 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 53769d40..0eb2c220 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 0eb2c220..53769d40 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 b703630e..ffdf21de 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 408a2436..bb390d5b 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 8a433224..bec54dfd 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 1177bc12..cc6d6ed5 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 9104f8b7..d7a79bd2 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 cc3b7140..9a56c6df 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 402e3738..4ce4a753 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 204e21ba..bbf989e0 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 936ecc16..98cbfcb8 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 7accee3e..40845e9b 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 06d7db63..b0d849b6 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 9ec0118b..ae066a98 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 b6296a4d..ed66e52f 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 d1f66af2..1aebdcf2 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 cb2ae158..d8f38fed 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 8ddea4b7..b59d6d80 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 07c1c01f..c204c508 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 87457744..6b9ffb5a 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 fc0abea7..f6745c3b 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 f9ff7099..acadc58a 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 7beaaf4c..64da058f 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 4cda7127..a4ebc76f 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 9e26dfee..1ebd9bc5 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 779856c5..88295e7c 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 379e36fa..93e46829 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 f02857f4..533a1712 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 7a82c53a..8aa75b0d 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 30956e22..cf3c275f 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 ffc8c590..2428a778 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 8aa75b0d..6a708e89 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 6a708e89..7a82c53a 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 cf3c275f..99f20539 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 62a380bf..46e2be6a 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 d9e0d209..a0dedac1 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 5f0ccdb6..b4322773 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 a0dedac1..c90827b5 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 c90827b5..44019dd8 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 44019dd8..e7d61ee7 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 e7d61ee7..e9b73f83 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 8dd187b1..5b68e902 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 169b1a14..36118ca3 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 bbf989e0..5d305ee5 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 37303a0c..6fcad22b 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 2428a778..ffc8c590 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 567ed242..718bfd97 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 2503372b..54683d21 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 ffdf21de..1ac23572 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 5d98e4ef..6f95d81f 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 00b861e9..4b0497b5 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 e7671a4a..3553b180 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 6ebe4f94..d0e195e9 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 bc387206..6f851162 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 f63badc9..bd73847f 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 bb390d5b..efcbd0f2 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 bec54dfd..bb07229d 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 078c9e95..49cf19a7 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 d7a79bd2..e13934b8 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 9a56c6df..ddb9b6e2 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 4ce4a753..fbe106d7 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 5d305ee5..920efd88 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 98cbfcb8..b492d6af 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 fc59852b..daebcba6 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 b0d849b6..605cc96b 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 6fcad22b..3d1644f0 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 84927023..f0006b18 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 ce7c9e8c..f4b7ffca 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 4557885e..dcd9d9c8 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 b59d6d80..7323ddcc 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 e5bf031c..a374d4bb 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 731b8467..6e2f563a 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 1b5a8007..dd189ebc 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 acadc58a..b48b31df 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 490c269f..abfd5e7e 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 928bd0d6..e626708e 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 8693a288..bacc1caf 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 bdb9beef..e4dad428 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 50d42d29..937bacbf 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 6fc76a84..58d0f9e1 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 eb7de6c2..59b27a40 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 3fac32bf..c19f3bdd 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 f054f795..3fbfc399 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 6375e0b3..d0236bb2 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 a86c1f6d..0c78c592 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 48bb6ac9..521cfd2a 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 ffdf21de..f1044809 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 bb390d5b..3b89d699 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 bec54dfd..ec12a78d 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 078c9e95..4d3aa4b6 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 d7a79bd2..73a93081 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 9a56c6df..0dfde3f5 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 4ce4a753..6b577239 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 5d305ee5..052a3c73 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 98cbfcb8..6f0d5eac 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 fc59852b..9ffc8aba 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 b0d849b6..9142657e 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 6fcad22b..b1919196 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 84927023..ce9549d9 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 ce7c9e8c..7679d272 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 4557885e..493eeb48 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 b59d6d80..e8104fa9 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 e5bf031c..cfef1709 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 731b8467..d3ca0bf4 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 1b5a8007..fe568e72 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 acadc58a..e496cd09 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 46e2be6a..50c47dc9 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 f2d09f35..de801ee0 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 8a14253e..0dc141c7 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 48bb6ac9..f4db904d 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 3a923fe8..22062273 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 1ebd9bc5..2b4bbbc7 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 cd7fb678..91956020 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 b931ca12..c2eae294 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 00000000..d7409b6d --- /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 48770388..ca6a6bf1 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 cd7fb678..f4cab7f6 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 1009bc40..6d186970 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 64da058f..e5b5f67e 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 51d24533..44c7a69c 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 a4ebc76f..2aee61c3 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 6852dfa2..6b71715f 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 46e2be6a..5a6f3591 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 3f9d9655..f9f0e07e 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 1391660e..1eb59a86 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 f2d09f35..daf49d9d 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 7286eb03..77559d48 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 5f76828e..5e9a462d 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 d0236bb2..6d0b4e6c 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 f9384195..720c32a8 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 17f26ea4..a503ec0e 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 e9b73f83..47b9b9c6 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 11ab80a9..3bd1b0bf 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 66911dc5..47593a3f 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 2f2713ad..6b8fe9d9 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 4e91a69d..094805a1 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 b1733fcd..7a890ec5 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 3a923fe8..d4028a39 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, ); } }