From 6d63e49938d0d783d3a9dee2e28d0a8bd20b16e4 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 06:54:27 -0800 Subject: [PATCH 01/23] add platforminfo helper --- lib/connector/meshcore_connector.dart | 4 ++-- lib/screens/scanner_screen.dart | 5 ++-- lib/services/background_service.dart | 9 ++++---- lib/utils/gpx_export.dart | 5 ++++ lib/utils/platform_info.dart | 32 ++++++++++++++++++++++++++ lib/widgets/repeater_login_dialog.dart | 4 ++-- lib/widgets/room_login_dialog.dart | 4 ++-- 7 files changed, 49 insertions(+), 14 deletions(-) create mode 100644 lib/utils/platform_info.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index afd1626..773e182 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -30,6 +30,7 @@ import '../storage/message_store.dart'; import '../storage/unread_store.dart'; import '../utils/app_logger.dart'; import '../utils/battery_utils.dart'; +import '../utils/platform_info.dart'; import 'meshcore_protocol.dart'; class MeshCoreUuids { @@ -693,8 +694,7 @@ class MeshCoreConnector extends ChangeNotifier { await _scanSubscription?.cancel(); // On iOS/macOS, wait for Bluetooth to be powered on before scanning - if (defaultTargetPlatform == TargetPlatform.iOS || - defaultTargetPlatform == TargetPlatform.macOS) { + if (PlatformInfo.isIOS || PlatformInfo.isMacOS) { // Wait for adapter state to be powered on final adapterState = await FlutterBluePlus.adapterState.first; if (adapterState != BluetoothAdapterState.on) { diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index af9d75e..b5dedc1 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -1,7 +1,6 @@ import 'dart:async'; -import 'dart:io' show Platform; - import 'package:flutter/material.dart'; +import '../utils/platform_info.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:provider/provider.dart'; @@ -265,7 +264,7 @@ class _ScannerScreenState extends State { ], ), ), - if (Platform.isAndroid) + if (PlatformInfo.isAndroid) TextButton( onPressed: () => FlutterBluePlus.turnOn(), child: Text(context.l10n.scanner_enableBluetooth), diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart index 0edd393..6202b3b 100644 --- a/lib/services/background_service.dart +++ b/lib/services/background_service.dart @@ -1,12 +1,11 @@ -import 'dart:io'; - +import '../utils/platform_info.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; class BackgroundService { bool _initialized = false; Future initialize() async { - if (!Platform.isAndroid || _initialized) return; + if (!PlatformInfo.isAndroid || _initialized) return; FlutterForegroundTask.init( androidNotificationOptions: AndroidNotificationOptions( channelId: 'meshcore_background', @@ -29,7 +28,7 @@ class BackgroundService { } Future start() async { - if (!Platform.isAndroid) return; + if (!PlatformInfo.isAndroid) return; if (!_initialized) { await initialize(); } @@ -43,7 +42,7 @@ class BackgroundService { } Future stop() async { - if (!Platform.isAndroid) return; + if (!PlatformInfo.isAndroid) return; final running = await FlutterForegroundTask.isRunningService; if (!running) return; await FlutterForegroundTask.stopService(); diff --git a/lib/utils/gpx_export.dart b/lib/utils/gpx_export.dart index 595479c..b0165bd 100644 --- a/lib/utils/gpx_export.dart +++ b/lib/utils/gpx_export.dart @@ -4,6 +4,7 @@ import 'package:meshcore_open/connector/meshcore_connector.dart'; import 'package:meshcore_open/connector/meshcore_protocol.dart'; import 'package:path_provider/path_provider.dart'; import 'dart:io'; +import '../utils/platform_info.dart'; import 'package:share_plus/share_plus.dart'; @@ -109,6 +110,10 @@ class GpxExport { String shareText, String subject, ) async { + if (PlatformInfo.isWeb) { + debugPrint("GPX export is not supported on Web."); + return gpxExportNotAvailable; + } if (_contacts.isEmpty) { debugPrint("No repeaters to export – nothing to share."); return gpxExportNoContacts; diff --git a/lib/utils/platform_info.dart b/lib/utils/platform_info.dart new file mode 100644 index 0000000..399bf13 --- /dev/null +++ b/lib/utils/platform_info.dart @@ -0,0 +1,32 @@ +import 'package:flutter/foundation.dart'; +import 'dart:io' show Platform; + +/// Utility class to safely check the current platform across web and native. +/// +/// Using `Platform` from `dart:io` directly on Web causes a crash. +/// This class handles the `kIsWeb` check first to avoid those crashes. +class PlatformInfo { + /// Whether the app is running in a web browser. + static bool get isWeb => kIsWeb; + + /// Whether the app is running on Android. + static bool get isAndroid => !kIsWeb && Platform.isAndroid; + + /// Whether the app is running on iOS. + static bool get isIOS => !kIsWeb && Platform.isIOS; + + /// Whether the app is running on macOS. + static bool get isMacOS => !kIsWeb && Platform.isMacOS; + + /// Whether the app is running on Windows. + static bool get isWindows => !kIsWeb && Platform.isWindows; + + /// Whether the app is running on Linux. + static bool get isLinux => !kIsWeb && Platform.isLinux; + + /// Whether the app is running on a mobile platform (Android or iOS). + static bool get isMobile => isAndroid || isIOS; + + /// Whether the app is running on a desktop platform (macOS, Windows, or Linux). + static bool get isDesktop => isMacOS || isWindows || isLinux; +} diff --git a/lib/widgets/repeater_login_dialog.dart b/lib/widgets/repeater_login_dialog.dart index b550cc2..ec0af66 100644 --- a/lib/widgets/repeater_login_dialog.dart +++ b/lib/widgets/repeater_login_dialog.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import '../utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; @@ -326,8 +327,7 @@ class _RepeaterLoginDialogState extends State { }, onSubmitted: (_) => _handleLogin(), autofocus: - !(defaultTargetPlatform == TargetPlatform.android || - defaultTargetPlatform == TargetPlatform.iOS) && + !PlatformInfo.isMobile && _passwordController.text.isEmpty, ), const SizedBox(height: 12), diff --git a/lib/widgets/room_login_dialog.dart b/lib/widgets/room_login_dialog.dart index cba7bec..7324f44 100644 --- a/lib/widgets/room_login_dialog.dart +++ b/lib/widgets/room_login_dialog.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import '../utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; @@ -274,8 +275,7 @@ class _RoomLoginDialogState extends State { ), onSubmitted: (_) => _handleLogin(), autofocus: - !(defaultTargetPlatform == TargetPlatform.android || - defaultTargetPlatform == TargetPlatform.iOS) && + !PlatformInfo.isMobile && _passwordController.text.isEmpty, ), const SizedBox(height: 12), From b5e47ce44f7dda117361a1683cb755c90ec26db3 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 07:09:35 -0800 Subject: [PATCH 02/23] filter BLE at OS level --- lib/connector/meshcore_connector.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 773e182..f8eabdb 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -727,6 +727,7 @@ class MeshCoreConnector extends ChangeNotifier { }); await FlutterBluePlus.startScan( + withServices: [Guid(MeshCoreUuids.service)], timeout: timeout, androidScanMode: AndroidScanMode.lowLatency, ); From cf8f01128b5a364d3e61ca131cecd94619390891 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 07:09:35 -0800 Subject: [PATCH 03/23] filter BLE at OS level --- lib/connector/meshcore_connector.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index afd1626..63da3ba 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -727,6 +727,7 @@ class MeshCoreConnector extends ChangeNotifier { }); await FlutterBluePlus.startScan( + withServices: [Guid(MeshCoreUuids.service)], timeout: timeout, androidScanMode: AndroidScanMode.lowLatency, ); From 5676cbd84e9de4835efd53f0f4d6a7688ca299ed Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 07:40:40 -0800 Subject: [PATCH 04/23] chrome required screen --- lib/l10n/app_bg.arb | 2 + lib/l10n/app_de.arb | 2 + lib/l10n/app_en.arb | 2 + lib/l10n/app_es.arb | 2 + lib/l10n/app_fr.arb | 2 + lib/l10n/app_it.arb | 2 + lib/l10n/app_localizations.dart | 12 ++++ lib/l10n/app_localizations_bg.dart | 7 ++ lib/l10n/app_localizations_de.dart | 7 ++ lib/l10n/app_localizations_en.dart | 7 ++ lib/l10n/app_localizations_es.dart | 7 ++ lib/l10n/app_localizations_fr.dart | 7 ++ lib/l10n/app_localizations_it.dart | 7 ++ lib/l10n/app_localizations_nl.dart | 7 ++ lib/l10n/app_localizations_pl.dart | 7 ++ lib/l10n/app_localizations_pt.dart | 7 ++ lib/l10n/app_localizations_ru.dart | 7 ++ lib/l10n/app_localizations_sk.dart | 7 ++ lib/l10n/app_localizations_sl.dart | 7 ++ lib/l10n/app_localizations_sv.dart | 7 ++ lib/l10n/app_localizations_uk.dart | 7 ++ lib/l10n/app_localizations_zh.dart | 7 ++ lib/l10n/app_nl.arb | 2 + lib/l10n/app_pl.arb | 2 + lib/l10n/app_pt.arb | 2 + lib/l10n/app_ru.arb | 2 + lib/l10n/app_sk.arb | 2 + lib/l10n/app_sl.arb | 2 + lib/l10n/app_sv.arb | 2 + lib/l10n/app_uk.arb | 2 + lib/l10n/app_zh.arb | 2 + lib/main.dart | 7 +- lib/screens/chrome_required_screen.dart | 91 +++++++++++++++++++++++++ lib/utils/browser_detection.dart | 2 + lib/utils/browser_detection_stub.dart | 3 + lib/utils/browser_detection_web.dart | 15 ++++ lib/utils/platform_info.dart | 4 ++ 37 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 lib/screens/chrome_required_screen.dart create mode 100644 lib/utils/browser_detection.dart create mode 100644 lib/utils/browser_detection_stub.dart create mode 100644 lib/utils/browser_detection_web.dart diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 8609023..848b6de 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1596,6 +1596,8 @@ "scanner_bluetoothOff": "Bluetooth е изключен.", "scanner_enableBluetooth": "Активирайте Bluetooth", "scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.", + "scanner_chromeRequired": "Изисква се браузър Chrome", + "scanner_chromeRequiredMessage": "Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.", "snrIndicator_lastSeen": "Последно видян", "snrIndicator_nearByRepeaters": "Близки повтарящи се устройства", "chat_ShowAllPaths": "Покажи всички пътища", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index e5c82f7..e645bdc 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1622,6 +1622,8 @@ "pathTrace_clearTooltip": "Pfad löschen", "map_pathTraceCancelled": "Pfadverfolgung abgebrochen.", "scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.", + "scanner_chromeRequired": "Chrome Browser erforderlich", + "scanner_chromeRequiredMessage": "Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.", "scanner_bluetoothOff": "Bluetooth ist deaktiviert.", "scanner_enableBluetooth": "Bluetooth aktivieren", "snrIndicator_lastSeen": "Zuletzt gesehen", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 67ca72e..7f1ecbd 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -72,6 +72,8 @@ "scanner_scan": "Scan", "scanner_bluetoothOff": "Bluetooth is off", "scanner_bluetoothOffMessage": "Please turn on Bluetooth to scan for devices", + "scanner_chromeRequired": "Chrome Browser Required", + "scanner_chromeRequiredMessage": "This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.", "scanner_enableBluetooth": "Enable Bluetooth", "device_quickSwitch": "Quick switch", "device_meshcore": "MeshCore", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 483b4d3..5e333e5 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1622,6 +1622,8 @@ "map_removeLast": "Eliminar último", "map_pathTraceCancelled": "Rastreo de ruta cancelado.", "scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.", + "scanner_chromeRequired": "Navegador Chrome requerido", + "scanner_chromeRequiredMessage": "Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.", "scanner_bluetoothOff": "Bluetooth está desactivado.", "scanner_enableBluetooth": "Habilitar Bluetooth", "snrIndicator_nearByRepeaters": "Repetidores cercanos", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index e162cdb..a0ae3b8 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1594,6 +1594,8 @@ "map_removeLast": "Supprimer le dernier", "map_runTrace": "Exécuter la traçage de chemin", "scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.", + "scanner_chromeRequired": "Navigateur Chrome requis", + "scanner_chromeRequiredMessage": "Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.", "scanner_bluetoothOff": "Le Bluetooth est désactivé.", "scanner_enableBluetooth": "Activer le Bluetooth", "snrIndicator_lastSeen": "Dernière fois vu", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 2f8d186..93359ac 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1595,6 +1595,8 @@ "map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.", "scanner_bluetoothOff": "Il Bluetooth è disattivato.", "scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.", + "scanner_chromeRequired": "Browser Chrome richiesto", + "scanner_chromeRequiredMessage": "Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.", "scanner_enableBluetooth": "Abilita il Bluetooth", "snrIndicator_nearByRepeaters": "Ripetitori vicini", "snrIndicator_lastSeen": "Ultimo accesso", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e9686ce..76210fd 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -388,6 +388,18 @@ abstract class AppLocalizations { /// **'Please turn on Bluetooth to scan for devices'** String get scanner_bluetoothOffMessage; + /// No description provided for @scanner_chromeRequired. + /// + /// In en, this message translates to: + /// **'Chrome Browser Required'** + String get scanner_chromeRequired; + + /// No description provided for @scanner_chromeRequiredMessage. + /// + /// In en, this message translates to: + /// **'This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.'** + String get scanner_chromeRequiredMessage; + /// No description provided for @scanner_enableBluetooth. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index cf4bf7b..971d189 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -150,6 +150,13 @@ class AppLocalizationsBg extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Моля, активирайте Bluetooth, за да сканирате за устройства.'; + @override + String get scanner_chromeRequired => 'Изисква се браузър Chrome'; + + @override + String get scanner_chromeRequiredMessage => + 'Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.'; + @override String get scanner_enableBluetooth => 'Активирайте Bluetooth'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index c6a07a4..f5cb3d4 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -150,6 +150,13 @@ class AppLocalizationsDe extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.'; + @override + String get scanner_chromeRequired => 'Chrome Browser erforderlich'; + + @override + String get scanner_chromeRequiredMessage => + 'Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.'; + @override String get scanner_enableBluetooth => 'Bluetooth aktivieren'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 254b5f4..a858737 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -149,6 +149,13 @@ class AppLocalizationsEn extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Please turn on Bluetooth to scan for devices'; + @override + String get scanner_chromeRequired => 'Chrome Browser Required'; + + @override + String get scanner_chromeRequiredMessage => + 'This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.'; + @override String get scanner_enableBluetooth => 'Enable Bluetooth'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index dcde365..1d0bb2d 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -150,6 +150,13 @@ class AppLocalizationsEs extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Por favor, active el Bluetooth para escanear dispositivos.'; + @override + String get scanner_chromeRequired => 'Navegador Chrome requerido'; + + @override + String get scanner_chromeRequiredMessage => + 'Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.'; + @override String get scanner_enableBluetooth => 'Habilitar Bluetooth'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index d572b8f..8ad6c74 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -150,6 +150,13 @@ class AppLocalizationsFr extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Veuillez activer le Bluetooth pour rechercher des appareils.'; + @override + String get scanner_chromeRequired => 'Navigateur Chrome requis'; + + @override + String get scanner_chromeRequiredMessage => + 'Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.'; + @override String get scanner_enableBluetooth => 'Activer le Bluetooth'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index d8e27f8..29bb627 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -150,6 +150,13 @@ class AppLocalizationsIt extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.'; + @override + String get scanner_chromeRequired => 'Browser Chrome richiesto'; + + @override + String get scanner_chromeRequiredMessage => + 'Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.'; + @override String get scanner_enableBluetooth => 'Abilita il Bluetooth'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 0a50e8b..77cc083 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -149,6 +149,13 @@ class AppLocalizationsNl extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.'; + @override + String get scanner_chromeRequired => 'Chrome-browser vereist'; + + @override + String get scanner_chromeRequiredMessage => + 'Deze webapplicatie vereist Google Chrome of een op Chromium gebaseerde browser voor Bluetooth-ondersteuning.'; + @override String get scanner_enableBluetooth => 'Activeer Bluetooth'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 31dd8b5..d9c0539 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -150,6 +150,13 @@ class AppLocalizationsPl extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Prosimy włączyć Bluetooth, aby przeskanować urządzenia.'; + @override + String get scanner_chromeRequired => 'Wymagana przeglądarka Chrome'; + + @override + String get scanner_chromeRequiredMessage => + 'Ta aplikacja internetowa wymaga przeglądarki Google Chrome lub opartej na Chromium do obsługi Bluetooth.'; + @override String get scanner_enableBluetooth => 'Włącz Bluetooth'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 5092826..640988f 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -150,6 +150,13 @@ class AppLocalizationsPt extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Por favor, ative o Bluetooth para escanear por dispositivos.'; + @override + String get scanner_chromeRequired => 'Navegador Chrome necessário'; + + @override + String get scanner_chromeRequiredMessage => + 'Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.'; + @override String get scanner_enableBluetooth => 'Ative o Bluetooth'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 570b7c8..4de8db2 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -149,6 +149,13 @@ class AppLocalizationsRu extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Пожалуйста, включите Bluetooth, чтобы найти устройства.'; + @override + String get scanner_chromeRequired => 'Требуется браузер Chrome'; + + @override + String get scanner_chromeRequiredMessage => + 'Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.'; + @override String get scanner_enableBluetooth => 'Включите Bluetooth'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 8bbb6de..fe87700 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -150,6 +150,13 @@ class AppLocalizationsSk extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.'; + @override + String get scanner_chromeRequired => 'Vyžaduje sa prehliadač Chrome'; + + @override + String get scanner_chromeRequiredMessage => + 'Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.'; + @override String get scanner_enableBluetooth => 'Povolte Bluetooth'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 61e3058..5d9901a 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -150,6 +150,13 @@ class AppLocalizationsSl extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Prosimo, vklopite Bluetooth, da lahko poiščete naprave.'; + @override + String get scanner_chromeRequired => 'Zahtevan brskalnik Chrome'; + + @override + String get scanner_chromeRequiredMessage => + 'Ta spletna aplikacija za podporo Bluetooth zahteva Google Chrome ali brskalnik na osnovi Chromiuma.'; + @override String get scanner_enableBluetooth => 'Omogočite Bluetooth'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 79b30b8..c5a0ee9 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -149,6 +149,13 @@ class AppLocalizationsSv extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Vänligen aktivera Bluetooth för att söka efter enheter.'; + @override + String get scanner_chromeRequired => 'Chrome-webbläsare krävs'; + + @override + String get scanner_chromeRequiredMessage => + 'Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.'; + @override String get scanner_enableBluetooth => 'Aktivera Bluetooth'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index f367002..35b5143 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -150,6 +150,13 @@ class AppLocalizationsUk extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.'; + @override + String get scanner_chromeRequired => 'Потрібен браузер Chrome'; + + @override + String get scanner_chromeRequiredMessage => + 'Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.'; + @override String get scanner_enableBluetooth => 'Увімкніть Bluetooth'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 7641800..a97e0fb 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -148,6 +148,13 @@ class AppLocalizationsZh extends AppLocalizations { @override String get scanner_bluetoothOffMessage => '请打开蓝牙功能,以便搜索设备。'; + @override + String get scanner_chromeRequired => '需要 Chrome 浏览器'; + + @override + String get scanner_chromeRequiredMessage => + '此 Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。'; + @override String get scanner_enableBluetooth => '启用蓝牙'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 57b2fdd..5a5b0ab 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1595,6 +1595,8 @@ "map_runTrace": "Padeshulp traceren", "scanner_enableBluetooth": "Activeer Bluetooth", "scanner_bluetoothOffMessage": "Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.", + "scanner_chromeRequired": "Chrome-browser vereist", + "scanner_chromeRequiredMessage": "Deze webapplicatie vereist Google Chrome of een op Chromium gebaseerde browser voor Bluetooth-ondersteuning.", "scanner_bluetoothOff": "Bluetooth is uitgeschakeld", "snrIndicator_lastSeen": "Laatst gezien", "snrIndicator_nearByRepeaters": "Nabije herhalingseenheden", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 3787fa7..72af443 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1594,6 +1594,8 @@ "map_removeLast": "Usuń ostatni", "map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki.", "scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urządzenia.", + "scanner_chromeRequired": "Wymagana przeglądarka Chrome", + "scanner_chromeRequiredMessage": "Ta aplikacja internetowa wymaga przeglądarki Google Chrome lub opartej na Chromium do obsługi Bluetooth.", "scanner_bluetoothOff": "Bluetooth jest wyłączony", "scanner_enableBluetooth": "Włącz Bluetooth", "snrIndicator_lastSeen": "Ostatnio widziany", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 7be6694..35c4635 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1596,6 +1596,8 @@ "scanner_enableBluetooth": "Ative o Bluetooth", "scanner_bluetoothOff": "Bluetooth está desativado", "scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos.", + "scanner_chromeRequired": "Navegador Chrome necessário", + "scanner_chromeRequiredMessage": "Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.", "snrIndicator_nearByRepeaters": "Repetidores Próximos", "snrIndicator_lastSeen": "Visto pela última vez", "chat_ShowAllPaths": "Mostrar todos os caminhos", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 26cfce3..d4d1939 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -836,6 +836,8 @@ "scanner_enableBluetooth": "Включите Bluetooth", "scanner_bluetoothOff": "Bluetooth выключен", "scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.", + "scanner_chromeRequired": "Требуется браузер Chrome", + "scanner_chromeRequiredMessage": "Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.", "snrIndicator_nearByRepeaters": "Ближайшие ретрансляторы", "snrIndicator_lastSeen": "Последний раз видели", "chat_ShowAllPaths": "Показать все пути", diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 8b2cb0a..8e8d1e8 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1594,6 +1594,8 @@ "map_runTrace": "Spustiť trasovaním cesty", "map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.", "scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.", + "scanner_chromeRequired": "Vyžaduje sa prehliadač Chrome", + "scanner_chromeRequiredMessage": "Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.", "scanner_bluetoothOff": "Bluetooth je vypnutý", "scanner_enableBluetooth": "Povolte Bluetooth", "snrIndicator_lastSeen": "Naposledy videný", diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 4d3415d..2e749b9 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1595,6 +1595,8 @@ "map_pathTraceCancelled": "Spremljanje poti je prekinjeno.", "scanner_enableBluetooth": "Omogočite Bluetooth", "scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.", + "scanner_chromeRequired": "Zahtevan brskalnik Chrome", + "scanner_chromeRequiredMessage": "Ta spletna aplikacija za podporo Bluetooth zahteva Google Chrome ali brskalnik na osnovi Chromiuma.", "scanner_bluetoothOff": "Bluetooth je izklopljen", "snrIndicator_lastSeen": "Zadnjič videno", "snrIndicator_nearByRepeaters": "Bližnji ponovitelji", diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 8c5e399..b311a45 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1595,6 +1595,8 @@ "map_removeLast": "Ta bort sista", "scanner_enableBluetooth": "Aktivera Bluetooth", "scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.", + "scanner_chromeRequired": "Chrome-webbläsare krävs", + "scanner_chromeRequiredMessage": "Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.", "scanner_bluetoothOff": "Bluetooth är avstängt", "snrIndicator_lastSeen": "Senast sedd", "snrIndicator_nearByRepeaters": "Närliggande uppreparstationer", diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 910f8b0..f5d3a42 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1595,6 +1595,8 @@ "map_pathTraceCancelled": "Відмінується трасування шляху", "scanner_enableBluetooth": "Увімкніть Bluetooth", "scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.", + "scanner_chromeRequired": "Потрібен браузер Chrome", + "scanner_chromeRequiredMessage": "Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.", "scanner_bluetoothOff": "Bluetooth вимкнено", "snrIndicator_lastSeen": "Останній раз бачили", "snrIndicator_nearByRepeaters": "Ближні ретранслятори", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index d9efce7..1fac003 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1594,6 +1594,8 @@ "map_removeLast": "删除最后一个", "map_runTrace": "运行路径跟踪", "scanner_bluetoothOffMessage": "请打开蓝牙功能,以便搜索设备。", + "scanner_chromeRequired": "需要 Chrome 浏览器", + "scanner_chromeRequiredMessage": "此 Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。", "scanner_bluetoothOff": "蓝牙已关闭", "scanner_enableBluetooth": "启用蓝牙", "snrIndicator_lastSeen": "最近访问", diff --git a/lib/main.dart b/lib/main.dart index 3650a7e..af065ef 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,9 @@ import 'package:flutter/foundation.dart'; import 'l10n/app_localizations.dart'; import 'package:provider/provider.dart'; +import 'screens/chrome_required_screen.dart'; +import 'utils/platform_info.dart'; + import 'connector/meshcore_connector.dart'; import 'screens/scanner_screen.dart'; import 'services/storage_service.dart'; @@ -179,7 +182,9 @@ class MeshCoreApp extends StatelessWidget { NotificationService().setLocale(locale); return child ?? const SizedBox.shrink(); }, - home: const ScannerScreen(), + home: (PlatformInfo.isWeb && !PlatformInfo.isChrome) + ? const ChromeRequiredScreen() + : const ScannerScreen(), ); }, ), diff --git a/lib/screens/chrome_required_screen.dart b/lib/screens/chrome_required_screen.dart new file mode 100644 index 0000000..a229c0a --- /dev/null +++ b/lib/screens/chrome_required_screen.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import '../l10n/l10n.dart'; + +class ChromeRequiredScreen extends StatelessWidget { + const ChromeRequiredScreen({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + + return Scaffold( + body: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 32), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isDark + ? [const Color(0xFF1A1A1A), const Color(0xFF0D0D0D)] + : [const Color(0xFFF5F7FA), const Color(0xFFE4E7EB)], + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.orange.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.browser_not_supported_rounded, + size: 80, + color: Colors.orange, + ), + ), + const SizedBox(height: 32), + Text( + l10n.scanner_chromeRequired, + textAlign: TextAlign.center, + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black87, + ), + ), + const SizedBox(height: 16), + Text( + l10n.scanner_chromeRequiredMessage, + textAlign: TextAlign.center, + style: theme.textTheme.bodyLarge?.copyWith( + color: isDark ? Colors.white70 : Colors.black54, + height: 1.5, + ), + ), + const SizedBox(height: 48), + // We can't really "fix" it for them other than telling them to use Chrome + // but we can provide a nice visual. + Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + decoration: BoxDecoration( + color: Colors.blue.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(30), + border: Border.all( + color: Colors.blue.withValues(alpha: 0.3), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.info_outline, size: 20, color: Colors.blue), + const SizedBox(width: 12), + Text( + "Web Bluetooth requires a Chromium browser", + style: theme.textTheme.bodyMedium?.copyWith( + color: Colors.blue, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/utils/browser_detection.dart b/lib/utils/browser_detection.dart new file mode 100644 index 0000000..aafb838 --- /dev/null +++ b/lib/utils/browser_detection.dart @@ -0,0 +1,2 @@ +export 'browser_detection_stub.dart' + if (dart.library.html) 'browser_detection_web.dart'; diff --git a/lib/utils/browser_detection_stub.dart b/lib/utils/browser_detection_stub.dart new file mode 100644 index 0000000..f872d9a --- /dev/null +++ b/lib/utils/browser_detection_stub.dart @@ -0,0 +1,3 @@ +class BrowserDetection { + static bool get isChrome => false; +} diff --git a/lib/utils/browser_detection_web.dart b/lib/utils/browser_detection_web.dart new file mode 100644 index 0000000..e5b4b69 --- /dev/null +++ b/lib/utils/browser_detection_web.dart @@ -0,0 +1,15 @@ +// ignore: avoid_web_libraries_in_flutter +import 'dart:html' as html; + +class BrowserDetection { + static bool get isChrome { + final userAgent = html.window.navigator.userAgent.toLowerCase(); + final vendor = html.window.navigator.vendor.toLowerCase(); + + // Chrome UA typically contains 'chrome' and vendor is 'Google Inc.' + // This also excludes Firefox, Safari, and sometimes Edge/Brave depending on strictness. + // For Web Bluetooth, Chrome, Edge (latest), and Brave usually work. + // But we'll follow the user's request for "isChrome". + return userAgent.contains('chrome') && vendor.contains('google'); + } +} diff --git a/lib/utils/platform_info.dart b/lib/utils/platform_info.dart index 399bf13..dc8e27e 100644 --- a/lib/utils/platform_info.dart +++ b/lib/utils/platform_info.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'dart:io' show Platform; +import 'browser_detection.dart'; /// Utility class to safely check the current platform across web and native. /// @@ -9,6 +10,9 @@ class PlatformInfo { /// Whether the app is running in a web browser. static bool get isWeb => kIsWeb; + /// Whether the app is running in the Chrome browser (only relevant if [isWeb] is true). + static bool get isChrome => isWeb && BrowserDetection.isChrome; + /// Whether the app is running on Android. static bool get isAndroid => !kIsWeb && Platform.isAndroid; From 5522f9a236648b270dc45cc83cb23a81317ae888 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 08:05:19 -0800 Subject: [PATCH 05/23] BLE select cancel --- lib/connector/meshcore_connector.dart | 145 +++++++++++++++++--------- lib/screens/scanner_screen.dart | 29 ++++-- 2 files changed, 111 insertions(+), 63 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index f8eabdb..ef24be7 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -686,64 +686,96 @@ class MeshCoreConnector extends ChangeNotifier { }) async { if (_state == MeshCoreConnectionState.scanning) return; - _scanResults.clear(); - _setState(MeshCoreConnectionState.scanning); - - // Ensure any previous scan is fully stopped - await FlutterBluePlus.stopScan(); - await _scanSubscription?.cancel(); - - // On iOS/macOS, wait for Bluetooth to be powered on before scanning - if (PlatformInfo.isIOS || PlatformInfo.isMacOS) { - // Wait for adapter state to be powered on - final adapterState = await FlutterBluePlus.adapterState.first; - if (adapterState != BluetoothAdapterState.on) { - // Wait for the adapter to turn on, with timeout - await FlutterBluePlus.adapterState - .firstWhere((state) => state == BluetoothAdapterState.on) - .timeout( - const Duration(seconds: 5), - onTimeout: () { - _setState(MeshCoreConnectionState.disconnected); - throw Exception('Bluetooth adapter not available'); - }, - ); - } - - // Add a small delay to allow BLE stack to fully initialize - await Future.delayed(const Duration(milliseconds: 300)); - } - - _scanSubscription = FlutterBluePlus.scanResults.listen((results) { + try { _scanResults.clear(); - for (var result in results) { - if (result.device.platformName.startsWith("MeshCore-") || - result.advertisementData.advName.startsWith("MeshCore-") || - result.advertisementData.advName.startsWith("Whisper-")) { - _scanResults.add(result); + _setState(MeshCoreConnectionState.scanning); + + // Ensure any previous scan is fully stopped + try { + await FlutterBluePlus.stopScan(); + } catch (_) {} + + try { + await _scanSubscription?.cancel(); + } catch (_) {} + _scanSubscription = null; + + // On iOS/macOS, wait for Bluetooth to be powered on before scanning + if (PlatformInfo.isIOS || PlatformInfo.isMacOS) { + // Wait for adapter state to be powered on + final adapterState = await FlutterBluePlus.adapterState.first; + if (adapterState != BluetoothAdapterState.on) { + // Wait for the adapter to turn on, with timeout + await FlutterBluePlus.adapterState + .firstWhere((state) => state == BluetoothAdapterState.on) + .timeout( + const Duration(seconds: 5), + onTimeout: () { + _setState(MeshCoreConnectionState.disconnected); + throw Exception('Bluetooth adapter not available'); + }, + ); } + + // Add a small delay to allow BLE stack to fully initialize + await Future.delayed(const Duration(milliseconds: 300)); } - notifyListeners(); - }); - await FlutterBluePlus.startScan( - withServices: [Guid(MeshCoreUuids.service)], - timeout: timeout, - androidScanMode: AndroidScanMode.lowLatency, - ); + _scanSubscription = FlutterBluePlus.scanResults.listen( + (results) { + _scanResults.clear(); + for (var result in results) { + if (result.device.platformName.startsWith("MeshCore-") || + result.advertisementData.advName.startsWith("MeshCore-") || + result.advertisementData.advName.startsWith("Whisper-")) { + _scanResults.add(result); + } + } + notifyListeners(); + }, + onError: (Object e) { + debugPrint("scanResults stream error: $e"); + stopScan(); + }, + ); - await Future.delayed(timeout); - await stopScan(); + await FlutterBluePlus.startScan( + withServices: [Guid(MeshCoreUuids.service)], + timeout: timeout, + androidScanMode: AndroidScanMode.lowLatency, + ); + + await Future.delayed(timeout); + } catch (e) { + debugPrint("Scan error: $e"); + // On web, suppress common cancellation and chooser errors + if (kIsWeb) return; + + if (!PlatformInfo.isWeb) { + rethrow; + } + } finally { + await stopScan(); + } } Future stopScan() async { - await FlutterBluePlus.stopScan(); - await _scanSubscription?.cancel(); - _scanSubscription = null; - if (_state == MeshCoreConnectionState.scanning) { _setState(MeshCoreConnectionState.disconnected); } + + try { + await FlutterBluePlus.stopScan(); + } catch (e) { + debugPrint("stopScan error: $e"); + } + + try { + if (_scanSubscription != null) { + await _scanSubscription!.cancel(); + } + } catch (_) {} + _scanSubscription = null; } Future connect(BluetoothDevice device, {String? displayName}) async { @@ -770,11 +802,17 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); try { - _connectionSubscription = device.connectionState.listen((state) { - if (state == BluetoothConnectionState.disconnected && isConnected) { - _handleDisconnection(); - } - }); + _connectionSubscription = device.connectionState.listen( + (state) { + if (state == BluetoothConnectionState.disconnected && isConnected) { + _handleDisconnection(); + } + }, + onError: (Object e) { + debugPrint("connectionState stream error: $e"); + if (isConnected) _handleDisconnection(); + }, + ); await device.connect( timeout: const Duration(seconds: 15), @@ -833,6 +871,9 @@ class MeshCoreConnector extends ChangeNotifier { } _notifySubscription = _txCharacteristic!.onValueReceived.listen( _handleFrame, + onError: (Object e) { + debugPrint("onValueReceived stream error: $e"); + }, ); _setState(MeshCoreConnectionState.connected); diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index b5dedc1..bfd1230 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -45,17 +45,22 @@ class _ScannerScreenState extends State { connector.addListener(_connectionListener); - _bluetoothStateSubscription = FlutterBluePlus.adapterState.listen((state) { - if (mounted) { - setState(() { - _bluetoothState = state; - }); - // Cancel scan if Bluetooth turns off while scanning - if (state != BluetoothAdapterState.on) { - unawaited(connector.stopScan()); + _bluetoothStateSubscription = FlutterBluePlus.adapterState.listen( + (state) { + if (mounted) { + setState(() { + _bluetoothState = state; + }); + // Cancel scan if Bluetooth turns off while scanning + if (state != BluetoothAdapterState.on) { + unawaited(connector.stopScan()); + } } - } - }); + }, + onError: (Object e) { + debugPrint("Scanner adapterState stream error: $e"); + }, + ); } @override @@ -107,7 +112,9 @@ class _ScannerScreenState extends State { if (isScanning) { connector.stopScan(); } else { - connector.startScan(); + unawaited(connector.startScan().catchError((e) { + debugPrint("Scanner screen startScan error: $e"); + })); } }, icon: isScanning From 6ac987e7cf0b80e86cbc963d5a4127ff2f39c508 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 08:10:16 -0800 Subject: [PATCH 06/23] select BLE device --- lib/connector/meshcore_connector.dart | 28 +++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index ef24be7..e4a3324 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -739,13 +739,23 @@ class MeshCoreConnector extends ChangeNotifier { }, ); - await FlutterBluePlus.startScan( - withServices: [Guid(MeshCoreUuids.service)], - timeout: timeout, - androidScanMode: AndroidScanMode.lowLatency, - ); + if (PlatformInfo.isWeb) { + await FlutterBluePlus.startScan( + withServices: [Guid(MeshCoreUuids.service)], + ); + // On web, the chooser returns once a device is picked, but the scanResults + // stream might take a moment to emit the last result. Wait briefly so the + // device appears in the UI before stopScan() clears the list. + await Future.delayed(const Duration(milliseconds: 500)); + } else { + await FlutterBluePlus.startScan( + withServices: [Guid(MeshCoreUuids.service)], + timeout: timeout, + androidScanMode: AndroidScanMode.lowLatency, + ); - await Future.delayed(timeout); + await Future.delayed(timeout); + } } catch (e) { debugPrint("Scan error: $e"); // On web, suppress common cancellation and chooser errors @@ -776,6 +786,12 @@ class MeshCoreConnector extends ChangeNotifier { } } catch (_) {} _scanSubscription = null; + + // On web, don't clear results immediately so the picked device remains visible + if (!PlatformInfo.isWeb) { + _scanResults.clear(); + notifyListeners(); + } } Future connect(BluetoothDevice device, {String? displayName}) async { From 452e5337f0ac84ad79f18ace74f22123f820bae7 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 08:31:29 -0800 Subject: [PATCH 07/23] chrome connect --- lib/connector/meshcore_connector.dart | 61 ++++++++++++++++++--------- 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index e4a3324..8eacdab 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -837,11 +837,17 @@ class MeshCoreConnector extends ChangeNotifier { ); // Request larger MTU for sending larger frames - try { - final mtu = await device.requestMtu(185); - debugPrint('MTU set to: $mtu'); - } catch (e) { - debugPrint('MTU request failed: $e, using default'); + if (!PlatformInfo.isWeb) { + try { + final mtu = await device.requestMtu(185); + debugPrint('MTU set to: $mtu'); + } catch (e) { + debugPrint('MTU request failed: $e, using default'); + } + } else { + // On Chrome Web Bluetooth, give the GATT connection a moment to settle + // before discovering services, which is a common quirk to avoid timeouts. + await Future.delayed(const Duration(milliseconds: 500)); } List services = await device.discoverServices(); @@ -871,20 +877,7 @@ class MeshCoreConnector extends ChangeNotifier { throw Exception("MeshCore characteristics not found"); } - // Retry setNotifyValue with increasing delays - bool notifySet = false; - for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { - try { - if (attempt > 0) { - await Future.delayed(Duration(milliseconds: 500 * attempt)); - } - await _txCharacteristic!.setNotifyValue(true); - notifySet = true; - } catch (e) { - debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); - if (attempt == 2) rethrow; - } - } + // Setup listener BEFORE enabling notifications so we don't miss anything _notifySubscription = _txCharacteristic!.onValueReceived.listen( _handleFrame, onError: (Object e) { @@ -892,6 +885,36 @@ class MeshCoreConnector extends ChangeNotifier { }, ); + debugPrint('Starting setNotifyValue(true)'); + if (PlatformInfo.isWeb) { + // On Web, setNotifyValue often hangs indefinitely on the Promise resolution. + // We trigger it but don't await its completion to avoid blocking the connection flow. + debugPrint('Web: Calling setNotifyValue(true) without awaiting'); + // ignore: unawaited_futures + _txCharacteristic!.setNotifyValue(true).catchError((e) { + debugPrint('Web setNotifyValue error (ignoring): $e'); + }); + // Give the browser a moment to process the underlying startNotifications call + await Future.delayed(const Duration(milliseconds: 500)); + } else { + // Native platforms handle setNotifyValue blockingly with CCCD descriptors + bool notifySet = false; + for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { + try { + if (attempt > 0) { + await Future.delayed(Duration(milliseconds: 500 * attempt)); + } + debugPrint('Calling setNotifyValue(true), attempt ${attempt + 1}'); + await _txCharacteristic!.setNotifyValue(true); + notifySet = true; + } catch (e) { + debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); + if (attempt == 2) rethrow; + } + } + } + debugPrint('setNotifyValue(true) configuration completed'); + _setState(MeshCoreConnectionState.connected); await _requestDeviceInfo(); From ab05cf8b3e8d07c5397e24b26f3a3cb4a8c6fca5 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 08:33:45 -0800 Subject: [PATCH 08/23] chrome BLE sync --- lib/connector/meshcore_connector.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 8eacdab..c558171 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -891,8 +891,9 @@ class MeshCoreConnector extends ChangeNotifier { // We trigger it but don't await its completion to avoid blocking the connection flow. debugPrint('Web: Calling setNotifyValue(true) without awaiting'); // ignore: unawaited_futures - _txCharacteristic!.setNotifyValue(true).catchError((e) { + _txCharacteristic!.setNotifyValue(true, timeout: 2).catchError((e) { debugPrint('Web setNotifyValue error (ignoring): $e'); + return false; // catchError must return a bool to match Future }); // Give the browser a moment to process the underlying startNotifications call await Future.delayed(const Duration(milliseconds: 500)); From 71129bdf4d773c030eb2ac2a9d405e71da2eebe4 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 08:37:07 -0800 Subject: [PATCH 09/23] chrome BLE load fix --- lib/connector/meshcore_connector.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index c558171..9f94e72 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -893,7 +893,6 @@ class MeshCoreConnector extends ChangeNotifier { // ignore: unawaited_futures _txCharacteristic!.setNotifyValue(true, timeout: 2).catchError((e) { debugPrint('Web setNotifyValue error (ignoring): $e'); - return false; // catchError must return a bool to match Future }); // Give the browser a moment to process the underlying startNotifications call await Future.delayed(const Duration(milliseconds: 500)); From 8e07440114074ea72ec8348b438a121eb8a092c3 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 08:38:22 -0800 Subject: [PATCH 10/23] BLE fix --- lib/connector/meshcore_connector.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 9f94e72..c558171 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -893,6 +893,7 @@ class MeshCoreConnector extends ChangeNotifier { // ignore: unawaited_futures _txCharacteristic!.setNotifyValue(true, timeout: 2).catchError((e) { debugPrint('Web setNotifyValue error (ignoring): $e'); + return false; // catchError must return a bool to match Future }); // Give the browser a moment to process the underlying startNotifications call await Future.delayed(const Duration(milliseconds: 500)); From 1b4d31a36e32b3eb2990dabd1bbcf4e399557263 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 09:11:49 -0800 Subject: [PATCH 11/23] gitignore update --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 2d9a3fc..0fc464a 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,6 @@ keystore.properties # IDE .vscode/launch.json .vscode/settings.json + +# Cloudflare Wrangler +.wrangler \ No newline at end of file From a5555bd6062ee4361d2aa45b9822623ae29e8b18 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 09:16:07 -0800 Subject: [PATCH 12/23] fix: return cursor to message window after send --- lib/screens/channel_chat_screen.dart | 1 + lib/screens/chat_screen.dart | 1 + lib/screens/repeater_cli_screen.dart | 1 + 3 files changed, 3 insertions(+) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index bf05110..9f40684 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -935,6 +935,7 @@ class _ChannelChatScreenState extends State { connector.sendChannelMessage(widget.channel, messageText); _textController.clear(); _cancelReply(); + _textFieldFocusNode.requestFocus(); } String _formatTime(DateTime time) { diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index ad897a0..ea657ff 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -429,6 +429,7 @@ class _ChatScreenState extends State { connector.sendMessage(widget.contact, text); _textController.clear(); + _textFieldFocusNode.requestFocus(); } void _showPathHistory(BuildContext context) { diff --git a/lib/screens/repeater_cli_screen.dart b/lib/screens/repeater_cli_screen.dart index abfb06a..1c7ff43 100644 --- a/lib/screens/repeater_cli_screen.dart +++ b/lib/screens/repeater_cli_screen.dart @@ -168,6 +168,7 @@ class _RepeaterCliScreenState extends State { _commandController.clear(); _historyIndex = -1; + _commandFocusNode.requestFocus(); // Auto-scroll to bottom Future.delayed(const Duration(milliseconds: 100), () { From 9865a03c536127ecc2373293e0f49895a082f33c Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 09:20:20 -0800 Subject: [PATCH 13/23] fix: to send giphy --- lib/screens/channel_chat_screen.dart | 59 +++++++++++++++++----------- lib/screens/chat_screen.dart | 55 ++++++++++++++++---------- 2 files changed, 72 insertions(+), 42 deletions(-) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 9f40684..8c979c0 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -849,30 +849,45 @@ class _ChannelChatScreenState extends State { builder: (context, value, child) { final gifId = _parseGifId(value.text); if (gifId != null) { - return Row( - children: [ - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - fallbackTextColor: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.6), - maxSize: 160, + return Focus( + autofocus: true, + onKeyEvent: (node, event) { + if (event is KeyDownEvent && + (event.logicalKey == LogicalKeyboardKey.enter || + event.logicalKey == LogicalKeyboardKey.numpadEnter)) { + _sendMessage(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: Row( + children: [ + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + fallbackTextColor: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.6), + maxSize: 160, + ), ), ), - ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => _textController.clear(), - ), - ], + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + _textController.clear(); + _textFieldFocusNode.requestFocus(); + }, + ), + ], + ), ); } diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index ea657ff..c604ea6 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -340,28 +340,43 @@ class _ChatScreenState extends State { builder: (context, value, child) { final gifId = _parseGifId(value.text); if (gifId != null) { - return Row( - children: [ - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: - colorScheme.surfaceContainerHighest, - fallbackTextColor: colorScheme.onSurface - .withValues(alpha: 0.6), - maxSize: 160, + return Focus( + autofocus: true, + onKeyEvent: (node, event) { + if (event is KeyDownEvent && + (event.logicalKey == LogicalKeyboardKey.enter || + event.logicalKey == LogicalKeyboardKey.numpadEnter)) { + _sendMessage(connector); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: Row( + children: [ + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: + colorScheme.surfaceContainerHighest, + fallbackTextColor: colorScheme.onSurface + .withValues(alpha: 0.6), + maxSize: 160, + ), ), ), - ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => _textController.clear(), - ), - ], + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + _textController.clear(); + _textFieldFocusNode.requestFocus(); + }, + ), + ], + ), ); } From 377f1df445aa64d890db6b609176d6032dd9e37a Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 10:47:51 -0800 Subject: [PATCH 14/23] fix: browser detection --- lib/utils/browser_detection_web.dart | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/utils/browser_detection_web.dart b/lib/utils/browser_detection_web.dart index e5b4b69..0e4fccb 100644 --- a/lib/utils/browser_detection_web.dart +++ b/lib/utils/browser_detection_web.dart @@ -4,12 +4,7 @@ import 'dart:html' as html; class BrowserDetection { static bool get isChrome { final userAgent = html.window.navigator.userAgent.toLowerCase(); - final vendor = html.window.navigator.vendor.toLowerCase(); - - // Chrome UA typically contains 'chrome' and vendor is 'Google Inc.' - // This also excludes Firefox, Safari, and sometimes Edge/Brave depending on strictness. - // For Web Bluetooth, Chrome, Edge (latest), and Brave usually work. - // But we'll follow the user's request for "isChrome". - return userAgent.contains('chrome') && vendor.contains('google'); + final isChrome = userAgent.contains('chrome'); + return isChrome; } } From 40ac95e8e665d20ed736c621fbccc01d130ce31f Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 10:48:22 -0800 Subject: [PATCH 15/23] wrangler deploy --- .github/workflows/deploy.yml | 38 ++++++++++++++++++++++++++++++++++ package.json | 7 +++++++ pubspec.lock | 40 ++++++++++++++++++++++++++++++++++++ pubspec.yaml | 14 +++++++++++++ wrangler.toml | 7 +++++++ 5 files changed, 106 insertions(+) create mode 100644 .github/workflows/deploy.yml create mode 100644 package.json create mode 100644 wrangler.toml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..892a95e --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,38 @@ +name: Deploy to Cloudflare Workers + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + # Match local development version which provides Dart 3.11.0 + flutter-version: '3.41.2' + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Get dependencies + run: flutter pub get + + - name: Build Web + run: bun run build + + - name: Deploy to Cloudflare + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: deploy diff --git a/package.json b/package.json new file mode 100644 index 0000000..1684721 --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "name": "meshcore-open", + "scripts": { + "build": "dart run build_pipe:build", + "deploy": "bun x wrangler deploy" + } +} diff --git a/pubspec.lock b/pubspec.lock index f695838..18eaff5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _discoveryapis_commons: + dependency: transitive + description: + name: _discoveryapis_commons + sha256: "113c4100b90a5b70a983541782431b82168b3cae166ab130649c36eb3559d498" + url: "https://pub.dev" + source: hosted + version: "1.0.7" archive: dependency: transitive description: @@ -41,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + build_pipe: + dependency: "direct main" + description: + name: build_pipe + sha256: "3cdfd6e2327805074a9a5c815e49b1cb781764c57d84097c257e0dce4534f419" + url: "https://pub.dev" + source: hosted + version: "0.3.1" cached_network_image: dependency: "direct main" description: @@ -365,6 +381,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" + url: "https://pub.dev" + source: hosted + version: "0.3.3+1" + googleapis: + dependency: transitive + description: + name: googleapis + sha256: "692fb9e90c321b61a7a2123de0353ec8a20691cd979db2553d8d732f710f6535" + url: "https://pub.dev" + source: hosted + version: "15.0.0" + googleapis_auth: + dependency: transitive + description: + name: googleapis_auth + sha256: b81fe352cc4a330b3710d2b7ad258d9bcef6f909bb759b306bf42973a7d046db + url: "https://pub.dev" + source: hosted + version: "2.0.0" gpx: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f5ceaaf..a17edde 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: gpx: ^2.3.0 path_provider: ^2.1.5 share_plus: ^12.0.1 + build_pipe: ^0.3.1 dev_dependencies: flutter_test: @@ -124,3 +125,16 @@ flutter_launcher_icons: # # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-package + +build_pipe: + workflows: + default: + clean_flutter: true # Optional: Cleans old build artifacts before running + generate_log: true # Optional: Outputs a build log file for debugging + platforms: + web: + build: + build_command: flutter build web --release --pwa-strategy=none + # Strongly recommended: disables the default service worker which often causes more cache headaches + add_version_query_param: true + # This is the key flag! It appends ?v= to bootstrap/JS files \ No newline at end of file diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 0000000..f3c57d9 --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,7 @@ +#:schema node_modules/wrangler/config-schema.json +name = "meshcore" +compatibility_date = "2025-10-08" + +[assets] +directory = "build/web" + From a1ee0789a6b5a3b68caefd84d73a6625948d002c Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 11:04:54 -0800 Subject: [PATCH 16/23] deploy on tag only --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 892a95e..c3926c9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,8 +2,8 @@ name: Deploy to Cloudflare Workers on: push: - branches: - - main + tags: + - '*' workflow_dispatch: jobs: From c284e571b089c4519d98078b038f00c25e762c1b Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 11:27:06 -0800 Subject: [PATCH 17/23] hide message tracing --- lib/l10n/app_bg.arb | 4 +- lib/l10n/app_de.arb | 4 +- lib/l10n/app_en.arb | 2 + lib/l10n/app_es.arb | 4 +- lib/l10n/app_fr.arb | 4 +- lib/l10n/app_it.arb | 4 +- 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_it.dart | 8 + lib/l10n/app_localizations_nl.dart | 7 + lib/l10n/app_localizations_pl.dart | 7 + lib/l10n/app_localizations_pt.dart | 8 + lib/l10n/app_localizations_ru.dart | 8 + lib/l10n/app_localizations_sk.dart | 7 + lib/l10n/app_localizations_sl.dart | 7 + lib/l10n/app_localizations_sv.dart | 7 + lib/l10n/app_localizations_uk.dart | 8 + lib/l10n/app_localizations_zh.dart | 6 + lib/l10n/app_nl.arb | 4 +- lib/l10n/app_pl.arb | 4 +- lib/l10n/app_pt.arb | 4 +- lib/l10n/app_ru.arb | 4 +- lib/l10n/app_sk.arb | 4 +- lib/l10n/app_sl.arb | 4 +- lib/l10n/app_sv.arb | 4 +- lib/l10n/app_uk.arb | 4 +- lib/l10n/app_zh.arb | 4 +- lib/models/app_settings.dart | 6 + lib/screens/app_settings_screen.dart | 12 ++ lib/screens/channel_chat_screen.dart | 231 ++++++++++++++++--------- lib/screens/chat_screen.dart | 225 +++++++++++++++--------- lib/services/app_settings_service.dart | 4 + macos/Podfile.lock | 17 +- 37 files changed, 491 insertions(+), 186 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 848b6de..bd7fb3f 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1554,6 +1554,8 @@ "contacts_clipboardEmpty": "Клипборда е празна.", "contacts_invalidAdvertFormat": "Невалидни данни за контакт", "appSettings_languageRu": "Руски", + "appSettings_enableMessageTracing": "Разрешаване на проследяване на съобщения", + "appSettings_enableMessageTracingSubtitle": "Показване на подробни метаданни за маршрутизация и синхронизация за съобщения", "contacts_contactImported": "Контактът е импортиран.", "contacts_zeroHopAdvert": "Реклама без скок", "contacts_contactImportFailed": "Контактът не е успешно импортиран.", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "Показване на LOS панел", "losHidePanelTooltip": "Скриване на LOS панела", "losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index e645bdc..e5b0c46 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1554,6 +1554,8 @@ "contacts_invalidAdvertFormat": "Ungültige Kontaktdaten", "contacts_clipboardEmpty": "Die Zwischenablage ist leer.", "appSettings_languageUk": "Ukrainisch", + "appSettings_enableMessageTracing": "Nachrichtenverfolgung aktivieren", + "appSettings_enableMessageTracingSubtitle": "Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen", "contacts_contactImported": "Kontakt wurde importiert.", "contacts_contactImportFailed": "Kontakt konnte nicht importiert werden", "contacts_zeroHopAdvert": "Zero-Hop-Ankündigung", @@ -1747,4 +1749,4 @@ "losShowPanelTooltip": "LOS-Panel anzeigen", "losHidePanelTooltip": "LOS-Panel ausblenden", "losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7f1ecbd..12145f0 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -185,6 +185,8 @@ "appSettings_languageBg": "Български", "appSettings_languageRu": "Русский", "appSettings_languageUk": "Українська", + "appSettings_enableMessageTracing": "Enable Message Tracing", + "appSettings_enableMessageTracingSubtitle": "Show detailed routing and timing metadata for messages", "appSettings_notifications": "Notifications", "appSettings_enableNotifications": "Enable Notifications", "appSettings_enableNotificationsSubtitle": "Receive notifications for messages and adverts", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 5e333e5..ab4febb 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1553,6 +1553,8 @@ "appSettings_languageUk": "Ucraniano", "contacts_clipboardEmpty": "El portapapeles está vacío.", "appSettings_languageRu": "Ruso", + "appSettings_enableMessageTracing": "Habilitar seguimiento de mensajes", + "appSettings_enableMessageTracingSubtitle": "Mostrar metadatos detallados de enrutamiento y tiempo para los mensajes", "contacts_invalidAdvertFormat": "Datos de contacto no válidos", "contacts_floodAdvert": "Anuncio de inundación", "contacts_contactImported": "El contacto ha sido importado.", @@ -1747,4 +1749,4 @@ "losShowPanelTooltip": "Mostrar panel LOS", "losHidePanelTooltip": "Ocultar panel LOS", "losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index a0ae3b8..61b4e63 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1553,6 +1553,8 @@ "contacts_invalidAdvertFormat": "Données de contact non valides", "appSettings_languageUk": "Ukrainien", "appSettings_languageRu": "Russe", + "appSettings_enableMessageTracing": "Activer le traçage des messages", + "appSettings_enableMessageTracingSubtitle": "Afficher les métadonnées détaillées de routage et de synchronisation des messages", "contacts_clipboardEmpty": "Le presse-papiers est vide.", "contacts_contactImported": "Le contact a été importé.", "contacts_floodAdvert": "Annonce à tout le réseau", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "Afficher le panneau LOS", "losHidePanelTooltip": "Masquer le panneau LOS", "losElevationAttribution": "Données d'altitude : Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 93359ac..894874b 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1553,6 +1553,8 @@ "appSettings_languageRu": "Russo", "contacts_invalidAdvertFormat": "Dati di contatto non validi", "appSettings_languageUk": "Ucraino", + "appSettings_enableMessageTracing": "Abilita tracciamento messaggi", + "appSettings_enableMessageTracingSubtitle": "Mostra metadati dettagliati su instradamento e tempi per i messaggi", "contacts_zeroHopAdvert": "Annuncio Zero Hop", "contacts_floodAdvert": "Annuncio alluvionale", "contacts_copyAdvertToClipboard": "Copia Annuncio negli Appunti", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "Mostra il pannello LOS", "losHidePanelTooltip": "Nascondi il pannello LOS", "losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 76210fd..b1f9762 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -982,6 +982,18 @@ abstract class AppLocalizations { /// **'Українська'** String get appSettings_languageUk; + /// No description provided for @appSettings_enableMessageTracing. + /// + /// In en, this message translates to: + /// **'Enable Message Tracing'** + String get appSettings_enableMessageTracing; + + /// No description provided for @appSettings_enableMessageTracingSubtitle. + /// + /// In en, this message translates to: + /// **'Show detailed routing and timing metadata for messages'** + String get appSettings_enableMessageTracingSubtitle; + /// No description provided for @appSettings_notifications. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 971d189..6e4f5fa 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -473,6 +473,14 @@ class AppLocalizationsBg extends AppLocalizations { @override String get appSettings_languageUk => 'Украински'; + @override + String get appSettings_enableMessageTracing => + 'Разрешаване на проследяване на съобщения'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Показване на подробни метаданни за маршрутизация и синхронизация за съобщения'; + @override String get appSettings_notifications => 'Уведомления'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index f5cb3d4..06da00e 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -467,6 +467,14 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_languageUk => 'Ukrainisch'; + @override + String get appSettings_enableMessageTracing => + 'Nachrichtenverfolgung aktivieren'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen'; + @override String get appSettings_notifications => 'Benachrichtigungen'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index a858737..00e60ec 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -465,6 +465,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get appSettings_languageUk => 'Українська'; + @override + String get appSettings_enableMessageTracing => 'Enable Message Tracing'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Show detailed routing and timing metadata for messages'; + @override String get appSettings_notifications => 'Notifications'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 1d0bb2d..d669219 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -470,6 +470,14 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_languageUk => 'Ucraniano'; + @override + String get appSettings_enableMessageTracing => + 'Habilitar seguimiento de mensajes'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Mostrar metadatos detallados de enrutamiento y tiempo para los mensajes'; + @override String get appSettings_notifications => 'Notificaciones'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 8ad6c74..bf747c9 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -471,6 +471,14 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_languageUk => 'Ukrainien'; + @override + String get appSettings_enableMessageTracing => + 'Activer le traçage des messages'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Afficher les métadonnées détaillées de routage et de synchronisation des messages'; + @override String get appSettings_notifications => 'Notifications'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 29bb627..caab1d3 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -469,6 +469,14 @@ class AppLocalizationsIt extends AppLocalizations { @override String get appSettings_languageUk => 'Ucraino'; + @override + String get appSettings_enableMessageTracing => + 'Abilita tracciamento messaggi'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Mostra metadati dettagliati su instradamento e tempi per i messaggi'; + @override String get appSettings_notifications => 'Notifiche'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 77cc083..fb3ce5e 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -467,6 +467,13 @@ class AppLocalizationsNl extends AppLocalizations { @override String get appSettings_languageUk => 'Oekraïens'; + @override + String get appSettings_enableMessageTracing => 'Berichttracking inschakelen'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Gedetailleerde routerings- en timing-metadata voor berichten weergeven'; + @override String get appSettings_notifications => 'Notificaties'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index d9c0539..7c79eb9 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -471,6 +471,13 @@ class AppLocalizationsPl extends AppLocalizations { @override String get appSettings_languageUk => 'Ukraińska'; + @override + String get appSettings_enableMessageTracing => 'Włącz śledzenie wiadomości'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Pokaż szczegółowe metadane trasowania i czasu dla wiadomości'; + @override String get appSettings_notifications => 'Powiadomienia'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 640988f..e352353 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -471,6 +471,14 @@ class AppLocalizationsPt extends AppLocalizations { @override String get appSettings_languageUk => 'Ucraniano'; + @override + String get appSettings_enableMessageTracing => + 'Ativar rastreamento de mensagens'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Mostrar metadados detalhados de roteamento e tempo para as mensagens'; + @override String get appSettings_notifications => 'Notificações'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 4de8db2..85c3341 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -469,6 +469,14 @@ class AppLocalizationsRu extends AppLocalizations { @override String get appSettings_languageUk => 'Українська'; + @override + String get appSettings_enableMessageTracing => + 'Включить трассировку сообщений'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Показывать подробные метаданные о маршрутизации и времени для сообщений'; + @override String get appSettings_notifications => 'Уведомления'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index fe87700..14750cf 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -467,6 +467,13 @@ class AppLocalizationsSk extends AppLocalizations { @override String get appSettings_languageUk => 'Ukrajinská'; + @override + String get appSettings_enableMessageTracing => 'Povoliť sledovanie správ'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Zobraziť podrobné metadáta o smerovaní a časovaní správ'; + @override String get appSettings_notifications => 'Upozornenia'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 5d9901a..eb7b814 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -466,6 +466,13 @@ class AppLocalizationsSl extends AppLocalizations { @override String get appSettings_languageUk => 'Ukrajinsko'; + @override + String get appSettings_enableMessageTracing => 'Omogoči sledenje sporočilom'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil'; + @override String get appSettings_notifications => 'Obvestila'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index c5a0ee9..f1cf84c 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -464,6 +464,13 @@ class AppLocalizationsSv extends AppLocalizations { @override String get appSettings_languageUk => 'Ukrainska'; + @override + String get appSettings_enableMessageTracing => 'Aktivera meddelandespårning'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden'; + @override String get appSettings_notifications => 'Meddelanden'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 35b5143..f236390 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -469,6 +469,14 @@ class AppLocalizationsUk extends AppLocalizations { @override String get appSettings_languageUk => 'Українська'; + @override + String get appSettings_enableMessageTracing => + 'Увімкнути відстеження повідомлень'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Показувати детальні метадані про маршрутизацію та час для повідомлень'; + @override String get appSettings_notifications => 'Сповіщення'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index a97e0fb..21323e0 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -453,6 +453,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get appSettings_languageUk => '乌克兰'; + @override + String get appSettings_enableMessageTracing => '启用消息追踪'; + + @override + String get appSettings_enableMessageTracingSubtitle => '显示消息的详细路由和时间元数据'; + @override String get appSettings_notifications => '通知'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 5a5b0ab..372be07 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1557,6 +1557,8 @@ "contacts_floodAdvert": "Overstromingsadvertentie", "contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren", "appSettings_languageRu": "Russisch", + "appSettings_enableMessageTracing": "Berichttracking inschakelen", + "appSettings_enableMessageTracingSubtitle": "Gedetailleerde routerings- en timing-metadata voor berichten weergeven", "contacts_clipboardEmpty": "Knipbord is leeg.", "contacts_addContactFromClipboard": "Contact uit klembord toevoegen", "contacts_contactImported": "Contact is geïmporteerd.", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "Toon LOS-paneel", "losHidePanelTooltip": "LOS-paneel verbergen", "losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 72af443..e36232e 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1552,6 +1552,8 @@ "contacts_chatTraceRoute": "Śledź trasę promienia", "appSettings_languageRu": "Rosyjski", "appSettings_languageUk": "Ukraińska", + "appSettings_enableMessageTracing": "Włącz śledzenie wiadomości", + "appSettings_enableMessageTracingSubtitle": "Pokaż szczegółowe metadane trasowania i czasu dla wiadomości", "contacts_contactImportFailed": "Kontakt nie został zaimportowany.", "contacts_zeroHopAdvert": "Reklama Zero Hop", "contacts_floodAdvert": "Reklama powodziowa", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "Pokaż panel LOS", "losHidePanelTooltip": "Ukryj panel LOS", "losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 35c4635..482451d 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1558,6 +1558,8 @@ "contacts_copyAdvertToClipboard": "Copiar Anúncio para Área de Transferência", "contacts_addContactFromClipboard": "Adicionar Contato da Área de Transferência", "appSettings_languageRu": "Russo", + "appSettings_enableMessageTracing": "Ativar rastreamento de mensagens", + "appSettings_enableMessageTracingSubtitle": "Mostrar metadados detalhados de roteamento e tempo para as mensagens", "contacts_ShareContact": "Copiar contato para Área de Transferência", "contacts_contactImportFailed": "Contato falhou ao ser importado.", "contacts_zeroHopContactAdvertSent": "Enviou contato por anúncio.", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "Mostrar painel LOS", "losHidePanelTooltip": "Ocultar painel LOS", "losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index d4d1939..85796ba 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -796,6 +796,8 @@ "contacts_invalidAdvertFormat": "Недействительные контактные данные", "contacts_zeroHopAdvert": "Реклама Zero Hop", "appSettings_languageUk": "Українська", + "appSettings_enableMessageTracing": "Включить трассировку сообщений", + "appSettings_enableMessageTracingSubtitle": "Показывать подробные метаданные о маршрутизации и времени для сообщений", "contacts_floodAdvert": "Рекламный поток", "contacts_clipboardEmpty": "Буфер обмена пуст.", "contacts_copyAdvertToClipboard": "Копировать рекламу в буфер обмена", @@ -959,4 +961,4 @@ "losShowPanelTooltip": "Показать панель LOS", "losHidePanelTooltip": "Скрыть панель LOS", "losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 8e8d1e8..f4d4671 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1558,6 +1558,8 @@ "contacts_copyAdvertToClipboard": "Kopírovať reklamu do schránky", "contacts_invalidAdvertFormat": "Neplatné kontaktné údaje", "appSettings_languageRu": "Ruština", + "appSettings_enableMessageTracing": "Povoliť sledovanie správ", + "appSettings_enableMessageTracingSubtitle": "Zobraziť podrobné metadáta o smerovaní a časovaní správ", "contacts_addContactFromClipboard": "Pridať kontakt z schránky", "contacts_contactImported": "Kontakt bol importovaný.", "contacts_zeroHopContactAdvertSent": "Poslal kontakt cez inzerát.", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "Zobraziť panel LOS", "losHidePanelTooltip": "Skryť panel LOS", "losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 2e749b9..e1d2ce4 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1552,6 +1552,8 @@ "contacts_pathTraceTo": "Trace route to {name}", "appSettings_languageRu": "Ruščina", "appSettings_languageUk": "Ukrajinsko", + "appSettings_enableMessageTracing": "Omogoči sledenje sporočilom", + "appSettings_enableMessageTracingSubtitle": "Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil", "contacts_contactImported": "Kontakt je bil uvožen.", "contacts_contactImportFailed": "Kontakt ni bil uspešno uvožen.", "contacts_zeroHopAdvert": "Reklama brez posrednikov", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "Pokaži ploščo LOS", "losHidePanelTooltip": "Skrij ploščo LOS", "losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index b311a45..ed4f9e5 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1558,6 +1558,8 @@ "contacts_copyAdvertToClipboard": "Kopiera annons till urklipp", "contacts_invalidAdvertFormat": "Ogiltiga kontaktuppgifter", "appSettings_languageUk": "Ukrainska", + "appSettings_enableMessageTracing": "Aktivera meddelandespårning", + "appSettings_enableMessageTracingSubtitle": "Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden", "contacts_addContactFromClipboard": "Lägg till kontakt från urklipp", "contacts_contactImported": "Kontakt har importerats.", "contacts_zeroHopContactAdvertSent": "Skickat kontakt via annons.", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "Visa LOS-panelen", "losHidePanelTooltip": "Dölj LOS-panelen", "losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index f5d3a42..0575302 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1559,6 +1559,8 @@ "contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну", "contacts_clipboardEmpty": "Буфер обміну порожній", "appSettings_languageRu": "Російська", + "appSettings_enableMessageTracing": "Увімкнути відстеження повідомлень", + "appSettings_enableMessageTracingSubtitle": "Показувати детальні метадані про маршрутизацію та час для повідомлень", "contacts_ShareContact": "Копіювати контакт у буфер обміну", "contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.", "contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "Показати панель LOS", "losHidePanelTooltip": "Приховати панель LOS", "losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 1fac003..37af48e 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -176,6 +176,8 @@ "appSettings_languageBg": "保加利亚", "appSettings_languageRu": "俄语", "appSettings_languageUk": "乌克兰", + "appSettings_enableMessageTracing": "启用消息追踪", + "appSettings_enableMessageTracingSubtitle": "显示消息的详细路由和时间元数据", "appSettings_notifications": "通知", "appSettings_enableNotifications": "启用通知", "appSettings_enableNotificationsSubtitle": "接收消息和广告的通知", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "显示 LOS 面板", "losHidePanelTooltip": "隐藏 LOS 面板", "losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index d9504b3..62ba9ca 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -22,6 +22,7 @@ class AppSettings { final bool mapKeyPrefixEnabled; final String mapKeyPrefix; final bool mapShowMarkers; + final bool enableMessageTracing; final Map? mapCacheBounds; final int mapCacheMinZoom; final int mapCacheMaxZoom; @@ -47,6 +48,7 @@ class AppSettings { this.mapKeyPrefixEnabled = false, this.mapKeyPrefix = '', this.mapShowMarkers = true, + this.enableMessageTracing = false, this.mapCacheBounds, this.mapCacheMinZoom = 10, this.mapCacheMaxZoom = 15, @@ -76,6 +78,7 @@ class AppSettings { 'map_key_prefix_enabled': mapKeyPrefixEnabled, 'map_key_prefix': mapKeyPrefix, 'map_show_markers': mapShowMarkers, + 'enable_message_tracing': enableMessageTracing, 'map_cache_bounds': mapCacheBounds, 'map_cache_min_zoom': mapCacheMinZoom, 'map_cache_max_zoom': mapCacheMaxZoom, @@ -112,6 +115,7 @@ class AppSettings { mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false, mapKeyPrefix: json['map_key_prefix'] as String? ?? '', mapShowMarkers: json['map_show_markers'] as bool? ?? true, + enableMessageTracing: json['enable_message_tracing'] as bool? ?? false, mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map( (key, value) => MapEntry(key.toString(), (value as num).toDouble()), ), @@ -155,6 +159,7 @@ class AppSettings { bool? mapKeyPrefixEnabled, String? mapKeyPrefix, bool? mapShowMarkers, + bool? enableMessageTracing, Object? mapCacheBounds = _unset, int? mapCacheMinZoom, int? mapCacheMaxZoom, @@ -180,6 +185,7 @@ class AppSettings { mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled, mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix, mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers, + enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing, mapCacheBounds: mapCacheBounds == _unset ? this.mapCacheBounds : mapCacheBounds as Map?, diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index b309b4d..a2c920e 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -82,6 +82,18 @@ class AppSettingsScreen extends StatelessWidget { trailing: const Icon(Icons.chevron_right), onTap: () => _showLanguageDialog(context, settingsService), ), + const Divider(height: 1), + SwitchListTile( + secondary: const Icon(Icons.location_searching), + title: Text(context.l10n.appSettings_enableMessageTracing), + subtitle: Text( + context.l10n.appSettings_enableMessageTracingSubtitle, + ), + value: settingsService.settings.enableMessageTracing, + onChanged: (value) { + settingsService.setEnableMessageTracing(value); + }, + ), ], ), ); diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 8c979c0..9ac5a23 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -17,6 +17,7 @@ import '../helpers/utf8_length_limiter.dart'; import '../l10n/l10n.dart'; import '../models/channel.dart'; import '../models/channel_message.dart'; +import '../services/app_settings_service.dart'; import '../utils/emoji_utils.dart'; import '../widgets/emoji_picker.dart'; import '../widgets/gif_message.dart'; @@ -263,6 +264,8 @@ class _ChannelChatScreenState extends State { } Widget _buildMessageBubble(ChannelMessage message) { + final settingsService = context.watch(); + final enableTracing = settingsService.settings.enableMessageTracing; final isOutgoing = message.isOutgoing; final gifId = _parseGifId(message.text); final poi = _parsePoiMessage(message.text); @@ -336,108 +339,166 @@ class _ChannelChatScreenState extends State { if (poi != null) _buildPoiMessage(context, poi, isOutgoing) else if (gifId != null) - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Colors.transparent, - fallbackTextColor: isOutgoing - ? Theme.of(context) - .colorScheme - .onPrimaryContainer - .withValues(alpha: 0.7) - : Theme.of(context).colorScheme.onSurface - .withValues(alpha: 0.6), - ), + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Colors.transparent, + fallbackTextColor: isOutgoing + ? Theme.of(context) + .colorScheme + .onPrimaryContainer + .withValues(alpha: 0.7) + : Theme.of(context).colorScheme.onSurface + .withValues(alpha: 0.6), + ), + ), + if (!enableTracing && isOutgoing) + Positioned( + top: 4, + right: 4, + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.3), + shape: BoxShape.circle, + ), + child: Icon( + (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + ? Icons.check_circle + : message.status == ChannelMessageStatus.failed + ? Icons.cancel + : Icons.cloud, + size: 14, + color: (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + ? Colors.green + : message.status == ChannelMessageStatus.failed + ? Colors.red + : Colors.white70, + ), + ), + ), + ], ) else - Linkify( - text: message.text, - style: const TextStyle(fontSize: 14), - linkStyle: const TextStyle( - fontSize: 14, - color: Colors.green, - decoration: TextDecoration.underline, - ), - options: const LinkifyOptions( - humanize: false, - defaultToHttps: false, - ), - linkifiers: const [UrlLinkifier()], - onOpen: (link) => - LinkHandler.handleLinkTap(context, link.url), - ), - if (displayPath.isNotEmpty) ...[ - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.symmetric(horizontal: 8) - : EdgeInsets.zero, - child: Text( - 'via ${_formatPathPrefixes(displayPath)}', - style: TextStyle( - fontSize: 11, - color: Colors.grey[600], - ), - ), - ), - ], - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.only( - left: 8, - right: 8, - bottom: 4, - ) - : EdgeInsets.zero, - child: Row( + Row( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text( - _formatTime(message.timestamp), + Flexible( + child: Linkify( + text: message.text, + style: const TextStyle(fontSize: 14), + linkStyle: const TextStyle( + fontSize: 14, + color: Colors.green, + decoration: TextDecoration.underline, + ), + options: const LinkifyOptions( + humanize: false, + defaultToHttps: false, + ), + linkifiers: const [UrlLinkifier()], + onOpen: (link) => + LinkHandler.handleLinkTap(context, link.url), + ), + ), + if (!enableTracing && isOutgoing) ...[ + const SizedBox(width: 4), + Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Icon( + (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + ? Icons.check_circle + : message.status == ChannelMessageStatus.failed + ? Icons.cancel + : Icons.cloud, + size: 14, + color: (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + ? Colors.green + : message.status == ChannelMessageStatus.failed + ? Colors.red + : Colors.grey, + ), + ), + ], + ], + ), + if (enableTracing) ...[ + if (displayPath.isNotEmpty) ...[ + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + 'via ${_formatPathPrefixes(displayPath)}', style: TextStyle( fontSize: 11, color: Colors.grey[600], ), ), - if (message.repeatCount > 0) ...[ - const SizedBox(width: 6), - Icon( - Icons.repeat, - size: 12, - color: Colors.grey[600], - ), - const SizedBox(width: 2), + ), + ], + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + right: 8, + bottom: 4, + ) + : EdgeInsets.zero, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ Text( - '${message.repeatCount}', + _formatTime(message.timestamp), style: TextStyle( fontSize: 11, color: Colors.grey[600], ), ), + if (message.repeatCount > 0) ...[ + const SizedBox(width: 6), + Icon( + Icons.repeat, + size: 12, + color: Colors.grey[600], + ), + const SizedBox(width: 2), + Text( + '${message.repeatCount}', + style: TextStyle( + fontSize: 11, + color: Colors.grey[600], + ), + ), + ], + if (isOutgoing) ...[ + const SizedBox(width: 4), + Icon( + message.status == ChannelMessageStatus.sent + ? Icons.check + : message.status == + ChannelMessageStatus.pending + ? Icons.schedule + : Icons.error_outline, + size: 14, + color: + message.status == + ChannelMessageStatus.failed + ? Colors.red + : Colors.grey[600], + ), + ], ], - if (isOutgoing) ...[ - const SizedBox(width: 4), - Icon( - message.status == ChannelMessageStatus.sent - ? Icons.check - : message.status == - ChannelMessageStatus.pending - ? Icons.schedule - : Icons.error_outline, - size: 14, - color: - message.status == - ChannelMessageStatus.failed - ? Colors.red - : Colors.grey[600], - ), - ], - ], + ), ), - ), + ], ], ), ), diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index c604ea6..a40a269 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -20,6 +20,7 @@ import '../models/channel_message.dart'; import '../models/contact.dart'; import '../models/message.dart'; import '../models/path_history.dart'; +import '../services/app_settings_service.dart'; import '../services/path_history_service.dart'; import '../widgets/elements_ui.dart'; import 'channel_message_path_screen.dart'; @@ -1188,6 +1189,8 @@ class _MessageBubble extends StatelessWidget { @override Widget build(BuildContext context) { + final settingsService = context.watch(); + final enableTracing = settingsService.settings.enableMessageTracing; final isOutgoing = message.isOutgoing; final colorScheme = Theme.of(context).colorScheme; final gifId = _parseGifId(message.text); @@ -1267,100 +1270,158 @@ class _MessageBubble extends StatelessWidget { if (poi != null) _buildPoiMessage(context, poi, textColor, metaColor) else if (gifId != null) - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Colors.transparent, - fallbackTextColor: textColor.withValues( - alpha: 0.7, + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Colors.transparent, + fallbackTextColor: textColor.withValues( + alpha: 0.7, + ), + ), ), - ), + if (!enableTracing && isOutgoing) + Positioned( + top: 4, + right: 4, + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.3), + shape: BoxShape.circle, + ), + child: Icon( + (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + ? Icons.check_circle + : message.status == MessageStatus.failed + ? Icons.cancel + : Icons.cloud, + size: 14, + color: (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + ? Colors.green + : message.status == MessageStatus.failed + ? Colors.red + : Colors.white70, + ), + ), + ), + ], ) else - Linkify( - text: messageText, - style: TextStyle(color: textColor), - linkStyle: const TextStyle( - color: Colors.green, - decoration: TextDecoration.underline, - ), - options: const LinkifyOptions( - humanize: false, - defaultToHttps: false, - ), - linkifiers: const [UrlLinkifier()], - onOpen: (link) => - LinkHandler.handleLinkTap(context, link.url), - ), - if (isOutgoing && message.retryCount > 0) ...[ - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.symmetric(horizontal: 8) - : EdgeInsets.zero, - child: Text( - context.l10n.chat_retryCount( - message.retryCount, - 4, - ), - style: TextStyle( - fontSize: 10, - color: metaColor, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.only( - left: 8, - right: 8, - bottom: 4, - ) - : EdgeInsets.zero, - child: Wrap( - spacing: 4, - crossAxisAlignment: WrapCrossAlignment.center, + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text( - _formatTime(message.timestamp), - style: TextStyle( - fontSize: 10, - color: metaColor, + Flexible( + child: Linkify( + text: messageText, + style: TextStyle(color: textColor), + linkStyle: const TextStyle( + color: Colors.green, + decoration: TextDecoration.underline, + ), + options: const LinkifyOptions( + humanize: false, + defaultToHttps: false, + ), + linkifiers: const [UrlLinkifier()], + onOpen: (link) => + LinkHandler.handleLinkTap(context, link.url), ), ), - if (isOutgoing) ...[ + if (!enableTracing && isOutgoing) ...[ const SizedBox(width: 4), - _buildStatusIcon(metaColor), - ], - if (message.tripTimeMs != null && - message.status == - MessageStatus.delivered) ...[ - const SizedBox(width: 4), - Icon( - Icons.speed, - size: 10, - color: isOutgoing - ? metaColor - : Colors.green[700], - ), - Text( - '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', - style: TextStyle( - fontSize: 9, - color: isOutgoing - ? metaColor - : Colors.green[700], + Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Icon( + (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + ? Icons.check_circle + : message.status == MessageStatus.failed + ? Icons.cancel + : Icons.cloud, + size: 14, + color: (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + ? Colors.green + : message.status == MessageStatus.failed + ? Colors.red + : Colors.grey, ), ), ], ], ), - ), + if (enableTracing) ...[ + if (isOutgoing && message.retryCount > 0) ...[ + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + context.l10n.chat_retryCount( + message.retryCount, + 4, + ), + style: TextStyle( + fontSize: 10, + color: metaColor, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + right: 8, + bottom: 4, + ) + : EdgeInsets.zero, + child: Wrap( + spacing: 4, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + _formatTime(message.timestamp), + style: TextStyle( + fontSize: 10, + color: metaColor, + ), + ), + if (isOutgoing) ...[ + const SizedBox(width: 4), + _buildStatusIcon(metaColor), + ], + if (message.tripTimeMs != null && + message.status == + MessageStatus.delivered) ...[ + const SizedBox(width: 4), + Icon( + Icons.speed, + size: 10, + color: isOutgoing + ? metaColor + : Colors.green[700], + ), + Text( + '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', + style: TextStyle( + fontSize: 9, + color: isOutgoing + ? metaColor + : Colors.green[700], + ), + ), + ], + ], + ), + ), + ], ], ), ), diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index e80f903..eacf26f 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -80,6 +80,10 @@ class AppSettingsService extends ChangeNotifier { await updateSettings(_settings.copyWith(mapShowMarkers: value)); } + Future setEnableMessageTracing(bool value) async { + await updateSettings(_settings.copyWith(enableMessageTracing: value)); + } + Future setMapCacheBounds(Map? value) async { await updateSettings(_settings.copyWith(mapCacheBounds: value)); } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 65fed26..8224cfb 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -5,10 +5,13 @@ PODS: - flutter_local_notifications (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) - - mobile_scanner (6.0.2): + - mobile_scanner (7.0.0): + - Flutter - FlutterMacOS - package_info_plus (0.0.1): - FlutterMacOS + - share_plus (0.0.1): + - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -24,8 +27,9 @@ DEPENDENCIES: - flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`) + - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) + - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) @@ -39,9 +43,11 @@ EXTERNAL SOURCES: FlutterMacOS: :path: Flutter/ephemeral mobile_scanner: - :path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos + :path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin package_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos + share_plus: + :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin sqflite_darwin: @@ -53,10 +59,11 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 - flutter_local_notifications: 13862b132e32eb858dea558a86d45d08daeacfe7 + flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 - mobile_scanner: 0e365ed56cad24f28c0fd858ca04edefb40dfac3 + mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd From 4975b5366edea931b8a65cc6ecbd8b4fd714cfdd Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 11:34:37 -0800 Subject: [PATCH 18/23] formatting fixes --- lib/connector/meshcore_connector.dart | 6 +-- lib/screens/channel_chat_screen.dart | 62 ++++++++++++++++--------- lib/screens/chat_screen.dart | 53 ++++++++++++++------- lib/screens/chrome_required_screen.dart | 4 +- lib/screens/scanner_screen.dart | 8 ++-- 5 files changed, 84 insertions(+), 49 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index c558171..e4eaaf6 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -694,7 +694,7 @@ class MeshCoreConnector extends ChangeNotifier { try { await FlutterBluePlus.stopScan(); } catch (_) {} - + try { await _scanSubscription?.cancel(); } catch (_) {} @@ -743,7 +743,7 @@ class MeshCoreConnector extends ChangeNotifier { await FlutterBluePlus.startScan( withServices: [Guid(MeshCoreUuids.service)], ); - // On web, the chooser returns once a device is picked, but the scanResults + // On web, the chooser returns once a device is picked, but the scanResults // stream might take a moment to emit the last result. Wait briefly so the // device appears in the UI before stopScan() clears the list. await Future.delayed(const Duration(milliseconds: 500)); @@ -760,7 +760,7 @@ class MeshCoreConnector extends ChangeNotifier { debugPrint("Scan error: $e"); // On web, suppress common cancellation and chooser errors if (kIsWeb) return; - + if (!PlatformInfo.isWeb) { rethrow; } diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 9ac5a23..b59a691 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -367,17 +367,24 @@ class _ChannelChatScreenState extends State { shape: BoxShape.circle, ), child: Icon( - (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + (message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty) ? Icons.check_circle - : message.status == ChannelMessageStatus.failed - ? Icons.cancel - : Icons.cloud, + : message.status == + ChannelMessageStatus.failed + ? Icons.cancel + : Icons.cloud, size: 14, - color: (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + color: + (message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty) ? Colors.green - : message.status == ChannelMessageStatus.failed - ? Colors.red - : Colors.white70, + : message.status == + ChannelMessageStatus.failed + ? Colors.red + : Colors.white70, ), ), ), @@ -402,8 +409,10 @@ class _ChannelChatScreenState extends State { defaultToHttps: false, ), linkifiers: const [UrlLinkifier()], - onOpen: (link) => - LinkHandler.handleLinkTap(context, link.url), + onOpen: (link) => LinkHandler.handleLinkTap( + context, + link.url, + ), ), ), if (!enableTracing && isOutgoing) ...[ @@ -411,17 +420,24 @@ class _ChannelChatScreenState extends State { Padding( padding: const EdgeInsets.only(bottom: 2), child: Icon( - (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + (message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty) ? Icons.check_circle - : message.status == ChannelMessageStatus.failed - ? Icons.cancel - : Icons.cloud, + : message.status == + ChannelMessageStatus.failed + ? Icons.cancel + : Icons.cloud, size: 14, - color: (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + color: + (message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty) ? Colors.green - : message.status == ChannelMessageStatus.failed - ? Colors.red - : Colors.grey, + : message.status == + ChannelMessageStatus.failed + ? Colors.red + : Colors.grey, ), ), ], @@ -915,7 +931,8 @@ class _ChannelChatScreenState extends State { onKeyEvent: (node, event) { if (event is KeyDownEvent && (event.logicalKey == LogicalKeyboardKey.enter || - event.logicalKey == LogicalKeyboardKey.numpadEnter)) { + event.logicalKey == + LogicalKeyboardKey.numpadEnter)) { _sendMessage(); return KeyEventResult.handled; } @@ -932,9 +949,10 @@ class _ChannelChatScreenState extends State { backgroundColor: Theme.of( context, ).colorScheme.surfaceContainerHighest, - fallbackTextColor: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.6), + fallbackTextColor: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.6), maxSize: 160, ), ), diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index a40a269..6ee846c 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -346,7 +346,8 @@ class _ChatScreenState extends State { onKeyEvent: (node, event) { if (event is KeyDownEvent && (event.logicalKey == LogicalKeyboardKey.enter || - event.logicalKey == LogicalKeyboardKey.numpadEnter)) { + event.logicalKey == + LogicalKeyboardKey.numpadEnter)) { _sendMessage(connector); return KeyEventResult.handled; } @@ -1290,21 +1291,30 @@ class _MessageBubble extends StatelessWidget { child: Container( padding: const EdgeInsets.all(2), decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.3), + color: Colors.black.withValues( + alpha: 0.3, + ), shape: BoxShape.circle, ), child: Icon( - (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + (message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty) ? Icons.check_circle - : message.status == MessageStatus.failed - ? Icons.cancel - : Icons.cloud, + : message.status == + MessageStatus.failed + ? Icons.cancel + : Icons.cloud, size: 14, - color: (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + color: + (message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty) ? Colors.green - : message.status == MessageStatus.failed - ? Colors.red - : Colors.white70, + : message.status == + MessageStatus.failed + ? Colors.red + : Colors.white70, ), ), ), @@ -1328,8 +1338,10 @@ class _MessageBubble extends StatelessWidget { defaultToHttps: false, ), linkifiers: const [UrlLinkifier()], - onOpen: (link) => - LinkHandler.handleLinkTap(context, link.url), + onOpen: (link) => LinkHandler.handleLinkTap( + context, + link.url, + ), ), ), if (!enableTracing && isOutgoing) ...[ @@ -1337,17 +1349,22 @@ class _MessageBubble extends StatelessWidget { Padding( padding: const EdgeInsets.only(bottom: 2), child: Icon( - (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + (message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty) ? Icons.check_circle : message.status == MessageStatus.failed - ? Icons.cancel - : Icons.cloud, + ? Icons.cancel + : Icons.cloud, size: 14, - color: (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + color: + (message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty) ? Colors.green : message.status == MessageStatus.failed - ? Colors.red - : Colors.grey, + ? Colors.red + : Colors.grey, ), ), ], diff --git a/lib/screens/chrome_required_screen.dart b/lib/screens/chrome_required_screen.dart index a229c0a..1827aeb 100644 --- a/lib/screens/chrome_required_screen.dart +++ b/lib/screens/chrome_required_screen.dart @@ -64,9 +64,7 @@ class ChromeRequiredScreen extends StatelessWidget { decoration: BoxDecoration( color: Colors.blue.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(30), - border: Border.all( - color: Colors.blue.withValues(alpha: 0.3), - ), + border: Border.all(color: Colors.blue.withValues(alpha: 0.3)), ), child: Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index bfd1230..4017408 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -112,9 +112,11 @@ class _ScannerScreenState extends State { if (isScanning) { connector.stopScan(); } else { - unawaited(connector.startScan().catchError((e) { - debugPrint("Scanner screen startScan error: $e"); - })); + unawaited( + connector.startScan().catchError((e) { + debugPrint("Scanner screen startScan error: $e"); + }), + ); } }, icon: isScanning From 53d073d8f2a10ffb376defeddd1ca087d6caa87f Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 23 Feb 2026 03:43:49 -0800 Subject: [PATCH 19/23] deprecation fix --- lib/utils/browser_detection.dart | 2 +- lib/utils/browser_detection_web.dart | 5 ++--- pubspec.lock | 2 +- pubspec.yaml | 1 + 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/utils/browser_detection.dart b/lib/utils/browser_detection.dart index aafb838..b8dca93 100644 --- a/lib/utils/browser_detection.dart +++ b/lib/utils/browser_detection.dart @@ -1,2 +1,2 @@ export 'browser_detection_stub.dart' - if (dart.library.html) 'browser_detection_web.dart'; + if (dart.library.js_interop) 'browser_detection_web.dart'; diff --git a/lib/utils/browser_detection_web.dart b/lib/utils/browser_detection_web.dart index 0e4fccb..bbb9c76 100644 --- a/lib/utils/browser_detection_web.dart +++ b/lib/utils/browser_detection_web.dart @@ -1,9 +1,8 @@ -// ignore: avoid_web_libraries_in_flutter -import 'dart:html' as html; +import 'package:web/web.dart' as web; class BrowserDetection { static bool get isChrome { - final userAgent = html.window.navigator.userAgent.toLowerCase(); + final userAgent = web.window.navigator.userAgent.toLowerCase(); final isChrome = userAgent.contains('chrome'); return isChrome; } diff --git a/pubspec.lock b/pubspec.lock index 18eaff5..756f192 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1083,7 +1083,7 @@ packages: source: hosted version: "1.3.0" web: - dependency: transitive + dependency: "direct main" description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" diff --git a/pubspec.yaml b/pubspec.yaml index a17edde..a7564a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,7 @@ dependencies: path_provider: ^2.1.5 share_plus: ^12.0.1 build_pipe: ^0.3.1 + web: ^1.1.1 dev_dependencies: flutter_test: From 549fc6263290b44d1f5ae6792960a9ae47d25ca9 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 23 Feb 2026 04:09:27 -0800 Subject: [PATCH 20/23] chat fixes --- assets/icons/done_all.svg | 1 + lib/screens/channel_chat_screen.dart | 104 +++++++++++++++------------ lib/screens/chat_screen.dart | 94 ++++++++++++------------ lib/widgets/message_status_icon.dart | 36 ++++++++++ pubspec.lock | 40 +++++++++++ pubspec.yaml | 2 + 6 files changed, 187 insertions(+), 90 deletions(-) create mode 100644 assets/icons/done_all.svg create mode 100644 lib/widgets/message_status_icon.dart diff --git a/assets/icons/done_all.svg b/assets/icons/done_all.svg new file mode 100644 index 0000000..bfeeec0 --- /dev/null +++ b/assets/icons/done_all.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index b59a691..c02425b 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -4,6 +4,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; @@ -23,6 +24,7 @@ import '../widgets/emoji_picker.dart'; import '../widgets/gif_message.dart'; import '../widgets/jump_to_bottom_button.dart'; import '../widgets/gif_picker.dart'; +import '../widgets/message_status_icon.dart'; import 'channel_message_path_screen.dart'; import 'map_screen.dart'; @@ -337,7 +339,23 @@ class _ChannelChatScreenState extends State { const SizedBox(height: 8), ], if (poi != null) - _buildPoiMessage(context, poi, isOutgoing) + _buildPoiMessage( + context, + poi, + isOutgoing, + trailing: (!enableTracing && isOutgoing) + ? Padding( + padding: const EdgeInsets.only(bottom: 2), + child: MessageStatusIcon( + isAcked: message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty, + isFailed: message.status == + ChannelMessageStatus.failed, + ), + ) + : null, + ) else if (gifId != null) Stack( children: [ @@ -358,33 +376,31 @@ class _ChannelChatScreenState extends State { ), if (!enableTracing && isOutgoing) Positioned( - top: 4, - right: 4, + top: 0, + right: 0, child: Container( - padding: const EdgeInsets.all(2), + padding: const EdgeInsets.all(3), decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.3), - shape: BoxShape.circle, + color: isOutgoing + ? Theme.of( + context, + ).colorScheme.primaryContainer + : Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(10), + topRight: Radius.circular(8), + ), ), - child: Icon( - (message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty) - ? Icons.check_circle - : message.status == - ChannelMessageStatus.failed - ? Icons.cancel - : Icons.cloud, - size: 14, - color: - (message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty) - ? Colors.green - : message.status == - ChannelMessageStatus.failed - ? Colors.red - : Colors.white70, + child: MessageStatusIcon( + isAcked: + message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty, + isFailed: + message.status == + ChannelMessageStatus.failed, ), ), ), @@ -419,25 +435,14 @@ class _ChannelChatScreenState extends State { const SizedBox(width: 4), Padding( padding: const EdgeInsets.only(bottom: 2), - child: Icon( - (message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty) - ? Icons.check_circle - : message.status == - ChannelMessageStatus.failed - ? Icons.cancel - : Icons.cloud, - size: 14, - color: - (message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty) - ? Colors.green - : message.status == - ChannelMessageStatus.failed - ? Colors.red - : Colors.grey, + child: MessageStatusIcon( + isAcked: + message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty, + isFailed: + message.status == + ChannelMessageStatus.failed, ), ), ], @@ -727,7 +732,12 @@ class _ChannelChatScreenState extends State { return _PoiInfo(lat: lat, lon: lon, label: label); } - Widget _buildPoiMessage(BuildContext context, _PoiInfo poi, bool isOutgoing) { + Widget _buildPoiMessage( + BuildContext context, + _PoiInfo poi, + bool isOutgoing, { + Widget? trailing, + }) { final colorScheme = Theme.of(context).colorScheme; final textColor = isOutgoing ? colorScheme.onPrimaryContainer @@ -773,6 +783,10 @@ class _ChannelChatScreenState extends State { ], ), ), + if (trailing != null) ...[ + const SizedBox(width: 4), + trailing, + ], ], ); } diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 6ee846c..a6d2399 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; @@ -13,6 +14,7 @@ import 'package:latlong2/latlong.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../helpers/reaction_helper.dart'; +import '../widgets/message_status_icon.dart'; import '../helpers/chat_scroll_controller.dart'; import '../helpers/link_handler.dart'; import '../helpers/utf8_length_limiter.dart'; @@ -1269,7 +1271,24 @@ class _MessageBubble extends StatelessWidget { if (gifId == null) const SizedBox(height: 4), ], if (poi != null) - _buildPoiMessage(context, poi, textColor, metaColor) + _buildPoiMessage( + context, + poi, + textColor, + metaColor, + trailing: (!enableTracing && isOutgoing) + ? Padding( + padding: const EdgeInsets.only(bottom: 2), + child: MessageStatusIcon( + isAcked: message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty, + isFailed: message.status == + MessageStatus.failed, + ), + ) + : null, + ) else if (gifId != null) Stack( children: [ @@ -1286,35 +1305,25 @@ class _MessageBubble extends StatelessWidget { ), if (!enableTracing && isOutgoing) Positioned( - top: 4, - right: 4, + top: 0, + right: 0, child: Container( - padding: const EdgeInsets.all(2), + padding: const EdgeInsets.all(3), decoration: BoxDecoration( - color: Colors.black.withValues( - alpha: 0.3, + color: bubbleColor, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(10), + topRight: Radius.circular(12), ), - shape: BoxShape.circle, ), - child: Icon( - (message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty) - ? Icons.check_circle - : message.status == - MessageStatus.failed - ? Icons.cancel - : Icons.cloud, - size: 14, - color: - (message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty) - ? Colors.green - : message.status == - MessageStatus.failed - ? Colors.red - : Colors.white70, + child: MessageStatusIcon( + isAcked: + message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty, + isFailed: + message.status == + MessageStatus.failed, ), ), ), @@ -1348,23 +1357,13 @@ class _MessageBubble extends StatelessWidget { const SizedBox(width: 4), Padding( padding: const EdgeInsets.only(bottom: 2), - child: Icon( - (message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty) - ? Icons.check_circle - : message.status == MessageStatus.failed - ? Icons.cancel - : Icons.cloud, - size: 14, - color: - (message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty) - ? Colors.green - : message.status == MessageStatus.failed - ? Colors.red - : Colors.grey, + child: MessageStatusIcon( + isAcked: + message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty, + isFailed: + message.status == MessageStatus.failed, ), ), ], @@ -1481,8 +1480,9 @@ class _MessageBubble extends StatelessWidget { BuildContext context, _PoiInfo poi, Color textColor, - Color metaColor, - ) { + Color metaColor, { + Widget? trailing, + }) { return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -1519,6 +1519,10 @@ class _MessageBubble extends StatelessWidget { ], ), ), + if (trailing != null) ...[ + const SizedBox(width: 4), + trailing, + ], ], ); } diff --git a/lib/widgets/message_status_icon.dart b/lib/widgets/message_status_icon.dart new file mode 100644 index 0000000..0689f0b --- /dev/null +++ b/lib/widgets/message_status_icon.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class MessageStatusIcon extends StatelessWidget { + final bool isAcked; + final bool isFailed; + final double size; + + const MessageStatusIcon({ + super.key, + required this.isAcked, + this.isFailed = false, + this.size = 14, + }); + + @override + Widget build(BuildContext context) { + if (isFailed) { + return Icon(Icons.cancel, size: size, color: Colors.red); + } + + final Color color; + if (isAcked) { + color = Colors.green; + } else { + color = Colors.grey; + } + + return SvgPicture.asset( + 'assets/icons/done_all.svg', + width: size, + height: size, + colorFilter: ColorFilter.mode(color, BlendMode.srcIn), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 756f192..e2254cb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -363,6 +363,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.2" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" + url: "https://pub.dev" + source: hosted + version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -637,6 +645,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -1050,6 +1066,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.2" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + url: "https://pub.dev" + source: hosted + version: "1.2.0" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a7564a9..3330d39 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,6 +62,7 @@ dependencies: share_plus: ^12.0.1 build_pipe: ^0.3.1 web: ^1.1.1 + flutter_svg: ^2.0.10+1 dev_dependencies: flutter_test: @@ -89,6 +90,7 @@ flutter: assets: - assets/images/ + - assets/icons/ flutter_launcher_icons: android: true From c8f93f990266f545a4e33c21494ca099f9039117 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 23 Feb 2026 04:30:13 -0800 Subject: [PATCH 21/23] code cleanup --- lib/screens/channel_chat_screen.dart | 1 - lib/screens/chat_screen.dart | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 10066e6..70c9c20 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -4,7 +4,6 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 49204f8..70cb036 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; From 96371c03ae38306079cd8f1b57a6b45fb3a51ef1 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Tue, 24 Feb 2026 21:17:24 -0800 Subject: [PATCH 22/23] pub lock upate --- pubspec.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index fa23b27..838513d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -529,10 +529,10 @@ packages: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.0" mgrs_dart: dependency: transitive description: From d88786bb0f6a2be72cfc1bd74b2bf5548baeebeb Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Tue, 24 Feb 2026 22:41:03 -0800 Subject: [PATCH 23/23] ble filtering --- lib/connector/meshcore_connector.dart | 220 ++++++++------------------ 1 file changed, 67 insertions(+), 153 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index e4eaaf6..ba62232 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -30,7 +30,6 @@ import '../storage/message_store.dart'; import '../storage/unread_store.dart'; import '../utils/app_logger.dart'; import '../utils/battery_utils.dart'; -import '../utils/platform_info.dart'; import 'meshcore_protocol.dart'; class MeshCoreUuids { @@ -686,111 +685,59 @@ class MeshCoreConnector extends ChangeNotifier { }) async { if (_state == MeshCoreConnectionState.scanning) return; - try { - _scanResults.clear(); - _setState(MeshCoreConnectionState.scanning); + _scanResults.clear(); + _setState(MeshCoreConnectionState.scanning); - // Ensure any previous scan is fully stopped - try { - await FlutterBluePlus.stopScan(); - } catch (_) {} + // Ensure any previous scan is fully stopped + await FlutterBluePlus.stopScan(); + await _scanSubscription?.cancel(); - try { - await _scanSubscription?.cancel(); - } catch (_) {} - _scanSubscription = null; - - // On iOS/macOS, wait for Bluetooth to be powered on before scanning - if (PlatformInfo.isIOS || PlatformInfo.isMacOS) { - // Wait for adapter state to be powered on - final adapterState = await FlutterBluePlus.adapterState.first; - if (adapterState != BluetoothAdapterState.on) { - // Wait for the adapter to turn on, with timeout - await FlutterBluePlus.adapterState - .firstWhere((state) => state == BluetoothAdapterState.on) - .timeout( - const Duration(seconds: 5), - onTimeout: () { - _setState(MeshCoreConnectionState.disconnected); - throw Exception('Bluetooth adapter not available'); - }, - ); - } - - // Add a small delay to allow BLE stack to fully initialize - await Future.delayed(const Duration(milliseconds: 300)); + // On iOS/macOS, wait for Bluetooth to be powered on before scanning + if (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS) { + // Wait for adapter state to be powered on + final adapterState = await FlutterBluePlus.adapterState.first; + if (adapterState != BluetoothAdapterState.on) { + // Wait for the adapter to turn on, with timeout + await FlutterBluePlus.adapterState + .firstWhere((state) => state == BluetoothAdapterState.on) + .timeout( + const Duration(seconds: 5), + onTimeout: () { + _setState(MeshCoreConnectionState.disconnected); + throw Exception('Bluetooth adapter not available'); + }, + ); } - _scanSubscription = FlutterBluePlus.scanResults.listen( - (results) { - _scanResults.clear(); - for (var result in results) { - if (result.device.platformName.startsWith("MeshCore-") || - result.advertisementData.advName.startsWith("MeshCore-") || - result.advertisementData.advName.startsWith("Whisper-")) { - _scanResults.add(result); - } - } - notifyListeners(); - }, - onError: (Object e) { - debugPrint("scanResults stream error: $e"); - stopScan(); - }, - ); - - if (PlatformInfo.isWeb) { - await FlutterBluePlus.startScan( - withServices: [Guid(MeshCoreUuids.service)], - ); - // On web, the chooser returns once a device is picked, but the scanResults - // stream might take a moment to emit the last result. Wait briefly so the - // device appears in the UI before stopScan() clears the list. - await Future.delayed(const Duration(milliseconds: 500)); - } else { - await FlutterBluePlus.startScan( - withServices: [Guid(MeshCoreUuids.service)], - timeout: timeout, - androidScanMode: AndroidScanMode.lowLatency, - ); - - await Future.delayed(timeout); - } - } catch (e) { - debugPrint("Scan error: $e"); - // On web, suppress common cancellation and chooser errors - if (kIsWeb) return; - - if (!PlatformInfo.isWeb) { - rethrow; - } - } finally { - await stopScan(); + // Add a small delay to allow BLE stack to fully initialize + await Future.delayed(const Duration(milliseconds: 300)); } + + _scanSubscription = FlutterBluePlus.scanResults.listen((results) { + _scanResults.clear(); + _scanResults.addAll(results); + notifyListeners(); + }); + + await FlutterBluePlus.startScan( + withKeywords: ["MeshCore-", "Whisper-"], + webOptionalServices: [Guid(MeshCoreUuids.service)], + timeout: timeout, + androidScanMode: AndroidScanMode.lowLatency, + ); + + await Future.delayed(timeout); + await stopScan(); } Future stopScan() async { - if (_state == MeshCoreConnectionState.scanning) { - _setState(MeshCoreConnectionState.disconnected); - } - - try { - await FlutterBluePlus.stopScan(); - } catch (e) { - debugPrint("stopScan error: $e"); - } - - try { - if (_scanSubscription != null) { - await _scanSubscription!.cancel(); - } - } catch (_) {} + await FlutterBluePlus.stopScan(); + await _scanSubscription?.cancel(); _scanSubscription = null; - // On web, don't clear results immediately so the picked device remains visible - if (!PlatformInfo.isWeb) { - _scanResults.clear(); - notifyListeners(); + if (_state == MeshCoreConnectionState.scanning) { + _setState(MeshCoreConnectionState.disconnected); } } @@ -818,17 +765,11 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); try { - _connectionSubscription = device.connectionState.listen( - (state) { - if (state == BluetoothConnectionState.disconnected && isConnected) { - _handleDisconnection(); - } - }, - onError: (Object e) { - debugPrint("connectionState stream error: $e"); - if (isConnected) _handleDisconnection(); - }, - ); + _connectionSubscription = device.connectionState.listen((state) { + if (state == BluetoothConnectionState.disconnected && isConnected) { + _handleDisconnection(); + } + }); await device.connect( timeout: const Duration(seconds: 15), @@ -837,17 +778,11 @@ class MeshCoreConnector extends ChangeNotifier { ); // Request larger MTU for sending larger frames - if (!PlatformInfo.isWeb) { - try { - final mtu = await device.requestMtu(185); - debugPrint('MTU set to: $mtu'); - } catch (e) { - debugPrint('MTU request failed: $e, using default'); - } - } else { - // On Chrome Web Bluetooth, give the GATT connection a moment to settle - // before discovering services, which is a common quirk to avoid timeouts. - await Future.delayed(const Duration(milliseconds: 500)); + try { + final mtu = await device.requestMtu(185); + debugPrint('MTU set to: $mtu'); + } catch (e) { + debugPrint('MTU request failed: $e, using default'); } List services = await device.discoverServices(); @@ -877,44 +812,23 @@ class MeshCoreConnector extends ChangeNotifier { throw Exception("MeshCore characteristics not found"); } - // Setup listener BEFORE enabling notifications so we don't miss anything - _notifySubscription = _txCharacteristic!.onValueReceived.listen( - _handleFrame, - onError: (Object e) { - debugPrint("onValueReceived stream error: $e"); - }, - ); - - debugPrint('Starting setNotifyValue(true)'); - if (PlatformInfo.isWeb) { - // On Web, setNotifyValue often hangs indefinitely on the Promise resolution. - // We trigger it but don't await its completion to avoid blocking the connection flow. - debugPrint('Web: Calling setNotifyValue(true) without awaiting'); - // ignore: unawaited_futures - _txCharacteristic!.setNotifyValue(true, timeout: 2).catchError((e) { - debugPrint('Web setNotifyValue error (ignoring): $e'); - return false; // catchError must return a bool to match Future - }); - // Give the browser a moment to process the underlying startNotifications call - await Future.delayed(const Duration(milliseconds: 500)); - } else { - // Native platforms handle setNotifyValue blockingly with CCCD descriptors - bool notifySet = false; - for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { - try { - if (attempt > 0) { - await Future.delayed(Duration(milliseconds: 500 * attempt)); - } - debugPrint('Calling setNotifyValue(true), attempt ${attempt + 1}'); - await _txCharacteristic!.setNotifyValue(true); - notifySet = true; - } catch (e) { - debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); - if (attempt == 2) rethrow; + // Retry setNotifyValue with increasing delays + bool notifySet = false; + for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { + try { + if (attempt > 0) { + await Future.delayed(Duration(milliseconds: 500 * attempt)); } + await _txCharacteristic!.setNotifyValue(true); + notifySet = true; + } catch (e) { + debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); + if (attempt == 2) rethrow; } } - debugPrint('setNotifyValue(true) configuration completed'); + _notifySubscription = _txCharacteristic!.onValueReceived.listen( + _handleFrame, + ); _setState(MeshCoreConnectionState.connected);