mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-29 06:48:43 +10:00
Dev discovery (#291)
* Refactor contact handling: replace DiscoveryContact with Contact, update related methods and settings * Enhance contact handling: include latitude, longitude, and last modified timestamp in contact updates; refactor path handling to accommodate discovered contacts across multiple screens * Enhance SNRIndicator: include discovered contacts in name resolution for repeaters * Refactor path handling: replace addReturnPath with buildPath to improve path construction logic and handle target contact types * Update lib/screens/map_screen.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add localization for "Show Discovery Contacts" in multiple languages and refactor location plausibility check in map screen * Enhance contact management: update discovered contacts' active status and improve contact handling with flags and raw packet data * Refactor ChannelsScreen: pass ChannelMessageStore to buildExpandedContent and ensure messages are cleared after channel creation * Update MapScreen: adjust label zoom threshold and refactor guessed marker building to include labels * Refactor ChannelsScreen: change channelMessageStore to a private getter and update its usage in buildExpandedContent calls * Enhance location plausibility check: add latitude and longitude bounds to ensure valid coordinates * Update lib/connector/meshcore_connector.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor MeshCoreConnector and related stores: update discovered contacts handling, migrate legacy keys, and set public key in community store * Refactor MeshCoreConnector and ChannelsScreen: update discovered contacts handling and set public key in community store; enhance location plausibility check in MapScreen * Update CMD_ADD_UPDATE_CONTACT frame format to include optional latitude and longitude fields --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2,7 +2,6 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:crypto/crypto.dart' as crypto;
|
import 'package:crypto/crypto.dart' as crypto;
|
||||||
import 'package:meshcore_open/models/discovery_contact.dart';
|
|
||||||
import 'package:pointycastle/export.dart';
|
import 'package:pointycastle/export.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||||
@@ -120,7 +119,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
final List<ScanResult> _scanResults = [];
|
final List<ScanResult> _scanResults = [];
|
||||||
final List<Contact> _contacts = [];
|
final List<Contact> _contacts = [];
|
||||||
final List<DiscoveryContact> _discoveredContacts = [];
|
final List<Contact> _discoveredContacts = [];
|
||||||
final List<Channel> _channels = [];
|
final List<Channel> _channels = [];
|
||||||
final Map<String, List<Message>> _conversations = {};
|
final Map<String, List<Message>> _conversations = {};
|
||||||
final Map<int, List<ChannelMessage>> _channelMessages = {};
|
final Map<int, List<ChannelMessage>> _channelMessages = {};
|
||||||
@@ -281,7 +280,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<DiscoveryContact> get discoveredContacts {
|
List<Contact> get discoveredContacts {
|
||||||
return List.unmodifiable(_discoveredContacts);
|
return List.unmodifiable(_discoveredContacts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,7 +663,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
// Initialize notification service
|
// Initialize notification service
|
||||||
_notificationService.initialize();
|
_notificationService.initialize();
|
||||||
_loadChannelOrder();
|
_loadChannelOrder();
|
||||||
_loadDiscoveredContactCache();
|
|
||||||
|
|
||||||
// Initialize retry service callbacks
|
// Initialize retry service callbacks
|
||||||
_retryService?.initialize(
|
_retryService?.initialize(
|
||||||
@@ -1904,7 +1902,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
Future<void> removeContact(Contact contact) async {
|
Future<void> removeContact(Contact contact) async {
|
||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
|
|
||||||
_handleDiscovery(contact, Uint8List(0), noNotify: true);
|
_handleDiscovery(
|
||||||
|
contact,
|
||||||
|
contact.rawPacket ?? Uint8List(0),
|
||||||
|
noNotify: true,
|
||||||
|
);
|
||||||
|
|
||||||
await sendFrame(buildRemoveContactFrame(contact.publicKey));
|
await sendFrame(buildRemoveContactFrame(contact.publicKey));
|
||||||
_contacts.removeWhere((c) => c.publicKeyHex == contact.publicKeyHex);
|
_contacts.removeWhere((c) => c.publicKeyHex == contact.publicKeyHex);
|
||||||
@@ -1920,7 +1922,20 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeDiscoveredContact(DiscoveryContact contact) async {
|
Future<void> updateKnownDiscovered() async {
|
||||||
|
if (!isConnected) return;
|
||||||
|
for (int i = 0; i < _discoveredContacts.length; i++) {
|
||||||
|
_discoveredContacts[i] = _discoveredContacts[i].copyWith(
|
||||||
|
isActive: _knownContactKeys.contains(
|
||||||
|
_discoveredContacts[i].publicKeyHex,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
unawaited(_persistDiscoveredContacts());
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeDiscoveredContact(Contact contact) async {
|
||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
_discoveredContacts.removeWhere(
|
_discoveredContacts.removeWhere(
|
||||||
(c) => c.publicKeyHex == contact.publicKeyHex,
|
(c) => c.publicKeyHex == contact.publicKeyHex,
|
||||||
@@ -1929,7 +1944,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> importDiscoveredContact(DiscoveryContact contact) async {
|
Future<void> importDiscoveredContact(Contact contact) async {
|
||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
|
|
||||||
await sendFrame(
|
await sendFrame(
|
||||||
@@ -1938,11 +1953,23 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
contact.path,
|
contact.path,
|
||||||
contact.pathLength,
|
contact.pathLength,
|
||||||
type: contact.type,
|
type: contact.type,
|
||||||
flags: 0,
|
flags: contact.flags,
|
||||||
name: contact.name,
|
name: contact.name,
|
||||||
|
lat: contact.latitude,
|
||||||
|
lon: contact.longitude,
|
||||||
|
lastModified: contact.lastSeen,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update the discovered contact to mark it as active (imported)
|
||||||
|
final discoveredIndex = _discoveredContacts.indexWhere(
|
||||||
|
(c) => c.publicKeyHex == contact.publicKeyHex,
|
||||||
|
);
|
||||||
|
if (discoveredIndex >= 0) {
|
||||||
|
_discoveredContacts[discoveredIndex] =
|
||||||
|
_discoveredContacts[discoveredIndex].copyWith(isActive: true);
|
||||||
|
}
|
||||||
|
|
||||||
_handleContactAdvert(
|
_handleContactAdvert(
|
||||||
Contact(
|
Contact(
|
||||||
publicKey: contact.publicKey,
|
publicKey: contact.publicKey,
|
||||||
@@ -1953,6 +1980,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
latitude: contact.latitude,
|
latitude: contact.latitude,
|
||||||
longitude: contact.longitude,
|
longitude: contact.longitude,
|
||||||
lastSeen: DateTime.now(),
|
lastSeen: DateTime.now(),
|
||||||
|
flags: contact.flags,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -1969,6 +1997,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
final existing = _contacts[existingIndex];
|
final existing = _contacts[existingIndex];
|
||||||
// Use copyWith to preserve pathOverride and pathOverrideBytes
|
// Use copyWith to preserve pathOverride and pathOverrideBytes
|
||||||
_contacts[existingIndex] = existing.copyWith(
|
_contacts[existingIndex] = existing.copyWith(
|
||||||
|
pathOverride: null,
|
||||||
|
pathOverrideBytes: null,
|
||||||
pathLength: -1,
|
pathLength: -1,
|
||||||
path: Uint8List(0),
|
path: Uint8List(0),
|
||||||
);
|
);
|
||||||
@@ -2324,6 +2354,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
debugPrint('Got END_OF_CONTACTS');
|
debugPrint('Got END_OF_CONTACTS');
|
||||||
_isLoadingContacts = false;
|
_isLoadingContacts = false;
|
||||||
_preserveContactsOnRefresh = false;
|
_preserveContactsOnRefresh = false;
|
||||||
|
unawaited(updateKnownDiscovered());
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
unawaited(_persistContacts());
|
unawaited(_persistContacts());
|
||||||
if (PlatformInfo.isWeb &&
|
if (PlatformInfo.isWeb &&
|
||||||
@@ -2510,6 +2541,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
// Load persisted channel messages
|
// Load persisted channel messages
|
||||||
loadAllChannelMessages();
|
loadAllChannelMessages();
|
||||||
loadUnreadState();
|
loadUnreadState();
|
||||||
|
_loadDiscoveredContactCache();
|
||||||
|
|
||||||
_awaitingSelfInfo = false;
|
_awaitingSelfInfo = false;
|
||||||
_selfInfoRetryTimer?.cancel();
|
_selfInfoRetryTimer?.cancel();
|
||||||
@@ -4406,7 +4438,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
importDiscoveredContact(
|
importDiscoveredContact(
|
||||||
DiscoveryContact(
|
Contact(
|
||||||
rawPacket: frame,
|
rawPacket: frame,
|
||||||
publicKey: publicKey,
|
publicKey: publicKey,
|
||||||
name: name,
|
name: name,
|
||||||
@@ -4477,6 +4509,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
if (isNewContact) {
|
if (isNewContact) {
|
||||||
final newContact = Contact(
|
final newContact = Contact(
|
||||||
|
rawPacket: rawPacket,
|
||||||
publicKey: publicKey,
|
publicKey: publicKey,
|
||||||
name: name,
|
name: name,
|
||||||
type: type,
|
type: type,
|
||||||
@@ -4622,13 +4655,15 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
latitude: contact.latitude,
|
latitude: contact.latitude,
|
||||||
longitude: contact.longitude,
|
longitude: contact.longitude,
|
||||||
lastSeen: contact.lastSeen,
|
lastSeen: contact.lastSeen,
|
||||||
|
flags: 0,
|
||||||
|
isActive: false,
|
||||||
);
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
unawaited(_persistDiscoveredContacts());
|
unawaited(_persistDiscoveredContacts());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final disContact = DiscoveryContact(
|
final disContact = Contact(
|
||||||
rawPacket: rawPacket,
|
rawPacket: rawPacket,
|
||||||
publicKey: contact.publicKey,
|
publicKey: contact.publicKey,
|
||||||
name: contact.name,
|
name: contact.name,
|
||||||
@@ -4638,6 +4673,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
latitude: contact.latitude,
|
latitude: contact.latitude,
|
||||||
longitude: contact.longitude,
|
longitude: contact.longitude,
|
||||||
lastSeen: contact.lastSeen,
|
lastSeen: contact.lastSeen,
|
||||||
|
lastMessageAt: contact.lastMessageAt,
|
||||||
|
isActive: false,
|
||||||
|
flags: 0,
|
||||||
);
|
);
|
||||||
_discoveredContacts.add(disContact);
|
_discoveredContacts.add(disContact);
|
||||||
|
|
||||||
|
|||||||
@@ -148,6 +148,19 @@ class BufferWriter {
|
|||||||
void writeHex(String hex) {
|
void writeHex(String hex) {
|
||||||
writeBytes(hex2Uint8List(hex));
|
writeBytes(hex2Uint8List(hex));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void writeBytesPadded(Uint8List bytes, int totalLength) {
|
||||||
|
// Path data (64 bytes, zero-padded)
|
||||||
|
final bytesPadded = Uint8List(totalLength);
|
||||||
|
final len = bytes.length < totalLength ? bytes.length : totalLength;
|
||||||
|
if (bytes.isNotEmpty && len > 0) {
|
||||||
|
final copyLen = bytes.length < totalLength ? bytes.length : totalLength;
|
||||||
|
for (int i = 0; i < copyLen; i++) {
|
||||||
|
bytesPadded[i] = bytes[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeBytes(bytesPadded);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint8List hex2Uint8List(String hex) {
|
Uint8List hex2Uint8List(String hex) {
|
||||||
@@ -676,14 +689,17 @@ Uint8List buildResetPathFrame(Uint8List pubKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build CMD_ADD_UPDATE_CONTACT frame to set custom path
|
// Build CMD_ADD_UPDATE_CONTACT frame to set custom path
|
||||||
// Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][timestamp x4]
|
// Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][Lat? x4, Lon? x4][timestamp? x4]
|
||||||
Uint8List buildUpdateContactPathFrame(
|
Uint8List buildUpdateContactPathFrame(
|
||||||
Uint8List pubKey,
|
Uint8List pubKey,
|
||||||
Uint8List customPath,
|
Uint8List path,
|
||||||
int pathLen, {
|
int pathLen, {
|
||||||
int type = 1, // ADV_TYPE_CHAT
|
int type = 1, // ADV_TYPE_CHAT
|
||||||
int flags = 0,
|
int flags = 0,
|
||||||
String name = '',
|
String name = '',
|
||||||
|
double? lat,
|
||||||
|
double? lon,
|
||||||
|
DateTime? lastModified,
|
||||||
}) {
|
}) {
|
||||||
final writer = BufferWriter();
|
final writer = BufferWriter();
|
||||||
writer.writeByte(cmdAddUpdateContact);
|
writer.writeByte(cmdAddUpdateContact);
|
||||||
@@ -692,17 +708,7 @@ Uint8List buildUpdateContactPathFrame(
|
|||||||
writer.writeByte(flags);
|
writer.writeByte(flags);
|
||||||
writer.writeByte(pathLen);
|
writer.writeByte(pathLen);
|
||||||
|
|
||||||
// Path data (64 bytes, zero-padded)
|
writer.writeBytesPadded(path, maxPathSize);
|
||||||
final pathPadded = Uint8List(maxPathSize);
|
|
||||||
if (customPath.isNotEmpty && pathLen > 0) {
|
|
||||||
final copyLen = customPath.length < maxPathSize
|
|
||||||
? customPath.length
|
|
||||||
: maxPathSize;
|
|
||||||
for (int i = 0; i < copyLen; i++) {
|
|
||||||
pathPadded[i] = customPath[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
writer.writeBytes(pathPadded);
|
|
||||||
|
|
||||||
// Name (32 bytes, null-padded)
|
// Name (32 bytes, null-padded)
|
||||||
writer.writeCString(name, maxNameSize);
|
writer.writeCString(name, maxNameSize);
|
||||||
@@ -711,6 +717,27 @@ Uint8List buildUpdateContactPathFrame(
|
|||||||
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||||
writer.writeUInt32LE(timestamp);
|
writer.writeUInt32LE(timestamp);
|
||||||
|
|
||||||
|
if ((lat == null || lon == null) && lastModified != null) {
|
||||||
|
// If lat/lon not provided, write zeros
|
||||||
|
writer.writeInt32LE(0);
|
||||||
|
writer.writeInt32LE(0);
|
||||||
|
} else {
|
||||||
|
// Latitude and Longitude are expected in degrees, convert to int by multiplying by 1e6
|
||||||
|
// Latitude
|
||||||
|
final latitude = lat ?? 0.0;
|
||||||
|
writer.writeInt32LE((latitude * 1e6).round());
|
||||||
|
|
||||||
|
// Longitude
|
||||||
|
final longitude = lon ?? 0.0;
|
||||||
|
writer.writeInt32LE((longitude * 1e6).round());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastModified != null) {
|
||||||
|
// Last modified
|
||||||
|
final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000;
|
||||||
|
writer.writeUInt32LE(lastModifiedTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
return writer.toBytes();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -1859,5 +1859,6 @@
|
|||||||
"usbConnectionFailed": "Неуспешно свързване през USB: {error}",
|
"usbConnectionFailed": "Неуспешно свързване през USB: {error}",
|
||||||
"usbStatus_notConnected": "Изберете USB устройство",
|
"usbStatus_notConnected": "Изберете USB устройство",
|
||||||
"usbStatus_searching": "Търсене на USB устройства...",
|
"usbStatus_searching": "Търсене на USB устройства...",
|
||||||
"usbErrorConnectTimedOut": "Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка."
|
"usbErrorConnectTimedOut": "Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка.",
|
||||||
|
"map_showDiscoveryContacts": "Покажи контакти за откриване"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1887,5 +1887,6 @@
|
|||||||
"usbStatus_notConnected": "Wählen Sie ein USB-Gerät aus",
|
"usbStatus_notConnected": "Wählen Sie ein USB-Gerät aus",
|
||||||
"usbStatus_connecting": "Verbindung zum USB-Gerät...",
|
"usbStatus_connecting": "Verbindung zum USB-Gerät...",
|
||||||
"usbConnectionFailed": "Fehler beim USB-Verbindungsaufbau: {error}",
|
"usbConnectionFailed": "Fehler beim USB-Verbindungsaufbau: {error}",
|
||||||
"usbErrorConnectTimedOut": "Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält."
|
"usbErrorConnectTimedOut": "Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält.",
|
||||||
|
"map_showDiscoveryContacts": "Entdeckungs-Kontakte anzeigen"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -807,6 +807,7 @@
|
|||||||
"map_markers": "Markers",
|
"map_markers": "Markers",
|
||||||
"map_showSharedMarkers": "Show shared markers",
|
"map_showSharedMarkers": "Show shared markers",
|
||||||
"map_showGuessedLocations": "Show guessed node locations",
|
"map_showGuessedLocations": "Show guessed node locations",
|
||||||
|
"map_showDiscoveryContacts": "Show Discovery Contacts",
|
||||||
"map_guessedLocation": "Guessed location",
|
"map_guessedLocation": "Guessed location",
|
||||||
"map_lastSeenTime": "Last Seen Time",
|
"map_lastSeenTime": "Last Seen Time",
|
||||||
"map_sharedPin": "Shared pin",
|
"map_sharedPin": "Shared pin",
|
||||||
|
|||||||
+2
-1
@@ -1887,5 +1887,6 @@
|
|||||||
"usbStatus_searching": "Buscando dispositivos USB...",
|
"usbStatus_searching": "Buscando dispositivos USB...",
|
||||||
"usbStatus_notConnected": "Seleccione un dispositivo USB",
|
"usbStatus_notConnected": "Seleccione un dispositivo USB",
|
||||||
"usbConnectionFailed": "Error al conectar mediante USB: {error}",
|
"usbConnectionFailed": "Error al conectar mediante USB: {error}",
|
||||||
"usbErrorConnectTimedOut": "La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion."
|
"usbErrorConnectTimedOut": "La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion.",
|
||||||
|
"map_showDiscoveryContacts": "Mostrar Contactos de Descubrimiento"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1859,5 +1859,6 @@
|
|||||||
"usbConnectionFailed": "Échec de la connexion USB : {error}",
|
"usbConnectionFailed": "Échec de la connexion USB : {error}",
|
||||||
"usbStatus_connecting": "Connexion au périphérique USB...",
|
"usbStatus_connecting": "Connexion au périphérique USB...",
|
||||||
"usbStatus_searching": "Recherche de périphériques USB...",
|
"usbStatus_searching": "Recherche de périphériques USB...",
|
||||||
"usbErrorConnectTimedOut": "La connexion a expiré. Assurez-vous que l'appareil dispose du firmware USB Companion."
|
"usbErrorConnectTimedOut": "La connexion a expiré. Assurez-vous que l'appareil dispose du firmware USB Companion.",
|
||||||
|
"map_showDiscoveryContacts": "Afficher les contacts de découverte"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1859,5 +1859,6 @@
|
|||||||
"usbConnectionFailed": "Errore nella connessione USB: {error}",
|
"usbConnectionFailed": "Errore nella connessione USB: {error}",
|
||||||
"usbStatus_notConnected": "Seleziona un dispositivo USB",
|
"usbStatus_notConnected": "Seleziona un dispositivo USB",
|
||||||
"usbStatus_connecting": "Connessione al dispositivo USB...",
|
"usbStatus_connecting": "Connessione al dispositivo USB...",
|
||||||
"usbErrorConnectTimedOut": "La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion."
|
"usbErrorConnectTimedOut": "La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion.",
|
||||||
|
"map_showDiscoveryContacts": "Mostra Contatti di Discovery"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2788,6 +2788,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'Show guessed node locations'**
|
/// **'Show guessed node locations'**
|
||||||
String get map_showGuessedLocations;
|
String get map_showGuessedLocations;
|
||||||
|
|
||||||
|
/// No description provided for @map_showDiscoveryContacts.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Show Discovery Contacts'**
|
||||||
|
String get map_showDiscoveryContacts;
|
||||||
|
|
||||||
/// No description provided for @map_guessedLocation.
|
/// No description provided for @map_guessedLocation.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -1531,6 +1531,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
String get map_showGuessedLocations =>
|
String get map_showGuessedLocations =>
|
||||||
'Покажете местоположенията на предположените възли.';
|
'Покажете местоположенията на предположените възли.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showDiscoveryContacts => 'Покажи контакти за откриване';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_guessedLocation => 'Предполагано местоположение';
|
String get map_guessedLocation => 'Предполагано местоположение';
|
||||||
|
|
||||||
|
|||||||
@@ -1531,6 +1531,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get map_showGuessedLocations =>
|
String get map_showGuessedLocations =>
|
||||||
'Zeige die vermuteten Knotenpositionen';
|
'Zeige die vermuteten Knotenpositionen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showDiscoveryContacts => 'Entdeckungs-Kontakte anzeigen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_guessedLocation => 'Geschätzter Ort';
|
String get map_guessedLocation => 'Geschätzter Ort';
|
||||||
|
|
||||||
|
|||||||
@@ -1506,6 +1506,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showGuessedLocations => 'Show guessed node locations';
|
String get map_showGuessedLocations => 'Show guessed node locations';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showDiscoveryContacts => 'Show Discovery Contacts';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_guessedLocation => 'Guessed location';
|
String get map_guessedLocation => 'Guessed location';
|
||||||
|
|
||||||
|
|||||||
@@ -1529,6 +1529,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
String get map_showGuessedLocations =>
|
String get map_showGuessedLocations =>
|
||||||
'Mostrar las ubicaciones estimadas de los nodos.';
|
'Mostrar las ubicaciones estimadas de los nodos.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showDiscoveryContacts => 'Mostrar Contactos de Descubrimiento';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_guessedLocation => 'Ubicación estimada';
|
String get map_guessedLocation => 'Ubicación estimada';
|
||||||
|
|
||||||
|
|||||||
@@ -1536,6 +1536,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get map_showGuessedLocations =>
|
String get map_showGuessedLocations =>
|
||||||
'Afficher les emplacements des nœuds estimés';
|
'Afficher les emplacements des nœuds estimés';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showDiscoveryContacts => 'Afficher les contacts de découverte';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_guessedLocation => 'Lieu deviné';
|
String get map_guessedLocation => 'Lieu deviné';
|
||||||
|
|
||||||
|
|||||||
@@ -1528,6 +1528,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showGuessedLocations => 'Mostra le posizioni stimate dei nodi';
|
String get map_showGuessedLocations => 'Mostra le posizioni stimate dei nodi';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showDiscoveryContacts => 'Mostra Contatti di Discovery';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_guessedLocation => 'Località indovinata';
|
String get map_guessedLocation => 'Località indovinata';
|
||||||
|
|
||||||
|
|||||||
@@ -1521,6 +1521,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
String get map_showGuessedLocations =>
|
String get map_showGuessedLocations =>
|
||||||
'Toon de voorspelde locaties van de knopen';
|
'Toon de voorspelde locaties van de knopen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showDiscoveryContacts => 'Ontdek contacten weergeven';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_guessedLocation => 'Geroerde locatie';
|
String get map_guessedLocation => 'Geroerde locatie';
|
||||||
|
|
||||||
|
|||||||
@@ -1530,6 +1530,9 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
String get map_showGuessedLocations =>
|
String get map_showGuessedLocations =>
|
||||||
'Wyświetl lokalizacje zgadanych węzłów';
|
'Wyświetl lokalizacje zgadanych węzłów';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showDiscoveryContacts => 'Pokaż kontakty odkrywania';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_guessedLocation => 'Wydana lokalizacja';
|
String get map_guessedLocation => 'Wydana lokalizacja';
|
||||||
|
|
||||||
|
|||||||
@@ -1530,6 +1530,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
String get map_showGuessedLocations =>
|
String get map_showGuessedLocations =>
|
||||||
'Mostrar as localizações dos nós estimados';
|
'Mostrar as localizações dos nós estimados';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showDiscoveryContacts => 'Mostrar Contatos de Descoberta';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_guessedLocation => 'Localização estimada';
|
String get map_guessedLocation => 'Localização estimada';
|
||||||
|
|
||||||
|
|||||||
@@ -1532,6 +1532,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
String get map_showGuessedLocations =>
|
String get map_showGuessedLocations =>
|
||||||
'Отобразить предполагаемые места расположения узлов';
|
'Отобразить предполагаемые места расположения узлов';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showDiscoveryContacts => 'Показать контакты Discovery';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_guessedLocation => 'Угаданное место';
|
String get map_guessedLocation => 'Угаданное место';
|
||||||
|
|
||||||
|
|||||||
@@ -1524,6 +1524,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
String get map_showGuessedLocations =>
|
String get map_showGuessedLocations =>
|
||||||
'Zobraziť umiestnenia odhadnutých uzlov';
|
'Zobraziť umiestnenia odhadnutých uzlov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showDiscoveryContacts => 'Zobraziť kontakty objavov';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_guessedLocation => 'Odhadnutá lokalita';
|
String get map_guessedLocation => 'Odhadnutá lokalita';
|
||||||
|
|
||||||
|
|||||||
@@ -1517,6 +1517,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showGuessedLocations => 'Pokaži lokacije domnevnih not.';
|
String get map_showGuessedLocations => 'Pokaži lokacije domnevnih not.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showDiscoveryContacts => 'Prikaži odkritja kontaktov';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_guessedLocation => 'Predpostavljena lokacija';
|
String get map_guessedLocation => 'Predpostavljena lokacija';
|
||||||
|
|
||||||
|
|||||||
@@ -1514,6 +1514,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
String get map_showGuessedLocations =>
|
String get map_showGuessedLocations =>
|
||||||
'Visa upp de antagna nodernas placeringar';
|
'Visa upp de antagna nodernas placeringar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showDiscoveryContacts => 'Visa Discovery-kontakter';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_guessedLocation => 'Gissad plats';
|
String get map_guessedLocation => 'Gissad plats';
|
||||||
|
|
||||||
|
|||||||
@@ -1529,6 +1529,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
String get map_showGuessedLocations =>
|
String get map_showGuessedLocations =>
|
||||||
'Показати місцезнаходження передбачених вузлів';
|
'Показати місцезнаходження передбачених вузлів';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showDiscoveryContacts => 'Показати контакти Відкриття';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_guessedLocation => 'Визначено місцезнаходження';
|
String get map_guessedLocation => 'Визначено місцезнаходження';
|
||||||
|
|
||||||
|
|||||||
@@ -1440,6 +1440,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showGuessedLocations => '显示猜测的节点位置';
|
String get map_showGuessedLocations => '显示猜测的节点位置';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showDiscoveryContacts => '显示发现联系人';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_guessedLocation => '猜测的位置';
|
String get map_guessedLocation => '猜测的位置';
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -1859,5 +1859,6 @@
|
|||||||
"usbStatus_notConnected": "Selecteer een USB-apparaat",
|
"usbStatus_notConnected": "Selecteer een USB-apparaat",
|
||||||
"usbStatus_connecting": "Verbinding maken met USB-apparaat...",
|
"usbStatus_connecting": "Verbinding maken met USB-apparaat...",
|
||||||
"usbStatus_searching": "Zoeken naar USB-apparaten...",
|
"usbStatus_searching": "Zoeken naar USB-apparaten...",
|
||||||
"usbErrorConnectTimedOut": "Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft."
|
"usbErrorConnectTimedOut": "Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft.",
|
||||||
|
"map_showDiscoveryContacts": "Ontdek contacten weergeven"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1859,5 +1859,6 @@
|
|||||||
"usbStatus_connecting": "Połączenie z urządzeniem USB...",
|
"usbStatus_connecting": "Połączenie z urządzeniem USB...",
|
||||||
"usbStatus_notConnected": "Wybierz urządzenie USB",
|
"usbStatus_notConnected": "Wybierz urządzenie USB",
|
||||||
"usbConnectionFailed": "Błąd połączenia USB: {error}",
|
"usbConnectionFailed": "Błąd połączenia USB: {error}",
|
||||||
"usbErrorConnectTimedOut": "Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\"."
|
"usbErrorConnectTimedOut": "Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\".",
|
||||||
|
"map_showDiscoveryContacts": "Pokaż kontakty odkrywania"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1859,5 +1859,6 @@
|
|||||||
"usbStatus_notConnected": "Selecione um dispositivo USB",
|
"usbStatus_notConnected": "Selecione um dispositivo USB",
|
||||||
"usbConnectionFailed": "Falha na conexão USB: {error}",
|
"usbConnectionFailed": "Falha na conexão USB: {error}",
|
||||||
"usbStatus_connecting": "Conectando ao dispositivo USB...",
|
"usbStatus_connecting": "Conectando ao dispositivo USB...",
|
||||||
"usbErrorConnectTimedOut": "A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion."
|
"usbErrorConnectTimedOut": "A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion.",
|
||||||
|
"map_showDiscoveryContacts": "Mostrar Contatos de Descoberta"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1099,5 +1099,6 @@
|
|||||||
"usbStatus_connecting": "Подключение к USB-устройству...",
|
"usbStatus_connecting": "Подключение к USB-устройству...",
|
||||||
"usbConnectionFailed": "Не удалось установить соединение через USB: {error}",
|
"usbConnectionFailed": "Не удалось установить соединение через USB: {error}",
|
||||||
"usbStatus_notConnected": "Выберите USB-устройство",
|
"usbStatus_notConnected": "Выберите USB-устройство",
|
||||||
"usbErrorConnectTimedOut": "Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion."
|
"usbErrorConnectTimedOut": "Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion.",
|
||||||
|
"map_showDiscoveryContacts": "Показать контакты Discovery"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1859,5 +1859,6 @@
|
|||||||
"usbConnectionFailed": "Neúspešné pripojenie cez USB: {error}",
|
"usbConnectionFailed": "Neúspešné pripojenie cez USB: {error}",
|
||||||
"usbStatus_notConnected": "Vyberte USB zariadenie",
|
"usbStatus_notConnected": "Vyberte USB zariadenie",
|
||||||
"usbStatus_connecting": "Pripojenie k USB zariadeniu...",
|
"usbStatus_connecting": "Pripojenie k USB zariadeniu...",
|
||||||
"usbErrorConnectTimedOut": "Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion."
|
"usbErrorConnectTimedOut": "Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion.",
|
||||||
|
"map_showDiscoveryContacts": "Zobraziť kontakty objavov"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1859,5 +1859,6 @@
|
|||||||
"usbStatus_connecting": "Povezava z USB napravo...",
|
"usbStatus_connecting": "Povezava z USB napravo...",
|
||||||
"usbStatus_searching": "Iskanje USB naprav...",
|
"usbStatus_searching": "Iskanje USB naprav...",
|
||||||
"usbConnectionFailed": "Napaka pri povezavi preko USB: {error}",
|
"usbConnectionFailed": "Napaka pri povezavi preko USB: {error}",
|
||||||
"usbErrorConnectTimedOut": "Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion."
|
"usbErrorConnectTimedOut": "Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion.",
|
||||||
|
"map_showDiscoveryContacts": "Prikaži odkritja kontaktov"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1859,5 +1859,6 @@
|
|||||||
"usbStatus_notConnected": "Välj en USB-enhet",
|
"usbStatus_notConnected": "Välj en USB-enhet",
|
||||||
"usbConnectionFailed": "Fel vid USB-anslutning: {error}",
|
"usbConnectionFailed": "Fel vid USB-anslutning: {error}",
|
||||||
"usbStatus_searching": "Söker efter USB-enheter...",
|
"usbStatus_searching": "Söker efter USB-enheter...",
|
||||||
"usbErrorConnectTimedOut": "Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware."
|
"usbErrorConnectTimedOut": "Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware.",
|
||||||
|
"map_showDiscoveryContacts": "Visa Discovery-kontakter"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1859,5 +1859,6 @@
|
|||||||
"usbStatus_notConnected": "Виберіть пристрій USB",
|
"usbStatus_notConnected": "Виберіть пристрій USB",
|
||||||
"usbConnectionFailed": "Не вдалося встановити з'єднання через USB: {error}",
|
"usbConnectionFailed": "Не вдалося встановити з'єднання через USB: {error}",
|
||||||
"usbStatus_connecting": "Підключення до USB-пристрою...",
|
"usbStatus_connecting": "Підключення до USB-пристрою...",
|
||||||
"usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion."
|
"usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion.",
|
||||||
|
"map_showDiscoveryContacts": "Показати контакти Відкриття"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1864,5 +1864,6 @@
|
|||||||
"usbStatus_connecting": "连接USB设备...",
|
"usbStatus_connecting": "连接USB设备...",
|
||||||
"usbStatus_notConnected": "选择一个 USB 设备",
|
"usbStatus_notConnected": "选择一个 USB 设备",
|
||||||
"usbConnectionFailed": "USB 连接失败:{error}",
|
"usbConnectionFailed": "USB 连接失败:{error}",
|
||||||
"usbErrorConnectTimedOut": "连接超时。请确保设备已安装 USB 伴侣固件。"
|
"usbErrorConnectTimedOut": "连接超时。请确保设备已安装 USB 伴侣固件。",
|
||||||
|
"map_showDiscoveryContacts": "显示发现联系人"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class AppSettings {
|
|||||||
final Map<String, String> batteryChemistryByRepeaterId;
|
final Map<String, String> batteryChemistryByRepeaterId;
|
||||||
final UnitSystem unitSystem;
|
final UnitSystem unitSystem;
|
||||||
final Set<String> mutedChannels;
|
final Set<String> mutedChannels;
|
||||||
|
final bool mapShowDiscoveryContacts;
|
||||||
|
|
||||||
AppSettings({
|
AppSettings({
|
||||||
this.clearPathOnMaxRetry = false,
|
this.clearPathOnMaxRetry = false,
|
||||||
@@ -66,6 +67,7 @@ class AppSettings {
|
|||||||
Map<String, String>? batteryChemistryByRepeaterId,
|
Map<String, String>? batteryChemistryByRepeaterId,
|
||||||
this.unitSystem = UnitSystem.metric,
|
this.unitSystem = UnitSystem.metric,
|
||||||
Set<String>? mutedChannels,
|
Set<String>? mutedChannels,
|
||||||
|
this.mapShowDiscoveryContacts = true,
|
||||||
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
|
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
|
||||||
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
|
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
|
||||||
mutedChannels = mutedChannels ?? {};
|
mutedChannels = mutedChannels ?? {};
|
||||||
@@ -97,6 +99,7 @@ class AppSettings {
|
|||||||
'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId,
|
'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId,
|
||||||
'unit_system': unitSystem.value,
|
'unit_system': unitSystem.value,
|
||||||
'muted_channels': mutedChannels.toList(),
|
'muted_channels': mutedChannels.toList(),
|
||||||
|
'map_show_discovery_contacts': mapShowDiscoveryContacts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +155,8 @@ class AppSettings {
|
|||||||
?.map((e) => e.toString())
|
?.map((e) => e.toString())
|
||||||
.toSet()) ??
|
.toSet()) ??
|
||||||
{},
|
{},
|
||||||
|
mapShowDiscoveryContacts:
|
||||||
|
json['map_show_discovery_contacts'] as bool? ?? true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +186,7 @@ class AppSettings {
|
|||||||
Map<String, String>? batteryChemistryByRepeaterId,
|
Map<String, String>? batteryChemistryByRepeaterId,
|
||||||
UnitSystem? unitSystem,
|
UnitSystem? unitSystem,
|
||||||
Set<String>? mutedChannels,
|
Set<String>? mutedChannels,
|
||||||
|
bool? mapShowDiscoveryContacts,
|
||||||
}) {
|
}) {
|
||||||
return AppSettings(
|
return AppSettings(
|
||||||
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
|
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
|
||||||
@@ -217,6 +223,8 @@ class AppSettings {
|
|||||||
batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId,
|
batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId,
|
||||||
unitSystem: unitSystem ?? this.unitSystem,
|
unitSystem: unitSystem ?? this.unitSystem,
|
||||||
mutedChannels: mutedChannels ?? this.mutedChannels,
|
mutedChannels: mutedChannels ?? this.mutedChannels,
|
||||||
|
mapShowDiscoveryContacts:
|
||||||
|
mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ class Contact {
|
|||||||
final double? longitude;
|
final double? longitude;
|
||||||
final DateTime lastSeen;
|
final DateTime lastSeen;
|
||||||
final DateTime lastMessageAt;
|
final DateTime lastMessageAt;
|
||||||
|
final bool isActive;
|
||||||
|
final Uint8List? rawPacket;
|
||||||
|
|
||||||
Contact({
|
Contact({
|
||||||
required this.publicKey,
|
required this.publicKey,
|
||||||
@@ -31,6 +33,8 @@ class Contact {
|
|||||||
this.longitude,
|
this.longitude,
|
||||||
required this.lastSeen,
|
required this.lastSeen,
|
||||||
DateTime? lastMessageAt,
|
DateTime? lastMessageAt,
|
||||||
|
this.isActive = true,
|
||||||
|
this.rawPacket,
|
||||||
}) : lastMessageAt = lastMessageAt ?? lastSeen;
|
}) : lastMessageAt = lastMessageAt ?? lastSeen;
|
||||||
|
|
||||||
String get publicKeyHex => pubKeyToHex(publicKey);
|
String get publicKeyHex => pubKeyToHex(publicKey);
|
||||||
@@ -78,6 +82,8 @@ class Contact {
|
|||||||
double? longitude,
|
double? longitude,
|
||||||
DateTime? lastSeen,
|
DateTime? lastSeen,
|
||||||
DateTime? lastMessageAt,
|
DateTime? lastMessageAt,
|
||||||
|
bool? isActive,
|
||||||
|
Uint8List? rawPacket,
|
||||||
}) {
|
}) {
|
||||||
return Contact(
|
return Contact(
|
||||||
publicKey: publicKey ?? this.publicKey,
|
publicKey: publicKey ?? this.publicKey,
|
||||||
@@ -96,6 +102,8 @@ class Contact {
|
|||||||
longitude: longitude ?? this.longitude,
|
longitude: longitude ?? this.longitude,
|
||||||
lastSeen: lastSeen ?? this.lastSeen,
|
lastSeen: lastSeen ?? this.lastSeen,
|
||||||
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
|
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
|
||||||
|
isActive: isActive ?? this.isActive,
|
||||||
|
rawPacket: rawPacket ?? this.rawPacket,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +212,8 @@ class Contact {
|
|||||||
latitude: lat,
|
latitude: lat,
|
||||||
longitude: lon,
|
longitude: lon,
|
||||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000),
|
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000),
|
||||||
|
isActive: true,
|
||||||
|
rawPacket: null,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appLogger.error('Failed to parse contact frame: $e');
|
appLogger.error('Failed to parse contact frame: $e');
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
import 'dart:typed_data';
|
|
||||||
import '../connector/meshcore_protocol.dart';
|
|
||||||
|
|
||||||
class DiscoveryContact {
|
|
||||||
final Uint8List rawPacket;
|
|
||||||
final Uint8List publicKey;
|
|
||||||
final String name;
|
|
||||||
final int type;
|
|
||||||
final int pathLength; // -1 = flood, 0+ = direct hops (from device)
|
|
||||||
final Uint8List path; // Path bytes from device
|
|
||||||
final double? latitude;
|
|
||||||
final double? longitude;
|
|
||||||
final DateTime lastSeen;
|
|
||||||
|
|
||||||
DiscoveryContact({
|
|
||||||
required this.rawPacket,
|
|
||||||
required this.publicKey,
|
|
||||||
required this.name,
|
|
||||||
required this.type,
|
|
||||||
required this.pathLength,
|
|
||||||
required this.path,
|
|
||||||
this.latitude,
|
|
||||||
this.longitude,
|
|
||||||
required this.lastSeen,
|
|
||||||
});
|
|
||||||
|
|
||||||
String get publicKeyHex => pubKeyToHex(publicKey);
|
|
||||||
|
|
||||||
String get typeLabel {
|
|
||||||
switch (type) {
|
|
||||||
case advTypeChat:
|
|
||||||
return 'Chat';
|
|
||||||
case advTypeRepeater:
|
|
||||||
return 'Repeater';
|
|
||||||
case advTypeRoom:
|
|
||||||
return 'Room';
|
|
||||||
case advTypeSensor:
|
|
||||||
return 'Sensor';
|
|
||||||
default:
|
|
||||||
return 'Unknown';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String get pathLabel {
|
|
||||||
if (pathLength < 0) return 'Flood';
|
|
||||||
if (pathLength == 0) return 'Direct';
|
|
||||||
return '$pathLength hops';
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get hasLocation => latitude != null && longitude != null;
|
|
||||||
|
|
||||||
DiscoveryContact copyWith({
|
|
||||||
Uint8List? rawPacket,
|
|
||||||
Uint8List? publicKey,
|
|
||||||
String? name,
|
|
||||||
int? type,
|
|
||||||
int? pathLength,
|
|
||||||
Uint8List? path,
|
|
||||||
double? latitude,
|
|
||||||
double? longitude,
|
|
||||||
DateTime? lastSeen,
|
|
||||||
}) {
|
|
||||||
return DiscoveryContact(
|
|
||||||
rawPacket: rawPacket ?? this.rawPacket,
|
|
||||||
publicKey: publicKey ?? this.publicKey,
|
|
||||||
name: name ?? this.name,
|
|
||||||
type: type ?? this.type,
|
|
||||||
pathLength: pathLength ?? this.pathLength,
|
|
||||||
path: path ?? this.path,
|
|
||||||
latitude: latitude ?? this.latitude,
|
|
||||||
longitude: longitude ?? this.longitude,
|
|
||||||
lastSeen: lastSeen ?? this.lastSeen,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String get pathIdList {
|
|
||||||
final pathBytes = path;
|
|
||||||
if (pathBytes.isEmpty) return '';
|
|
||||||
final parts = <String>[];
|
|
||||||
final groupSize = pathHashSize;
|
|
||||||
for (int i = 0; i < pathBytes.length; i += groupSize) {
|
|
||||||
final end = (i + groupSize) <= pathBytes.length
|
|
||||||
? (i + groupSize)
|
|
||||||
: pathBytes.length;
|
|
||||||
final chunk = pathBytes.sublist(i, end);
|
|
||||||
parts.add(
|
|
||||||
chunk
|
|
||||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
|
||||||
.join(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return parts.join(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
String get shortPubKeyHex {
|
|
||||||
return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>";
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
other is DiscoveryContact && publicKeyHex == other.publicKeyHex;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => publicKeyHex.hashCode;
|
|
||||||
}
|
|
||||||
@@ -118,6 +118,19 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
|||||||
: Icons.download,
|
: Icons.download,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
|
onLongPress: () async {
|
||||||
|
await Clipboard.setData(
|
||||||
|
ClipboardData(
|
||||||
|
text: entry.payload
|
||||||
|
.map(
|
||||||
|
(b) => b
|
||||||
|
.toRadixString(16)
|
||||||
|
.padLeft(2, '0'),
|
||||||
|
)
|
||||||
|
.join(''),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,11 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
final primaryPath = !channelMessage && !message.isOutgoing
|
final primaryPath = !channelMessage && !message.isOutgoing
|
||||||
? Uint8List.fromList(primaryPathTmp.reversed.toList())
|
? Uint8List.fromList(primaryPathTmp.reversed.toList())
|
||||||
: primaryPathTmp;
|
: primaryPathTmp;
|
||||||
|
final contacts = <Contact>[
|
||||||
final hops = _buildPathHops(primaryPath, connector.contacts, l10n);
|
...connector.contacts,
|
||||||
|
...connector.discoveredContacts,
|
||||||
|
];
|
||||||
|
final hops = _buildPathHops(primaryPath, contacts, l10n);
|
||||||
final hasHopDetails = primaryPath.isNotEmpty;
|
final hasHopDetails = primaryPath.isNotEmpty;
|
||||||
final observedLabel = _formatObservedHops(
|
final observedLabel = _formatObservedHops(
|
||||||
primaryPath.length,
|
primaryPath.length,
|
||||||
@@ -364,11 +367,11 @@ class _ChannelMessagePathMapScreenState
|
|||||||
: selectedPathTmp;
|
: selectedPathTmp;
|
||||||
|
|
||||||
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
||||||
final hops = _buildPathHops(
|
final contacts = <Contact>[
|
||||||
selectedPath,
|
...connector.contacts,
|
||||||
connector.contacts,
|
...connector.discoveredContacts,
|
||||||
context.l10n,
|
];
|
||||||
);
|
final hops = _buildPathHops(selectedPath, contacts, context.l10n);
|
||||||
|
|
||||||
final points = <LatLng>[];
|
final points = <LatLng>[];
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
// Cache of PSK hex -> Community for quick lookup
|
// Cache of PSK hex -> Community for quick lookup
|
||||||
final Map<String, Community> _pskToCommunity = {};
|
final Map<String, Community> _pskToCommunity = {};
|
||||||
|
|
||||||
|
ChannelMessageStore get _channelMessageStore => ChannelMessageStore();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -61,6 +63,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadCommunities() async {
|
Future<void> _loadCommunities() async {
|
||||||
|
final connector = context.read<MeshCoreConnector>();
|
||||||
|
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||||
final communities = await _communityStore.loadCommunities();
|
final communities = await _communityStore.loadCommunities();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -714,6 +718,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
bool isRegularHashtag = true;
|
bool isRegularHashtag = true;
|
||||||
Community? selectedCommunity;
|
Community? selectedCommunity;
|
||||||
|
|
||||||
|
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (dialogContext) => StatefulBuilder(
|
builder: (dialogContext) => StatefulBuilder(
|
||||||
@@ -765,7 +771,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget? buildExpandedContent() {
|
Widget? buildExpandedContent(
|
||||||
|
ChannelMessageStore channelMessageStore,
|
||||||
|
) {
|
||||||
switch (selectedOption) {
|
switch (selectedOption) {
|
||||||
case 0: // Create Private Channel
|
case 0: // Create Private Channel
|
||||||
return Column(
|
return Column(
|
||||||
@@ -790,7 +798,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FilledButton(
|
child: FilledButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
final name = nameController.text.trim();
|
final name = nameController.text.trim();
|
||||||
if (name.isEmpty) {
|
if (name.isEmpty) {
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(
|
||||||
@@ -812,7 +820,14 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
psk[i] = random.nextInt(256);
|
psk[i] = random.nextInt(256);
|
||||||
}
|
}
|
||||||
Navigator.pop(dialogContext);
|
Navigator.pop(dialogContext);
|
||||||
connector.setChannel(nextIndex, name, psk);
|
await connector.setChannel(
|
||||||
|
nextIndex,
|
||||||
|
name,
|
||||||
|
psk,
|
||||||
|
);
|
||||||
|
await channelMessageStore.clearChannelMessages(
|
||||||
|
nextIndex,
|
||||||
|
);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
@@ -1331,7 +1346,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
subtitle:
|
subtitle:
|
||||||
dialogContext.l10n.channels_createPrivateChannelDesc,
|
dialogContext.l10n.channels_createPrivateChannelDesc,
|
||||||
),
|
),
|
||||||
if (selectedOption == 0) buildExpandedContent()!,
|
if (selectedOption == 0)
|
||||||
|
buildExpandedContent(_channelMessageStore)!,
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
buildOptionTile(
|
buildOptionTile(
|
||||||
optionIndex: 1,
|
optionIndex: 1,
|
||||||
@@ -1340,7 +1356,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
subtitle:
|
subtitle:
|
||||||
dialogContext.l10n.channels_joinPrivateChannelDesc,
|
dialogContext.l10n.channels_joinPrivateChannelDesc,
|
||||||
),
|
),
|
||||||
if (selectedOption == 1) buildExpandedContent()!,
|
if (selectedOption == 1)
|
||||||
|
buildExpandedContent(_channelMessageStore)!,
|
||||||
if (!hasPublicChannel) ...[
|
if (!hasPublicChannel) ...[
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
buildOptionTile(
|
buildOptionTile(
|
||||||
@@ -1350,7 +1367,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
subtitle:
|
subtitle:
|
||||||
dialogContext.l10n.channels_joinPublicChannelDesc,
|
dialogContext.l10n.channels_joinPublicChannelDesc,
|
||||||
),
|
),
|
||||||
if (selectedOption == 2) buildExpandedContent()!,
|
if (selectedOption == 2)
|
||||||
|
buildExpandedContent(_channelMessageStore)!,
|
||||||
],
|
],
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
buildOptionTile(
|
buildOptionTile(
|
||||||
@@ -1360,7 +1378,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
subtitle:
|
subtitle:
|
||||||
dialogContext.l10n.channels_joinHashtagChannelDesc,
|
dialogContext.l10n.channels_joinHashtagChannelDesc,
|
||||||
),
|
),
|
||||||
if (selectedOption == 3) buildExpandedContent()!,
|
if (selectedOption == 3)
|
||||||
|
buildExpandedContent(_channelMessageStore)!,
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
buildOptionTile(
|
buildOptionTile(
|
||||||
optionIndex: 4,
|
optionIndex: 4,
|
||||||
@@ -1368,7 +1387,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
title: dialogContext.l10n.community_scanQr,
|
title: dialogContext.l10n.community_scanQr,
|
||||||
subtitle: dialogContext.l10n.community_join,
|
subtitle: dialogContext.l10n.community_join,
|
||||||
),
|
),
|
||||||
if (selectedOption == 4) buildExpandedContent()!,
|
if (selectedOption == 4)
|
||||||
|
buildExpandedContent(_channelMessageStore)!,
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
buildOptionTile(
|
buildOptionTile(
|
||||||
optionIndex: 5,
|
optionIndex: 5,
|
||||||
@@ -1376,7 +1396,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
title: dialogContext.l10n.community_create,
|
title: dialogContext.l10n.community_create,
|
||||||
subtitle: dialogContext.l10n.community_createDesc,
|
subtitle: dialogContext.l10n.community_createDesc,
|
||||||
),
|
),
|
||||||
if (selectedOption == 5) buildExpandedContent()!,
|
if (selectedOption == 5)
|
||||||
|
buildExpandedContent(_channelMessageStore)!,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1526,7 +1547,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
try {
|
try {
|
||||||
await connector.deleteChannel(channel.index);
|
await connector.deleteChannel(channel.index);
|
||||||
|
|
||||||
channelMessageStore.clearChannelMessages(channel.index);
|
await channelMessageStore.clearChannelMessages(channel.index);
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
|
|
||||||
@@ -1751,6 +1772,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
final channelCount = communityChannels.length;
|
final channelCount = communityChannels.length;
|
||||||
|
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
|||||||
_isProcessing = true;
|
_isProcessing = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final connector = context.read<MeshCoreConnector>();
|
||||||
|
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Parse the community data
|
// Parse the community data
|
||||||
final community = Community.fromQrData(const Uuid().v4(), data);
|
final community = Community.fromQrData(const Uuid().v4(), data);
|
||||||
@@ -209,6 +212,8 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
|||||||
bool addPublicChannel,
|
bool addPublicChannel,
|
||||||
) async {
|
) async {
|
||||||
// Save community to local storage
|
// Save community to local storage
|
||||||
|
final connector = context.read<MeshCoreConnector>();
|
||||||
|
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||||
await _communityStore.addCommunity(community);
|
await _communityStore.addCommunity(community);
|
||||||
|
|
||||||
// Optionally add the community public channel to the device
|
// Optionally add the community public channel to the device
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import 'package:provider/provider.dart';
|
|||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../models/discovery_contact.dart';
|
import '../models/contact.dart';
|
||||||
import '../utils/contact_search.dart';
|
import '../utils/contact_search.dart';
|
||||||
import '../widgets/app_bar.dart';
|
import '../widgets/app_bar.dart';
|
||||||
import '../widgets/list_filter_widget.dart';
|
import '../widgets/list_filter_widget.dart';
|
||||||
@@ -129,7 +129,7 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showContactContextMenu(
|
Future<void> _showContactContextMenu(
|
||||||
DiscoveryContact contact,
|
Contact contact,
|
||||||
MeshCoreConnector connector,
|
MeshCoreConnector connector,
|
||||||
) async {
|
) async {
|
||||||
final action = await showModalBottomSheet<String>(
|
final action = await showModalBottomSheet<String>(
|
||||||
@@ -169,7 +169,8 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
|||||||
connector.importDiscoveredContact(contact);
|
connector.importDiscoveredContact(contact);
|
||||||
break;
|
break;
|
||||||
case 'copy_contact':
|
case 'copy_contact':
|
||||||
final hexString = pubKeyToHex(contact.rawPacket);
|
if (contact.rawPacket == null) return;
|
||||||
|
final hexString = pubKeyToHex(contact.rawPacket!);
|
||||||
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
|
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -207,7 +208,7 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFilters(
|
Widget _buildFilters(
|
||||||
List<DiscoveryContact> filteredAndSorted,
|
List<Contact> filteredAndSorted,
|
||||||
MeshCoreConnector connector,
|
MeshCoreConnector connector,
|
||||||
) {
|
) {
|
||||||
String hintText = "";
|
String hintText = "";
|
||||||
@@ -309,8 +310,8 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<DiscoveryContact> _filterAndSortContacts(
|
List<Contact> _filterAndSortContacts(
|
||||||
List<DiscoveryContact> contacts,
|
List<Contact> contacts,
|
||||||
MeshCoreConnector connector,
|
MeshCoreConnector connector,
|
||||||
) {
|
) {
|
||||||
var filtered = contacts.where((contact) {
|
var filtered = contacts.where((contact) {
|
||||||
@@ -350,7 +351,7 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
|||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _matchesTypeFilter(DiscoveryContact contact) {
|
bool _matchesTypeFilter(Contact contact) {
|
||||||
switch (typeFilter) {
|
switch (typeFilter) {
|
||||||
case ContactTypeFilter.all:
|
case ContactTypeFilter.all:
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
+120
-39
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
@@ -50,7 +51,8 @@ class MapScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MapScreenState extends State<MapScreen> {
|
class _MapScreenState extends State<MapScreen> {
|
||||||
static const double _labelZoomThreshold = 8.5;
|
// Zoom level at which node labels start to appear
|
||||||
|
static const double _labelZoomThreshold = 12.0;
|
||||||
|
|
||||||
final MapController _mapController = MapController();
|
final MapController _mapController = MapController();
|
||||||
final MapMarkerService _markerService = MapMarkerService();
|
final MapMarkerService _markerService = MapMarkerService();
|
||||||
@@ -91,6 +93,15 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _checkLocationPlausibility(double lat, double lon) {
|
||||||
|
const double epsilon = 1e-6;
|
||||||
|
return (lat.abs() > epsilon || lon.abs() > epsilon) &&
|
||||||
|
lat >= -90.0 &&
|
||||||
|
lat <= 90.0 &&
|
||||||
|
lon >= -180.0 &&
|
||||||
|
lon <= 180.0;
|
||||||
|
}
|
||||||
|
|
||||||
double _standardDeviation(List<double> values) {
|
double _standardDeviation(List<double> values) {
|
||||||
if (values.length <= 1) {
|
if (values.length <= 1) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
@@ -126,7 +137,15 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
builder: (context, connector, settingsService, pathHistory, child) {
|
builder: (context, connector, settingsService, pathHistory, child) {
|
||||||
final tileCache = context.read<MapTileCacheService>();
|
final tileCache = context.read<MapTileCacheService>();
|
||||||
final settings = settingsService.settings;
|
final settings = settingsService.settings;
|
||||||
final contacts = connector.contacts;
|
final allContacts = <Contact>[
|
||||||
|
...connector.contacts,
|
||||||
|
...connector.discoveredContacts.where((c) => !c.isActive),
|
||||||
|
];
|
||||||
|
|
||||||
|
final contacts = settings.mapShowDiscoveryContacts
|
||||||
|
? allContacts
|
||||||
|
: allContacts.where((c) => c.isActive).toList();
|
||||||
|
|
||||||
final highlightPosition = widget.highlightPosition;
|
final highlightPosition = widget.highlightPosition;
|
||||||
final sharedMarkers = settings.mapShowMarkers
|
final sharedMarkers = settings.mapShowMarkers
|
||||||
? _collectSharedMarkers(connector)
|
? _collectSharedMarkers(connector)
|
||||||
@@ -159,14 +178,21 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
: filteredByTime;
|
: filteredByTime;
|
||||||
|
|
||||||
// Filter by location
|
// Filter by location
|
||||||
final contactsWithLocation = filteredByKeyPrefix
|
final contactsWithLocation = filteredByKeyPrefix.where((c) {
|
||||||
.where((c) => c.hasLocation)
|
if (!c.hasLocation) {
|
||||||
.toList();
|
return false;
|
||||||
|
}
|
||||||
|
return _checkLocationPlausibility(c.latitude!, c.longitude!);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
// All contacts with a known location — used as anchors regardless of
|
// All contacts with a known location — used as anchors regardless of
|
||||||
// time/key-prefix filters so that repeaters are always available.
|
// time/key-prefix filters so that repeaters are always available.
|
||||||
final allContactsWithLocation = contacts
|
final allContactsWithLocation = allContacts
|
||||||
.where((c) => c.hasLocation)
|
.where(
|
||||||
|
(c) =>
|
||||||
|
c.hasLocation &&
|
||||||
|
_checkLocationPlausibility(c.latitude!, c.longitude!),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// Compute guessed locations with caching
|
// Compute guessed locations with caching
|
||||||
@@ -468,7 +494,10 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!_isBuildingPathTrace)
|
if (!_isBuildingPathTrace)
|
||||||
...guessedLocations.map(_buildGuessedMarker),
|
..._buildGuessedMarker(
|
||||||
|
guessedLocations,
|
||||||
|
showLabels: _showNodeLabels,
|
||||||
|
),
|
||||||
..._buildMarkers(
|
..._buildMarkers(
|
||||||
contactsWithLocation,
|
contactsWithLocation,
|
||||||
settings,
|
settings,
|
||||||
@@ -630,6 +659,13 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
anchors[0].latitude + offsetDeg * cos(angle),
|
anchors[0].latitude + offsetDeg * cos(angle),
|
||||||
anchors[0].longitude + offsetDeg * sin(angle),
|
anchors[0].longitude + offsetDeg * sin(angle),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!_checkLocationPlausibility(
|
||||||
|
position.latitude,
|
||||||
|
position.longitude,
|
||||||
|
)) {
|
||||||
|
continue; // discard implausible guesses near (0, 0)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
double lat = 0, lon = 0;
|
double lat = 0, lon = 0;
|
||||||
for (final a in anchors) {
|
for (final a in anchors) {
|
||||||
@@ -637,6 +673,12 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
lon += a.longitude;
|
lon += a.longitude;
|
||||||
}
|
}
|
||||||
position = LatLng(lat / anchors.length, lon / anchors.length);
|
position = LatLng(lat / anchors.length, lon / anchors.length);
|
||||||
|
if (!_checkLocationPlausibility(
|
||||||
|
position.latitude,
|
||||||
|
position.longitude,
|
||||||
|
)) {
|
||||||
|
continue; // discard implausible guesses near (0, 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result.add(
|
result.add(
|
||||||
_GuessedLocation(
|
_GuessedLocation(
|
||||||
@@ -710,40 +752,61 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Marker _buildGuessedMarker(_GuessedLocation guess) {
|
List<Marker> _buildGuessedMarker(
|
||||||
final color = _getNodeColor(guess.contact.type);
|
List<_GuessedLocation> guessed, {
|
||||||
return Marker(
|
required bool showLabels,
|
||||||
point: guess.position,
|
}) {
|
||||||
width: 35,
|
final markers = <Marker>[];
|
||||||
height: 35,
|
|
||||||
child: GestureDetector(
|
for (final guess in guessed) {
|
||||||
onTap: () => _showNodeInfo(
|
final color = _getNodeColor(guess.contact.type);
|
||||||
context,
|
final marker = Marker(
|
||||||
guess.contact,
|
point: guess.position,
|
||||||
guessedPosition: guess.position,
|
width: 35,
|
||||||
),
|
height: 35,
|
||||||
child: Container(
|
child: GestureDetector(
|
||||||
padding: const EdgeInsets.all(4),
|
onTap: () => _showNodeInfo(
|
||||||
decoration: BoxDecoration(
|
context,
|
||||||
color: color.withValues(alpha: guess.highConfidence ? 0.55 : 0.30),
|
guess.contact,
|
||||||
shape: BoxShape.circle,
|
guessedPosition: guess.position,
|
||||||
border: Border.all(color: Colors.white, width: 2),
|
),
|
||||||
boxShadow: [
|
child: Container(
|
||||||
BoxShadow(
|
padding: const EdgeInsets.all(4),
|
||||||
color: Colors.black.withValues(alpha: 0.3),
|
decoration: BoxDecoration(
|
||||||
blurRadius: 4,
|
color: color.withValues(
|
||||||
offset: const Offset(0, 2),
|
alpha: guess.highConfidence ? 0.55 : 0.30,
|
||||||
),
|
),
|
||||||
],
|
shape: BoxShape.circle,
|
||||||
),
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
child: const Icon(
|
boxShadow: [
|
||||||
Icons.not_listed_location,
|
BoxShadow(
|
||||||
color: Colors.white,
|
color: Colors.black.withValues(alpha: 0.3),
|
||||||
size: 20,
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.not_listed_location,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
|
||||||
|
markers.add(marker);
|
||||||
|
|
||||||
|
if (showLabels) {
|
||||||
|
markers.add(
|
||||||
|
_buildNodeLabelMarker(
|
||||||
|
point: guess.position,
|
||||||
|
label: guess.contact.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return markers;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Marker> _buildMarkers(
|
List<Marker> _buildMarkers(
|
||||||
@@ -1203,6 +1266,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
Contact contact, {
|
Contact contact, {
|
||||||
LatLng? guessedPosition,
|
LatLng? guessedPosition,
|
||||||
}) {
|
}) {
|
||||||
|
final connector = context.read<MeshCoreConnector>();
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (dialogContext) => AlertDialog(
|
builder: (dialogContext) => AlertDialog(
|
||||||
@@ -1248,6 +1312,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
advTypeChat) // Only show chat button for chat nodes
|
advTypeChat) // Only show chat button for chat nodes
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
if (!contact.isActive) {
|
||||||
|
connector.importDiscoveredContact(contact);
|
||||||
|
}
|
||||||
Navigator.pop(dialogContext);
|
Navigator.pop(dialogContext);
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
@@ -1261,6 +1328,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
if (contact.type == advTypeRepeater)
|
if (contact.type == advTypeRepeater)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
if (!contact.isActive) {
|
||||||
|
connector.importDiscoveredContact(contact);
|
||||||
|
}
|
||||||
Navigator.pop(dialogContext);
|
Navigator.pop(dialogContext);
|
||||||
_showRepeaterLogin(context, contact);
|
_showRepeaterLogin(context, contact);
|
||||||
},
|
},
|
||||||
@@ -1269,6 +1339,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
if (contact.type == advTypeRoom)
|
if (contact.type == advTypeRoom)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
if (!contact.isActive) {
|
||||||
|
connector.importDiscoveredContact(contact);
|
||||||
|
}
|
||||||
Navigator.pop(dialogContext);
|
Navigator.pop(dialogContext);
|
||||||
_showRoomLogin(context, contact);
|
_showRoomLogin(context, contact);
|
||||||
},
|
},
|
||||||
@@ -1745,6 +1818,14 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
},
|
},
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
title: Text(context.l10n.map_showDiscoveryContacts),
|
||||||
|
value: settings.mapShowDiscoveryContacts,
|
||||||
|
onChanged: (value) {
|
||||||
|
service.setMapShowDiscoveryContacts(value ?? true);
|
||||||
|
},
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
context.l10n.map_keyPrefix,
|
context.l10n.map_keyPrefix,
|
||||||
|
|||||||
@@ -124,12 +124,14 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
|||||||
|
|
||||||
void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) {
|
void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) {
|
||||||
final buffer = BufferReader(frame);
|
final buffer = BufferReader(frame);
|
||||||
|
final contacts = <Contact>[
|
||||||
|
...connector.contacts,
|
||||||
|
...connector.discoveredContacts,
|
||||||
|
];
|
||||||
try {
|
try {
|
||||||
final neighborCount = buffer.readUInt16LE();
|
final neighborCount = buffer.readUInt16LE();
|
||||||
final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE());
|
final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE());
|
||||||
connector.contacts.where((c) => c.type == advTypeRepeater).forEach((
|
contacts.where((c) => c.type == advTypeRepeater).forEach((repeater) {
|
||||||
repeater,
|
|
||||||
) {
|
|
||||||
for (var neighborData in parsedNeighbors) {
|
for (var neighborData in parsedNeighbors) {
|
||||||
final publicKey = neighborData['publicKey'];
|
final publicKey = neighborData['publicKey'];
|
||||||
if (listEquals(
|
if (listEquals(
|
||||||
|
|||||||
@@ -114,14 +114,37 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint8List addReturnPath(Uint8List pathBytes) {
|
Uint8List buildPath(Uint8List pathBytes) {
|
||||||
Uint8List? traceBytes;
|
Uint8List traceBytes;
|
||||||
final len = (pathBytes.length + pathBytes.length - 1);
|
|
||||||
traceBytes = Uint8List(len);
|
if (pathBytes.isEmpty) {
|
||||||
for (int i = 0; i < pathBytes.length; i++) {
|
traceBytes = Uint8List(1);
|
||||||
traceBytes[i] = pathBytes[i];
|
traceBytes[0] = widget.targetContact?.publicKey[0] ?? 0;
|
||||||
if (i < pathBytes.length - 1) {
|
return traceBytes;
|
||||||
traceBytes[len - 1 - i] = pathBytes[i];
|
}
|
||||||
|
|
||||||
|
if (widget.targetContact?.type == advTypeRepeater ||
|
||||||
|
widget.targetContact?.type == advTypeRoom) {
|
||||||
|
final len = (pathBytes.length + pathBytes.length + 1);
|
||||||
|
traceBytes = Uint8List(len);
|
||||||
|
traceBytes[pathBytes.length] = widget.targetContact?.publicKey[0] ?? 0;
|
||||||
|
for (int i = 0; i < pathBytes.length; i++) {
|
||||||
|
traceBytes[i] = pathBytes[i];
|
||||||
|
if (i < pathBytes.length) {
|
||||||
|
traceBytes[len - 1 - i] = pathBytes[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (pathBytes.length < 2) {
|
||||||
|
return pathBytes[0] == 0 ? Uint8List(0) : pathBytes;
|
||||||
|
}
|
||||||
|
final len = (pathBytes.length + pathBytes.length - 1);
|
||||||
|
traceBytes = Uint8List(len);
|
||||||
|
for (int i = 0; i < pathBytes.length; i++) {
|
||||||
|
traceBytes[i] = pathBytes[i];
|
||||||
|
if (i < pathBytes.length - 1) {
|
||||||
|
traceBytes[len - 1 - i] = pathBytes[i];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return traceBytes;
|
return traceBytes;
|
||||||
@@ -142,11 +165,16 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
: widget.path;
|
: widget.path;
|
||||||
|
|
||||||
if (widget.flipPathRound) {
|
if (widget.flipPathRound) {
|
||||||
path = addReturnPath(pathTmp);
|
path = buildPath(pathTmp);
|
||||||
} else {
|
} else {
|
||||||
path = pathTmp;
|
path = pathTmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appLogger.info(
|
||||||
|
'Initiating path trace with path: ${_formatPathPrefixes(path)}',
|
||||||
|
tag: 'PathTraceMapScreen',
|
||||||
|
);
|
||||||
|
|
||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
final frame = buildTraceReq(
|
final frame = buildTraceReq(
|
||||||
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
@@ -235,10 +263,11 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
Map<int, Contact> pathContacts = {};
|
Map<int, Contact> pathContacts = {};
|
||||||
|
final contacts = <Contact>[
|
||||||
connector.contacts.where((c) => c.type != advTypeChat).forEach((
|
...connector.contacts,
|
||||||
repeater,
|
...connector.discoveredContacts,
|
||||||
) {
|
];
|
||||||
|
contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
|
||||||
for (var repeaterData in pathData) {
|
for (var repeaterData in pathData) {
|
||||||
if (listEquals(
|
if (listEquals(
|
||||||
repeater.publicKey.sublist(0, 1),
|
repeater.publicKey.sublist(0, 1),
|
||||||
|
|||||||
@@ -134,6 +134,10 @@ class AppSettingsService extends ChangeNotifier {
|
|||||||
appLogger.setEnabled(value);
|
appLogger.setEnabled(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setMapShowDiscoveryContacts(bool value) async {
|
||||||
|
await updateSettings(_settings.copyWith(mapShowDiscoveryContacts: value));
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setBatteryChemistryForDevice(
|
Future<void> setBatteryChemistryForDevice(
|
||||||
String deviceId,
|
String deviceId,
|
||||||
String chemistry,
|
String chemistry,
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class ChannelMessageStore {
|
|||||||
final key = '$keyFor$channelIndex';
|
final key = '$keyFor$channelIndex';
|
||||||
final oldKey = '$_keyPrefix$channelIndex';
|
final oldKey = '$_keyPrefix$channelIndex';
|
||||||
|
|
||||||
String? jsonString = prefs.getString(oldKey);
|
String? jsonString = prefs.getString(key);
|
||||||
if (jsonString == null || jsonString.isEmpty) {
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
// Attempt migration from legacy unscoped key on first load
|
// Attempt migration from legacy unscoped key on first load
|
||||||
final legacyJsonString = prefs.getString(oldKey);
|
final legacyJsonString = prefs.getString(oldKey);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class ChannelOrderStore {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
String? jsonString = prefs.getString(_keyPrefix);
|
String? jsonString = prefs.getString(keyFor);
|
||||||
if (jsonString == null || jsonString.isEmpty) {
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
// Attempt migration from legacy unscoped key on first load
|
// Attempt migration from legacy unscoped key on first load
|
||||||
final legacyJsonString = prefs.getString(_keyPrefix);
|
final legacyJsonString = prefs.getString(_keyPrefix);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class ChannelSettingsStore {
|
|||||||
await prefs.setBool(key, enabled);
|
await prefs.setBool(key, enabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return prefs.getBool(key) ?? false;
|
return enabled ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveSmazEnabled(int channelIndex, bool enabled) async {
|
Future<void> saveSmazEnabled(int channelIndex, bool enabled) async {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class ChannelStore {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
String? jsonString = prefs.getString(_keyPrefix);
|
String? jsonString = prefs.getString(keyFor);
|
||||||
if (jsonString == null || jsonString.isEmpty) {
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
// Attempt migration from legacy unscoped key on first load
|
// Attempt migration from legacy unscoped key on first load
|
||||||
final legacyJsonString = prefs.getString(_keyPrefix);
|
final legacyJsonString = prefs.getString(_keyPrefix);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class CommunityStore {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
String? jsonString = prefs.getString(_keyPrefix);
|
String? jsonString = prefs.getString(keyFor);
|
||||||
if (jsonString == null || jsonString.isEmpty) {
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
// Attempt migration from legacy unscoped key on first load
|
// Attempt migration from legacy unscoped key on first load
|
||||||
final legacyJsonString = prefs.getString(_keyPrefix);
|
final legacyJsonString = prefs.getString(_keyPrefix);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import '../models/discovery_contact.dart';
|
import '../models/contact.dart';
|
||||||
import 'prefs_manager.dart';
|
import 'prefs_manager.dart';
|
||||||
|
|
||||||
class ContactDiscoveryStore {
|
class ContactDiscoveryStore {
|
||||||
static const String _keyPrefix = 'discovered_contacts';
|
static const String _keyPrefix = 'discovered_contacts';
|
||||||
|
|
||||||
Future<List<DiscoveryContact>> loadContacts() async {
|
Future<List<Contact>> loadContacts() async {
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final jsonStr = prefs.getString(_keyPrefix);
|
final jsonStr = prefs.getString(_keyPrefix);
|
||||||
if (jsonStr == null) return [];
|
if (jsonStr == null) return [];
|
||||||
@@ -22,40 +22,62 @@ class ContactDiscoveryStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveContacts(List<DiscoveryContact> contacts) async {
|
Future<void> saveContacts(List<Contact> contacts) async {
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final jsonList = contacts.map(_toJson).toList();
|
final jsonList = contacts.map(_toJson).toList();
|
||||||
await prefs.setString(_keyPrefix, jsonEncode(jsonList));
|
await prefs.setString(_keyPrefix, jsonEncode(jsonList));
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _toJson(DiscoveryContact contact) {
|
Map<String, dynamic> _toJson(Contact contact) {
|
||||||
return {
|
return {
|
||||||
'rawPacket': base64Encode(contact.rawPacket),
|
|
||||||
'publicKey': base64Encode(contact.publicKey),
|
'publicKey': base64Encode(contact.publicKey),
|
||||||
'name': contact.name,
|
'name': contact.name,
|
||||||
'type': contact.type,
|
'type': contact.type,
|
||||||
|
'flags': contact.flags,
|
||||||
'pathLength': contact.pathLength,
|
'pathLength': contact.pathLength,
|
||||||
'path': base64Encode(contact.path),
|
'path': base64Encode(contact.path),
|
||||||
|
'pathOverride': contact.pathOverride,
|
||||||
|
'pathOverrideBytes': contact.pathOverrideBytes != null
|
||||||
|
? base64Encode(contact.pathOverrideBytes!)
|
||||||
|
: null,
|
||||||
'latitude': contact.latitude,
|
'latitude': contact.latitude,
|
||||||
'longitude': contact.longitude,
|
'longitude': contact.longitude,
|
||||||
'lastSeen': contact.lastSeen.millisecondsSinceEpoch,
|
'lastSeen': contact.lastSeen.millisecondsSinceEpoch,
|
||||||
|
'lastMessageAt': contact.lastMessageAt.millisecondsSinceEpoch,
|
||||||
|
'rawPacket': contact.rawPacket != null
|
||||||
|
? base64Encode(contact.rawPacket!)
|
||||||
|
: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
DiscoveryContact _fromJson(Map<String, dynamic> json) {
|
Contact _fromJson(Map<String, dynamic> json) {
|
||||||
final lastSeenMs = json['lastSeen'] as int? ?? 0;
|
final lastSeenMs = json['lastSeen'] as int? ?? 0;
|
||||||
return DiscoveryContact(
|
final lastMessageMs = json['lastMessageAt'] as int?;
|
||||||
rawPacket: Uint8List.fromList(base64Decode(json['rawPacket'] as String)),
|
return Contact(
|
||||||
publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)),
|
publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)),
|
||||||
name: json['name'] as String? ?? 'Unknown',
|
name: json['name'] as String? ?? 'Unknown',
|
||||||
type: json['type'] as int? ?? 0,
|
type: json['type'] as int? ?? 0,
|
||||||
|
flags: json['flags'] as int? ?? 0,
|
||||||
pathLength: json['pathLength'] as int? ?? -1,
|
pathLength: json['pathLength'] as int? ?? -1,
|
||||||
path: json['path'] != null
|
path: json['path'] != null
|
||||||
? Uint8List.fromList(base64Decode(json['path'] as String))
|
? Uint8List.fromList(base64Decode(json['path'] as String))
|
||||||
: Uint8List(0),
|
: Uint8List(0),
|
||||||
|
pathOverride: json['pathOverride'] as int?,
|
||||||
|
pathOverrideBytes: json['pathOverrideBytes'] != null
|
||||||
|
? Uint8List.fromList(
|
||||||
|
base64Decode(json['pathOverrideBytes'] as String),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
latitude: (json['latitude'] as num?)?.toDouble(),
|
latitude: (json['latitude'] as num?)?.toDouble(),
|
||||||
longitude: (json['longitude'] as num?)?.toDouble(),
|
longitude: (json['longitude'] as num?)?.toDouble(),
|
||||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs),
|
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs),
|
||||||
|
lastMessageAt: DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
lastMessageMs ?? lastSeenMs,
|
||||||
|
),
|
||||||
|
isActive: false,
|
||||||
|
rawPacket: json['rawPacket'] != null
|
||||||
|
? Uint8List.fromList(base64Decode(json['rawPacket'] as String))
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class ContactGroupStore {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
String? jsonString = prefs.getString(_keyPrefix);
|
String? jsonString = prefs.getString(keyFor);
|
||||||
if (jsonString == null || jsonString.isEmpty) {
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
// Attempt migration from legacy unscoped key on first load
|
// Attempt migration from legacy unscoped key on first load
|
||||||
final legacyJsonString = prefs.getString(_keyPrefix);
|
final legacyJsonString = prefs.getString(_keyPrefix);
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ class ContactStore {
|
|||||||
'longitude': contact.longitude,
|
'longitude': contact.longitude,
|
||||||
'lastSeen': contact.lastSeen.millisecondsSinceEpoch,
|
'lastSeen': contact.lastSeen.millisecondsSinceEpoch,
|
||||||
'lastMessageAt': contact.lastMessageAt.millisecondsSinceEpoch,
|
'lastMessageAt': contact.lastMessageAt.millisecondsSinceEpoch,
|
||||||
|
'isActive': contact.isActive,
|
||||||
|
'rawPacket': contact.rawPacket != null
|
||||||
|
? base64Encode(contact.rawPacket!)
|
||||||
|
: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +107,10 @@ class ContactStore {
|
|||||||
lastMessageAt: DateTime.fromMillisecondsSinceEpoch(
|
lastMessageAt: DateTime.fromMillisecondsSinceEpoch(
|
||||||
lastMessageMs ?? lastSeenMs,
|
lastMessageMs ?? lastSeenMs,
|
||||||
),
|
),
|
||||||
|
isActive: json['isActive'] as bool? ?? true,
|
||||||
|
rawPacket: json['rawPacket'] != null
|
||||||
|
? Uint8List.fromList(base64Decode(json['rawPacket'] as String))
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class UnreadStore {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
String? jsonString = prefs.getString(_keyPrefix);
|
String? jsonString = prefs.getString(keyFor);
|
||||||
if (jsonString == null || jsonString.isEmpty) {
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
// Attempt migration from legacy unscoped key on first load
|
// Attempt migration from legacy unscoped key on first load
|
||||||
final legacyJsonString = prefs.getString(_keyPrefix);
|
final legacyJsonString = prefs.getString(_keyPrefix);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'package:meshcore_open/models/discovery_contact.dart';
|
|
||||||
|
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
|
|
||||||
bool matchesContactQuery(Contact contact, String query) {
|
bool matchesContactQuery(Contact contact, String query) {
|
||||||
@@ -16,7 +14,7 @@ bool matchesContactQuery(Contact contact, String query) {
|
|||||||
return contact.publicKeyHex.toLowerCase().startsWith(hexPrefix);
|
return contact.publicKeyHex.toLowerCase().startsWith(hexPrefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool matchesDiscoveryContactQuery(DiscoveryContact contact, String query) {
|
bool matchesDiscoveryContactQuery(Contact contact, String query) {
|
||||||
final normalizedQuery = query.trim().toLowerCase();
|
final normalizedQuery = query.trim().toLowerCase();
|
||||||
if (normalizedQuery.isEmpty) return true;
|
if (normalizedQuery.isEmpty) return true;
|
||||||
|
|
||||||
|
|||||||
@@ -157,8 +157,11 @@ class _SNRIndicatorState extends State<SNRIndicator> {
|
|||||||
repeater.snr,
|
repeater.snr,
|
||||||
widget.connector.currentSf,
|
widget.connector.currentSf,
|
||||||
);
|
);
|
||||||
|
final allContacts = [
|
||||||
final name = widget.connector.contacts
|
...widget.connector.contacts,
|
||||||
|
...widget.connector.discoveredContacts,
|
||||||
|
];
|
||||||
|
final name = allContacts
|
||||||
.where((c) => c.publicKey.first == repeater.pubkeyFirstByte)
|
.where((c) => c.publicKey.first == repeater.pubkeyFirstByte)
|
||||||
.map((c) => c.name)
|
.map((c) => c.name)
|
||||||
.firstOrNull;
|
.firstOrNull;
|
||||||
|
|||||||
Reference in New Issue
Block a user