From 094d5d2706b3b57a3b810a9774cf6799bd9c06f3 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 10 May 2026 16:02:58 -0700 Subject: [PATCH] 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. --- lib/connector/meshcore_connector.dart | 106 +++++++++++++++++---- lib/l10n/app_bg.arb | 3 +- lib/l10n/app_de.arb | 3 +- lib/l10n/app_en.arb | 3 +- lib/l10n/app_es.arb | 3 +- lib/l10n/app_fr.arb | 3 +- lib/l10n/app_hu.arb | 3 +- lib/l10n/app_it.arb | 3 +- lib/l10n/app_ja.arb | 3 +- lib/l10n/app_ko.arb | 3 +- lib/l10n/app_localizations.dart | 6 ++ lib/l10n/app_localizations_bg.dart | 4 + lib/l10n/app_localizations_de.dart | 4 + lib/l10n/app_localizations_en.dart | 4 + lib/l10n/app_localizations_es.dart | 4 + lib/l10n/app_localizations_fr.dart | 4 + lib/l10n/app_localizations_hu.dart | 4 + lib/l10n/app_localizations_it.dart | 4 + lib/l10n/app_localizations_ja.dart | 3 + lib/l10n/app_localizations_ko.dart | 3 + lib/l10n/app_localizations_nl.dart | 4 + lib/l10n/app_localizations_pl.dart | 4 + lib/l10n/app_localizations_pt.dart | 4 + lib/l10n/app_localizations_ru.dart | 4 + lib/l10n/app_localizations_sk.dart | 4 + lib/l10n/app_localizations_sl.dart | 4 + lib/l10n/app_localizations_sv.dart | 4 + lib/l10n/app_localizations_uk.dart | 4 + lib/l10n/app_localizations_zh.dart | 3 + lib/l10n/app_nl.arb | 3 +- lib/l10n/app_pl.arb | 3 +- lib/l10n/app_pt.arb | 3 +- lib/l10n/app_ru.arb | 3 +- lib/l10n/app_sk.arb | 3 +- lib/l10n/app_sl.arb | 3 +- lib/l10n/app_sv.arb | 3 +- lib/l10n/app_uk.arb | 3 +- lib/l10n/app_zh.arb | 3 +- lib/main.dart | 13 +-- lib/screens/channel_chat_screen.dart | 7 +- lib/screens/channels_screen.dart | 44 +++++---- lib/screens/chat_screen.dart | 5 +- lib/screens/contacts_screen.dart | 70 ++++++++++---- lib/screens/map_screen.dart | 36 +++++-- lib/utils/disconnect_navigation_mixin.dart | 20 ---- 45 files changed, 320 insertions(+), 110 deletions(-) delete mode 100644 lib/utils/disconnect_navigation_mixin.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 538ee38a..6ec4db99 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -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; @@ -476,6 +479,9 @@ class MeshCoreConnector extends ChangeNotifier { } List getMessages(Contact contact) { + if (!_loadedConversationKeys.contains(contact.publicKeyHex)) { + unawaited(_loadMessagesForContact(contact.publicKeyHex)); + } return _conversations[contact.publicKeyHex] ?? []; } @@ -653,7 +659,14 @@ class MeshCoreConnector extends ChangeNotifier { } Future loadCachedChannels() async { - _cachedChannels = await _channelStore.loadChannels(); + final loaded = await _channelStore.loadChannels(); + _cachedChannels = loaded; + if (_channels.isEmpty && loaded.isNotEmpty) { + _channels + ..clear() + ..addAll(loaded); + notifyListeners(); + } } void setActiveContact(String? contactKeyHex) { @@ -913,6 +926,69 @@ class MeshCoreConnector extends ChangeNotifier { } } + Future loadAllCachedDataForCurrentCompanion() async { + await loadContactCache(); + await _loadDiscoveredContactCache(); + await loadChannelSettings(); + await loadCachedChannels(); + await loadAllChannelMessages(); + await loadUnreadState(); + } + + Future restoreLastCompanionScope() async { + final prefs = PrefsManager.instance; + final lastCompanionPublicKeyHex = prefs.getString( + _lastCompanionPublicKeyPref, + ); + if (lastCompanionPublicKeyHex == null || + lastCompanionPublicKeyHex.trim().isEmpty) { + return; + } + _setScopedStorePublicKey(lastCompanionPublicKeyHex); + } + + Future 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(); + _cachedChannels.clear(); + _knownContactKeys.clear(); + _contactUnreadCount.clear(); + _unreadStateLoaded = false; + notifyListeners(); + } + + Future _persistLastCompanionScope() async { + final keyHex = selfPublicKeyHex; + if (keyHex.isEmpty) return; + final prefs = PrefsManager.instance; + await prefs.setString(_lastCompanionPublicKeyPref, keyHex); + } + + Future _reloadOfflineCachesForLastCompanion() async { + if (_state != MeshCoreConnectionState.disconnected) { + return; + } + await restoreLastCompanionScope(); + await loadContactCache(); + await _loadDiscoveredContactCache(); + } + Future _loadDiscoveredContactCache() async { final cached = await _discoveryContactStore.loadContacts(); _discoveredContacts @@ -1450,6 +1526,7 @@ class MeshCoreConnector extends ChangeNotifier { _cancelReconnectTimer(); _manualDisconnect = false; _resetConnectionHandshakeState(); + _clearCachedCompanionData(); _activeTransport = MeshCoreTransportType.usb; _setState(MeshCoreConnectionState.connecting); @@ -1531,6 +1608,7 @@ class MeshCoreConnector extends ChangeNotifier { _cancelReconnectTimer(); _manualDisconnect = false; _resetConnectionHandshakeState(); + _clearCachedCompanionData(); _activeTransport = MeshCoreTransportType.tcp; _setState(MeshCoreConnectionState.connecting); @@ -1665,6 +1743,7 @@ class MeshCoreConnector extends ChangeNotifier { _activeTransport = MeshCoreTransportType.bluetooth; await stopScan(); + _clearCachedCompanionData(); _setState(MeshCoreConnectionState.connecting); _device = device; _deviceId = device.remoteId.toString(); @@ -2416,6 +2495,7 @@ class MeshCoreConnector extends ChangeNotifier { 'Disconnect complete transport=$transportLabel manual=$manual', tag: 'Connection', ); + unawaited(_reloadOfflineCachesForLastCompanion()); if (!manual && transportAtDisconnect == MeshCoreTransportType.bluetooth) { _scheduleReconnect(); } @@ -3796,26 +3876,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(); diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 45536cf6..ef855c3b 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -2311,5 +2311,6 @@ "settings_companionDebugLogSubtitle": "Команди, отговори и сурови данни за протоколите BLE/TCP/USB", "chat_newMessages": "Нови съобщения", "settings_companionDebugLog": "Лог за отстраняване на грешки (за съпътстваща програма)", - "repeater_chanUtil": "Използване на канала" + "repeater_chanUtil": "Използване на канала", + "contact_connectCompanion": "Свържете се с придружител, за да получите достъп до функциите на ретранслатора и сървъра за стаи." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 7f491d4f..e6f501c9 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -2339,5 +2339,6 @@ "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", + "contact_connectCompanion": "Verbinden Sie sich mit einem Companion, um auf die Funktionen des Repeaters und des Raumservers zuzugreifen." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 456fab56..e58b760d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -2366,5 +2366,6 @@ "contact_typeRepeater": "Repeater", "contact_typeRoom": "Room", "contact_typeSensor": "Sensor", - "contact_typeUnknown": "Unknown" + "contact_typeUnknown": "Unknown", + "contact_connectCompanion": "Connect to a companion to access repeater and room server features." } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index d4cf27da..490e6927 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -2339,5 +2339,6 @@ "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", + "contact_connectCompanion": "Conéctate a un compañero para acceder a las funciones de repetidor y servidor de sala." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 7817c20d..2fb2b6d2 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -2318,5 +2318,6 @@ "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", + "contact_connectCompanion": "Connectez-vous à un compagnon pour accéder aux fonctionnalités de répéteur et de serveur de salle." } diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index 97426183..4da7df69 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -2349,5 +2349,6 @@ "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", + "contact_connectCompanion": "Csatlakozzon egy kísérőhöz a ismétlő és szobaszerver funkciók eléréséhez." } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 664bceef..4eddedfc 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -2311,5 +2311,6 @@ "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", + "contact_connectCompanion": "Connettiti a un dispositivo companion per accedere alle funzionalità di ripetitore e server stanza." } diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 9d2cef21..b47cd8e4 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -2349,5 +2349,6 @@ "settings_companionDebugLog": "同伴デバッグログ", "chat_newMessages": "新しいメッセージ", "chat_markAsUnread": "未読としてマークする", - "repeater_chanUtil": "チャンネルの利用状況" + "repeater_chanUtil": "チャンネルの利用状況", + "contact_connectCompanion": "コネクトしてリピーターとルームサーバー機能にアクセス" } diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index d778429b..6383ad6a 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -2349,5 +2349,6 @@ "chat_newMessages": "새로운 메시지", "settings_companionDebugLogSubtitle": "BLE/TCP/USB 명령어, 응답 및 원시 데이터", "chat_markAsUnread": "미리 읽지 않음으로 표시", - "repeater_chanUtil": "채널 활용도" + "repeater_chanUtil": "채널 활용도", + "contact_connectCompanion": "리피터 및 룸 서버 기능에 액세스하려면 컴패니언에 연결하세요." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 159d755c..a38178da 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -7335,6 +7335,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Unknown'** String get contact_typeUnknown; + + /// No description provided for @contact_connectCompanion. + /// + /// In en, this message translates to: + /// **'Connect to a companion to access repeater and room server features.'** + String get contact_connectCompanion; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 409803b2..1a754860 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -4288,4 +4288,8 @@ class AppLocalizationsBg extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get contact_connectCompanion => + 'Свържете се с придружител, за да получите достъп до функциите на ретранслатора и сървъра за стаи.'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 9f06906d..a8bc48f1 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -4305,4 +4305,8 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get contact_connectCompanion => + 'Verbinden Sie sich mit einem Companion, um auf die Funktionen des Repeaters und des Raumservers zuzugreifen.'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 6def06b7..007a9053 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -4210,4 +4210,8 @@ class AppLocalizationsEn extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get contact_connectCompanion => + 'Connect to a companion to access repeater and room server features.'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index bb4f40e8..5be56b39 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -4292,4 +4292,8 @@ class AppLocalizationsEs extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get contact_connectCompanion => + 'Conéctate a un compañero para acceder a las funciones de repetidor y servidor de sala.'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 44e40657..5fc63fe0 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -4321,4 +4321,8 @@ class AppLocalizationsFr extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get contact_connectCompanion => + 'Connectez-vous à un compagnon pour accéder aux fonctionnalités de répéteur et de serveur de salle.'; } diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 298a6fd6..4209c62c 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -4309,4 +4309,8 @@ class AppLocalizationsHu extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get contact_connectCompanion => + 'Csatlakozzon egy kísérőhöz a ismétlő és szobaszerver funkciók eléréséhez.'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index e3f7ee58..5b7b5400 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -4297,4 +4297,8 @@ class AppLocalizationsIt extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get contact_connectCompanion => + 'Connettiti a un dispositivo companion per accedere alle funzionalità di ripetitore e server stanza.'; } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 263a906e..3bbf527d 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -4063,4 +4063,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get contact_connectCompanion => 'コネクトしてリピーターとルームサーバー機能にアクセス'; } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index f9c0d760..b4378a79 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -4064,4 +4064,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get contact_connectCompanion => '리피터 및 룸 서버 기능에 액세스하려면 컴패니언에 연결하세요.'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index ad140bf3..604654c1 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -4273,4 +4273,8 @@ class AppLocalizationsNl extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get contact_connectCompanion => + 'Maak verbinding met een companion om repeater- en kamerserverfuncties te gebruiken.'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 6c7c3696..91e14365 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -4309,4 +4309,8 @@ class AppLocalizationsPl extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get contact_connectCompanion => + 'Połącz się z towarzyszem, aby uzyskać dostęp do funkcji powtarzacza i serwera pokoi.'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index e6fd49e3..2388caba 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -4285,4 +4285,8 @@ class AppLocalizationsPt extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get contact_connectCompanion => + 'Conecte-se a um dispositivo companion para acessar as funcionalidades de repetidor e servidor de salas.'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 25452003..02baa06a 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -4303,4 +4303,8 @@ class AppLocalizationsRu extends AppLocalizations { @override String get contact_typeUnknown => 'Неизвестно'; + + @override + String get contact_connectCompanion => + 'Подключитесь к компаньону, чтобы получить доступ к функциям ретранслятора и сервера комнат.'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 91461385..a656eab1 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -4269,4 +4269,8 @@ class AppLocalizationsSk extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get contact_connectCompanion => + 'Pripojte sa k sprievodcovi a získajte prístup k funkciám opakovača a serveru miestností.'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 48103647..08faa527 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -4267,4 +4267,8 @@ class AppLocalizationsSl extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get contact_connectCompanion => + 'Povežite se s spremljevalnikom za dostop do funkcij ponavljalnika in strežnika sob.'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 7453c18f..057a55c2 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -4241,4 +4241,8 @@ class AppLocalizationsSv extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get contact_connectCompanion => + 'Anslut till en sällskapstjänst för att komma åt upprepning och rumsserverfunktioner.'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 9c30fd8e..e930f28c 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -4304,4 +4304,8 @@ class AppLocalizationsUk extends AppLocalizations { @override String get contact_typeUnknown => 'Невідомо'; + + @override + String get contact_connectCompanion => + 'Підключіться до супутнього пристрою, щоб отримати доступ до функцій ретранслятора та сервера кімнат.'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index d2304a66..ffe5f1ec 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -3938,4 +3938,7 @@ class AppLocalizationsZh extends AppLocalizations { @override String get contact_typeUnknown => 'Unknown'; + + @override + String get contact_connectCompanion => '连接伴机以访问中继器和房间服务器功能。'; } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 7e83215b..e6865a44 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -2311,5 +2311,6 @@ "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", + "contact_connectCompanion": "Maak verbinding met een companion om repeater- en kamerserverfuncties te gebruiken." } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 4366e273..ce0421d9 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -2349,5 +2349,6 @@ "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", + "contact_connectCompanion": "Połącz się z towarzyszem, aby uzyskać dostęp do funkcji powtarzacza i serwera pokoi." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 6b8abbbc..b4cfcbe8 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -2311,5 +2311,6 @@ "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", + "contact_connectCompanion": "Conecte-se a um dispositivo companion para acessar as funcionalidades de repetidor e servidor de salas." } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 5d5bcfc3..e7ef34bf 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1614,5 +1614,6 @@ "repeater_cliHelpStatsCore": "(Только для серийного оборудования) Отображает основные статистические данные прошивки.", "settings_companionDebugLogSubtitle": "Команды, ответы и необработанные данные, используемые для протоколов BLE, TCP и USB.", "repeater_chanUtil": "Использование канала", - "settings_companionDebugLog": "Журнал отладки (для сопутствующего приложения)" + "settings_companionDebugLog": "Журнал отладки (для сопутствующего приложения)", + "contact_connectCompanion": "Подключитесь к компаньону, чтобы получить доступ к функциям ретранслятора и сервера комнат." } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 713c3976..8f898f33 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -2311,5 +2311,6 @@ "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", + "contact_connectCompanion": "Pripojte sa k sprievodcovi a získajte prístup k funkciám opakovača a serveru miestností." } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 00930c85..ef1477b7 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -2311,5 +2311,6 @@ "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", + "contact_connectCompanion": "Povežite se s spremljevalnikom za dostop do funkcij ponavljalnika in strežnika sob." } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 00fd3b08..4364b489 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -2311,5 +2311,6 @@ "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", + "contact_connectCompanion": "Anslut till en sällskapstjänst för att komma åt upprepning och rumsserverfunktioner." } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 8330d364..15c96e5a 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -2291,5 +2291,6 @@ "settings_companionDebugLogSubtitle": "Команди, відповіді та необроблена інформація для протоколів BLE/TCP/USB", "chat_newMessages": "Нові повідомлення", "chat_markAsUnread": "Позначити як непрочитане", - "repeater_chanUtil": "Використання каналу" + "repeater_chanUtil": "Використання каналу", + "contact_connectCompanion": "Підключіться до супутнього пристрою, щоб отримати доступ до функцій ретранслятора та сервера кімнат." } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 1e289315..0750b2ba 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -2316,5 +2316,6 @@ "settings_companionDebugLog": "调试日志", "chat_newMessages": "新的消息", "settings_companionDebugLogSubtitle": "BLE/TCP/USB 协议、响应和原始数据", - "repeater_chanUtil": "频道利用率" + "repeater_chanUtil": "频道利用率", + "contact_connectCompanion": "连接伴机以访问中继器和房间服务器功能。" } diff --git a/lib/main.dart b/lib/main.dart index cd622811..936a0398 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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(), ); }, ), diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index fcd50861..8ba7ae21 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -1057,6 +1057,9 @@ class _ChannelChatScreenState extends State { Widget _buildMessageComposer() { final connector = context.watch(); + if (!connector.isConnected) { + return const SizedBox.shrink(); + } final maxBytes = maxChannelMessageBytes(connector.selfName); final settings = context.watch().settings; return Column( @@ -1208,6 +1211,9 @@ class _ChannelChatScreenState extends State { } Future _sendMessage() async { + final connector = context.read(); + if (!connector.isConnected) return; + final text = _textController.text.trim(); if (text.isEmpty) return; @@ -1222,7 +1228,6 @@ class _ChannelChatScreenState extends State { } _lastChannelSendAt = now; - final connector = context.read(); final settings = context.read().settings; final translationService = context.read(); diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 4613a8ee..4e65eeb1 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -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 { @@ -41,7 +41,7 @@ class ChannelsScreen extends StatefulWidget { } class _ChannelsScreenState extends State - with DisconnectNavigationMixin { +{ final TextEditingController _searchController = TextEditingController(); final CommunityStore _communityStore = CommunityStore(); Timer? _searchDebounce; @@ -117,11 +117,6 @@ class _ChannelsScreenState extends State 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( @@ -134,16 +129,33 @@ class _ChannelsScreenState extends State 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( diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 8d3bc66c..03062994 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -414,7 +414,7 @@ class _ChatScreenState extends State { ], ), ), - _buildInputBar(connector), + if (connector.isConnected) _buildInputBar(connector), ], ); }, @@ -693,6 +693,9 @@ class _ChatScreenState extends State { } Future _sendMessage(MeshCoreConnector connector) async { + if (!connector.isConnected) { + return; + } final text = _textController.text.trim(); if (text.isEmpty) return; diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index a4cc35ca..bff825b9 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -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 } @@ -50,7 +50,7 @@ class ContactsScreen extends StatefulWidget { } class _ContactsScreenState extends State - with DisconnectNavigationMixin { +{ final TextEditingController _searchController = TextEditingController(); final ContactGroupStore _groupStore = ContactGroupStore(); MeshCoreConnector? _scopeSyncConnector; @@ -306,11 +306,6 @@ class _ContactsScreenState extends State Widget build(BuildContext context) { final connector = context.watch(); - // Auto-navigate back to scanner if disconnected - if (!checkConnectionAndNavigate(connector)) { - return const SizedBox.shrink(); - } - final allowBack = !connector.isConnected; return PopScope( canPop: allowBack, @@ -378,16 +373,33 @@ class _ContactsScreenState extends State ), 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: [ @@ -965,6 +977,11 @@ class _ContactsScreenState extends State } void _showRepeaterLogin(BuildContext context, Contact repeater) { + final connector = context.read(); + if (!connector.isConnected) { + _showCompanionRequiredDialog(context); + return; + } showDialog( context: context, builder: (context) => RepeaterLoginDialog( @@ -991,6 +1008,11 @@ class _ContactsScreenState extends State Contact room, RoomLoginDestination destination, ) { + final connector = context.read(); + if (!connector.isConnected) { + _showCompanionRequiredDialog(context); + return; + } showDialog( context: context, builder: (context) => RoomLoginDialog( @@ -1019,6 +1041,22 @@ class _ContactsScreenState extends State ); } + void _showCompanionRequiredDialog(BuildContext context) { + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(context.l10n.scanner_notConnected), + content: Text(context.l10n.contact_connectCompanion), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(context.l10n.common_ok), + ), + ], + ), + ); + } + void _confirmDeleteGroup(BuildContext context, ContactGroup group) { if (!_hasGroupStoreScope(context.read())) { _showGroupsUnavailableMessage(context); diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 4ae2fd43..96785b7a 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -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'; @@ -466,16 +467,33 @@ class _MapScreenState extends State { ), 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: [ diff --git a/lib/utils/disconnect_navigation_mixin.dart b/lib/utils/disconnect_navigation_mixin.dart deleted file mode 100644 index e5bc6142..00000000 --- a/lib/utils/disconnect_navigation_mixin.dart +++ /dev/null @@ -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 on State { - /// 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; - } -}