Compare commits

...

16 Commits

Author SHA1 Message Date
Winston Lowe ba4fd3eff5 Merge branch 'dev-offline' of github.com:zjs81/meshcore-open into dev-offline 2026-05-27 10:31:23 -07:00
Winston Lowe beb3e1996d fix: Improve error handling when restoring last companion public key and update disconnection message in Dutch localization 2026-05-27 10:26:19 -07:00
Winston Lowe 3f4e6f4e13 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-12 18:49:25 -07:00
Winston Lowe e1cc285c8a fix: Correct channel addition logic to prevent duplicates based on pskHex 2026-05-12 18:31:55 -07:00
Winston Lowe f4b32e8a8a Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-12 18:30:39 -07:00
Winston Lowe 01bf95c98e fix: Implement disconnect method in _FakeMeshCoreConnector for TCP and USB flow tests 2026-05-12 18:21:18 -07:00
Winston Lowe fa044dd204 feat: Add dialog messages for companion connection and disconnection
- Introduced new dialog messages for connecting to a companion and handling disconnection across multiple languages.
- Updated localization files for French, Hungarian, Italian, Japanese, Korean, Bulgarian, German, English, Spanish, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, and Chinese.
- Modified the contacts and map screens to utilize the new dialog messages.
- Enhanced the disconnect confirmation dialog to show a message upon successful disconnection.
- Updated app bar to conditionally display radio stats based on companion connection status.
2026-05-12 18:01:31 -07:00
Winston Lowe 72fea3fc32 fix: Enhance channel deduplication logic in loadChannels method 2026-05-12 18:01:31 -07:00
Winston Lowe f0bd61144c fix: Remove unnecessary whitespace in keyedMessageIds initialization 2026-05-12 18:01:31 -07:00
Winston Lowe 61c897630c fix: Improve message key handling and enhance contact tile key uniqueness 2026-05-12 18:01:31 -07:00
Winston Lowe a270e2e6d1 fix: Add byteCount initialization in PathRecord.fromJson method 2026-05-12 18:01:31 -07:00
Winston Lowe 247db6a36d fix: Initialize _selfPublicKey with lastCompanionPublicKeyHex in MeshCoreConnector 2026-05-12 18:01:31 -07:00
Winston Lowe 78d08afb47 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-12 18:01:31 -07:00
Winston Lowe c77264cc81 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-12 18:01:31 -07:00
Winston Lowe d6ed8c5f13 feat: Enhance companion connection features and UI updates
- Added functionality to load and restore the last companion's scope on app startup.
- Implemented caching mechanisms for contacts, channels, and messages related to the current companion.
- Updated UI to reflect connection status, including disabling message input when disconnected.
- Introduced new dialog prompts to inform users when they need to connect to a companion for accessing features.
- Refactored navigation logic to improve user experience when disconnected, directing users to the scanner screen.
- Added localization strings for new companion connection prompts in multiple languages.
2026-05-12 18:01:31 -07:00
Winston Lowe 209fee48ca Add byteCount field to PathRecord class with default value 2026-05-12 17:59:58 -07:00
51 changed files with 662 additions and 176 deletions
+98 -22
View File
@@ -40,6 +40,7 @@ import '../storage/contact_discovery_store.dart';
import '../storage/contact_settings_store.dart';
import '../storage/contact_store.dart';
import '../storage/message_store.dart';
import '../storage/prefs_manager.dart';
import '../storage/unread_store.dart';
import '../utils/app_logger.dart';
import '../utils/battery_utils.dart';
@@ -124,6 +125,8 @@ class MeshCoreRadioStateSnapshot {
class MeshCoreConnector extends ChangeNotifier {
// Message windowing to limit memory usage
static const int _messageWindowSize = 200;
static const String _lastCompanionPublicKeyPref =
'last_companion_public_key_hex';
MeshCoreConnectionState _state = MeshCoreConnectionState.disconnected;
BluetoothDevice? _device;
@@ -478,6 +481,9 @@ class MeshCoreConnector extends ChangeNotifier {
}
List<Message> getMessages(Contact contact) {
if (!_loadedConversationKeys.contains(contact.publicKeyHex)) {
unawaited(_loadMessagesForContact(contact.publicKeyHex));
}
return _conversations[contact.publicKeyHex] ?? [];
}
@@ -682,8 +688,14 @@ class MeshCoreConnector extends ChangeNotifier {
}
Future<void> loadCachedChannels() async {
_cachedChannels = await _channelStore.loadChannels();
_recalculateCachedChannelsUnreadTotal();
final loaded = await _channelStore.loadChannels();
_cachedChannels = loaded;
if (_channels.isEmpty && loaded.isNotEmpty) {
_channels
..clear()
..addAll(loaded);
notifyListeners();
}
}
void setActiveContact(String? contactKeyHex) {
@@ -947,6 +959,79 @@ class MeshCoreConnector extends ChangeNotifier {
}
}
Future<void> loadAllCachedDataForCurrentCompanion() async {
await loadContactCache();
await _loadDiscoveredContactCache();
await loadChannelSettings();
await loadCachedChannels();
await loadAllChannelMessages();
await loadUnreadState();
}
Future<void> restoreLastCompanionScope() async {
final prefs = PrefsManager.instance;
final lastCompanionPublicKeyHex = prefs.getString(
_lastCompanionPublicKeyPref,
);
try {
if (lastCompanionPublicKeyHex == null ||
lastCompanionPublicKeyHex.trim().isEmpty) {
return;
}
_selfPublicKey = hexToPubKey(lastCompanionPublicKeyHex);
_setScopedStorePublicKey(lastCompanionPublicKeyHex);
} catch (e) {
_appDebugLogService?.error(
'Failed to restore last companion scope with public key hex: $lastCompanionPublicKeyHex, error: $e',
);
}
}
Future<void> loadDiscoveredContactCache() => _loadDiscoveredContactCache();
void _setScopedStorePublicKey(String publicKeyHex) {
_channelMessageStore.setPublicKeyHex = publicKeyHex;
_messageStore.setPublicKeyHex = publicKeyHex;
_channelOrderStore.setPublicKeyHex = publicKeyHex;
_channelSettingsStore.setPublicKeyHex = publicKeyHex;
_contactSettingsStore.setPublicKeyHex = publicKeyHex;
_contactStore.setPublicKeyHex = publicKeyHex;
_channelStore.setPublicKeyHex = publicKeyHex;
_unreadStore.setPublicKeyHex = publicKeyHex;
}
void _clearCachedCompanionData() {
_contacts.clear();
_discoveredContacts.clear();
_conversations.clear();
_loadedConversationKeys.clear();
_channelMessages.clear();
_channels.clear();
_cachedChannels.clear();
_previousChannelsCache.clear();
_knownContactKeys.clear();
_contactUnreadCount.clear();
_unreadStateLoaded = false;
notifyListeners();
}
Future<void> _persistLastCompanionScope() async {
final keyHex = selfPublicKeyHex;
if (keyHex.isEmpty) return;
final prefs = PrefsManager.instance;
await prefs.setString(_lastCompanionPublicKeyPref, keyHex);
}
Future<void> _reloadOfflineCachesForLastCompanion() async {
if (_state != MeshCoreConnectionState.disconnected) {
return;
}
await restoreLastCompanionScope();
await loadContactCache();
await _loadDiscoveredContactCache();
notifyListeners();
}
Future<void> _loadDiscoveredContactCache() async {
final cached = await _discoveryContactStore.loadContacts();
_discoveredContacts
@@ -1484,6 +1569,7 @@ class MeshCoreConnector extends ChangeNotifier {
_cancelReconnectTimer();
_manualDisconnect = false;
_resetConnectionHandshakeState();
_clearCachedCompanionData();
_activeTransport = MeshCoreTransportType.usb;
_setState(MeshCoreConnectionState.connecting);
@@ -1565,6 +1651,7 @@ class MeshCoreConnector extends ChangeNotifier {
_cancelReconnectTimer();
_manualDisconnect = false;
_resetConnectionHandshakeState();
_clearCachedCompanionData();
_activeTransport = MeshCoreTransportType.tcp;
_setState(MeshCoreConnectionState.connecting);
@@ -1699,6 +1786,7 @@ class MeshCoreConnector extends ChangeNotifier {
_activeTransport = MeshCoreTransportType.bluetooth;
await stopScan();
_clearCachedCompanionData();
_setState(MeshCoreConnectionState.connecting);
_device = device;
_deviceId = device.remoteId.toString();
@@ -2450,6 +2538,7 @@ class MeshCoreConnector extends ChangeNotifier {
'Disconnect complete transport=$transportLabel manual=$manual',
tag: 'Connection',
);
unawaited(_reloadOfflineCachesForLastCompanion());
if (!manual && transportAtDisconnect == MeshCoreTransportType.bluetooth) {
_scheduleReconnect();
}
@@ -3834,26 +3923,12 @@ class MeshCoreConnector extends ChangeNotifier {
return;
}
//set all the stores' public key so they can load the correct data
_channelMessageStore.setPublicKeyHex = selfPublicKeyHex;
_messageStore.setPublicKeyHex = selfPublicKeyHex;
_channelOrderStore.setPublicKeyHex = selfPublicKeyHex;
_channelSettingsStore.setPublicKeyHex = selfPublicKeyHex;
_contactSettingsStore.setPublicKeyHex = selfPublicKeyHex;
_contactStore.setPublicKeyHex = selfPublicKeyHex;
_channelStore.setPublicKeyHex = selfPublicKeyHex;
_unreadStore.setPublicKeyHex = selfPublicKeyHex;
// Set scoped stores to this companion and remember it for next launch.
_setScopedStorePublicKey(selfPublicKeyHex);
unawaited(_persistLastCompanionScope());
// Now that we have self info, we can load all the persisted data for this node
_loadChannelOrder();
loadContactCache();
loadChannelSettings();
loadCachedChannels();
// Load persisted channel messages
loadAllChannelMessages();
loadUnreadState();
_loadDiscoveredContactCache();
// Now that we have self info, we can load all the persisted data for this node.
unawaited(loadAllCachedDataForCurrentCompanion());
_awaitingSelfInfo = false;
_selfInfoRetryTimer?.cancel();
@@ -5082,7 +5157,8 @@ class MeshCoreConnector extends ChangeNotifier {
_channelSyncRetries = 0; // Reset retry counter on success
// Only add non-empty channels
if (!channel.isEmpty) {
if (!channel.isEmpty &&
!_channels.any((c) => c.pskHex == channel.pskHex)) {
_channels.add(channel);
}
+5 -1
View File
@@ -2311,5 +2311,9 @@
"settings_companionDebugLogSubtitle": "Команди, отговори и сурови данни за протоколите BLE/TCP/USB",
"chat_newMessages": "Нови съобщения",
"settings_companionDebugLog": "Лог за отстраняване на грешки (за съпътстваща програма)",
"repeater_chanUtil": "Използване на канала"
"repeater_chanUtil": "Използване на канала",
"dialog_connectCompanion": "Свържете се с придружител, за да получите достъп до функциите на ретранслатора и сървъра за стаи.",
"dialog_disconnectedTitle": "Прекъснато",
"dialog_disconnectedMessage": "Свързването ви с вашия спътник е прекъснато.",
"contact_connectCompanion": "Свържете се с спътник, за да получите достъп до функциите на repeater и room server."
}
+5 -1
View File
@@ -2339,5 +2339,9 @@
"chat_newMessages": "Neue Nachrichten",
"settings_companionDebugLog": "Debug-Protokoll für die Begleitsoftware",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB-Befehle, Antworten und Rohdaten",
"repeater_chanUtil": "Nutzung des Kanals"
"repeater_chanUtil": "Nutzung des Kanals",
"dialog_connectCompanion": "Verbinden Sie sich mit einem Companion, um auf die Funktionen des Repeaters und des Raumservers zuzugreifen.",
"dialog_disconnectedTitle": "Getrennt",
"dialog_disconnectedMessage": "Du wurdest von deinem Begleiter getrennt.",
"contact_connectCompanion": "Mit einem Companion verbinden, um auf Repeater- und Raumserver-Funktionen zuzugreifen."
}
+4 -1
View File
@@ -1062,6 +1062,9 @@
"time_allTime": "All Time",
"dialog_disconnect": "Disconnect",
"dialog_disconnectConfirm": "Are you sure you want to disconnect from this device?",
"dialog_disconnectedTitle": "Disconnected",
"dialog_disconnectedMessage": "You have been disconnected from your companion.",
"dialog_connectCompanion": "Connect to a companion to access repeater and room server features.",
"login_repeaterLogin": "Repeater Login",
"login_roomLogin": "Room Server Login",
"login_password": "Password",
@@ -2367,4 +2370,4 @@
"contact_typeRoom": "Room",
"contact_typeSensor": "Sensor",
"contact_typeUnknown": "Unknown"
}
}
+5 -1
View File
@@ -2339,5 +2339,9 @@
"chat_newMessages": "Nuevos mensajes",
"settings_companionDebugLogSubtitle": "Comandos, respuestas y datos brutos para protocolos BLE/TCP/USB",
"chat_markAsUnread": "Marcar como no leído",
"repeater_chanUtil": "Utilización del canal"
"repeater_chanUtil": "Utilización del canal",
"dialog_connectCompanion": "Conéctate a un compañero para acceder a las funciones de repetidor y servidor de sala.",
"dialog_disconnectedTitle": "Desconectado",
"dialog_disconnectedMessage": "Te has desconectado de tu compañero.",
"contact_connectCompanion": "Conéctate a un compañero para acceder a las funciones del repetidor y del servidor de la sala."
}
+5 -1
View File
@@ -2318,5 +2318,9 @@
"chat_markAsUnread": "Signaler comme non lu",
"chat_newMessages": "Nouveaux messages",
"settings_companionDebugLogSubtitle": "Commandes, réponses et données brutes pour les protocoles BLE/TCP/USB",
"repeater_chanUtil": "Utilisation du canal"
"repeater_chanUtil": "Utilisation du canal",
"dialog_connectCompanion": "Connectez-vous à un compagnon pour accéder aux fonctionnalités de répéteur et de serveur de salle.",
"dialog_disconnectedTitle": "Déconnecté",
"dialog_disconnectedMessage": "Vous avez été déconnecté de votre compagnon.",
"contact_connectCompanion": "Connectez-vous à un compagnon pour accéder aux fonctionnalités du répéteur et du serveur de salle."
}
+5 -1
View File
@@ -2349,5 +2349,9 @@
"chat_newMessages": "Új üzenetek",
"settings_companionDebugLog": "Párhuzamos hibakeresési napló",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB parancsok, válaszok és alapvető adatok",
"repeater_chanUtil": "Csatorna-használat"
"repeater_chanUtil": "Csatorna-használat",
"dialog_connectCompanion": "Csatlakozz egy kísérőhöz az ismétlő- és szobaszerver-funkciók eléréséhez.",
"dialog_disconnectedTitle": "Kapcsolat megszakadt",
"dialog_disconnectedMessage": "A kapcsolat megszakadt a kísérővel.",
"contact_connectCompanion": "Csatlakozz egy kísérőhöz az ismétlő- és szobaszerver-funkciók eléréséhez."
}
+5 -1
View File
@@ -2311,5 +2311,9 @@
"settings_companionDebugLog": "Registro di debug per il supporto",
"chat_newMessages": "Nuovi messaggi",
"chat_markAsUnread": "Segna come non letto",
"repeater_chanUtil": "Utilizzo del canale"
"repeater_chanUtil": "Utilizzo del canale",
"dialog_connectCompanion": "Connettiti a un dispositivo companion per accedere alle funzionalità di ripetitore e server stanza.",
"dialog_disconnectedTitle": "Disconnesso",
"dialog_disconnectedMessage": "Sei stato disconnesso dal tuo compagno.",
"contact_connectCompanion": "Connettiti a un companion per accedere alle funzioni del repeater e del server di stanza."
}
+5 -1
View File
@@ -2349,5 +2349,9 @@
"settings_companionDebugLog": "同伴デバッグログ",
"chat_newMessages": "新しいメッセージ",
"chat_markAsUnread": "未読としてマークする",
"repeater_chanUtil": "チャンネルの利用状況"
"repeater_chanUtil": "チャンネルの利用状況",
"dialog_connectCompanion": "コネクトしてリピーターとルームサーバー機能にアクセス",
"dialog_disconnectedTitle": "切断済み",
"dialog_disconnectedMessage": "コンパニオンとの接続が切れました。",
"contact_connectCompanion": "リピーターおよびルームサーバー機能にアクセスするには、コンパニオンに接続してください。"
}
+5 -1
View File
@@ -2349,5 +2349,9 @@
"chat_newMessages": "새로운 메시지",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB 명령어, 응답 및 원시 데이터",
"chat_markAsUnread": "미리 읽지 않음으로 표시",
"repeater_chanUtil": "채널 활용도"
"repeater_chanUtil": "채널 활용도",
"dialog_connectCompanion": "리피터 및 룸 서버 기능에 액세스하려면 컴패니언에 연결하세요.",
"dialog_disconnectedTitle": "연결 끊김",
"dialog_disconnectedMessage": "컴패니언과의 연결이 끊어졌습니다.",
"contact_connectCompanion": "리피터 및 룸 서버 기능에 액세스하려면 컴패니언에 연결하세요."
}
+18
View File
@@ -3561,6 +3561,24 @@ abstract class AppLocalizations {
/// **'Are you sure you want to disconnect from this device?'**
String get dialog_disconnectConfirm;
/// No description provided for @dialog_disconnectedTitle.
///
/// In en, this message translates to:
/// **'Disconnected'**
String get dialog_disconnectedTitle;
/// No description provided for @dialog_disconnectedMessage.
///
/// In en, this message translates to:
/// **'You have been disconnected from your companion.'**
String get dialog_disconnectedMessage;
/// No description provided for @dialog_connectCompanion.
///
/// In en, this message translates to:
/// **'Connect to a companion to access repeater and room server features.'**
String get dialog_connectCompanion;
/// No description provided for @login_repeaterLogin.
///
/// In en, this message translates to:
+11
View File
@@ -1979,6 +1979,17 @@ class AppLocalizationsBg extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Сигурни ли сте, че искате да се откъснете от това устройство?';
@override
String get dialog_disconnectedTitle => 'Прекъснато';
@override
String get dialog_disconnectedMessage =>
'Свързването ви с вашия спътник е прекъснато.';
@override
String get dialog_connectCompanion =>
'Свържете се с придружител, за да получите достъп до функциите на ретранслатора и сървъра за стаи.';
@override
String get login_repeaterLogin => 'Повторител Вход';
+11
View File
@@ -1977,6 +1977,17 @@ class AppLocalizationsDe extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?';
@override
String get dialog_disconnectedTitle => 'Getrennt';
@override
String get dialog_disconnectedMessage =>
'Du wurdest von deinem Begleiter getrennt.';
@override
String get dialog_connectCompanion =>
'Verbinden Sie sich mit einem Companion, um auf die Funktionen des Repeaters und des Raumservers zuzugreifen.';
@override
String get login_repeaterLogin => 'Beim Repeater anmelden';
+11
View File
@@ -1938,6 +1938,17 @@ class AppLocalizationsEn extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Are you sure you want to disconnect from this device?';
@override
String get dialog_disconnectedTitle => 'Disconnected';
@override
String get dialog_disconnectedMessage =>
'You have been disconnected from your companion.';
@override
String get dialog_connectCompanion =>
'Connect to a companion to access repeater and room server features.';
@override
String get login_repeaterLogin => 'Repeater Login';
+11
View File
@@ -1974,6 +1974,17 @@ class AppLocalizationsEs extends AppLocalizations {
String get dialog_disconnectConfirm =>
'¿Está seguro de que desea desconectarse de este dispositivo?';
@override
String get dialog_disconnectedTitle => 'Desconectado';
@override
String get dialog_disconnectedMessage =>
'Te has desconectado de tu compañero.';
@override
String get dialog_connectCompanion =>
'Conéctate a un compañero para acceder a las funciones de repetidor y servidor de sala.';
@override
String get login_repeaterLogin => 'Iniciar sesión en el Repetidor';
+11
View File
@@ -1985,6 +1985,17 @@ class AppLocalizationsFr extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?';
@override
String get dialog_disconnectedTitle => 'Déconnecté';
@override
String get dialog_disconnectedMessage =>
'Vous avez été déconnecté de votre compagnon.';
@override
String get dialog_connectCompanion =>
'Connectez-vous à un compagnon pour accéder aux fonctionnalités de répéteur et de serveur de salle.';
@override
String get login_repeaterLogin => 'Connexion au répéteur';
+10
View File
@@ -1986,6 +1986,16 @@ class AppLocalizationsHu extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Biztosan szeretné kiírni ezt a készüléket?';
@override
String get dialog_disconnectedTitle => 'Lejárat';
@override
String get dialog_disconnectedMessage => 'Lehentetőtől megszakadtál.';
@override
String get dialog_connectCompanion =>
'Csatlakozzon egy kísérőhöz a ismétlő és szobaszerver funkciók eléréséhez.';
@override
String get login_repeaterLogin => 'Ismételt bejelentkezés';
+11
View File
@@ -1976,6 +1976,17 @@ class AppLocalizationsIt extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Sei sicuro di voler disconnetterti da questo dispositivo?';
@override
String get dialog_disconnectedTitle => 'Disconnesso';
@override
String get dialog_disconnectedMessage =>
'Sei stato disconnesso dal tuo compagno.';
@override
String get dialog_connectCompanion =>
'Connettiti a un dispositivo companion per accedere alle funzionalità di ripetitore e server stanza.';
@override
String get login_repeaterLogin => 'Login Ripetitore';
+9
View File
@@ -1894,6 +1894,15 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get dialog_disconnectConfirm => '本当にこのデバイスとの接続を解除したいですか?';
@override
String get dialog_disconnectedTitle => '切断済み';
@override
String get dialog_disconnectedMessage => 'コンパニオンとの接続が切れました。';
@override
String get dialog_connectCompanion => 'コネクトしてリピーターとルームサーバー機能にアクセス';
@override
String get login_repeaterLogin => '再ログイン';
+9
View File
@@ -1890,6 +1890,15 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get dialog_disconnectConfirm => '이 장치와의 연결을 해제하시겠습니까?';
@override
String get dialog_disconnectedTitle => '연결 끊김';
@override
String get dialog_disconnectedMessage => '컴패니언과의 연결이 끊어졌습니다.';
@override
String get dialog_connectCompanion => '리피터 및 룸 서버 기능에 액세스하려면 컴패니언에 연결하세요.';
@override
String get login_repeaterLogin => '다시 로그인';
+11
View File
@@ -1963,6 +1963,17 @@ class AppLocalizationsNl extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Ben je er zeker van dat je verbinding met dit apparaat wilt verbreken?';
@override
String get dialog_disconnectedTitle => 'Verbroken';
@override
String get dialog_disconnectedMessage =>
'Je bent losgekoppeld van je companion.';
@override
String get dialog_connectCompanion =>
'Maak verbinding met een companion om repeater- en kamerserverfuncties te gebruiken.';
@override
String get login_repeaterLogin => 'Inloggen Repeater';
+11
View File
@@ -1990,6 +1990,17 @@ class AppLocalizationsPl extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Czy na pewno chcesz się odłączyć od tego urządzenia?';
@override
String get dialog_disconnectedTitle => 'Rozłączono';
@override
String get dialog_disconnectedMessage =>
'Zostałeś rozłączony ze swoim towarzyszem.';
@override
String get dialog_connectCompanion =>
'Połącz się z towarzyszem, aby uzyskać dostęp do funkcji powtarzacza i serwera pokoi.';
@override
String get login_repeaterLogin => 'Logowanie do przekaźnika';
+11
View File
@@ -1973,6 +1973,17 @@ class AppLocalizationsPt extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Tem certeza de que deseja desconectar deste dispositivo?';
@override
String get dialog_disconnectedTitle => 'Desconectado';
@override
String get dialog_disconnectedMessage =>
'Você foi desconectado do seu companheiro.';
@override
String get dialog_connectCompanion =>
'Conecte-se a um dispositivo companion para acessar as funcionalidades de repetidor e servidor de salas.';
@override
String get login_repeaterLogin => 'Login ao Repetidor';
+11
View File
@@ -1977,6 +1977,17 @@ class AppLocalizationsRu extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Вы уверены, что хотите отключиться от этого устройства?';
@override
String get dialog_disconnectedTitle => 'Отключено';
@override
String get dialog_disconnectedMessage =>
'Вы были отключены от вашего компаньона.';
@override
String get dialog_connectCompanion =>
'Подключитесь к компаньону, чтобы получить доступ к функциям ретранслятора и сервера комнат.';
@override
String get login_repeaterLogin => 'Вход в репитер';
+11
View File
@@ -1963,6 +1963,17 @@ class AppLocalizationsSk extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Ste si istý/á, že chcete odpojiť od tohto zariadenia?';
@override
String get dialog_disconnectedTitle => 'Odpojené';
@override
String get dialog_disconnectedMessage =>
'Od vášho spoločníka ste boli odpojený.';
@override
String get dialog_connectCompanion =>
'Pripojte sa k sprievodcovi a získajte prístup k funkciám opakovača a serveru miestností.';
@override
String get login_repeaterLogin => 'Opätovné prihlásenie';
+11
View File
@@ -1961,6 +1961,17 @@ class AppLocalizationsSl extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Ste prepričani, da želite se odklopiti s tega naprave?';
@override
String get dialog_disconnectedTitle => 'Prekinjeno';
@override
String get dialog_disconnectedMessage =>
'Prekinjena povezava s vašim spre伴ovalcem.';
@override
String get dialog_connectCompanion =>
'Povežite se s spremljevalnikom za dostop do funkcij ponavljalnika in strežnika sob.';
@override
String get login_repeaterLogin => 'Ponovni vnos';
+11
View File
@@ -1950,6 +1950,17 @@ class AppLocalizationsSv extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Är du säker på att du vill koppla från enheten?';
@override
String get dialog_disconnectedTitle => 'Ansluten ej';
@override
String get dialog_disconnectedMessage =>
'Du har kopplats från din companion.';
@override
String get dialog_connectCompanion =>
'Anslut till en sällskapstjänst för att komma åt upprepning och rumsserverfunktioner.';
@override
String get login_repeaterLogin => 'Återuppta Inloggning';
+11
View File
@@ -1972,6 +1972,17 @@ class AppLocalizationsUk extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Ви впевнені, що хочете відключитись від цього пристрою?';
@override
String get dialog_disconnectedTitle => 'Від’єднано';
@override
String get dialog_disconnectedMessage =>
'Вас від’єднано від вашого супутника.';
@override
String get dialog_connectCompanion =>
'Підключіться до супутнього пристрою, щоб отримати доступ до функцій ретранслятора та сервера кімнат.';
@override
String get login_repeaterLogin => 'Вхід у ретранслятор';
+9
View File
@@ -1861,6 +1861,15 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get dialog_disconnectConfirm => '确定要断开与此设备的连接吗?';
@override
String get dialog_disconnectedTitle => '已断开连接';
@override
String get dialog_disconnectedMessage => '你已与你的伙伴断开连接。';
@override
String get dialog_connectCompanion => '连接伴机以访问中继器和房间服务器功能。';
@override
String get login_repeaterLogin => '转发节点登录';
+5 -1
View File
@@ -2311,5 +2311,9 @@
"chat_newMessages": "Nieuwe berichten",
"chat_markAsUnread": "Markeer als ongelezen",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB commando's, antwoorden en ruwe data",
"repeater_chanUtil": "Gebruik van het kanaal"
"repeater_chanUtil": "Gebruik van het kanaal",
"dialog_connectCompanion": "Maak verbinding met een companion om repeater- en kamerserverfuncties te gebruiken.",
"dialog_disconnectedTitle": "Verbroken",
"dialog_disconnectedMessage": "Je bent losgekoppeld van je companion.",
"contact_connectCompanion": "Maak verbinding met een companion om toegang te krijgen tot repeater- en roomserverfuncties."
}
+5 -1
View File
@@ -2349,5 +2349,9 @@
"settings_companionDebugLogSubtitle": "Polecenia, odpowiedzi i surowe dane związane z protokołami BLE/TCP/USB",
"chat_markAsUnread": "Oznacz jako nieprzeczytane",
"settings_companionDebugLog": "Log debugowania (dla pomocy w rozwiązywaniu problemów)",
"repeater_chanUtil": "Wykorzystanie kanału"
"repeater_chanUtil": "Wykorzystanie kanału",
"dialog_connectCompanion": "Połącz się z towarzyszem, aby uzyskać dostęp do funkcji powtarzacza i serwera pokoi.",
"dialog_disconnectedTitle": "Rozłączono",
"dialog_disconnectedMessage": "Zostałeś rozłączony ze swoim towarzyszem.",
"contact_connectCompanion": "Połącz się z towarzyszem, aby uzyskać dostęp do funkcji repeatera i serwera pokojowego."
}
+5 -1
View File
@@ -2311,5 +2311,9 @@
"settings_companionDebugLogSubtitle": "Comandos, respostas e dados brutos para protocolos BLE/TCP/USB",
"chat_markAsUnread": "Marcar como não lido",
"chat_newMessages": "Novas mensagens",
"repeater_chanUtil": "Utilização do canal"
"repeater_chanUtil": "Utilização do canal",
"dialog_connectCompanion": "Conecte-se a um dispositivo companion para acessar as funcionalidades de repetidor e servidor de salas.",
"dialog_disconnectedTitle": "Desconectado",
"dialog_disconnectedMessage": "Você foi desconectado do seu companheiro.",
"contact_connectCompanion": "Conecte-se a um companheiro para acessar recursos de repetidor e servidor de sala."
}
+5 -1
View File
@@ -1614,5 +1614,9 @@
"repeater_cliHelpStatsCore": "(Только для серийного оборудования) Отображает основные статистические данные прошивки.",
"settings_companionDebugLogSubtitle": "Команды, ответы и необработанные данные, используемые для протоколов BLE, TCP и USB.",
"repeater_chanUtil": "Использование канала",
"settings_companionDebugLog": "Журнал отладки (для сопутствующего приложения)"
"settings_companionDebugLog": "Журнал отладки (для сопутствующего приложения)",
"dialog_connectCompanion": "Подключитесь к компаньону, чтобы получить доступ к функциям ретранслятора и сервера комнат.",
"dialog_disconnectedTitle": "Отключено",
"dialog_disconnectedMessage": "Вы были отключены от вашего компаньона.",
"contact_connectCompanion": "Подключитесь к компаньону, чтобы получить доступ к функциям репитера и серверу комнаты."
}
+5 -1
View File
@@ -2311,5 +2311,9 @@
"settings_companionDebugLogSubtitle": "Príkazy, odpovede a surové dáta pre protokoly BLE/TCP/USB",
"settings_companionDebugLog": "Logovanie pre ladenie (sprievodný log)",
"chat_newMessages": "Nové správy",
"repeater_chanUtil": "Využitie kanálu"
"repeater_chanUtil": "Využitie kanálu",
"dialog_connectCompanion": "Pripojte sa k sprievodcovi a získajte prístup k funkciám opakovača a serveru miestností.",
"dialog_disconnectedTitle": "Odpojené",
"dialog_disconnectedMessage": "Od vášho spoločníka ste boli odpojený.",
"contact_connectCompanion": "Pripojte sa k spoločníkovi pre prístup k funkciám opakovača a miestneho servera."
}
+5 -1
View File
@@ -2311,5 +2311,9 @@
"chat_markAsUnread": "Označiti kot neneobdelano",
"chat_newMessages": "Nove novice",
"settings_companionDebugLogSubtitle": "Navodila, odgovori in surova podatka za BLE/TCP/USB.",
"repeater_chanUtil": "Uporaba kanala"
"repeater_chanUtil": "Uporaba kanala",
"dialog_connectCompanion": "Povežite se s spremljevalnikom za dostop do funkcij ponavljalnika in strežnika sob.",
"dialog_disconnectedTitle": "Prekinjeno",
"dialog_disconnectedMessage": "Prekinjena povezava s vašim spre伴ovalcem.",
"contact_connectCompanion": "Povežite se s ponсоbnikom za dostop do funkcij pon 반복nika in strežnika prostorov."
}
+5 -1
View File
@@ -2311,5 +2311,9 @@
"settings_companionDebugLog": "Följande felsökningslogg",
"chat_newMessages": "Nya meddelanden",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB-kommandon, svar och rådata",
"repeater_chanUtil": "Användning av kanal"
"repeater_chanUtil": "Användning av kanal",
"dialog_connectCompanion": "Anslut till en sällskapstjänst för att komma åt upprepning och rumsserverfunktioner.",
"dialog_disconnectedTitle": "Ansluten ej",
"dialog_disconnectedMessage": "Du har kopplats från din companion.",
"contact_connectCompanion": "Anslut till en companion för att få tillgång till repeater- och rumsserverfunktioner."
}
+5 -1
View File
@@ -2291,5 +2291,9 @@
"settings_companionDebugLogSubtitle": "Команди, відповіді та необроблена інформація для протоколів BLE/TCP/USB",
"chat_newMessages": "Нові повідомлення",
"chat_markAsUnread": "Позначити як непрочитане",
"repeater_chanUtil": "Використання каналу"
"repeater_chanUtil": "Використання каналу",
"dialog_connectCompanion": "Підключіться до супутнього пристрою, щоб отримати доступ до функцій ретранслятора та сервера кімнат.",
"dialog_disconnectedTitle": "Від’єднано",
"dialog_disconnectedMessage": "Вас від’єднано від вашого супутника.",
"contact_connectCompanion": "Підключіться до супутника, щоб отримати доступ до функцій репітера та серверів кімнат."
}
+5 -1
View File
@@ -2316,5 +2316,9 @@
"settings_companionDebugLog": "调试日志",
"chat_newMessages": "新的消息",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB 协议、响应和原始数据",
"repeater_chanUtil": "频道利用率"
"repeater_chanUtil": "频道利用率",
"dialog_connectCompanion": "连接伴机以访问中继器和房间服务器功能。",
"dialog_disconnectedTitle": "已断开连接",
"dialog_disconnectedMessage": "你已与你的伙伴断开连接。",
"contact_connectCompanion": "连接至伴侣设备以访问中继器和房间服务器功能。"
}
+4 -9
View File
@@ -5,10 +5,10 @@ import 'l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'screens/chrome_required_screen.dart';
import 'screens/contacts_screen.dart';
import 'utils/platform_info.dart';
import 'connector/meshcore_connector.dart';
import 'screens/scanner_screen.dart';
import 'services/storage_service.dart';
import 'services/message_retry_service.dart';
import 'services/path_history_service.dart';
@@ -81,13 +81,8 @@ void main() async {
timeoutPredictionService: timeoutPredictionService,
);
await connector.loadContactCache();
await connector.loadChannelSettings();
await connector.loadCachedChannels();
// Load persisted channel messages
await connector.loadAllChannelMessages();
await connector.loadUnreadState();
await connector.restoreLastCompanionScope();
await connector.loadAllCachedDataForCurrentCompanion();
runApp(
MeshCoreApp(
@@ -218,7 +213,7 @@ class MeshCoreApp extends StatelessWidget {
},
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
? const ChromeRequiredScreen()
: const ScannerScreen(),
: const ContactsScreen(),
);
},
),
+3
View File
@@ -3,6 +3,7 @@ class PathRecord {
final int tripTimeMs;
final DateTime? timestamp;
final bool wasFloodDiscovery;
final int byteCount;
final List<int> pathBytes;
final int successCount;
final int failureCount;
@@ -17,6 +18,7 @@ class PathRecord {
required this.successCount,
required this.failureCount,
this.routeWeight = 1.0,
this.byteCount = 0,
});
String get displayText =>
@@ -48,6 +50,7 @@ class PathRecord {
successCount: json['success_count'] as int? ?? 0,
failureCount: json['failure_count'] as int? ?? 0,
routeWeight: (json['route_weight'] as num?)?.toDouble() ?? 1.0,
byteCount: json['byte_count'] as int? ?? 0,
);
}
}
+15 -3
View File
@@ -377,6 +377,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final reversedMessages = messages.reversed.toList();
final itemCount =
reversedMessages.length + (_isLoadingOlder ? 1 : 0);
final keyedMessageIds = <String>{};
// Auto-scroll to bottom if user is already at bottom
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -413,14 +414,20 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
}
final messageIndex = index;
final message = reversedMessages[messageIndex];
if (!_messageKeys.containsKey(message.messageId)) {
final shouldAttachMessageKey = keyedMessageIds.add(
message.messageId,
);
if (shouldAttachMessageKey &&
!_messageKeys.containsKey(message.messageId)) {
_messageKeys[message.messageId] = GlobalKey();
}
final isUnreadAnchor =
_unreadDividerMessageId != null &&
message.messageId == _unreadDividerMessageId;
return Container(
key: _messageKeys[message.messageId]!,
key: shouldAttachMessageKey
? _messageKeys[message.messageId]
: null,
child: Builder(
builder: (context) {
final textScale = context
@@ -1123,6 +1130,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Widget _buildMessageComposer() {
final connector = context.watch<MeshCoreConnector>();
if (!connector.isConnected) {
return const SizedBox.shrink();
}
final maxBytes = maxChannelMessageBytes(connector.selfName);
final settings = context.watch<AppSettingsService>().settings;
return Column(
@@ -1274,6 +1284,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
}
Future<void> _sendMessage() async {
final connector = context.read<MeshCoreConnector>();
if (!connector.isConnected) return;
final text = _textController.text.trim();
if (text.isEmpty) return;
@@ -1288,7 +1301,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
}
_lastChannelSendAt = now;
final connector = context.read<MeshCoreConnector>();
final settings = context.read<AppSettingsService>().settings;
final translationService = context.read<TranslationService>();
+29 -18
View File
@@ -17,7 +17,6 @@ import '../models/channel.dart';
import '../models/community.dart';
import '../storage/community_store.dart';
import '../utils/dialog_utils.dart';
import '../utils/disconnect_navigation_mixin.dart';
import '../utils/route_transitions.dart';
import '../widgets/list_filter_widget.dart';
import '../widgets/empty_state.dart';
@@ -29,6 +28,7 @@ import 'channel_chat_screen.dart';
import 'community_qr_scanner_screen.dart';
import 'contacts_screen.dart';
import 'map_screen.dart';
import 'scanner_screen.dart';
import 'settings_screen.dart';
class ChannelsScreen extends StatefulWidget {
@@ -40,8 +40,7 @@ class ChannelsScreen extends StatefulWidget {
State<ChannelsScreen> createState() => _ChannelsScreenState();
}
class _ChannelsScreenState extends State<ChannelsScreen>
with DisconnectNavigationMixin {
class _ChannelsScreenState extends State<ChannelsScreen> {
final TextEditingController _searchController = TextEditingController();
final CommunityStore _communityStore = CommunityStore();
final CommunityPskIndex _communityIndex = CommunityPskIndex();
@@ -89,11 +88,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
final channelMessageStore = ChannelMessageStore();
channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex;
// Auto-navigate back to scanner if disconnected
if (!checkConnectionAndNavigate(connector)) {
return const SizedBox.shrink();
}
final allowBack = !connector.isConnected;
return PopScope(
@@ -106,16 +100,33 @@ class _ChannelsScreenState extends State<ChannelsScreen>
actions: [
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.logout, color: Colors.red),
const SizedBox(width: 8),
Text(context.l10n.common_disconnect),
],
if (connector.isConnected)
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.logout, color: Colors.red),
const SizedBox(width: 8),
Text(context.l10n.common_disconnect),
],
),
onTap: () => _disconnect(context),
)
else
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.bluetooth_searching),
const SizedBox(width: 8),
Text(context.l10n.common_connect),
],
),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ScannerScreen(),
),
),
),
onTap: () => _disconnect(context),
),
if (_communities.isNotEmpty)
PopupMenuItem(
child: Row(
@@ -383,7 +394,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
}
return Card(
key: ValueKey('channel_${channel.index}'),
key: ValueKey('${channel.index}_${channel.pskHex}_${channel.name}'),
margin: const EdgeInsets.only(bottom: 12),
child: GestureDetector(
onSecondaryTapUp: PlatformInfo.isDesktop
+10 -2
View File
@@ -414,7 +414,7 @@ class _ChatScreenState extends State<ChatScreen> {
],
),
),
_buildInputBar(connector),
if (connector.isConnected) _buildInputBar(connector),
],
);
},
@@ -451,6 +451,7 @@ class _ChatScreenState extends State<ChatScreen> {
) {
// Reverse messages so newest appear at bottom with reverse: true
final reversedMessages = messages.reversed.toList();
var unreadAnchorKeyAssigned = false;
final itemCount = reversedMessages.length + (_isLoadingOlder ? 1 : 0);
// Auto-scroll to bottom if user is already at bottom
@@ -525,7 +526,11 @@ class _ChatScreenState extends State<ChatScreen> {
children: [const UnreadDivider(), bubble],
)
: bubble;
if (identical(message, _pendingUnreadScrollTarget)) {
final shouldAttachUnreadScrollKey =
!unreadAnchorKeyAssigned &&
identical(message, _pendingUnreadScrollTarget);
if (shouldAttachUnreadScrollKey) {
unreadAnchorKeyAssigned = true;
return KeyedSubtree(key: _unreadScrollKey, child: child);
}
return child;
@@ -693,6 +698,9 @@ class _ChatScreenState extends State<ChatScreen> {
}
Future<void> _sendMessage(MeshCoreConnector connector) async {
if (!connector.isConnected) {
return;
}
final text = _textController.text.trim();
if (text.isEmpty) return;
+112 -72
View File
@@ -19,7 +19,6 @@ import '../services/ui_view_state_service.dart';
import '../utils/contact_search.dart';
import '../storage/contact_group_store.dart';
import '../utils/dialog_utils.dart';
import '../utils/disconnect_navigation_mixin.dart';
import '../utils/emoji_utils.dart';
import '../utils/route_transitions.dart';
import '../widgets/list_filter_widget.dart';
@@ -34,6 +33,7 @@ import 'chat_screen.dart';
import 'discovery_screen.dart';
import 'map_screen.dart';
import 'repeater_hub_screen.dart';
import 'scanner_screen.dart';
import 'settings_screen.dart';
enum RoomLoginDestination { chat, management }
@@ -49,8 +49,7 @@ class ContactsScreen extends StatefulWidget {
State<ContactsScreen> createState() => _ContactsScreenState();
}
class _ContactsScreenState extends State<ContactsScreen>
with DisconnectNavigationMixin {
class _ContactsScreenState extends State<ContactsScreen> {
final TextEditingController _searchController = TextEditingController();
final ContactGroupStore _groupStore = ContactGroupStore();
MeshCoreConnector? _scopeSyncConnector;
@@ -306,11 +305,6 @@ class _ContactsScreenState extends State<ContactsScreen>
Widget build(BuildContext context) {
final connector = context.watch<MeshCoreConnector>();
// Auto-navigate back to scanner if disconnected
if (!checkConnectionAndNavigate(connector)) {
return const SizedBox.shrink();
}
final allowBack = !connector.isConnected;
return PopScope(
canPop: allowBack,
@@ -319,75 +313,93 @@ class _ContactsScreenState extends State<ContactsScreen>
title: AppBarTitle(context.l10n.contacts_title),
automaticallyImplyLeading: false,
actions: [
if (connector.isConnected)
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.connect_without_contact),
const SizedBox(width: 8),
Text(context.l10n.contacts_zeroHopAdvert),
],
),
onTap: () async {
await connector.sendSelfAdvert(flood: false);
showDismissibleSnackBar(
context,
content: Text(context.l10n.settings_advertisementSent),
);
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.cell_tower),
const SizedBox(width: 8),
Text(context.l10n.contacts_floodAdvert),
],
),
onTap: () async {
await connector.sendSelfAdvert(flood: true);
showDismissibleSnackBar(
context,
content: Text(context.l10n.settings_advertisementSent),
);
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.copy),
const SizedBox(width: 8),
Text(context.l10n.contacts_copyAdvertToClipboard),
],
),
onTap: () => _contactExport(Uint8List.fromList([])),
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.paste),
const SizedBox(width: 8),
Text(context.l10n.contacts_addContactFromClipboard),
],
),
onTap: () => _contactImport(),
),
],
icon: const Icon(Icons.connect_without_contact),
),
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.connect_without_contact),
const SizedBox(width: 8),
Text(context.l10n.contacts_zeroHopAdvert),
],
),
onTap: () => {
connector.sendSelfAdvert(flood: false),
showDismissibleSnackBar(
context,
content: Text(context.l10n.settings_advertisementSent),
if (connector.isConnected)
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.logout, color: Colors.red),
const SizedBox(width: 8),
Text(context.l10n.common_disconnect),
],
),
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.cell_tower),
const SizedBox(width: 8),
Text(context.l10n.contacts_floodAdvert),
],
),
onTap: () => {
connector.sendSelfAdvert(flood: true),
showDismissibleSnackBar(
context,
content: Text(context.l10n.settings_advertisementSent),
onTap: () => _disconnect(context, connector),
)
else
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.bluetooth_searching),
const SizedBox(width: 8),
Text(context.l10n.common_connect),
],
),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ScannerScreen(),
),
),
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.copy),
const SizedBox(width: 8),
Text(context.l10n.contacts_copyAdvertToClipboard),
],
),
onTap: () => _contactExport(Uint8List.fromList([])),
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.paste),
const SizedBox(width: 8),
Text(context.l10n.contacts_addContactFromClipboard),
],
),
onTap: () => _contactImport(),
),
],
icon: const Icon(Icons.connect_without_contact),
),
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.logout, color: Colors.red),
const SizedBox(width: 8),
Text(context.l10n.common_disconnect),
],
),
onTap: () => _disconnect(context, connector),
),
PopupMenuItem(
child: Row(
children: [
@@ -816,6 +828,7 @@ class _ContactsScreenState extends State<ContactsScreen>
contact,
);
return _ContactTile(
key: ValueKey(contact.publicKeyHex),
contact: contact,
lastSeen: _resolveLastSeen(contact),
unreadCount: unreadCount,
@@ -967,6 +980,11 @@ class _ContactsScreenState extends State<ContactsScreen>
}
void _showRepeaterLogin(BuildContext context, Contact repeater) {
final connector = context.read<MeshCoreConnector>();
if (!connector.isConnected) {
_showCompanionRequiredDialog(context);
return;
}
showDialog(
context: context,
builder: (context) => RepeaterLoginDialog(
@@ -993,6 +1011,11 @@ class _ContactsScreenState extends State<ContactsScreen>
Contact room,
RoomLoginDestination destination,
) {
final connector = context.read<MeshCoreConnector>();
if (!connector.isConnected) {
_showCompanionRequiredDialog(context);
return;
}
showDialog(
context: context,
builder: (context) => RoomLoginDialog(
@@ -1021,6 +1044,22 @@ class _ContactsScreenState extends State<ContactsScreen>
);
}
void _showCompanionRequiredDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.l10n.scanner_notConnected),
content: Text(context.l10n.dialog_connectCompanion),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(context.l10n.common_ok),
),
],
),
);
}
void _confirmDeleteGroup(BuildContext context, ContactGroup group) {
if (!_hasGroupStoreScope(context.read<MeshCoreConnector>())) {
_showGroupsUnavailableMessage(context);
@@ -1458,6 +1497,7 @@ class _ContactTile extends StatelessWidget {
final VoidCallback onLongPress;
const _ContactTile({
super.key,
required this.contact,
required this.lastSeen,
required this.unreadCount,
+54 -10
View File
@@ -31,6 +31,7 @@ import '../widgets/repeater_login_dialog.dart';
import '../widgets/room_login_dialog.dart';
import '../helpers/snack_bar_builder.dart';
import 'repeater_hub_screen.dart';
import 'scanner_screen.dart';
import 'settings_screen.dart';
import 'line_of_sight_map_screen.dart';
@@ -415,7 +416,7 @@ class _MapScreenState extends State<MapScreen> {
centerTitle: true,
automaticallyImplyLeading: false,
actions: [
if (!_isBuildingPathTrace)
if (!_isBuildingPathTrace && connector.isConnected)
IconButton(
icon: const Icon(Icons.radar),
onPressed: () => _startPath(
@@ -466,16 +467,33 @@ class _MapScreenState extends State<MapScreen> {
),
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.logout, color: Colors.red),
const SizedBox(width: 8),
Text(context.l10n.common_disconnect),
],
if (connector.isConnected)
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.logout, color: Colors.red),
const SizedBox(width: 8),
Text(context.l10n.common_disconnect),
],
),
onTap: () => _disconnect(context, connector),
)
else
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.bluetooth_searching),
const SizedBox(width: 8),
Text(context.l10n.common_connect),
],
),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ScannerScreen(),
),
),
),
onTap: () => _disconnect(context, connector),
),
PopupMenuItem(
child: Row(
children: [
@@ -1482,7 +1500,28 @@ class _MapScreenState extends State<MapScreen> {
);
}
void _showCompanionRequiredDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.l10n.scanner_notConnected),
content: Text(context.l10n.dialog_connectCompanion),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(context.l10n.common_ok),
),
],
),
);
}
void _showRepeaterLogin(BuildContext context, Contact repeater) {
final connector = context.read<MeshCoreConnector>();
if (!connector.isConnected) {
_showCompanionRequiredDialog(context);
return;
}
showDialog(
context: context,
builder: (context) => RepeaterLoginDialog(
@@ -1505,6 +1544,11 @@ class _MapScreenState extends State<MapScreen> {
}
void _showRoomLogin(BuildContext context, Contact room) {
final connector = context.read<MeshCoreConnector>();
if (!connector.isConnected) {
_showCompanionRequiredDialog(context);
return;
}
showDialog(
context: context,
builder: (context) => RoomLoginDialog(
+10 -1
View File
@@ -42,9 +42,18 @@ class ChannelStore {
try {
final jsonList = jsonDecode(jsonString) as List<dynamic>;
return jsonList
final channels = jsonList
.map((entry) => _fromJson(entry as Map<String, dynamic>))
.toList();
// Deduplicate: keep the last entry per channel index
final seen = <int>{};
final deduped = <Channel>[];
for (final channel in channels.reversed) {
if (seen.add(channel.index)) {
deduped.add(channel);
}
}
return deduped.reversed.toList();
} catch (_) {
return [];
}
+15
View File
@@ -31,6 +31,21 @@ Future<bool> showDisconnectDialog(
if (confirmed == true) {
appLogger.info('Disconnect confirmed from popup', tag: 'Connection');
await connector.disconnect();
if (context.mounted) {
await showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: Text(context.l10n.dialog_disconnectedTitle),
content: Text(context.l10n.dialog_disconnectedMessage),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.l10n.common_ok),
),
],
),
);
}
return true;
}
return false;
@@ -1,20 +0,0 @@
import 'package:flutter/material.dart';
import '../connector/meshcore_connector.dart';
/// Mixin that automatically navigates back to scanner when disconnected.
/// Use in State classes for screens that require active connection.
mixin DisconnectNavigationMixin<T extends StatefulWidget> on State<T> {
/// Call this in your Widget build method to enable auto-navigation.
/// Returns true if still connected, false if navigation was triggered.
bool checkConnectionAndNavigate(MeshCoreConnector connector) {
if (!connector.isConnected) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
Navigator.popUntil(context, (route) => route.isFirst);
}
});
return false;
}
return true;
}
}
+2 -1
View File
@@ -69,7 +69,8 @@ class AppBarTitle extends StatelessWidget {
if (showBattery) BatteryIndicator(connector: connector),
if (showSnr) SNRIndicator(connector: connector),
if (connector.supportsCompanionRadioStats)
const RadioStatsIconButton(compact: true),
if (connector.isConnected)
const RadioStatsIconButton(compact: true),
],
),
trailing ?? const SizedBox.shrink(),
+6
View File
@@ -38,6 +38,12 @@ class _FakeMeshCoreConnector extends MeshCoreConnector {
lastHost = host;
lastPort = port;
}
@override
Future<void> disconnect({
bool manual = true,
bool skipBleDeviceDisconnect = false,
}) async {}
}
Widget _buildTestApp({
+6
View File
@@ -73,6 +73,12 @@ class _FakeMeshCoreConnector extends MeshCoreConnector {
void setUsbFallbackDeviceName(String label) {
fallbackDeviceName = label;
}
@override
Future<void> disconnect({
bool manual = true,
bool skipBleDeviceDisconnect = false,
}) async {}
}
Widget _buildTestApp({