mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-15 15:14:26 +10:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba4fd3eff5 | |||
| beb3e1996d | |||
| 3f4e6f4e13 | |||
| e1cc285c8a | |||
| f4b32e8a8a | |||
| 01bf95c98e | |||
| fa044dd204 | |||
| 72fea3fc32 | |||
| f0bd61144c | |||
| 61c897630c | |||
| a270e2e6d1 | |||
| 247db6a36d | |||
| 78d08afb47 | |||
| c77264cc81 | |||
| d6ed8c5f13 | |||
| 209fee48ca |
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -2349,5 +2349,9 @@
|
||||
"settings_companionDebugLog": "同伴デバッグログ",
|
||||
"chat_newMessages": "新しいメッセージ",
|
||||
"chat_markAsUnread": "未読としてマークする",
|
||||
"repeater_chanUtil": "チャンネルの利用状況"
|
||||
"repeater_chanUtil": "チャンネルの利用状況",
|
||||
"dialog_connectCompanion": "コネクトしてリピーターとルームサーバー機能にアクセス",
|
||||
"dialog_disconnectedTitle": "切断済み",
|
||||
"dialog_disconnectedMessage": "コンパニオンとの接続が切れました。",
|
||||
"contact_connectCompanion": "リピーターおよびルームサーバー機能にアクセスするには、コンパニオンに接続してください。"
|
||||
}
|
||||
|
||||
+5
-1
@@ -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": "리피터 및 룸 서버 기능에 액세스하려면 컴패니언에 연결하세요."
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 => 'Повторител Вход';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 => '再ログイン';
|
||||
|
||||
|
||||
@@ -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 => '다시 로그인';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 => 'Вход в репитер';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 => 'Вхід у ретранслятор';
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user