mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-19 00:45:33 +10:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba4fd3eff5 | |||
| beb3e1996d | |||
| 3f4e6f4e13 | |||
| e1cc285c8a | |||
| f4b32e8a8a | |||
| 01bf95c98e | |||
| fa044dd204 | |||
| 72fea3fc32 | |||
| f0bd61144c | |||
| 61c897630c | |||
| a270e2e6d1 | |||
| 247db6a36d | |||
| 78d08afb47 | |||
| c77264cc81 | |||
| d6ed8c5f13 | |||
| 209fee48ca | |||
| 8892823337 | |||
| e738664f89 | |||
| e37616fa15 | |||
| 2763d83fe4 | |||
| 77018dc358 | |||
| 21c58d4e13 | |||
| 3af97ff6dd | |||
| 703d5a1ec4 | |||
| e801a497f8 | |||
| d3c7d8e43a | |||
| 0c1e163b88 | |||
| d0d6a34fb5 | |||
| 16ce1359d7 | |||
| 9fe4a3710d | |||
| 8611adab1f | |||
| 7d457cb863 | |||
| 297516fc80 | |||
| 1b94442ab6 | |||
| 3ae14781f0 | |||
| ecc496f2af | |||
| 87b25655d0 | |||
| c47a4cb622 | |||
| a30fc439f3 | |||
| afcc4db405 | |||
| 87bcb6a6a3 | |||
| 68bb031bb6 | |||
| c4f5c7b171 | |||
| 2bce14224d | |||
| fd305fd55b | |||
| d0dd805244 | |||
| 8668564464 |
@@ -193,6 +193,7 @@ Devices are discovered by scanning for BLE advertisements with known MeshCore de
|
||||
- `WisCore-`
|
||||
- `HT-`
|
||||
- `LowMesh_MC_`
|
||||
- `NRF52`
|
||||
|
||||
New device prefixes can be added in `lib/connector/meshcore_uuids.dart`.
|
||||
|
||||
|
||||
@@ -305,6 +305,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
final Map<String, int> _contactUnreadCount = {};
|
||||
final Map<String, RepeaterBatterySnapshot> _repeaterBatterySnapshots = {};
|
||||
bool _unreadStateLoaded = false;
|
||||
int _cachedContactsUnreadTotal = 0;
|
||||
int _cachedChannelsUnreadTotal = 0;
|
||||
final Map<String, _RepeaterAckContext> _pendingRepeaterAcks = {};
|
||||
String? _activeContactKey;
|
||||
int? _activeChannelIndex;
|
||||
@@ -612,16 +614,42 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
int getTotalUnreadCount() {
|
||||
if (!_unreadStateLoaded) return 0;
|
||||
var total = 0;
|
||||
// Count unread contact messages
|
||||
for (final contact in _contacts) {
|
||||
total += getUnreadCountForContact(contact);
|
||||
return getTotalContactsUnreadCount() + getTotalChannelsUnreadCount();
|
||||
}
|
||||
// Count unread channel messages
|
||||
for (final channelIndex in _channelMessages.keys) {
|
||||
total += getUnreadCountForChannelIndex(channelIndex);
|
||||
|
||||
int getTotalContactsUnreadCount() {
|
||||
if (!_unreadStateLoaded) return 0;
|
||||
return _cachedContactsUnreadTotal;
|
||||
}
|
||||
return total;
|
||||
|
||||
int getTotalChannelsUnreadCount() {
|
||||
if (!_unreadStateLoaded) return 0;
|
||||
return _cachedChannelsUnreadTotal;
|
||||
}
|
||||
|
||||
/// Recalculates both cached unread totals from scratch.
|
||||
/// Called when unread state is first loaded.
|
||||
void _recalculateCachedUnreadTotals() {
|
||||
_recalculateCachedContactsUnreadTotal();
|
||||
_recalculateCachedChannelsUnreadTotal();
|
||||
}
|
||||
|
||||
void _recalculateCachedContactsUnreadTotal() {
|
||||
int total = 0;
|
||||
_contactUnreadCount.forEach((contactKeyHex, count) {
|
||||
if (_shouldTrackUnreadForContactKey(contactKeyHex)) {
|
||||
total += count;
|
||||
}
|
||||
});
|
||||
_cachedContactsUnreadTotal = total;
|
||||
}
|
||||
|
||||
void _recalculateCachedChannelsUnreadTotal() {
|
||||
final allChannels = _channels.isNotEmpty ? _channels : _cachedChannels;
|
||||
_cachedChannelsUnreadTotal = allChannels.fold(
|
||||
0,
|
||||
(total, ch) => total + ch.unreadCount,
|
||||
);
|
||||
}
|
||||
|
||||
bool isChannelSmazEnabled(int channelIndex) {
|
||||
@@ -655,6 +683,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
..clear()
|
||||
..addAll(await _unreadStore.loadContactUnreadCount());
|
||||
_unreadStateLoaded = true;
|
||||
_recalculateCachedUnreadTotals();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -693,6 +722,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
final previousCount = _contactUnreadCount[contactKeyHex] ?? 0;
|
||||
if (previousCount > 0) {
|
||||
_contactUnreadCount[contactKeyHex] = 0;
|
||||
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - previousCount)
|
||||
.clamp(0, _cachedContactsUnreadTotal);
|
||||
_appDebugLogService?.info(
|
||||
'Contact $contactKeyHex marked as read (was $previousCount unread)',
|
||||
tag: 'Unread',
|
||||
@@ -734,6 +765,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (channel != null && channel.unreadCount > 0) {
|
||||
final previousCount = channel.unreadCount;
|
||||
channel.unreadCount = 0;
|
||||
_cachedChannelsUnreadTotal = (_cachedChannelsUnreadTotal - previousCount)
|
||||
.clamp(0, _cachedChannelsUnreadTotal);
|
||||
_appDebugLogService?.info(
|
||||
'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} marked as read (was $previousCount unread)',
|
||||
tag: 'Unread',
|
||||
@@ -940,11 +973,18 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
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();
|
||||
@@ -966,7 +1006,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_conversations.clear();
|
||||
_loadedConversationKeys.clear();
|
||||
_channelMessages.clear();
|
||||
_channels.clear();
|
||||
_cachedChannels.clear();
|
||||
_previousChannelsCache.clear();
|
||||
_knownContactKeys.clear();
|
||||
_contactUnreadCount.clear();
|
||||
_unreadStateLoaded = false;
|
||||
@@ -987,6 +1029,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
await restoreLastCompanionScope();
|
||||
await loadContactCache();
|
||||
await _loadDiscoveredContactCache();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _loadDiscoveredContactCache() async {
|
||||
@@ -3203,6 +3246,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
unawaited(_persistContacts());
|
||||
_conversations.remove(contact.publicKeyHex);
|
||||
_loadedConversationKeys.remove(contact.publicKeyHex);
|
||||
final removedCount = _contactUnreadCount[contact.publicKeyHex] ?? 0;
|
||||
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - removedCount)
|
||||
.clamp(0, _cachedContactsUnreadTotal);
|
||||
_contactUnreadCount.remove(contact.publicKeyHex);
|
||||
_unreadStore.saveContactUnreadCount(
|
||||
Map<String, int>.from(_contactUnreadCount),
|
||||
@@ -3596,6 +3642,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
// Cache channels for offline use
|
||||
_cachedChannels = List<Channel>.from(_channels);
|
||||
unawaited(_channelStore.saveChannels(_channels));
|
||||
_recalculateCachedChannelsUnreadTotal();
|
||||
|
||||
// Apply ordering and notify UI
|
||||
_applyChannelOrder();
|
||||
@@ -4134,6 +4181,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_handleDiscovery(contact, frame, noNotify: true, addActive: true);
|
||||
|
||||
if (contact.type == advTypeRepeater) {
|
||||
final removedCount = _contactUnreadCount[contact.publicKeyHex] ?? 0;
|
||||
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - removedCount)
|
||||
.clamp(0, _cachedContactsUnreadTotal);
|
||||
_contactUnreadCount.remove(contact.publicKeyHex);
|
||||
_unreadStore.saveContactUnreadCount(
|
||||
Map<String, int>.from(_contactUnreadCount),
|
||||
@@ -4224,6 +4274,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
|
||||
if (contact.type == advTypeRepeater) {
|
||||
final removedCount = _contactUnreadCount[contact.publicKeyHex] ?? 0;
|
||||
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - removedCount)
|
||||
.clamp(0, _cachedContactsUnreadTotal);
|
||||
_contactUnreadCount.remove(contact.publicKeyHex);
|
||||
_unreadStore.saveContactUnreadCount(
|
||||
Map<String, int>.from(_contactUnreadCount),
|
||||
@@ -5104,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);
|
||||
}
|
||||
|
||||
@@ -5236,6 +5290,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
final channel = _findChannelByIndex(channelIndex);
|
||||
if (channel != null) {
|
||||
channel.unreadCount++;
|
||||
_cachedChannelsUnreadTotal++;
|
||||
_appDebugLogService?.info(
|
||||
'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} unread count incremented to ${channel.unreadCount}',
|
||||
tag: 'Unread',
|
||||
@@ -5280,6 +5335,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
final currentCount = _contactUnreadCount[contactKey] ?? 0;
|
||||
_contactUnreadCount[contactKey] = currentCount + 1;
|
||||
_cachedContactsUnreadTotal++;
|
||||
_appDebugLogService?.info(
|
||||
'Contact $contactKey unread count incremented to ${currentCount + 1}',
|
||||
tag: 'Unread',
|
||||
|
||||
@@ -11,5 +11,6 @@ class MeshCoreUuids {
|
||||
"Lilygo",
|
||||
"HT-",
|
||||
"LowMesh_MC_",
|
||||
"NRF52",
|
||||
];
|
||||
}
|
||||
|
||||
+4
-1
@@ -2312,5 +2312,8 @@
|
||||
"chat_newMessages": "Нови съобщения",
|
||||
"settings_companionDebugLog": "Лог за отстраняване на грешки (за съпътстваща програма)",
|
||||
"repeater_chanUtil": "Използване на канала",
|
||||
"contact_connectCompanion": "Свържете се с придружител, за да получите достъп до функциите на ретранслатора и сървъра за стаи."
|
||||
"dialog_connectCompanion": "Свържете се с придружител, за да получите достъп до функциите на ретранслатора и сървъра за стаи.",
|
||||
"dialog_disconnectedTitle": "Прекъснато",
|
||||
"dialog_disconnectedMessage": "Свързването ви с вашия спътник е прекъснато.",
|
||||
"contact_connectCompanion": "Свържете се с спътник, за да получите достъп до функциите на repeater и room server."
|
||||
}
|
||||
|
||||
+4
-1
@@ -2340,5 +2340,8 @@
|
||||
"settings_companionDebugLog": "Debug-Protokoll für die Begleitsoftware",
|
||||
"settings_companionDebugLogSubtitle": "BLE/TCP/USB-Befehle, Antworten und Rohdaten",
|
||||
"repeater_chanUtil": "Nutzung des Kanals",
|
||||
"contact_connectCompanion": "Verbinden Sie sich mit einem Companion, um auf die Funktionen des Repeaters und des Raumservers zuzugreifen."
|
||||
"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
-2
@@ -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",
|
||||
@@ -2366,6 +2369,5 @@
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"contact_connectCompanion": "Connect to a companion to access repeater and room server features."
|
||||
"contact_typeUnknown": "Unknown"
|
||||
}
|
||||
+4
-1
@@ -2340,5 +2340,8 @@
|
||||
"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",
|
||||
"contact_connectCompanion": "Conéctate a un compañero para acceder a las funciones de repetidor y servidor de sala."
|
||||
"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."
|
||||
}
|
||||
|
||||
+4
-1
@@ -2319,5 +2319,8 @@
|
||||
"chat_newMessages": "Nouveaux messages",
|
||||
"settings_companionDebugLogSubtitle": "Commandes, réponses et données brutes pour les protocoles BLE/TCP/USB",
|
||||
"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."
|
||||
"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."
|
||||
}
|
||||
|
||||
+4
-1
@@ -2350,5 +2350,8 @@
|
||||
"settings_companionDebugLog": "Párhuzamos hibakeresési napló",
|
||||
"settings_companionDebugLogSubtitle": "BLE/TCP/USB parancsok, válaszok és alapvető adatok",
|
||||
"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."
|
||||
"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."
|
||||
}
|
||||
|
||||
+4
-1
@@ -2312,5 +2312,8 @@
|
||||
"chat_newMessages": "Nuovi messaggi",
|
||||
"chat_markAsUnread": "Segna come non letto",
|
||||
"repeater_chanUtil": "Utilizzo del canale",
|
||||
"contact_connectCompanion": "Connettiti a un dispositivo companion per accedere alle funzionalità di ripetitore e server stanza."
|
||||
"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."
|
||||
}
|
||||
|
||||
+4
-1
@@ -2350,5 +2350,8 @@
|
||||
"chat_newMessages": "新しいメッセージ",
|
||||
"chat_markAsUnread": "未読としてマークする",
|
||||
"repeater_chanUtil": "チャンネルの利用状況",
|
||||
"contact_connectCompanion": "コネクトしてリピーターとルームサーバー機能にアクセス"
|
||||
"dialog_connectCompanion": "コネクトしてリピーターとルームサーバー機能にアクセス",
|
||||
"dialog_disconnectedTitle": "切断済み",
|
||||
"dialog_disconnectedMessage": "コンパニオンとの接続が切れました。",
|
||||
"contact_connectCompanion": "リピーターおよびルームサーバー機能にアクセスするには、コンパニオンに接続してください。"
|
||||
}
|
||||
|
||||
@@ -2350,5 +2350,8 @@
|
||||
"settings_companionDebugLogSubtitle": "BLE/TCP/USB 명령어, 응답 및 원시 데이터",
|
||||
"chat_markAsUnread": "미리 읽지 않음으로 표시",
|
||||
"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:
|
||||
@@ -7335,12 +7353,6 @@ 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
|
||||
|
||||
@@ -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 => 'Повторител Вход';
|
||||
|
||||
@@ -4288,8 +4299,4 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get contact_connectCompanion =>
|
||||
'Свържете се с придружител, за да получите достъп до функциите на ретранслатора и сървъра за стаи.';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -4305,8 +4316,4 @@ 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.';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -4210,8 +4221,4 @@ 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.';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -4292,8 +4303,4 @@ 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.';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -4321,8 +4332,4 @@ 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.';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -4309,8 +4319,4 @@ 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.';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -4297,8 +4308,4 @@ 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.';
|
||||
}
|
||||
|
||||
@@ -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 => '再ログイン';
|
||||
|
||||
@@ -4063,7 +4072,4 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get contact_connectCompanion => 'コネクトしてリピーターとルームサーバー機能にアクセス';
|
||||
}
|
||||
|
||||
@@ -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 => '다시 로그인';
|
||||
|
||||
@@ -4064,7 +4073,4 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get contact_connectCompanion => '리피터 및 룸 서버 기능에 액세스하려면 컴패니언에 연결하세요.';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -4273,8 +4284,4 @@ 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.';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -4309,8 +4320,4 @@ 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.';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -4285,8 +4296,4 @@ 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.';
|
||||
}
|
||||
|
||||
@@ -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 => 'Вход в репитер';
|
||||
|
||||
@@ -4303,8 +4314,4 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Неизвестно';
|
||||
|
||||
@override
|
||||
String get contact_connectCompanion =>
|
||||
'Подключитесь к компаньону, чтобы получить доступ к функциям ретранслятора и сервера комнат.';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -4269,8 +4280,4 @@ 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í.';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -4267,8 +4278,4 @@ 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.';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -4241,8 +4252,4 @@ 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.';
|
||||
}
|
||||
|
||||
@@ -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 => 'Вхід у ретранслятор';
|
||||
|
||||
@@ -4304,8 +4315,4 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Невідомо';
|
||||
|
||||
@override
|
||||
String get contact_connectCompanion =>
|
||||
'Підключіться до супутнього пристрою, щоб отримати доступ до функцій ретранслятора та сервера кімнат.';
|
||||
}
|
||||
|
||||
@@ -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 => '转发节点登录';
|
||||
|
||||
@@ -3938,7 +3947,4 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get contact_connectCompanion => '连接伴机以访问中继器和房间服务器功能。';
|
||||
}
|
||||
|
||||
+4
-1
@@ -2312,5 +2312,8 @@
|
||||
"chat_markAsUnread": "Markeer als ongelezen",
|
||||
"settings_companionDebugLogSubtitle": "BLE/TCP/USB commando's, antwoorden en ruwe data",
|
||||
"repeater_chanUtil": "Gebruik van het kanaal",
|
||||
"contact_connectCompanion": "Maak verbinding met een companion om repeater- en kamerserverfuncties te gebruiken."
|
||||
"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."
|
||||
}
|
||||
|
||||
+4
-1
@@ -2350,5 +2350,8 @@
|
||||
"chat_markAsUnread": "Oznacz jako nieprzeczytane",
|
||||
"settings_companionDebugLog": "Log debugowania (dla pomocy w rozwiązywaniu problemów)",
|
||||
"repeater_chanUtil": "Wykorzystanie kanału",
|
||||
"contact_connectCompanion": "Połącz się z towarzyszem, aby uzyskać dostęp do funkcji powtarzacza i serwera pokoi."
|
||||
"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."
|
||||
}
|
||||
|
||||
+4
-1
@@ -2312,5 +2312,8 @@
|
||||
"chat_markAsUnread": "Marcar como não lido",
|
||||
"chat_newMessages": "Novas mensagens",
|
||||
"repeater_chanUtil": "Utilização do canal",
|
||||
"contact_connectCompanion": "Conecte-se a um dispositivo companion para acessar as funcionalidades de repetidor e servidor de salas."
|
||||
"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."
|
||||
}
|
||||
|
||||
+4
-1
@@ -1615,5 +1615,8 @@
|
||||
"settings_companionDebugLogSubtitle": "Команды, ответы и необработанные данные, используемые для протоколов BLE, TCP и USB.",
|
||||
"repeater_chanUtil": "Использование канала",
|
||||
"settings_companionDebugLog": "Журнал отладки (для сопутствующего приложения)",
|
||||
"contact_connectCompanion": "Подключитесь к компаньону, чтобы получить доступ к функциям ретранслятора и сервера комнат."
|
||||
"dialog_connectCompanion": "Подключитесь к компаньону, чтобы получить доступ к функциям ретранслятора и сервера комнат.",
|
||||
"dialog_disconnectedTitle": "Отключено",
|
||||
"dialog_disconnectedMessage": "Вы были отключены от вашего компаньона.",
|
||||
"contact_connectCompanion": "Подключитесь к компаньону, чтобы получить доступ к функциям репитера и серверу комнаты."
|
||||
}
|
||||
|
||||
+4
-1
@@ -2312,5 +2312,8 @@
|
||||
"settings_companionDebugLog": "Logovanie pre ladenie (sprievodný log)",
|
||||
"chat_newMessages": "Nové správy",
|
||||
"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í."
|
||||
"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."
|
||||
}
|
||||
|
||||
+4
-1
@@ -2312,5 +2312,8 @@
|
||||
"chat_newMessages": "Nove novice",
|
||||
"settings_companionDebugLogSubtitle": "Navodila, odgovori in surova podatka za BLE/TCP/USB.",
|
||||
"repeater_chanUtil": "Uporaba kanala",
|
||||
"contact_connectCompanion": "Povežite se s spremljevalnikom za dostop do funkcij ponavljalnika in strežnika sob."
|
||||
"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."
|
||||
}
|
||||
|
||||
+4
-1
@@ -2312,5 +2312,8 @@
|
||||
"chat_newMessages": "Nya meddelanden",
|
||||
"settings_companionDebugLogSubtitle": "BLE/TCP/USB-kommandon, svar och rådata",
|
||||
"repeater_chanUtil": "Användning av kanal",
|
||||
"contact_connectCompanion": "Anslut till en sällskapstjänst för att komma åt upprepning och rumsserverfunktioner."
|
||||
"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."
|
||||
}
|
||||
|
||||
+4
-1
@@ -2292,5 +2292,8 @@
|
||||
"chat_newMessages": "Нові повідомлення",
|
||||
"chat_markAsUnread": "Позначити як непрочитане",
|
||||
"repeater_chanUtil": "Використання каналу",
|
||||
"contact_connectCompanion": "Підключіться до супутнього пристрою, щоб отримати доступ до функцій ретранслятора та сервера кімнат."
|
||||
"dialog_connectCompanion": "Підключіться до супутнього пристрою, щоб отримати доступ до функцій ретранслятора та сервера кімнат.",
|
||||
"dialog_disconnectedTitle": "Від’єднано",
|
||||
"dialog_disconnectedMessage": "Вас від’єднано від вашого супутника.",
|
||||
"contact_connectCompanion": "Підключіться до супутника, щоб отримати доступ до функцій репітера та серверів кімнат."
|
||||
}
|
||||
|
||||
+4
-1
@@ -2317,5 +2317,8 @@
|
||||
"chat_newMessages": "新的消息",
|
||||
"settings_companionDebugLogSubtitle": "BLE/TCP/USB 协议、响应和原始数据",
|
||||
"repeater_chanUtil": "频道利用率",
|
||||
"contact_connectCompanion": "连接伴机以访问中继器和房间服务器功能。"
|
||||
"dialog_connectCompanion": "连接伴机以访问中继器和房间服务器功能。",
|
||||
"dialog_disconnectedTitle": "已断开连接",
|
||||
"dialog_disconnectedMessage": "你已与你的伙伴断开连接。",
|
||||
"contact_connectCompanion": "连接至伴侣设备以访问中继器和房间服务器功能。"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import 'dart:typed_data';
|
||||
import 'package:crypto/crypto.dart' as crypto;
|
||||
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import 'community.dart';
|
||||
|
||||
enum ChannelType { public, private, hashtag, communityPublic, communityHashtag }
|
||||
|
||||
class Channel {
|
||||
final int index;
|
||||
@@ -111,5 +114,36 @@ class Channel {
|
||||
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
||||
}
|
||||
|
||||
static bool isCommunityChannel(ChannelType channelType) {
|
||||
switch (channelType) {
|
||||
case ChannelType.communityPublic:
|
||||
case ChannelType.communityHashtag:
|
||||
return true;
|
||||
case ChannelType.public:
|
||||
case ChannelType.private:
|
||||
case ChannelType.hashtag:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static ChannelType getChannelType(
|
||||
Channel channel,
|
||||
CommunityPskIndex communityIndex,
|
||||
) {
|
||||
Community? community = communityIndex.getCommunityForChannel(channel);
|
||||
if (community != null) {
|
||||
if (Community.isCommunityPublicChannel(channel, community)) {
|
||||
return ChannelType.communityPublic;
|
||||
}
|
||||
return ChannelType.communityHashtag;
|
||||
}
|
||||
if (channel.isPublicChannel) {
|
||||
return ChannelType.public;
|
||||
} else if (channel.name.startsWith('#')) {
|
||||
return ChannelType.hashtag;
|
||||
}
|
||||
return ChannelType.private;
|
||||
}
|
||||
|
||||
static const String publicChannelPsk = '8b3387e9c5cdea6ac9e5edbaa115cd72';
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart' as crypto;
|
||||
|
||||
import 'channel.dart';
|
||||
|
||||
/// Represents a community with a shared secret for deriving channel PSKs.
|
||||
///
|
||||
/// A Community is a namespace with a shared secret K (32 random bytes),
|
||||
@@ -162,6 +164,12 @@ class Community {
|
||||
return hashtag.replaceFirst(RegExp(r'^#'), '').toLowerCase().trim();
|
||||
}
|
||||
|
||||
/// Returns true if this is the community's public channel
|
||||
static bool isCommunityPublicChannel(Channel channel, Community community) {
|
||||
final publicPsk = community.deriveCommunityPublicPsk();
|
||||
return channel.pskHex == Channel.formatPskHex(publicPsk);
|
||||
}
|
||||
|
||||
/// Add a hashtag channel to this community's list
|
||||
Community addHashtagChannel(String hashtag) {
|
||||
final normalized = _normalizeCommunityHashtag(hashtag);
|
||||
@@ -237,3 +245,28 @@ class Community {
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
}
|
||||
|
||||
class CommunityPskIndex {
|
||||
// Cache of PSK hex -> Community for quick lookup
|
||||
final Map<String, Community> _pskToCommunity = {};
|
||||
|
||||
void initialize(List<Community> communities) {
|
||||
_pskToCommunity.clear();
|
||||
for (final community in communities) {
|
||||
// Map the community public channel PSK
|
||||
final publicPsk = community.deriveCommunityPublicPsk();
|
||||
_pskToCommunity[Channel.formatPskHex(publicPsk)] = community;
|
||||
|
||||
// Map all known hashtag channel PSKs
|
||||
for (final hashtag in community.hashtagChannels) {
|
||||
final hashtagPsk = community.deriveCommunityHashtagPsk(hashtag);
|
||||
_pskToCommunity[Channel.formatPskHex(hashtagPsk)] = community;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the community this channel belongs to, or null if not a community channel
|
||||
Community? getCommunityForChannel(Channel channel) {
|
||||
return _pskToCommunity[channel.pskHex];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../models/community.dart';
|
||||
import '../storage/community_store.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
import '../helpers/chat_scroll_controller.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
@@ -56,8 +58,11 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
final ChatScrollController _scrollController = ChatScrollController();
|
||||
final FocusNode _textFieldFocusNode = FocusNode();
|
||||
ChannelMessage? _replyingToMessage;
|
||||
final CommunityStore _communityStore = CommunityStore();
|
||||
final CommunityPskIndex _communityIndex = CommunityPskIndex();
|
||||
final Map<String, GlobalKey> _messageKeys = {};
|
||||
bool _isLoadingOlder = false;
|
||||
bool _communitiesLoaded = false;
|
||||
|
||||
MeshCoreConnector? _connector;
|
||||
DateTime? _lastChannelSendAt;
|
||||
@@ -81,6 +86,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
final idx = widget.channel.index;
|
||||
final unread = widget.initialUnreadCount;
|
||||
final messages = connector.getChannelMessages(widget.channel);
|
||||
_loadCommunities();
|
||||
ChannelMessage? anchor;
|
||||
if (unread > 0) {
|
||||
anchor = _findOldestUnreadChannelAnchor(messages, unread);
|
||||
@@ -107,6 +113,19 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Reload communities when returning from another screen
|
||||
Future<void> _loadCommunities() async {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||
final communities = await _communityStore.loadCommunities();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_communityIndex.initialize(communities);
|
||||
_communitiesLoaded = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ChannelMessage? _findOldestUnreadChannelAnchor(
|
||||
List<ChannelMessage> messages,
|
||||
int unreadCount,
|
||||
@@ -193,16 +212,63 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _channelIcon(Channel channel) {
|
||||
// Determine icon based on channel type
|
||||
final ChannelType channelType = Channel.getChannelType(
|
||||
channel,
|
||||
_communityIndex,
|
||||
);
|
||||
final bool isCommunityChannel = Channel.isCommunityChannel(channelType);
|
||||
IconData icon;
|
||||
switch (channelType) {
|
||||
case ChannelType.communityPublic:
|
||||
icon = Icons.groups;
|
||||
case ChannelType.communityHashtag:
|
||||
icon = Icons.tag;
|
||||
case ChannelType.public:
|
||||
icon = Icons.public;
|
||||
case ChannelType.hashtag:
|
||||
icon = Icons.tag;
|
||||
case ChannelType.private:
|
||||
icon = Icons.lock;
|
||||
}
|
||||
return Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
child: _communitiesLoaded
|
||||
? Icon(icon, size: 20)
|
||||
: SizedBox.square(dimension: 20),
|
||||
),
|
||||
if (isCommunityChannel)
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.purple,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).cardColor,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: const Icon(Icons.people, size: 8, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
widget.channel.isPublicChannel ? Icons.public : Icons.tag,
|
||||
size: 20,
|
||||
),
|
||||
_channelIcon(widget.channel),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -311,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((_) {
|
||||
@@ -347,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
|
||||
|
||||
@@ -40,15 +40,12 @@ class ChannelsScreen extends StatefulWidget {
|
||||
State<ChannelsScreen> createState() => _ChannelsScreenState();
|
||||
}
|
||||
|
||||
class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
{
|
||||
class _ChannelsScreenState extends State<ChannelsScreen> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final CommunityStore _communityStore = CommunityStore();
|
||||
Timer? _searchDebounce;
|
||||
final CommunityPskIndex _communityIndex = CommunityPskIndex();
|
||||
List<Community> _communities = [];
|
||||
|
||||
// Cache of PSK hex -> Community for quick lookup
|
||||
final Map<String, Community> _pskToCommunity = {};
|
||||
Timer? _searchDebounce;
|
||||
|
||||
ChannelMessageStore get _channelMessageStore => ChannelMessageStore();
|
||||
|
||||
@@ -71,37 +68,11 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_communities = communities;
|
||||
_buildPskCommunityMap();
|
||||
_communityIndex.initialize(communities);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _buildPskCommunityMap() {
|
||||
_pskToCommunity.clear();
|
||||
for (final community in _communities) {
|
||||
// Map the community public channel PSK
|
||||
final publicPsk = community.deriveCommunityPublicPsk();
|
||||
_pskToCommunity[Channel.formatPskHex(publicPsk)] = community;
|
||||
|
||||
// Map all known hashtag channel PSKs
|
||||
for (final hashtag in community.hashtagChannels) {
|
||||
final hashtagPsk = community.deriveCommunityHashtagPsk(hashtag);
|
||||
_pskToCommunity[Channel.formatPskHex(hashtagPsk)] = community;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the community this channel belongs to, or null if not a community channel
|
||||
Community? _getCommunityForChannel(Channel channel) {
|
||||
return _pskToCommunity[channel.pskHex];
|
||||
}
|
||||
|
||||
/// Returns true if this is the community's public channel
|
||||
bool _isCommunityPublicChannel(Channel channel, Community community) {
|
||||
final publicPsk = community.deriveCommunityPublicPsk();
|
||||
return channel.pskHex == Channel.formatPskHex(publicPsk);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchDebounce?.cancel();
|
||||
@@ -372,6 +343,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
selectedIndex: 1,
|
||||
onDestinationSelected: (index) =>
|
||||
_handleQuickSwitch(index, context),
|
||||
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
|
||||
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -387,41 +360,41 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
int? dragIndex,
|
||||
}) {
|
||||
final unreadCount = connector.getUnreadCountForChannel(channel);
|
||||
final community = _getCommunityForChannel(channel);
|
||||
final isCommunityChannel = community != null;
|
||||
final isCommunityPublic =
|
||||
isCommunityChannel && _isCommunityPublicChannel(channel, community);
|
||||
|
||||
// Determine icon and colors based on channel type
|
||||
IconData icon;
|
||||
Color iconColor;
|
||||
Color bgColor;
|
||||
|
||||
if (isCommunityChannel) {
|
||||
// Community channel styling
|
||||
final ChannelType channelType = Channel.getChannelType(
|
||||
channel,
|
||||
_communityIndex,
|
||||
);
|
||||
final bool isCommunityChannel = Channel.isCommunityChannel(channelType);
|
||||
switch (channelType) {
|
||||
case ChannelType.communityPublic:
|
||||
icon = Icons.groups;
|
||||
iconColor = Colors.purple;
|
||||
bgColor = Colors.purple.withValues(alpha: 0.2);
|
||||
if (isCommunityPublic) {
|
||||
icon = Icons.groups;
|
||||
} else {
|
||||
case ChannelType.communityHashtag:
|
||||
icon = Icons.tag;
|
||||
}
|
||||
} else if (channel.isPublicChannel) {
|
||||
iconColor = Colors.purple;
|
||||
bgColor = Colors.purple.withValues(alpha: 0.2);
|
||||
case ChannelType.public:
|
||||
icon = Icons.public;
|
||||
iconColor = Colors.green;
|
||||
bgColor = Colors.green.withValues(alpha: 0.2);
|
||||
} else if (channel.name.startsWith('#')) {
|
||||
case ChannelType.hashtag:
|
||||
icon = Icons.tag;
|
||||
iconColor = Colors.blue;
|
||||
bgColor = Colors.blue.withValues(alpha: 0.2);
|
||||
} else {
|
||||
case ChannelType.private:
|
||||
icon = Icons.lock;
|
||||
iconColor = Colors.blue;
|
||||
bgColor = Colors.blue.withValues(alpha: 0.2);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -49,8 +49,7 @@ class ContactsScreen extends StatefulWidget {
|
||||
State<ContactsScreen> createState() => _ContactsScreenState();
|
||||
}
|
||||
|
||||
class _ContactsScreenState extends State<ContactsScreen>
|
||||
{
|
||||
class _ContactsScreenState extends State<ContactsScreen> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final ContactGroupStore _groupStore = ContactGroupStore();
|
||||
MeshCoreConnector? _scopeSyncConnector;
|
||||
@@ -314,6 +313,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
title: AppBarTitle(context.l10n.contacts_title),
|
||||
automaticallyImplyLeading: false,
|
||||
actions: [
|
||||
if (connector.isConnected)
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
@@ -324,12 +324,12 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
Text(context.l10n.contacts_zeroHopAdvert),
|
||||
],
|
||||
),
|
||||
onTap: () => {
|
||||
connector.sendSelfAdvert(flood: false),
|
||||
onTap: () async {
|
||||
await connector.sendSelfAdvert(flood: false);
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.settings_advertisementSent),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
@@ -340,12 +340,12 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
Text(context.l10n.contacts_floodAdvert),
|
||||
],
|
||||
),
|
||||
onTap: () => {
|
||||
connector.sendSelfAdvert(flood: true),
|
||||
onTap: () async {
|
||||
await connector.sendSelfAdvert(flood: true);
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.settings_advertisementSent),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
@@ -442,6 +442,8 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
selectedIndex: 0,
|
||||
onDestinationSelected: (index) =>
|
||||
_handleQuickSwitch(index, context),
|
||||
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
|
||||
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -826,6 +828,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
contact,
|
||||
);
|
||||
return _ContactTile(
|
||||
key: ValueKey(contact.publicKeyHex),
|
||||
contact: contact,
|
||||
lastSeen: _resolveLastSeen(contact),
|
||||
unreadCount: unreadCount,
|
||||
@@ -1046,7 +1049,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(context.l10n.scanner_notConnected),
|
||||
content: Text(context.l10n.contact_connectCompanion),
|
||||
content: Text(context.l10n.dialog_connectCompanion),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
@@ -1494,6 +1497,7 @@ class _ContactTile extends StatelessWidget {
|
||||
final VoidCallback onLongPress;
|
||||
|
||||
const _ContactTile({
|
||||
super.key,
|
||||
required this.contact,
|
||||
required this.lastSeen,
|
||||
required this.unreadCount,
|
||||
|
||||
@@ -539,6 +539,12 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
child: QuickSwitchBar(
|
||||
selectedIndex: 2,
|
||||
onDestinationSelected: (index) => _handleQuickSwitch(index, context),
|
||||
contactsUnreadCount: context
|
||||
.watch<MeshCoreConnector>()
|
||||
.getTotalContactsUnreadCount(),
|
||||
channelsUnreadCount: context
|
||||
.watch<MeshCoreConnector>()
|
||||
.getTotalChannelsUnreadCount(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -416,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(
|
||||
@@ -694,6 +694,8 @@ class _MapScreenState extends State<MapScreen> {
|
||||
selectedIndex: 2,
|
||||
onDestinationSelected: (index) =>
|
||||
_handleQuickSwitch(index, context),
|
||||
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
|
||||
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
@@ -1498,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(
|
||||
@@ -1521,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;
|
||||
|
||||
@@ -69,6 +69,7 @@ class AppBarTitle extends StatelessWidget {
|
||||
if (showBattery) BatteryIndicator(connector: connector),
|
||||
if (showSnr) SNRIndicator(connector: connector),
|
||||
if (connector.supportsCompanionRadioStats)
|
||||
if (connector.isConnected)
|
||||
const RadioStatsIconButton(compact: true),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -6,11 +6,15 @@ import '../l10n/l10n.dart';
|
||||
class QuickSwitchBar extends StatelessWidget {
|
||||
final int selectedIndex;
|
||||
final ValueChanged<int> onDestinationSelected;
|
||||
final int contactsUnreadCount;
|
||||
final int channelsUnreadCount;
|
||||
|
||||
const QuickSwitchBar({
|
||||
super.key,
|
||||
required this.selectedIndex,
|
||||
required this.onDestinationSelected,
|
||||
this.contactsUnreadCount = 0,
|
||||
this.channelsUnreadCount = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -62,15 +66,30 @@ class QuickSwitchBar extends StatelessWidget {
|
||||
onDestinationSelected: onDestinationSelected,
|
||||
destinations: [
|
||||
NavigationDestination(
|
||||
icon: const Icon(Icons.people_outline),
|
||||
icon: _buildIconWithBadge(
|
||||
const Icon(Icons.people_outline),
|
||||
contactsUnreadCount,
|
||||
),
|
||||
selectedIcon: _buildIconWithBadge(
|
||||
const Icon(Icons.people),
|
||||
contactsUnreadCount,
|
||||
),
|
||||
label: context.l10n.nav_contacts,
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: const Icon(Icons.tag),
|
||||
icon: _buildIconWithBadge(
|
||||
const Icon(Icons.tag),
|
||||
channelsUnreadCount,
|
||||
),
|
||||
selectedIcon: _buildIconWithBadge(
|
||||
const Icon(Icons.tag),
|
||||
channelsUnreadCount,
|
||||
),
|
||||
label: context.l10n.nav_channels,
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: const Icon(Icons.map_outlined),
|
||||
selectedIcon: const Icon(Icons.map),
|
||||
label: context.l10n.nav_map,
|
||||
),
|
||||
],
|
||||
@@ -81,4 +100,27 @@ class QuickSwitchBar extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIconWithBadge(Icon icon, int count) {
|
||||
if (count <= 0) return icon;
|
||||
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
icon,
|
||||
Positioned(
|
||||
right: -2,
|
||||
top: -2,
|
||||
child: Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.redAccent,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
+373
-881
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user