mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-16 23:54:28 +10:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 566e3aadf8 | |||
| 86e9b7fe01 |
@@ -1 +0,0 @@
|
|||||||
6.2.4
|
|
||||||
@@ -199,9 +199,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
int _queueSyncRetries = 0;
|
int _queueSyncRetries = 0;
|
||||||
static const int _maxQueueSyncRetries = 3;
|
static const int _maxQueueSyncRetries = 3;
|
||||||
static const int _queueSyncTimeoutMs = 5000; // 5 second timeout
|
static const int _queueSyncTimeoutMs = 5000; // 5 second timeout
|
||||||
// Serializes path operations (setContactPath/clearContactPath) to prevent
|
|
||||||
// interleaved async calls from leaving in-memory state inconsistent with device.
|
|
||||||
Future<void> _pathOpLock = Future.value();
|
|
||||||
Map<String, String>? _currentCustomVars;
|
Map<String, String>? _currentCustomVars;
|
||||||
|
|
||||||
// Channel syncing state (sequential pattern)
|
// Channel syncing state (sequential pattern)
|
||||||
@@ -289,10 +286,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Contact> get allContacts => List.unmodifiable([
|
|
||||||
..._contacts,
|
|
||||||
..._discoveredContacts.where((c) => !c.isActive),
|
|
||||||
]);
|
|
||||||
List<Contact> get discoveredContacts {
|
List<Contact> get discoveredContacts {
|
||||||
return List.unmodifiable(_discoveredContacts);
|
return List.unmodifiable(_discoveredContacts);
|
||||||
}
|
}
|
||||||
@@ -565,10 +558,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_unreadStore.saveContactUnreadCount(
|
_unreadStore.saveContactUnreadCount(
|
||||||
Map<String, int>.from(_contactUnreadCount),
|
Map<String, int>.from(_contactUnreadCount),
|
||||||
);
|
);
|
||||||
_notificationService.clearContactNotification(
|
|
||||||
contactKeyHex,
|
|
||||||
getTotalUnreadCount(),
|
|
||||||
);
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -587,10 +576,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_channels.isNotEmpty ? _channels : _cachedChannels,
|
_channels.isNotEmpty ? _channels : _cachedChannels,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
_notificationService.clearChannelNotification(
|
|
||||||
channelIndex,
|
|
||||||
getTotalUnreadCount(),
|
|
||||||
);
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -708,9 +693,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_knownContactKeys
|
_knownContactKeys
|
||||||
..clear()
|
..clear()
|
||||||
..addAll(cached.map((c) => c.publicKeyHex));
|
..addAll(cached.map((c) => c.publicKeyHex));
|
||||||
_contacts
|
|
||||||
..clear()
|
|
||||||
..addAll(cached);
|
|
||||||
for (final contact in cached) {
|
for (final contact in cached) {
|
||||||
_ensureContactSmazSettingLoaded(contact.publicKeyHex);
|
_ensureContactSmazSettingLoaded(contact.publicKeyHex);
|
||||||
}
|
}
|
||||||
@@ -1543,10 +1525,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
if (_activeTransport == MeshCoreTransportType.usb) {
|
if (_activeTransport == MeshCoreTransportType.usb) {
|
||||||
await _usbManager.write(data);
|
await _usbManager.write(data);
|
||||||
// Brief pause so the device firmware can process each frame before the
|
|
||||||
// next arrives. Without this, rapid-fire frames over USB can cause the
|
|
||||||
// device to miss responses (especially on reconnect).
|
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
|
||||||
} else if (_activeTransport == MeshCoreTransportType.tcp) {
|
} else if (_activeTransport == MeshCoreTransportType.tcp) {
|
||||||
await _tcpConnector.write(data);
|
await _tcpConnector.write(data);
|
||||||
} else {
|
} else {
|
||||||
@@ -1762,33 +1740,18 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
Uint8List customPath,
|
Uint8List customPath,
|
||||||
int pathLen,
|
int pathLen,
|
||||||
) async {
|
) async {
|
||||||
// Serialize path operations to prevent interleaved async calls from
|
if (!isConnected) return;
|
||||||
// leaving in-memory state inconsistent with the device.
|
|
||||||
final prev = _pathOpLock;
|
|
||||||
final completer = Completer<void>();
|
|
||||||
_pathOpLock = completer.future;
|
|
||||||
await prev;
|
|
||||||
try {
|
|
||||||
if (!isConnected) return;
|
|
||||||
|
|
||||||
await sendFrame(
|
await sendFrame(
|
||||||
buildUpdateContactPathFrame(
|
buildUpdateContactPathFrame(
|
||||||
contact.publicKey,
|
contact.publicKey,
|
||||||
customPath,
|
customPath,
|
||||||
pathLen,
|
pathLen,
|
||||||
type: contact.type,
|
type: contact.type,
|
||||||
flags: contact.flags,
|
flags: contact.flags,
|
||||||
name: contact.name,
|
name: contact.name,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// USB writes return instantly (no BLE flow control), so give the firmware
|
|
||||||
// time to persist the path change before subsequent commands.
|
|
||||||
if (_activeTransport == MeshCoreTransportType.usb) {
|
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
completer.complete();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setContactFavorite(Contact contact, bool isFavorite) async {
|
Future<void> setContactFavorite(Contact contact, bool isFavorite) async {
|
||||||
@@ -2173,34 +2136,25 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clearContactPath(Contact contact) async {
|
Future<void> clearContactPath(Contact contact) async {
|
||||||
// Serialize path operations to prevent interleaved async calls.
|
if (!isConnected) return;
|
||||||
final prev = _pathOpLock;
|
|
||||||
final completer = Completer<void>();
|
|
||||||
_pathOpLock = completer.future;
|
|
||||||
await prev;
|
|
||||||
try {
|
|
||||||
if (!isConnected) return;
|
|
||||||
|
|
||||||
await sendFrame(buildResetPathFrame(contact.publicKey));
|
await sendFrame(buildResetPathFrame(contact.publicKey));
|
||||||
if (_activeTransport == MeshCoreTransportType.usb) {
|
final existingIndex = _contacts.indexWhere(
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
(c) => c.publicKeyHex == contact.publicKeyHex,
|
||||||
}
|
);
|
||||||
final existingIndex = _contacts.indexWhere(
|
if (existingIndex >= 0) {
|
||||||
(c) => c.publicKeyHex == contact.publicKeyHex,
|
final existing = _contacts[existingIndex];
|
||||||
|
// Use copyWith to preserve pathOverride and pathOverrideBytes
|
||||||
|
_contacts[existingIndex] = existing.copyWith(
|
||||||
|
pathOverride: null,
|
||||||
|
pathOverrideBytes: null,
|
||||||
|
pathLength: -1,
|
||||||
|
path: Uint8List(0),
|
||||||
);
|
);
|
||||||
if (existingIndex >= 0) {
|
notifyListeners();
|
||||||
final existing = _contacts[existingIndex];
|
unawaited(_persistContacts());
|
||||||
// Preserve pathOverride and pathOverrideBytes — only reset device path
|
|
||||||
_contacts[existingIndex] = existing.copyWith(
|
|
||||||
pathLength: -1,
|
|
||||||
path: Uint8List(0),
|
|
||||||
);
|
|
||||||
notifyListeners();
|
|
||||||
unawaited(_persistContacts());
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
completer.complete();
|
|
||||||
}
|
}
|
||||||
|
// The device will send updated contact info with path_len = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateContactInMemory(
|
void updateContactInMemory(
|
||||||
@@ -2536,9 +2490,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_isLoadingContacts = true;
|
_isLoadingContacts = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
break;
|
break;
|
||||||
case pushCodeAdvert:
|
|
||||||
// Known contact was seen again - just a pub key, no action needed
|
|
||||||
break;
|
|
||||||
case pushCodeNewAdvert:
|
case pushCodeNewAdvert:
|
||||||
debugPrint('Got New CONTACT');
|
debugPrint('Got New CONTACT');
|
||||||
// It's the same format as respCodeContact, so we can reuse the handler
|
// It's the same format as respCodeContact, so we can reuse the handler
|
||||||
@@ -2920,8 +2871,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
void _handleContact(Uint8List frame, {bool isContact = true}) {
|
void _handleContact(Uint8List frame, {bool isContact = true}) {
|
||||||
final contact = Contact.fromFrame(frame);
|
final contact = Contact.fromFrame(frame);
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
_handleDiscovery(contact, frame, noNotify: true, addActive: true);
|
|
||||||
|
|
||||||
if (contact.type == advTypeRepeater) {
|
if (contact.type == advTypeRepeater) {
|
||||||
_contactUnreadCount.remove(contact.publicKeyHex);
|
_contactUnreadCount.remove(contact.publicKeyHex);
|
||||||
_unreadStore.saveContactUnreadCount(
|
_unreadStore.saveContactUnreadCount(
|
||||||
@@ -4730,12 +4679,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
(_autoAddRoomServers && type == advTypeRoom) ||
|
(_autoAddRoomServers && type == advTypeRoom) ||
|
||||||
(_autoAddSensors && type == advTypeSensor)) {
|
(_autoAddSensors && type == advTypeSensor)) {
|
||||||
_handleContactAdvert(newContact);
|
_handleContactAdvert(newContact);
|
||||||
_handleDiscovery(
|
|
||||||
newContact,
|
|
||||||
rawPacket,
|
|
||||||
noNotify: true,
|
|
||||||
addActive: true,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
_handleDiscovery(newContact, rawPacket);
|
_handleDiscovery(newContact, rawPacket);
|
||||||
}
|
}
|
||||||
@@ -4760,20 +4703,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
// CRITICAL: Preserve user's path override when contact is refreshed from device
|
// CRITICAL: Preserve user's path override when contact is refreshed from device
|
||||||
_contacts[existingIndex] = existing.copyWith(
|
_contacts[existingIndex] = existing.copyWith(
|
||||||
latitude:
|
latitude: hasLocation ? latitude : existing.latitude,
|
||||||
hasLocation &&
|
longitude: hasLocation ? longitude : existing.longitude,
|
||||||
latitude != null &&
|
|
||||||
latitude.abs() <= 90 &&
|
|
||||||
(latitude != 0 || longitude != 0)
|
|
||||||
? latitude
|
|
||||||
: existing.latitude,
|
|
||||||
longitude:
|
|
||||||
hasLocation &&
|
|
||||||
longitude != null &&
|
|
||||||
longitude.abs() <= 180 &&
|
|
||||||
(latitude != 0 || longitude != 0)
|
|
||||||
? longitude
|
|
||||||
: existing.longitude,
|
|
||||||
name: hasName ? name : existing.name,
|
name: hasName ? name : existing.name,
|
||||||
path: Uint8List.fromList(path.reversed.toList()),
|
path: Uint8List.fromList(path.reversed.toList()),
|
||||||
pathLength: path.length,
|
pathLength: path.length,
|
||||||
@@ -4844,11 +4775,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
reader.skipBytes(1); // Skip the response code byte
|
reader.skipBytes(1); // Skip the response code byte
|
||||||
final flags = reader.readByte();
|
final flags = reader.readByte();
|
||||||
_autoAddUsers = (flags & autoAddChatFlag) != 0;
|
_autoAddUsers = flags & autoAddChatFlag != 0;
|
||||||
_autoAddRepeaters = (flags & autoAddRepeaterFlag) != 0;
|
_autoAddRepeaters = flags & autoAddRepeaterFlag != 0;
|
||||||
_autoAddRoomServers = (flags & autoAddRoomServerFlag) != 0;
|
_autoAddRoomServers = flags & autoAddRoomServerFlag != 0;
|
||||||
_autoAddSensors = (flags & autoAddSensorFlag) != 0;
|
_autoAddSensors = flags & autoAddSensorFlag != 0;
|
||||||
_overwriteOldest = (flags & autoAddOverwriteOldestFlag) != 0;
|
_overwriteOldest = flags & autoAddOverwriteOldestFlag != 0;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appLogger.error('Failed to parse auto-add config: $e', tag: 'Connector');
|
appLogger.error('Failed to parse auto-add config: $e', tag: 'Connector');
|
||||||
}
|
}
|
||||||
@@ -4858,7 +4789,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
Contact contact,
|
Contact contact,
|
||||||
Uint8List rawPacket, {
|
Uint8List rawPacket, {
|
||||||
bool noNotify = false,
|
bool noNotify = false,
|
||||||
bool addActive = false,
|
|
||||||
}) {
|
}) {
|
||||||
appLogger.info('Discovered new contact: ${contact.name}', tag: 'Connector');
|
appLogger.info('Discovered new contact: ${contact.name}', tag: 'Connector');
|
||||||
|
|
||||||
@@ -4879,7 +4809,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
longitude: contact.longitude,
|
longitude: contact.longitude,
|
||||||
lastSeen: contact.lastSeen,
|
lastSeen: contact.lastSeen,
|
||||||
flags: 0,
|
flags: 0,
|
||||||
isActive: addActive,
|
isActive: false,
|
||||||
);
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
unawaited(_persistDiscoveredContacts());
|
unawaited(_persistDiscoveredContacts());
|
||||||
@@ -4897,7 +4827,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
longitude: contact.longitude,
|
longitude: contact.longitude,
|
||||||
lastSeen: contact.lastSeen,
|
lastSeen: contact.lastSeen,
|
||||||
lastMessageAt: contact.lastMessageAt,
|
lastMessageAt: contact.lastMessageAt,
|
||||||
isActive: addActive,
|
isActive: false,
|
||||||
flags: 0,
|
flags: 0,
|
||||||
);
|
);
|
||||||
_discoveredContacts.add(disContact);
|
_discoveredContacts.add(disContact);
|
||||||
|
|||||||
@@ -64,8 +64,6 @@ class MeshCoreUsbManager {
|
|||||||
|
|
||||||
Future<void> write(Uint8List data) => _service.write(data);
|
Future<void> write(Uint8List data) => _service.write(data);
|
||||||
|
|
||||||
Future<void> writeRaw(Uint8List data) => _service.writeRaw(data);
|
|
||||||
|
|
||||||
// --- Label management ---
|
// --- Label management ---
|
||||||
void updateConnectedLabel(String selfName) {
|
void updateConnectedLabel(String selfName) {
|
||||||
_service.updateConnectedLabel(selfName);
|
_service.updateConnectedLabel(selfName);
|
||||||
|
|||||||
+2
-2
@@ -285,6 +285,7 @@
|
|||||||
"contacts_newGroup": "Нова група",
|
"contacts_newGroup": "Нова група",
|
||||||
"contacts_groupName": "Група",
|
"contacts_groupName": "Група",
|
||||||
"contacts_groupNameRequired": "Името на групата е задължително.",
|
"contacts_groupNameRequired": "Името на групата е задължително.",
|
||||||
|
"contacts_groupNameReserved": "Това име на група е запазено",
|
||||||
"contacts_groupAlreadyExists": "Групата \"{name}\" вече съществува.",
|
"contacts_groupAlreadyExists": "Групата \"{name}\" вече съществува.",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1887,6 +1888,5 @@
|
|||||||
"tcpErrorUnsupported": "Транспортът чрез TCP не се поддържа на тази платформа.",
|
"tcpErrorUnsupported": "Транспортът чрез TCP не се поддържа на тази платформа.",
|
||||||
"tcpErrorTimedOut": "Връзката TCP изтекла.",
|
"tcpErrorTimedOut": "Връзката TCP изтекла.",
|
||||||
"tcpConnectionFailed": "Неуспешно е установено TCP връзката: {error}",
|
"tcpConnectionFailed": "Неуспешно е установено TCP връзката: {error}",
|
||||||
"map_showDiscoveryContacts": "Покажи контакти за откриване",
|
"map_showDiscoveryContacts": "Покажи контакти за откриване"
|
||||||
"map_setAsMyLocation": "Задайте като моя местоположение"
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -285,6 +285,7 @@
|
|||||||
"contacts_newGroup": "Neue Gruppe",
|
"contacts_newGroup": "Neue Gruppe",
|
||||||
"contacts_groupName": "Gruppenname",
|
"contacts_groupName": "Gruppenname",
|
||||||
"contacts_groupNameRequired": "Der Gruppennamen ist erforderlich.",
|
"contacts_groupNameRequired": "Der Gruppennamen ist erforderlich.",
|
||||||
|
"contacts_groupNameReserved": "Dieser Gruppenname ist reserviert",
|
||||||
"contacts_groupAlreadyExists": "Die Gruppe \"{name}\" existiert bereits.",
|
"contacts_groupAlreadyExists": "Die Gruppe \"{name}\" existiert bereits.",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1915,6 +1916,5 @@
|
|||||||
"tcpErrorUnsupported": "Die TCP-Übertragung wird auf dieser Plattform nicht unterstützt.",
|
"tcpErrorUnsupported": "Die TCP-Übertragung wird auf dieser Plattform nicht unterstützt.",
|
||||||
"tcpErrorTimedOut": "Die TCP-Verbindung ist abgelaufen.",
|
"tcpErrorTimedOut": "Die TCP-Verbindung ist abgelaufen.",
|
||||||
"tcpConnectionFailed": "Fehler beim TCP-Verbindungsaufbau: {error}",
|
"tcpConnectionFailed": "Fehler beim TCP-Verbindungsaufbau: {error}",
|
||||||
"map_showDiscoveryContacts": "Entdeckungs-Kontakte anzeigen",
|
"map_showDiscoveryContacts": "Entdeckungs-Kontakte anzeigen"
|
||||||
"map_setAsMyLocation": "Als meine aktuelle Position festlegen"
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -416,6 +416,7 @@
|
|||||||
"contacts_newGroup": "New Group",
|
"contacts_newGroup": "New Group",
|
||||||
"contacts_groupName": "Group name",
|
"contacts_groupName": "Group name",
|
||||||
"contacts_groupNameRequired": "Group name is required",
|
"contacts_groupNameRequired": "Group name is required",
|
||||||
|
"contacts_groupNameReserved": "This group name is reserved",
|
||||||
"contacts_groupAlreadyExists": "Group \"{name}\" already exists",
|
"contacts_groupAlreadyExists": "Group \"{name}\" already exists",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -807,7 +808,6 @@
|
|||||||
"map_source": "Source",
|
"map_source": "Source",
|
||||||
"map_flags": "Flags",
|
"map_flags": "Flags",
|
||||||
"map_shareMarkerHere": "Share marker here",
|
"map_shareMarkerHere": "Share marker here",
|
||||||
"map_setAsMyLocation": "Set as my location",
|
|
||||||
"map_pinLabel": "Pin label",
|
"map_pinLabel": "Pin label",
|
||||||
"map_label": "Label",
|
"map_label": "Label",
|
||||||
"map_pointOfInterest": "Point of interest",
|
"map_pointOfInterest": "Point of interest",
|
||||||
|
|||||||
+2
-2
@@ -285,6 +285,7 @@
|
|||||||
"contacts_newGroup": "Nuevo Grupo",
|
"contacts_newGroup": "Nuevo Grupo",
|
||||||
"contacts_groupName": "Nombre del grupo",
|
"contacts_groupName": "Nombre del grupo",
|
||||||
"contacts_groupNameRequired": "El nombre del grupo es obligatorio",
|
"contacts_groupNameRequired": "El nombre del grupo es obligatorio",
|
||||||
|
"contacts_groupNameReserved": "Este nombre de grupo está reservado",
|
||||||
"contacts_groupAlreadyExists": "El grupo \"{name}\" ya existe",
|
"contacts_groupAlreadyExists": "El grupo \"{name}\" ya existe",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1915,6 +1916,5 @@
|
|||||||
"tcpErrorUnsupported": "El protocolo de transporte TCP no está soportado en esta plataforma.",
|
"tcpErrorUnsupported": "El protocolo de transporte TCP no está soportado en esta plataforma.",
|
||||||
"tcpErrorTimedOut": "La conexión TCP ha caducado.",
|
"tcpErrorTimedOut": "La conexión TCP ha caducado.",
|
||||||
"tcpConnectionFailed": "Error en la conexión TCP: {error}",
|
"tcpConnectionFailed": "Error en la conexión TCP: {error}",
|
||||||
"map_showDiscoveryContacts": "Mostrar Contactos de Descubrimiento",
|
"map_showDiscoveryContacts": "Mostrar Contactos de Descubrimiento"
|
||||||
"map_setAsMyLocation": "Establecer mi ubicación"
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -285,6 +285,7 @@
|
|||||||
"contacts_newGroup": "Nouveau Groupe",
|
"contacts_newGroup": "Nouveau Groupe",
|
||||||
"contacts_groupName": "Nom du groupe",
|
"contacts_groupName": "Nom du groupe",
|
||||||
"contacts_groupNameRequired": "Le nom du groupe est obligatoire.",
|
"contacts_groupNameRequired": "Le nom du groupe est obligatoire.",
|
||||||
|
"contacts_groupNameReserved": "Ce nom de groupe est réservé",
|
||||||
"contacts_groupAlreadyExists": "Le groupe \"{name}\" existe déjà.",
|
"contacts_groupAlreadyExists": "Le groupe \"{name}\" existe déjà.",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1887,6 +1888,5 @@
|
|||||||
"tcpErrorUnsupported": "Le protocole TCP n'est pas pris en charge sur cette plateforme.",
|
"tcpErrorUnsupported": "Le protocole TCP n'est pas pris en charge sur cette plateforme.",
|
||||||
"tcpErrorTimedOut": "La connexion TCP a expiré.",
|
"tcpErrorTimedOut": "La connexion TCP a expiré.",
|
||||||
"tcpConnectionFailed": "Échec de la connexion TCP : {error}",
|
"tcpConnectionFailed": "Échec de la connexion TCP : {error}",
|
||||||
"map_showDiscoveryContacts": "Afficher les contacts de découverte",
|
"map_showDiscoveryContacts": "Afficher les contacts de découverte"
|
||||||
"map_setAsMyLocation": "Définir comme ma localisation"
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -285,6 +285,7 @@
|
|||||||
"contacts_newGroup": "Nuovo Gruppo",
|
"contacts_newGroup": "Nuovo Gruppo",
|
||||||
"contacts_groupName": "Nome gruppo",
|
"contacts_groupName": "Nome gruppo",
|
||||||
"contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.",
|
"contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.",
|
||||||
|
"contacts_groupNameReserved": "Questo nome del gruppo è riservato",
|
||||||
"contacts_groupAlreadyExists": "Il gruppo \"{name}\" esiste già.",
|
"contacts_groupAlreadyExists": "Il gruppo \"{name}\" esiste già.",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1887,6 +1888,5 @@
|
|||||||
"tcpErrorUnsupported": "Il protocollo TCP non è supportato su questa piattaforma.",
|
"tcpErrorUnsupported": "Il protocollo TCP non è supportato su questa piattaforma.",
|
||||||
"tcpErrorTimedOut": "La connessione TCP è scaduta.",
|
"tcpErrorTimedOut": "La connessione TCP è scaduta.",
|
||||||
"tcpConnectionFailed": "Impossibile stabilire la connessione TCP: {error}",
|
"tcpConnectionFailed": "Impossibile stabilire la connessione TCP: {error}",
|
||||||
"map_showDiscoveryContacts": "Mostra Contatti di Discovery",
|
"map_showDiscoveryContacts": "Mostra Contatti di Discovery"
|
||||||
"map_setAsMyLocation": "Imposta come la mia posizione"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1714,6 +1714,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'Group name is required'**
|
/// **'Group name is required'**
|
||||||
String get contacts_groupNameRequired;
|
String get contacts_groupNameRequired;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_groupNameReserved.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'This group name is reserved'**
|
||||||
|
String get contacts_groupNameReserved;
|
||||||
|
|
||||||
/// No description provided for @contacts_groupAlreadyExists.
|
/// No description provided for @contacts_groupAlreadyExists.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -2746,12 +2752,6 @@ abstract class AppLocalizations {
|
|||||||
/// **'Share marker here'**
|
/// **'Share marker here'**
|
||||||
String get map_shareMarkerHere;
|
String get map_shareMarkerHere;
|
||||||
|
|
||||||
/// No description provided for @map_setAsMyLocation.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Set as my location'**
|
|
||||||
String get map_setAsMyLocation;
|
|
||||||
|
|
||||||
/// No description provided for @map_pinLabel.
|
/// No description provided for @map_pinLabel.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -902,6 +902,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'Името на групата е задължително.';
|
String get contacts_groupNameRequired => 'Името на групата е задължително.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'Това име на група е запазено';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'Групата \"$name\" вече съществува.';
|
return 'Групата \"$name\" вече съществува.';
|
||||||
@@ -1511,9 +1514,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Споделете маркер тук';
|
String get map_shareMarkerHere => 'Споделете маркер тук';
|
||||||
|
|
||||||
@override
|
|
||||||
String get map_setAsMyLocation => 'Задайте като моя местоположение';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Етикетиране на пин';
|
String get map_pinLabel => 'Етикетиране на пин';
|
||||||
|
|
||||||
|
|||||||
@@ -902,6 +902,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'Der Gruppennamen ist erforderlich.';
|
String get contacts_groupNameRequired => 'Der Gruppennamen ist erforderlich.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'Dieser Gruppenname ist reserviert';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'Die Gruppe \"$name\" existiert bereits.';
|
return 'Die Gruppe \"$name\" existiert bereits.';
|
||||||
@@ -1513,9 +1516,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Teilen Sie den Marker hier.';
|
String get map_shareMarkerHere => 'Teilen Sie den Marker hier.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get map_setAsMyLocation => 'Als meine aktuelle Position festlegen';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Pin Name';
|
String get map_pinLabel => 'Pin Name';
|
||||||
|
|
||||||
|
|||||||
@@ -889,6 +889,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'Group name is required';
|
String get contacts_groupNameRequired => 'Group name is required';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'This group name is reserved';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'Group \"$name\" already exists';
|
return 'Group \"$name\" already exists';
|
||||||
@@ -1487,9 +1490,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Share marker here';
|
String get map_shareMarkerHere => 'Share marker here';
|
||||||
|
|
||||||
@override
|
|
||||||
String get map_setAsMyLocation => 'Set as my location';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Pin label';
|
String get map_pinLabel => 'Pin label';
|
||||||
|
|
||||||
|
|||||||
@@ -901,6 +901,10 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'El nombre del grupo es obligatorio';
|
String get contacts_groupNameRequired => 'El nombre del grupo es obligatorio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved =>
|
||||||
|
'Este nombre de grupo está reservado';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'El grupo \"$name\" ya existe';
|
return 'El grupo \"$name\" ya existe';
|
||||||
@@ -1509,9 +1513,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Compartir marcador aquí';
|
String get map_shareMarkerHere => 'Compartir marcador aquí';
|
||||||
|
|
||||||
@override
|
|
||||||
String get map_setAsMyLocation => 'Establecer mi ubicación';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Etiqueta de marcador';
|
String get map_pinLabel => 'Etiqueta de marcador';
|
||||||
|
|
||||||
|
|||||||
@@ -905,6 +905,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'Le nom du groupe est obligatoire.';
|
String get contacts_groupNameRequired => 'Le nom du groupe est obligatoire.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'Ce nom de groupe est réservé';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'Le groupe \"$name\" existe déjà.';
|
return 'Le groupe \"$name\" existe déjà.';
|
||||||
@@ -1518,9 +1521,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Partager le marqueur ici';
|
String get map_shareMarkerHere => 'Partager le marqueur ici';
|
||||||
|
|
||||||
@override
|
|
||||||
String get map_setAsMyLocation => 'Définir comme ma localisation';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Étiquete de repin';
|
String get map_pinLabel => 'Étiquete de repin';
|
||||||
|
|
||||||
|
|||||||
@@ -901,6 +901,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'Il nome del gruppo è obbligatorio.';
|
String get contacts_groupNameRequired => 'Il nome del gruppo è obbligatorio.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'Questo nome del gruppo è riservato';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'Il gruppo \"$name\" esiste già.';
|
return 'Il gruppo \"$name\" esiste già.';
|
||||||
@@ -1510,9 +1513,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Condividi marcatore qui';
|
String get map_shareMarkerHere => 'Condividi marcatore qui';
|
||||||
|
|
||||||
@override
|
|
||||||
String get map_setAsMyLocation => 'Imposta come la mia posizione';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Etichetta PIN';
|
String get map_pinLabel => 'Etichetta PIN';
|
||||||
|
|
||||||
|
|||||||
@@ -895,6 +895,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'De groepnaam is verplicht.';
|
String get contacts_groupNameRequired => 'De groepnaam is verplicht.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'Deze groepsnaam is gereserveerd';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'De groep \"$name\" bestaat al.';
|
return 'De groep \"$name\" bestaat al.';
|
||||||
@@ -1502,9 +1505,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Deel marker hier';
|
String get map_shareMarkerHere => 'Deel marker hier';
|
||||||
|
|
||||||
@override
|
|
||||||
String get map_setAsMyLocation => 'Stel dit in als mijn locatie';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Label vastzetten';
|
String get map_pinLabel => 'Label vastzetten';
|
||||||
|
|
||||||
|
|||||||
@@ -904,6 +904,9 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'Nazwa grupy jest wymagana';
|
String get contacts_groupNameRequired => 'Nazwa grupy jest wymagana';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'Ta nazwa grupy jest zastrzeżona';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'Grupa \"$name\" już istnieje';
|
return 'Grupa \"$name\" już istnieje';
|
||||||
@@ -1512,9 +1515,6 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Udostępnij znacznik tutaj';
|
String get map_shareMarkerHere => 'Udostępnij znacznik tutaj';
|
||||||
|
|
||||||
@override
|
|
||||||
String get map_setAsMyLocation => 'Ustaw jako moje lokalizację';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Oznacz etykietę';
|
String get map_pinLabel => 'Oznacz etykietę';
|
||||||
|
|
||||||
|
|||||||
@@ -903,6 +903,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'O nome do grupo é obrigatório.';
|
String get contacts_groupNameRequired => 'O nome do grupo é obrigatório.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'Este nome de grupo está reservado';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'O grupo \"$name\" já existe';
|
return 'O grupo \"$name\" já existe';
|
||||||
@@ -1511,9 +1514,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Compartilhar marcador aqui';
|
String get map_shareMarkerHere => 'Compartilhar marcador aqui';
|
||||||
|
|
||||||
@override
|
|
||||||
String get map_setAsMyLocation => 'Defina minha localização';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Rótulo de marcador';
|
String get map_pinLabel => 'Rótulo de marcador';
|
||||||
|
|
||||||
|
|||||||
@@ -902,6 +902,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'Имя группы обязательно';
|
String get contacts_groupNameRequired => 'Имя группы обязательно';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'Это имя группы зарезервировано';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'Группа \"$name\" уже существует';
|
return 'Группа \"$name\" уже существует';
|
||||||
@@ -1513,9 +1516,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Поделиться меткой здесь';
|
String get map_shareMarkerHere => 'Поделиться меткой здесь';
|
||||||
|
|
||||||
@override
|
|
||||||
String get map_setAsMyLocation => 'Установить мое местоположение';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Метка';
|
String get map_pinLabel => 'Метка';
|
||||||
|
|
||||||
|
|||||||
@@ -894,6 +894,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'Skupina musí mať názov.';
|
String get contacts_groupNameRequired => 'Skupina musí mať názov.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'Tento názov skupiny je rezervovaný';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'Skupina \"$name\" už existuje';
|
return 'Skupina \"$name\" už existuje';
|
||||||
@@ -1504,9 +1507,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Zdieľte značku tu';
|
String get map_shareMarkerHere => 'Zdieľte značku tu';
|
||||||
|
|
||||||
@override
|
|
||||||
String get map_setAsMyLocation => 'Nastavte ako moju polohu';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Označka upozornenia';
|
String get map_pinLabel => 'Označka upozornenia';
|
||||||
|
|
||||||
|
|||||||
@@ -892,6 +892,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'Ime skupine je obvezno.';
|
String get contacts_groupNameRequired => 'Ime skupine je obvezno.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'To ime skupine je rezervirano';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'Skupina \"$name\" že obstaja';
|
return 'Skupina \"$name\" že obstaja';
|
||||||
@@ -1498,9 +1501,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Delite točke tukaj.';
|
String get map_shareMarkerHere => 'Delite točke tukaj.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get map_setAsMyLocation => 'Nastavite to kot mojo lokacijo';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Oznaka za pritrditev';
|
String get map_pinLabel => 'Oznaka za pritrditev';
|
||||||
|
|
||||||
|
|||||||
@@ -888,6 +888,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'Gruppnamnet är obligatoriskt';
|
String get contacts_groupNameRequired => 'Gruppnamnet är obligatoriskt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'Detta gruppnamn är reserverat';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'Gruppen \"$name\" finns redan.';
|
return 'Gruppen \"$name\" finns redan.';
|
||||||
@@ -1494,9 +1497,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Dela markeringen här';
|
String get map_shareMarkerHere => 'Dela markeringen här';
|
||||||
|
|
||||||
@override
|
|
||||||
String get map_setAsMyLocation => 'Ange som min plats';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Fästetikett';
|
String get map_pinLabel => 'Fästetikett';
|
||||||
|
|
||||||
|
|||||||
@@ -898,6 +898,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'Назва групи обов\'язкова.';
|
String get contacts_groupNameRequired => 'Назва групи обов\'язкова.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'Ця назва групи зарезервована';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'Група «$name» вже існує.';
|
return 'Група «$name» вже існує.';
|
||||||
@@ -1510,9 +1513,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Поділитися маркером тут';
|
String get map_shareMarkerHere => 'Поділитися маркером тут';
|
||||||
|
|
||||||
@override
|
|
||||||
String get map_setAsMyLocation => 'Встановити моє місцезнаходження';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Мітка піна';
|
String get map_pinLabel => 'Мітка піна';
|
||||||
|
|
||||||
|
|||||||
@@ -845,6 +845,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => '请输入群聊名称';
|
String get contacts_groupNameRequired => '请输入群聊名称';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => '该群组名称已被保留';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return '名为 \"$name\" 的群聊已存在';
|
return '名为 \"$name\" 的群聊已存在';
|
||||||
@@ -1421,9 +1424,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => '在此分享标记';
|
String get map_shareMarkerHere => '在此分享标记';
|
||||||
|
|
||||||
@override
|
|
||||||
String get map_setAsMyLocation => '设置为我的位置';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => '标签';
|
String get map_pinLabel => '标签';
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -285,6 +285,7 @@
|
|||||||
"contacts_newGroup": "Nieuwe Groep",
|
"contacts_newGroup": "Nieuwe Groep",
|
||||||
"contacts_groupName": "Groepnaam",
|
"contacts_groupName": "Groepnaam",
|
||||||
"contacts_groupNameRequired": "De groepnaam is verplicht.",
|
"contacts_groupNameRequired": "De groepnaam is verplicht.",
|
||||||
|
"contacts_groupNameReserved": "Deze groepsnaam is gereserveerd",
|
||||||
"contacts_groupAlreadyExists": "De groep \"{name}\" bestaat al.",
|
"contacts_groupAlreadyExists": "De groep \"{name}\" bestaat al.",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1887,6 +1888,5 @@
|
|||||||
"tcpErrorUnsupported": "TCP-transport wordt niet ondersteund op deze platform.",
|
"tcpErrorUnsupported": "TCP-transport wordt niet ondersteund op deze platform.",
|
||||||
"tcpErrorTimedOut": "De TCP-verbinding is verlopen.",
|
"tcpErrorTimedOut": "De TCP-verbinding is verlopen.",
|
||||||
"tcpConnectionFailed": "Verbinding met TCP mislukt: {error}",
|
"tcpConnectionFailed": "Verbinding met TCP mislukt: {error}",
|
||||||
"map_showDiscoveryContacts": "Ontdek contacten weergeven",
|
"map_showDiscoveryContacts": "Ontdek contacten weergeven"
|
||||||
"map_setAsMyLocation": "Stel dit in als mijn locatie"
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -285,6 +285,7 @@
|
|||||||
"contacts_newGroup": "Nowa Grupa",
|
"contacts_newGroup": "Nowa Grupa",
|
||||||
"contacts_groupName": "Nazwa grupy",
|
"contacts_groupName": "Nazwa grupy",
|
||||||
"contacts_groupNameRequired": "Nazwa grupy jest wymagana",
|
"contacts_groupNameRequired": "Nazwa grupy jest wymagana",
|
||||||
|
"contacts_groupNameReserved": "Ta nazwa grupy jest zastrzeżona",
|
||||||
"contacts_groupAlreadyExists": "Grupa \"{name}\" już istnieje",
|
"contacts_groupAlreadyExists": "Grupa \"{name}\" już istnieje",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1887,6 +1888,5 @@
|
|||||||
"tcpErrorUnsupported": "Transport protokoł TCP nie jest obsługiwany na tym urządzeniu.",
|
"tcpErrorUnsupported": "Transport protokoł TCP nie jest obsługiwany na tym urządzeniu.",
|
||||||
"tcpErrorTimedOut": "Połączenie TCP zakończyło się bez powodzenia.",
|
"tcpErrorTimedOut": "Połączenie TCP zakończyło się bez powodzenia.",
|
||||||
"tcpConnectionFailed": "Błąd połączenia TCP: {error}",
|
"tcpConnectionFailed": "Błąd połączenia TCP: {error}",
|
||||||
"map_showDiscoveryContacts": "Pokaż kontakty odkrywania",
|
"map_showDiscoveryContacts": "Pokaż kontakty odkrywania"
|
||||||
"map_setAsMyLocation": "Ustaw jako moje lokalizację"
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -285,6 +285,7 @@
|
|||||||
"contacts_newGroup": "Novo Grupo",
|
"contacts_newGroup": "Novo Grupo",
|
||||||
"contacts_groupName": "Nome do grupo",
|
"contacts_groupName": "Nome do grupo",
|
||||||
"contacts_groupNameRequired": "O nome do grupo é obrigatório.",
|
"contacts_groupNameRequired": "O nome do grupo é obrigatório.",
|
||||||
|
"contacts_groupNameReserved": "Este nome de grupo está reservado",
|
||||||
"contacts_groupAlreadyExists": "O grupo \"{name}\" já existe",
|
"contacts_groupAlreadyExists": "O grupo \"{name}\" já existe",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1887,6 +1888,5 @@
|
|||||||
"tcpErrorUnsupported": "O protocolo TCP não é suportado nesta plataforma.",
|
"tcpErrorUnsupported": "O protocolo TCP não é suportado nesta plataforma.",
|
||||||
"tcpErrorTimedOut": "A conexão TCP expirou.",
|
"tcpErrorTimedOut": "A conexão TCP expirou.",
|
||||||
"tcpConnectionFailed": "Falha na conexão TCP: {error}",
|
"tcpConnectionFailed": "Falha na conexão TCP: {error}",
|
||||||
"map_showDiscoveryContacts": "Mostrar Contatos de Descoberta",
|
"map_showDiscoveryContacts": "Mostrar Contatos de Descoberta"
|
||||||
"map_setAsMyLocation": "Defina minha localização"
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -212,6 +212,7 @@
|
|||||||
"contacts_newGroup": "Новая группа",
|
"contacts_newGroup": "Новая группа",
|
||||||
"contacts_groupName": "Имя группы",
|
"contacts_groupName": "Имя группы",
|
||||||
"contacts_groupNameRequired": "Имя группы обязательно",
|
"contacts_groupNameRequired": "Имя группы обязательно",
|
||||||
|
"contacts_groupNameReserved": "Это имя группы зарезервировано",
|
||||||
"contacts_groupAlreadyExists": "Группа \"{name}\" уже существует",
|
"contacts_groupAlreadyExists": "Группа \"{name}\" уже существует",
|
||||||
"contacts_filterContacts": "Фильтр контактов...",
|
"contacts_filterContacts": "Фильтр контактов...",
|
||||||
"contacts_noContactsMatchFilter": "Нет контактов, соответствующих фильтру",
|
"contacts_noContactsMatchFilter": "Нет контактов, соответствующих фильтру",
|
||||||
@@ -1127,6 +1128,5 @@
|
|||||||
"tcpErrorUnsupported": "Протокол TCP не поддерживается на этой платформе.",
|
"tcpErrorUnsupported": "Протокол TCP не поддерживается на этой платформе.",
|
||||||
"tcpErrorTimedOut": "Соединение TCP не удалось установить.",
|
"tcpErrorTimedOut": "Соединение TCP не удалось установить.",
|
||||||
"tcpConnectionFailed": "Не удалось установить соединение TCP: {error}",
|
"tcpConnectionFailed": "Не удалось установить соединение TCP: {error}",
|
||||||
"map_showDiscoveryContacts": "Показать контакты Discovery",
|
"map_showDiscoveryContacts": "Показать контакты Discovery"
|
||||||
"map_setAsMyLocation": "Установить мое местоположение"
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -285,6 +285,7 @@
|
|||||||
"contacts_newGroup": "Nová skupina",
|
"contacts_newGroup": "Nová skupina",
|
||||||
"contacts_groupName": "Názov skupiny",
|
"contacts_groupName": "Názov skupiny",
|
||||||
"contacts_groupNameRequired": "Skupina musí mať názov.",
|
"contacts_groupNameRequired": "Skupina musí mať názov.",
|
||||||
|
"contacts_groupNameReserved": "Tento názov skupiny je rezervovaný",
|
||||||
"contacts_groupAlreadyExists": "Skupina \"{name}\" už existuje",
|
"contacts_groupAlreadyExists": "Skupina \"{name}\" už existuje",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1887,6 +1888,5 @@
|
|||||||
"tcpErrorUnsupported": "Prevoz prostredníctvom protokolu TCP nie je na tejto platforme podporovaný.",
|
"tcpErrorUnsupported": "Prevoz prostredníctvom protokolu TCP nie je na tejto platforme podporovaný.",
|
||||||
"tcpErrorTimedOut": "Pripojenie TCP vypršalo.",
|
"tcpErrorTimedOut": "Pripojenie TCP vypršalo.",
|
||||||
"tcpConnectionFailed": "Neúspešné vytvorenie TCP spojenia: {error}",
|
"tcpConnectionFailed": "Neúspešné vytvorenie TCP spojenia: {error}",
|
||||||
"map_showDiscoveryContacts": "Zobraziť kontakty objavov",
|
"map_showDiscoveryContacts": "Zobraziť kontakty objavov"
|
||||||
"map_setAsMyLocation": "Nastavte ako moju polohu"
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -285,6 +285,7 @@
|
|||||||
"contacts_newGroup": "Nova skupina",
|
"contacts_newGroup": "Nova skupina",
|
||||||
"contacts_groupName": "Ime skupine",
|
"contacts_groupName": "Ime skupine",
|
||||||
"contacts_groupNameRequired": "Ime skupine je obvezno.",
|
"contacts_groupNameRequired": "Ime skupine je obvezno.",
|
||||||
|
"contacts_groupNameReserved": "To ime skupine je rezervirano",
|
||||||
"contacts_groupAlreadyExists": "Skupina \"{name}\" že obstaja",
|
"contacts_groupAlreadyExists": "Skupina \"{name}\" že obstaja",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1887,6 +1888,5 @@
|
|||||||
"tcpErrorUnsupported": "Transport preko protokola TCP ni podprt na tej platformi.",
|
"tcpErrorUnsupported": "Transport preko protokola TCP ni podprt na tej platformi.",
|
||||||
"tcpErrorTimedOut": "Povezava TCP je presegla časovno obdobje.",
|
"tcpErrorTimedOut": "Povezava TCP je presegla časovno obdobje.",
|
||||||
"tcpConnectionFailed": "Napaka pri povezavi TCP: {error}",
|
"tcpConnectionFailed": "Napaka pri povezavi TCP: {error}",
|
||||||
"map_showDiscoveryContacts": "Prikaži odkritja kontaktov",
|
"map_showDiscoveryContacts": "Prikaži odkritja kontaktov"
|
||||||
"map_setAsMyLocation": "Nastavite to kot mojo lokacijo"
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -285,6 +285,7 @@
|
|||||||
"contacts_newGroup": "Ny grupp",
|
"contacts_newGroup": "Ny grupp",
|
||||||
"contacts_groupName": "Gruppnamn",
|
"contacts_groupName": "Gruppnamn",
|
||||||
"contacts_groupNameRequired": "Gruppnamnet är obligatoriskt",
|
"contacts_groupNameRequired": "Gruppnamnet är obligatoriskt",
|
||||||
|
"contacts_groupNameReserved": "Detta gruppnamn är reserverat",
|
||||||
"contacts_groupAlreadyExists": "Gruppen \"{name}\" finns redan.",
|
"contacts_groupAlreadyExists": "Gruppen \"{name}\" finns redan.",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1887,6 +1888,5 @@
|
|||||||
"tcpErrorUnsupported": "TCP-transport fungerar inte på denna plattform.",
|
"tcpErrorUnsupported": "TCP-transport fungerar inte på denna plattform.",
|
||||||
"tcpErrorTimedOut": "TCP-anslutningen har tidsut gått.",
|
"tcpErrorTimedOut": "TCP-anslutningen har tidsut gått.",
|
||||||
"tcpConnectionFailed": "Fel vid TCP-anslutning: {error}",
|
"tcpConnectionFailed": "Fel vid TCP-anslutning: {error}",
|
||||||
"map_showDiscoveryContacts": "Visa Discovery-kontakter",
|
"map_showDiscoveryContacts": "Visa Discovery-kontakter"
|
||||||
"map_setAsMyLocation": "Ange som min plats"
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -286,6 +286,7 @@
|
|||||||
"contacts_newGroup": "Нова група",
|
"contacts_newGroup": "Нова група",
|
||||||
"contacts_groupName": "Назва групи",
|
"contacts_groupName": "Назва групи",
|
||||||
"contacts_groupNameRequired": "Назва групи обов'язкова.",
|
"contacts_groupNameRequired": "Назва групи обов'язкова.",
|
||||||
|
"contacts_groupNameReserved": "Ця назва групи зарезервована",
|
||||||
"contacts_groupAlreadyExists": "Група «{name}» вже існує.",
|
"contacts_groupAlreadyExists": "Група «{name}» вже існує.",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1887,6 +1888,5 @@
|
|||||||
"tcpErrorUnsupported": "Транспорт TCP не підтримується на цій платформі.",
|
"tcpErrorUnsupported": "Транспорт TCP не підтримується на цій платформі.",
|
||||||
"tcpErrorTimedOut": "З'єднання TCP завершилося через закінчення часу очікування.",
|
"tcpErrorTimedOut": "З'єднання TCP завершилося через закінчення часу очікування.",
|
||||||
"tcpConnectionFailed": "Не вдалося встановити з'єднання TCP: {error}",
|
"tcpConnectionFailed": "Не вдалося встановити з'єднання TCP: {error}",
|
||||||
"map_showDiscoveryContacts": "Показати контакти Відкриття",
|
"map_showDiscoveryContacts": "Показати контакти Відкриття"
|
||||||
"map_setAsMyLocation": "Встановити моє місцезнаходження"
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -300,6 +300,7 @@
|
|||||||
"contacts_newGroup": "新建群聊",
|
"contacts_newGroup": "新建群聊",
|
||||||
"contacts_groupName": "群聊名称",
|
"contacts_groupName": "群聊名称",
|
||||||
"contacts_groupNameRequired": "请输入群聊名称",
|
"contacts_groupNameRequired": "请输入群聊名称",
|
||||||
|
"contacts_groupNameReserved": "该群组名称已被保留",
|
||||||
"contacts_groupAlreadyExists": "名为 \"{name}\" 的群聊已存在",
|
"contacts_groupAlreadyExists": "名为 \"{name}\" 的群聊已存在",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1892,6 +1893,5 @@
|
|||||||
"tcpErrorUnsupported": "此平台不支持 TCP 传输。",
|
"tcpErrorUnsupported": "此平台不支持 TCP 传输。",
|
||||||
"tcpErrorTimedOut": "TCP 连接超时。",
|
"tcpErrorTimedOut": "TCP 连接超时。",
|
||||||
"tcpConnectionFailed": "TCP 连接失败:{error}",
|
"tcpConnectionFailed": "TCP 连接失败:{error}",
|
||||||
"map_showDiscoveryContacts": "显示发现联系人",
|
"map_showDiscoveryContacts": "显示发现联系人"
|
||||||
"map_setAsMyLocation": "设置为我的位置"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import 'services/app_debug_log_service.dart';
|
|||||||
import 'services/background_service.dart';
|
import 'services/background_service.dart';
|
||||||
import 'services/map_tile_cache_service.dart';
|
import 'services/map_tile_cache_service.dart';
|
||||||
import 'services/chat_text_scale_service.dart';
|
import 'services/chat_text_scale_service.dart';
|
||||||
|
import 'services/ui_view_state_service.dart';
|
||||||
import 'storage/prefs_manager.dart';
|
import 'storage/prefs_manager.dart';
|
||||||
import 'utils/app_logger.dart';
|
import 'utils/app_logger.dart';
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ void main() async {
|
|||||||
final backgroundService = BackgroundService();
|
final backgroundService = BackgroundService();
|
||||||
final mapTileCacheService = MapTileCacheService();
|
final mapTileCacheService = MapTileCacheService();
|
||||||
final chatTextScaleService = ChatTextScaleService();
|
final chatTextScaleService = ChatTextScaleService();
|
||||||
|
final uiViewStateService = UiViewStateService();
|
||||||
|
|
||||||
// Load settings
|
// Load settings
|
||||||
await appSettingsService.loadSettings();
|
await appSettingsService.loadSettings();
|
||||||
@@ -56,6 +58,7 @@ void main() async {
|
|||||||
_registerThirdPartyLicenses();
|
_registerThirdPartyLicenses();
|
||||||
|
|
||||||
await chatTextScaleService.initialize();
|
await chatTextScaleService.initialize();
|
||||||
|
await uiViewStateService.initialize();
|
||||||
|
|
||||||
// Wire up connector with services
|
// Wire up connector with services
|
||||||
connector.initialize(
|
connector.initialize(
|
||||||
@@ -86,6 +89,7 @@ void main() async {
|
|||||||
appDebugLogService: appDebugLogService,
|
appDebugLogService: appDebugLogService,
|
||||||
mapTileCacheService: mapTileCacheService,
|
mapTileCacheService: mapTileCacheService,
|
||||||
chatTextScaleService: chatTextScaleService,
|
chatTextScaleService: chatTextScaleService,
|
||||||
|
uiViewStateService: uiViewStateService,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -121,6 +125,7 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
final AppDebugLogService appDebugLogService;
|
final AppDebugLogService appDebugLogService;
|
||||||
final MapTileCacheService mapTileCacheService;
|
final MapTileCacheService mapTileCacheService;
|
||||||
final ChatTextScaleService chatTextScaleService;
|
final ChatTextScaleService chatTextScaleService;
|
||||||
|
final UiViewStateService uiViewStateService;
|
||||||
|
|
||||||
const MeshCoreApp({
|
const MeshCoreApp({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -133,6 +138,7 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
required this.appDebugLogService,
|
required this.appDebugLogService,
|
||||||
required this.mapTileCacheService,
|
required this.mapTileCacheService,
|
||||||
required this.chatTextScaleService,
|
required this.chatTextScaleService,
|
||||||
|
required this.uiViewStateService,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -146,6 +152,7 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
ChangeNotifierProvider.value(value: bleDebugLogService),
|
ChangeNotifierProvider.value(value: bleDebugLogService),
|
||||||
ChangeNotifierProvider.value(value: appDebugLogService),
|
ChangeNotifierProvider.value(value: appDebugLogService),
|
||||||
ChangeNotifierProvider.value(value: chatTextScaleService),
|
ChangeNotifierProvider.value(value: chatTextScaleService),
|
||||||
|
ChangeNotifierProvider.value(value: uiViewStateService),
|
||||||
Provider.value(value: storage),
|
Provider.value(value: storage),
|
||||||
Provider.value(value: mapTileCacheService),
|
Provider.value(value: mapTileCacheService),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ class AppSettings {
|
|||||||
final UnitSystem unitSystem;
|
final UnitSystem unitSystem;
|
||||||
final Set<String> mutedChannels;
|
final Set<String> mutedChannels;
|
||||||
final bool mapShowDiscoveryContacts;
|
final bool mapShowDiscoveryContacts;
|
||||||
final String tcpServerAddress;
|
|
||||||
final int tcpServerPort;
|
|
||||||
|
|
||||||
AppSettings({
|
AppSettings({
|
||||||
this.clearPathOnMaxRetry = false,
|
this.clearPathOnMaxRetry = false,
|
||||||
@@ -70,8 +68,6 @@ class AppSettings {
|
|||||||
this.unitSystem = UnitSystem.metric,
|
this.unitSystem = UnitSystem.metric,
|
||||||
Set<String>? mutedChannels,
|
Set<String>? mutedChannels,
|
||||||
this.mapShowDiscoveryContacts = true,
|
this.mapShowDiscoveryContacts = true,
|
||||||
this.tcpServerAddress = '',
|
|
||||||
this.tcpServerPort = 0,
|
|
||||||
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
|
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
|
||||||
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
|
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
|
||||||
mutedChannels = mutedChannels ?? {};
|
mutedChannels = mutedChannels ?? {};
|
||||||
@@ -104,8 +100,6 @@ class AppSettings {
|
|||||||
'unit_system': unitSystem.value,
|
'unit_system': unitSystem.value,
|
||||||
'muted_channels': mutedChannels.toList(),
|
'muted_channels': mutedChannels.toList(),
|
||||||
'map_show_discovery_contacts': mapShowDiscoveryContacts,
|
'map_show_discovery_contacts': mapShowDiscoveryContacts,
|
||||||
'tcp_server_address': tcpServerAddress,
|
|
||||||
'tcp_server_port': tcpServerPort,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,8 +157,6 @@ class AppSettings {
|
|||||||
{},
|
{},
|
||||||
mapShowDiscoveryContacts:
|
mapShowDiscoveryContacts:
|
||||||
json['map_show_discovery_contacts'] as bool? ?? true,
|
json['map_show_discovery_contacts'] as bool? ?? true,
|
||||||
tcpServerAddress: json['tcp_server_address'] as String? ?? '',
|
|
||||||
tcpServerPort: json['tcp_server_port'] as int? ?? 0,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,8 +187,6 @@ class AppSettings {
|
|||||||
UnitSystem? unitSystem,
|
UnitSystem? unitSystem,
|
||||||
Set<String>? mutedChannels,
|
Set<String>? mutedChannels,
|
||||||
bool? mapShowDiscoveryContacts,
|
bool? mapShowDiscoveryContacts,
|
||||||
String? tcpServerAddress,
|
|
||||||
int? tcpServerPort,
|
|
||||||
}) {
|
}) {
|
||||||
return AppSettings(
|
return AppSettings(
|
||||||
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
|
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
|
||||||
@@ -235,8 +225,6 @@ class AppSettings {
|
|||||||
mutedChannels: mutedChannels ?? this.mutedChannels,
|
mutedChannels: mutedChannels ?? this.mutedChannels,
|
||||||
mapShowDiscoveryContacts:
|
mapShowDiscoveryContacts:
|
||||||
mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts,
|
mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts,
|
||||||
tcpServerAddress: tcpServerAddress ?? this.tcpServerAddress,
|
|
||||||
tcpServerPort: tcpServerPort ?? this.tcpServerPort,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-14
@@ -65,17 +65,7 @@ class Contact {
|
|||||||
return '$pathLength hops';
|
return '$pathLength hops';
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get hasLocation {
|
bool get hasLocation => latitude != null && longitude != null;
|
||||||
const double epsilon = 1e-6;
|
|
||||||
final lat = latitude ?? 0.0;
|
|
||||||
final lon = longitude ?? 0.0;
|
|
||||||
return (lat.abs() > epsilon || lon.abs() > epsilon) &&
|
|
||||||
lat >= -90.0 &&
|
|
||||||
lat <= 90.0 &&
|
|
||||||
lon >= -180.0 &&
|
|
||||||
lon <= 180.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get isFavorite => (flags & contactFlagFavorite) != 0;
|
bool get isFavorite => (flags & contactFlagFavorite) != 0;
|
||||||
|
|
||||||
Contact copyWith({
|
Contact copyWith({
|
||||||
@@ -118,7 +108,7 @@ class Contact {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String get pathIdList {
|
String get pathIdList {
|
||||||
final pathBytes = pathBytesForDisplay;
|
final pathBytes = _pathBytesForDisplay;
|
||||||
if (pathBytes.isEmpty) return '';
|
if (pathBytes.isEmpty) return '';
|
||||||
final parts = <String>[];
|
final parts = <String>[];
|
||||||
final groupSize = pathHashSize;
|
final groupSize = pathHashSize;
|
||||||
@@ -140,7 +130,43 @@ class Contact {
|
|||||||
return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>";
|
return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>";
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint8List get pathBytesForDisplay {
|
Uint8List? get traceRouteBytes {
|
||||||
|
final pathBytes = _pathBytesForDisplay;
|
||||||
|
Uint8List? traceBytes;
|
||||||
|
|
||||||
|
if (pathBytes.isEmpty) {
|
||||||
|
traceBytes = Uint8List(1);
|
||||||
|
traceBytes[0] = publicKey[0];
|
||||||
|
return traceBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == advTypeRepeater || type == advTypeRoom) {
|
||||||
|
final len = (pathBytes.length + pathBytes.length + 1);
|
||||||
|
traceBytes = Uint8List(len);
|
||||||
|
traceBytes[pathBytes.length] = publicKey[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 ? null : 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List get _pathBytesForDisplay {
|
||||||
if (pathOverride != null) {
|
if (pathOverride != null) {
|
||||||
if (pathOverride! < 0) return Uint8List(0);
|
if (pathOverride! < 0) return Uint8List(0);
|
||||||
return pathOverrideBytes ?? Uint8List(0);
|
return pathOverrideBytes ?? Uint8List(0);
|
||||||
@@ -171,7 +197,6 @@ class Contact {
|
|||||||
double? lat, lon;
|
double? lat, lon;
|
||||||
final latRaw = reader.readInt32LE();
|
final latRaw = reader.readInt32LE();
|
||||||
final lonRaw = reader.readInt32LE();
|
final lonRaw = reader.readInt32LE();
|
||||||
|
|
||||||
if (latRaw != 0 || lonRaw != 0) {
|
if (latRaw != 0 || lonRaw != 0) {
|
||||||
lat = latRaw / 1e6;
|
lat = latRaw / 1e6;
|
||||||
lon = lonRaw / 1e6;
|
lon = lonRaw / 1e6;
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ 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 = connector.allContacts;
|
final contacts = <Contact>[
|
||||||
|
...connector.contacts,
|
||||||
|
...connector.discoveredContacts,
|
||||||
|
];
|
||||||
final hops = _buildPathHops(primaryPath, contacts, l10n);
|
final hops = _buildPathHops(primaryPath, contacts, l10n);
|
||||||
final hasHopDetails = primaryPath.isNotEmpty;
|
final hasHopDetails = primaryPath.isNotEmpty;
|
||||||
final observedLabel = _formatObservedHops(
|
final observedLabel = _formatObservedHops(
|
||||||
@@ -62,9 +65,8 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
builder: (context) => PathTraceMapScreen(
|
builder: (context) => PathTraceMapScreen(
|
||||||
title: context.l10n.contacts_repeaterPathTrace,
|
title: context.l10n.contacts_repeaterPathTrace,
|
||||||
path: primaryPath,
|
path: primaryPath,
|
||||||
flipPathAround: true,
|
flipPathRound: true,
|
||||||
reversePathAround:
|
reversePathRound: !message.isOutgoing && !channelMessage,
|
||||||
!(!channelMessage && !message.isOutgoing),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -365,7 +367,10 @@ class _ChannelMessagePathMapScreenState
|
|||||||
: selectedPathTmp;
|
: selectedPathTmp;
|
||||||
|
|
||||||
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
||||||
final contacts = connector.allContacts;
|
final contacts = <Contact>[
|
||||||
|
...connector.contacts,
|
||||||
|
...connector.discoveredContacts,
|
||||||
|
];
|
||||||
final hops = _buildPathHops(selectedPath, contacts, context.l10n);
|
final hops = _buildPathHops(selectedPath, contacts, context.l10n);
|
||||||
|
|
||||||
final points = <LatLng>[];
|
final points = <LatLng>[];
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:uuid/uuid.dart';
|
|||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../services/app_settings_service.dart';
|
import '../services/app_settings_service.dart';
|
||||||
|
import '../services/ui_view_state_service.dart';
|
||||||
import '../models/channel.dart';
|
import '../models/channel.dart';
|
||||||
import '../models/community.dart';
|
import '../models/community.dart';
|
||||||
import '../storage/community_store.dart';
|
import '../storage/community_store.dart';
|
||||||
@@ -28,8 +29,6 @@ import 'contacts_screen.dart';
|
|||||||
import 'map_screen.dart';
|
import 'map_screen.dart';
|
||||||
import 'settings_screen.dart';
|
import 'settings_screen.dart';
|
||||||
|
|
||||||
enum ChannelSortOption { manual, name, latestMessages, unread }
|
|
||||||
|
|
||||||
class ChannelsScreen extends StatefulWidget {
|
class ChannelsScreen extends StatefulWidget {
|
||||||
final bool hideBackButton;
|
final bool hideBackButton;
|
||||||
|
|
||||||
@@ -43,9 +42,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
with DisconnectNavigationMixin {
|
with DisconnectNavigationMixin {
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
final CommunityStore _communityStore = CommunityStore();
|
final CommunityStore _communityStore = CommunityStore();
|
||||||
String _searchQuery = '';
|
|
||||||
Timer? _searchDebounce;
|
Timer? _searchDebounce;
|
||||||
ChannelSortOption _sortOption = ChannelSortOption.manual;
|
|
||||||
List<Community> _communities = [];
|
List<Community> _communities = [];
|
||||||
|
|
||||||
// Cache of PSK hex -> Community for quick lookup
|
// Cache of PSK hex -> Community for quick lookup
|
||||||
@@ -56,6 +53,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_searchController.text = context
|
||||||
|
.read<UiViewStateService>()
|
||||||
|
.channelsSearchText;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
context.read<MeshCoreConnector>().getChannels();
|
context.read<MeshCoreConnector>().getChannels();
|
||||||
_loadCommunities();
|
_loadCommunities();
|
||||||
@@ -110,6 +110,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final connector = context.watch<MeshCoreConnector>();
|
final connector = context.watch<MeshCoreConnector>();
|
||||||
|
final viewState = context.watch<UiViewStateService>();
|
||||||
|
|
||||||
final channelMessageStore = ChannelMessageStore();
|
final channelMessageStore = ChannelMessageStore();
|
||||||
channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||||
@@ -205,6 +206,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
final filteredChannels = _filterAndSortChannels(
|
final filteredChannels = _filterAndSortChannels(
|
||||||
channels,
|
channels,
|
||||||
connector,
|
connector,
|
||||||
|
viewState,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
@@ -219,17 +221,19 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
suffixIcon: Row(
|
suffixIcon: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (_searchQuery.isNotEmpty)
|
if (viewState.channelsSearchText.isNotEmpty)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.clear),
|
icon: const Icon(Icons.clear),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
_searchDebounce?.cancel();
|
||||||
|
_searchDebounce = null;
|
||||||
_searchController.clear();
|
_searchController.clear();
|
||||||
setState(() {
|
context
|
||||||
_searchQuery = '';
|
.read<UiViewStateService>()
|
||||||
});
|
.setChannelsSearchText('');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_buildFilterButton(),
|
_buildFilterButton(viewState),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
@@ -246,9 +250,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
const Duration(milliseconds: 300),
|
const Duration(milliseconds: 300),
|
||||||
() {
|
() {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
context
|
||||||
_searchQuery = value.toLowerCase();
|
.read<UiViewStateService>()
|
||||||
});
|
.setChannelsSearchText(value);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -283,8 +287,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: (_sortOption == ChannelSortOption.manual &&
|
: (viewState.channelsSortOption ==
|
||||||
_searchQuery.isEmpty)
|
ChannelSortOption.manual &&
|
||||||
|
viewState.channelsSearchText.isEmpty)
|
||||||
? ReorderableListView.builder(
|
? ReorderableListView.builder(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
left: 16,
|
left: 16,
|
||||||
@@ -584,59 +589,40 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
await showDisconnectDialog(context, connector);
|
await showDisconnectDialog(context, connector);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFilterButton() {
|
Widget _buildFilterButton(UiViewStateService viewState) {
|
||||||
const actionSortManual = 0;
|
return SortFilterMenu<ChannelSortOption>(
|
||||||
const actionSortName = 1;
|
|
||||||
const actionSortLatest = 2;
|
|
||||||
const actionSortUnread = 3;
|
|
||||||
|
|
||||||
return SortFilterMenu(
|
|
||||||
tooltip: context.l10n.listFilter_tooltip,
|
tooltip: context.l10n.listFilter_tooltip,
|
||||||
sections: [
|
sections: [
|
||||||
SortFilterMenuSection(
|
SortFilterMenuSection<ChannelSortOption>(
|
||||||
title: context.l10n.channels_sortBy,
|
title: context.l10n.channels_sortBy,
|
||||||
options: [
|
options: [
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption<ChannelSortOption>(
|
||||||
value: actionSortManual,
|
value: ChannelSortOption.manual,
|
||||||
label: context.l10n.channels_sortManual,
|
label: context.l10n.channels_sortManual,
|
||||||
checked: _sortOption == ChannelSortOption.manual,
|
checked: viewState.channelsSortOption == ChannelSortOption.manual,
|
||||||
),
|
),
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption<ChannelSortOption>(
|
||||||
value: actionSortName,
|
value: ChannelSortOption.name,
|
||||||
label: context.l10n.channels_sortAZ,
|
label: context.l10n.channels_sortAZ,
|
||||||
checked: _sortOption == ChannelSortOption.name,
|
checked: viewState.channelsSortOption == ChannelSortOption.name,
|
||||||
),
|
),
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption<ChannelSortOption>(
|
||||||
value: actionSortLatest,
|
value: ChannelSortOption.latestMessages,
|
||||||
label: context.l10n.channels_sortLatestMessages,
|
label: context.l10n.channels_sortLatestMessages,
|
||||||
checked: _sortOption == ChannelSortOption.latestMessages,
|
checked:
|
||||||
|
viewState.channelsSortOption ==
|
||||||
|
ChannelSortOption.latestMessages,
|
||||||
),
|
),
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption<ChannelSortOption>(
|
||||||
value: actionSortUnread,
|
value: ChannelSortOption.unread,
|
||||||
label: context.l10n.channels_sortUnread,
|
label: context.l10n.channels_sortUnread,
|
||||||
checked: _sortOption == ChannelSortOption.unread,
|
checked: viewState.channelsSortOption == ChannelSortOption.unread,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onSelected: (action) {
|
onSelected: (sortOption) {
|
||||||
setState(() {
|
viewState.setChannelsSortOption(sortOption);
|
||||||
switch (action) {
|
|
||||||
case actionSortManual:
|
|
||||||
_sortOption = ChannelSortOption.manual;
|
|
||||||
break;
|
|
||||||
case actionSortLatest:
|
|
||||||
_sortOption = ChannelSortOption.latestMessages;
|
|
||||||
break;
|
|
||||||
case actionSortUnread:
|
|
||||||
_sortOption = ChannelSortOption.unread;
|
|
||||||
break;
|
|
||||||
case actionSortName:
|
|
||||||
default:
|
|
||||||
_sortOption = ChannelSortOption.name;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -644,11 +630,14 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
List<Channel> _filterAndSortChannels(
|
List<Channel> _filterAndSortChannels(
|
||||||
List<Channel> channels,
|
List<Channel> channels,
|
||||||
MeshCoreConnector connector,
|
MeshCoreConnector connector,
|
||||||
|
UiViewStateService viewState,
|
||||||
) {
|
) {
|
||||||
var filtered = channels.where((channel) {
|
var filtered = channels.where((channel) {
|
||||||
if (_searchQuery.isEmpty) return true;
|
if (viewState.channelsSearchText.isEmpty) return true;
|
||||||
final label = _normalizeChannelName(channel);
|
final label = _normalizeChannelName(channel);
|
||||||
return label.toLowerCase().contains(_searchQuery);
|
return label.toLowerCase().contains(
|
||||||
|
viewState.channelsSearchText.toLowerCase(),
|
||||||
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
int compareByName(Channel a, Channel b) {
|
int compareByName(Channel a, Channel b) {
|
||||||
@@ -657,7 +646,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
return nameA.toLowerCase().compareTo(nameB.toLowerCase());
|
return nameA.toLowerCase().compareTo(nameB.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (_sortOption) {
|
switch (viewState.channelsSortOption) {
|
||||||
case ChannelSortOption.manual:
|
case ChannelSortOption.manual:
|
||||||
break;
|
break;
|
||||||
case ChannelSortOption.latestMessages:
|
case ChannelSortOption.latestMessages:
|
||||||
|
|||||||
@@ -106,9 +106,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
final unreadLabel = context.l10n.chat_unread(unreadCount);
|
final unreadLabel = context.l10n.chat_unread(unreadCount);
|
||||||
final pathLabel = _currentPathLabel(contact);
|
final pathLabel = _currentPathLabel(contact);
|
||||||
|
|
||||||
// Show path details if we have non-empty path data (from device or override)
|
// Show path details if we have path data (from device or override)
|
||||||
|
final hasPathData =
|
||||||
|
contact.path.isNotEmpty || contact.pathOverrideBytes != null;
|
||||||
final effectivePath = contact.pathOverrideBytes ?? contact.path;
|
final effectivePath = contact.pathOverrideBytes ?? contact.path;
|
||||||
final hasPathData = effectivePath.isNotEmpty;
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -142,25 +143,12 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
final contact = _resolveContact(connector);
|
final contact = _resolveContact(connector);
|
||||||
final isFloodMode = contact.pathOverride == -1;
|
final isFloodMode = contact.pathOverride == -1;
|
||||||
|
|
||||||
final isDirectMode = contact.pathOverride == 0;
|
|
||||||
final activeMode = isFloodMode
|
|
||||||
? 'flood'
|
|
||||||
: isDirectMode
|
|
||||||
? 'direct'
|
|
||||||
: 'auto';
|
|
||||||
|
|
||||||
return PopupMenuButton<String>(
|
return PopupMenuButton<String>(
|
||||||
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
|
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
|
||||||
tooltip: context.l10n.chat_routingMode,
|
tooltip: context.l10n.chat_routingMode,
|
||||||
onSelected: (mode) async {
|
onSelected: (mode) async {
|
||||||
if (mode == 'flood') {
|
if (mode == 'flood') {
|
||||||
await connector.setPathOverride(contact, pathLen: -1);
|
await connector.setPathOverride(contact, pathLen: -1);
|
||||||
} else if (mode == 'direct') {
|
|
||||||
await connector.setPathOverride(
|
|
||||||
contact,
|
|
||||||
pathLen: 0,
|
|
||||||
pathBytes: Uint8List(0),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
await connector.setPathOverride(contact, pathLen: null);
|
await connector.setPathOverride(contact, pathLen: null);
|
||||||
}
|
}
|
||||||
@@ -173,7 +161,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.auto_mode,
|
Icons.auto_mode,
|
||||||
size: 20,
|
size: 20,
|
||||||
color: activeMode == 'auto'
|
color: !isFloodMode
|
||||||
? Theme.of(context).primaryColor
|
? Theme.of(context).primaryColor
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@@ -181,30 +169,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
Text(
|
Text(
|
||||||
context.l10n.chat_autoUseSavedPath,
|
context.l10n.chat_autoUseSavedPath,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: activeMode == 'auto'
|
fontWeight: !isFloodMode
|
||||||
? FontWeight.bold
|
|
||||||
: FontWeight.normal,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
value: 'direct',
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.near_me,
|
|
||||||
size: 20,
|
|
||||||
color: activeMode == 'direct'
|
|
||||||
? Theme.of(context).primaryColor
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
context.l10n.chat_direct,
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: activeMode == 'direct'
|
|
||||||
? FontWeight.bold
|
? FontWeight.bold
|
||||||
: FontWeight.normal,
|
: FontWeight.normal,
|
||||||
),
|
),
|
||||||
@@ -219,7 +184,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.waves,
|
Icons.waves,
|
||||||
size: 20,
|
size: 20,
|
||||||
color: activeMode == 'flood'
|
color: isFloodMode
|
||||||
? Theme.of(context).primaryColor
|
? Theme.of(context).primaryColor
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@@ -227,7 +192,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
Text(
|
Text(
|
||||||
context.l10n.chat_forceFloodMode,
|
context.l10n.chat_forceFloodMode,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: activeMode == 'flood'
|
fontWeight: isFloodMode
|
||||||
? FontWeight.bold
|
? FontWeight.bold
|
||||||
: FontWeight.normal,
|
: FontWeight.normal,
|
||||||
),
|
),
|
||||||
@@ -286,9 +251,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
context.l10n.chat_sendMessageTo(
|
context.l10n.chat_sendMessageTo(widget.contact.name),
|
||||||
_resolveContact(context.read<MeshCoreConnector>()).name,
|
|
||||||
),
|
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
|
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -306,7 +269,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
|
|
||||||
// Auto-scroll to bottom if user is already at bottom
|
// Auto-scroll to bottom if user is already at bottom
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!mounted) return;
|
|
||||||
_scrollController.scrollToBottomIfAtBottom();
|
_scrollController.scrollToBottomIfAtBottom();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -331,10 +293,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
final messageIndex = index;
|
final messageIndex = index;
|
||||||
Contact contact = _resolveContact(connector);
|
Contact contact = widget.contact;
|
||||||
final message = reversedMessages[messageIndex];
|
final message = reversedMessages[messageIndex];
|
||||||
String fourByteHex = '';
|
String fourByteHex = '';
|
||||||
if (contact.type == advTypeRoom) {
|
if (widget.contact.type == advTypeRoom) {
|
||||||
contact = _resolveContactFrom4Bytes(
|
contact = _resolveContactFrom4Bytes(
|
||||||
connector,
|
connector,
|
||||||
message.fourByteRoomContactKey.isEmpty
|
message.fourByteRoomContactKey.isEmpty
|
||||||
@@ -352,13 +314,12 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
final textScale = context.select<ChatTextScaleService, double>(
|
final textScale = context.select<ChatTextScaleService, double>(
|
||||||
(service) => service.scale,
|
(service) => service.scale,
|
||||||
);
|
);
|
||||||
final resolvedContact = _resolveContact(connector);
|
|
||||||
return _MessageBubble(
|
return _MessageBubble(
|
||||||
message: message,
|
message: message,
|
||||||
senderName: resolvedContact.type == advTypeRoom
|
senderName: widget.contact.type == advTypeRoom
|
||||||
? "${contact.name} [$fourByteHex]"
|
? "${contact.name} [$fourByteHex]"
|
||||||
: contact.name,
|
: contact.name,
|
||||||
isRoomServer: resolvedContact.type == advTypeRoom,
|
isRoomServer: widget.contact.type == advTypeRoom,
|
||||||
textScale: textScale,
|
textScale: textScale,
|
||||||
onTap: () => _openMessagePath(message, contact),
|
onTap: () => _openMessagePath(message, contact),
|
||||||
onLongPress: () => _showMessageActions(message, contact),
|
onLongPress: () => _showMessageActions(message, contact),
|
||||||
@@ -496,7 +457,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
connector.sendMessage(_resolveContact(connector), text);
|
connector.sendMessage(widget.contact, text);
|
||||||
_textController.clear();
|
_textController.clear();
|
||||||
_textFieldFocusNode.requestFocus();
|
_textFieldFocusNode.requestFocus();
|
||||||
}
|
}
|
||||||
@@ -693,7 +654,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
|
|
||||||
// Set the path override to persist user's choice
|
// Set the path override to persist user's choice
|
||||||
await connector.setPathOverride(
|
await connector.setPathOverride(
|
||||||
_resolveContact(connector),
|
widget.contact,
|
||||||
pathLen: pathLength,
|
pathLen: pathLength,
|
||||||
pathBytes: pathBytes,
|
pathBytes: pathBytes,
|
||||||
);
|
);
|
||||||
@@ -702,7 +663,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
await _notifyPathSet(
|
await _notifyPathSet(
|
||||||
connector,
|
connector,
|
||||||
_resolveContact(connector),
|
widget.contact,
|
||||||
pathBytes,
|
pathBytes,
|
||||||
path.hopCount,
|
path.hopCount,
|
||||||
);
|
);
|
||||||
@@ -761,9 +722,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
style: const TextStyle(fontSize: 11),
|
style: const TextStyle(fontSize: 11),
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await connector.clearContactPath(
|
await connector.clearContactPath(widget.contact);
|
||||||
_resolveContact(connector),
|
|
||||||
);
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
@@ -791,7 +750,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await connector.setPathOverride(
|
await connector.setPathOverride(
|
||||||
_resolveContact(connector),
|
widget.contact,
|
||||||
pathLen: -1,
|
pathLen: -1,
|
||||||
);
|
);
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
@@ -858,7 +817,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
builder: (context) => PathTraceMapScreen(
|
builder: (context) => PathTraceMapScreen(
|
||||||
title: context.l10n.contacts_repeaterPathTrace,
|
title: context.l10n.contacts_repeaterPathTrace,
|
||||||
path: Uint8List.fromList(pathBytes),
|
path: Uint8List.fromList(pathBytes),
|
||||||
flipPathAround: true,
|
flipPathRound: true,
|
||||||
targetContact: widget.contact,
|
targetContact: widget.contact,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1027,7 +986,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
final currentPathLabel = _currentPathLabel(currentContact);
|
final currentPathLabel = _currentPathLabel(currentContact);
|
||||||
|
|
||||||
// Filter out the current contact from available contacts
|
// Filter out the current contact from available contacts
|
||||||
final availableContacts = connector.allContacts
|
final availableContacts = connector.contacts
|
||||||
.where((c) => c != widget.contact)
|
.where((c) => c != widget.contact)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@@ -1046,7 +1005,11 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
return; // Cancelled — keep existing path
|
appLogger.info(
|
||||||
|
'PathSelectionDialog was cancelled or returned null',
|
||||||
|
tag: 'ChatScreen',
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
@@ -1062,19 +1025,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
tag: 'ChatScreen',
|
tag: 'ChatScreen',
|
||||||
);
|
);
|
||||||
await connector.setPathOverride(
|
await connector.setPathOverride(
|
||||||
_resolveContact(connector),
|
widget.contact,
|
||||||
pathLen: result.length,
|
pathLen: result.length,
|
||||||
pathBytes: result,
|
pathBytes: result,
|
||||||
);
|
);
|
||||||
appLogger.info('setPathOverride completed', tag: 'ChatScreen');
|
appLogger.info('setPathOverride completed', tag: 'ChatScreen');
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await _notifyPathSet(
|
await _notifyPathSet(connector, widget.contact, result, result.length);
|
||||||
connector,
|
|
||||||
_resolveContact(connector),
|
|
||||||
result,
|
|
||||||
result.length,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openMessagePath(Message message, Contact contact) {
|
void _openMessagePath(Message message, Contact contact) {
|
||||||
@@ -1086,10 +1044,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
final String senderName;
|
final String senderName;
|
||||||
if (message.isOutgoing) {
|
if (message.isOutgoing) {
|
||||||
senderName = connector.selfName ?? context.l10n.chat_me;
|
senderName = connector.selfName ?? context.l10n.chat_me;
|
||||||
} else if (_resolveContact(connector).type == advTypeRoom) {
|
} else if (widget.contact.type == advTypeRoom) {
|
||||||
senderName = "${contact.name} [$fourByteHex]";
|
senderName = "${contact.name} [$fourByteHex]";
|
||||||
} else {
|
} else {
|
||||||
senderName = _resolveContact(connector).name;
|
senderName = widget.contact.name;
|
||||||
}
|
}
|
||||||
final pathMessage = ChannelMessage(
|
final pathMessage = ChannelMessage(
|
||||||
senderKey: null,
|
senderKey: null,
|
||||||
@@ -1152,8 +1110,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
_retryMessage(message);
|
_retryMessage(message);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (_resolveContact(context.read<MeshCoreConnector>()).type ==
|
if (widget.contact.type == advTypeRoom)
|
||||||
advTypeRoom)
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.chat),
|
leading: const Icon(Icons.chat),
|
||||||
title: Text(context.l10n.contacts_openChat),
|
title: Text(context.l10n.contacts_openChat),
|
||||||
@@ -1191,7 +1148,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
void _retryMessage(Message message) {
|
void _retryMessage(Message message) {
|
||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
// Retry using the contact's current path override setting
|
// Retry using the contact's current path override setting
|
||||||
connector.sendMessage(_resolveContact(connector), message.text);
|
connector.sendMessage(widget.contact, message.text);
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(
|
||||||
context,
|
context,
|
||||||
).showSnackBar(SnackBar(content: Text(context.l10n.chat_retryingMessage)));
|
).showSnackBar(SnackBar(content: Text(context.l10n.chat_retryingMessage)));
|
||||||
@@ -1217,8 +1174,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
|
|
||||||
// For room servers, include sender name (like channels) since multiple users
|
// For room servers, include sender name (like channels) since multiple users
|
||||||
// For 1:1 chats, sender is implicit (null)
|
// For 1:1 chats, sender is implicit (null)
|
||||||
final liveContact = _resolveContact(connector);
|
final senderName = widget.contact.type == advTypeRoom
|
||||||
final senderName = liveContact.type == advTypeRoom
|
|
||||||
? senderContact.name
|
? senderContact.name
|
||||||
: null;
|
: null;
|
||||||
final hash = ReactionHelper.computeReactionHash(
|
final hash = ReactionHelper.computeReactionHash(
|
||||||
@@ -1227,7 +1183,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
message.text,
|
message.text,
|
||||||
);
|
);
|
||||||
final reactionText = 'r:$hash:$emojiIndex';
|
final reactionText = 'r:$hash:$emojiIndex';
|
||||||
connector.sendMessage(_resolveContact(connector), reactionText);
|
connector.sendMessage(widget.contact, reactionText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+457
-295
File diff suppressed because it is too large
Load Diff
+13
-20
@@ -137,7 +137,10 @@ 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 allContacts = connector.allContacts;
|
final allContacts = <Contact>[
|
||||||
|
...connector.contacts,
|
||||||
|
...connector.discoveredContacts.where((c) => !c.isActive),
|
||||||
|
];
|
||||||
|
|
||||||
final contacts = settings.mapShowDiscoveryContacts
|
final contacts = settings.mapShowDiscoveryContacts
|
||||||
? allContacts
|
? allContacts
|
||||||
@@ -176,13 +179,20 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
|
|
||||||
// Filter by location
|
// Filter by location
|
||||||
final contactsWithLocation = filteredByKeyPrefix.where((c) {
|
final contactsWithLocation = filteredByKeyPrefix.where((c) {
|
||||||
return c.hasLocation;
|
if (!c.hasLocation) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return _checkLocationPlausibility(c.latitude!, c.longitude!);
|
||||||
}).toList();
|
}).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 = allContacts
|
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
|
||||||
@@ -1499,23 +1509,6 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.my_location),
|
|
||||||
title: Text(context.l10n.map_setAsMyLocation),
|
|
||||||
onTap: () async {
|
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
|
||||||
final successMsg = context.l10n.settings_locationUpdated;
|
|
||||||
Navigator.pop(sheetContext);
|
|
||||||
if (!connector.isConnected) return;
|
|
||||||
await connector.setNodeLocation(
|
|
||||||
lat: position.latitude,
|
|
||||||
lon: position.longitude,
|
|
||||||
);
|
|
||||||
await connector.refreshDeviceInfo();
|
|
||||||
if (!mounted) return;
|
|
||||||
messenger.showSnackBar(SnackBar(content: Text(successMsg)));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.close),
|
leading: const Icon(Icons.close),
|
||||||
title: Text(context.l10n.common_cancel),
|
title: Text(context.l10n.common_cancel),
|
||||||
|
|||||||
@@ -124,7 +124,10 @@ 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 = connector.allContacts;
|
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());
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ class PathTraceMapScreen extends StatefulWidget {
|
|||||||
final String title;
|
final String title;
|
||||||
final Uint8List path;
|
final Uint8List path;
|
||||||
final int? repeaterId;
|
final int? repeaterId;
|
||||||
final bool flipPathAround;
|
final bool flipPathRound;
|
||||||
final bool reversePathAround;
|
final bool reversePathRound;
|
||||||
final Contact? targetContact;
|
final Contact? targetContact;
|
||||||
|
|
||||||
const PathTraceMapScreen({
|
const PathTraceMapScreen({
|
||||||
@@ -61,8 +61,8 @@ class PathTraceMapScreen extends StatefulWidget {
|
|||||||
required this.title,
|
required this.title,
|
||||||
required this.path,
|
required this.path,
|
||||||
this.repeaterId,
|
this.repeaterId,
|
||||||
this.flipPathAround = false,
|
this.flipPathRound = false,
|
||||||
this.reversePathAround = false,
|
this.reversePathRound = false,
|
||||||
this.targetContact,
|
this.targetContact,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -93,7 +93,6 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
ValueKey<String> _mapKey = const ValueKey('initial');
|
ValueKey<String> _mapKey = const ValueKey('initial');
|
||||||
double _pathDistanceMeters = 0.0;
|
double _pathDistanceMeters = 0.0;
|
||||||
bool _showNodeLabels = true;
|
bool _showNodeLabels = true;
|
||||||
Contact? _targetContact;
|
|
||||||
|
|
||||||
String _formatPathPrefixes(Uint8List pathBytes) {
|
String _formatPathPrefixes(Uint8List pathBytes) {
|
||||||
return pathBytes
|
return pathBytes
|
||||||
@@ -159,16 +158,21 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final pathTmp = widget.reversePathAround
|
final Uint8List path;
|
||||||
|
|
||||||
|
Uint8List pathTmp = widget.reversePathRound
|
||||||
? Uint8List.fromList(widget.path.reversed.toList())
|
? Uint8List.fromList(widget.path.reversed.toList())
|
||||||
: widget.path;
|
: widget.path;
|
||||||
|
|
||||||
final path = widget.flipPathAround ? buildPath(pathTmp) : pathTmp;
|
if (widget.flipPathRound) {
|
||||||
|
path = buildPath(pathTmp);
|
||||||
|
} else {
|
||||||
|
path = pathTmp;
|
||||||
|
}
|
||||||
|
|
||||||
appLogger.info(
|
appLogger.info(
|
||||||
'Initiating path trace with path: ${_formatPathPrefixes(path)}',
|
'Initiating path trace with path: ${_formatPathPrefixes(path)}',
|
||||||
tag: 'PathTraceMapScreen',
|
tag: 'PathTraceMapScreen',
|
||||||
noNotify: !mounted,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
@@ -259,7 +263,10 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
Map<int, Contact> pathContacts = {};
|
Map<int, Contact> pathContacts = {};
|
||||||
final contacts = connector.allContacts;
|
final contacts = <Contact>[
|
||||||
|
...connector.contacts,
|
||||||
|
...connector.discoveredContacts,
|
||||||
|
];
|
||||||
contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
|
contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
|
||||||
for (var repeaterData in pathData) {
|
for (var repeaterData in pathData) {
|
||||||
if (listEquals(
|
if (listEquals(
|
||||||
@@ -305,21 +312,18 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
// Compute endpoint position for the target contact.
|
// Compute endpoint position for the target contact.
|
||||||
LatLng? targetPos;
|
LatLng? targetPos;
|
||||||
bool targetGuessed = false;
|
bool targetGuessed = false;
|
||||||
_targetContact = widget.targetContact;
|
final target = widget.targetContact;
|
||||||
|
if (target != null) {
|
||||||
if (_targetContact != null) {
|
if (target.hasLocation) {
|
||||||
final tc = _targetContact!;
|
targetPos = LatLng(target.latitude!, target.longitude!);
|
||||||
if (tc.hasLocation) {
|
} else if (pathData.isNotEmpty) {
|
||||||
targetPos = LatLng(tc.latitude!, tc.longitude!);
|
|
||||||
} else if (widget.path.length > 1) {
|
|
||||||
// Infer from the last hop: average GPS contacts sharing that hop.
|
// Infer from the last hop: average GPS contacts sharing that hop.
|
||||||
// For a round-trip path (flipPathAround/reversePathAround), the target-side hop
|
// For a round-trip path (flipPathRound), the target-side hop sits
|
||||||
// sits in the middle of the symmetric sequence; .last is the local side.
|
// in the middle of the symmetric sequence; .last is the local side.
|
||||||
final lastHop = widget.reversePathAround
|
final lastHop = (widget.flipPathRound && pathData.length > 1)
|
||||||
? widget.path.first
|
? pathData[(pathData.length - 1) ~/ 2]
|
||||||
: widget.path.last;
|
: pathData.last;
|
||||||
|
final peers = connector.contacts
|
||||||
final peers = connector.allContacts
|
|
||||||
.where(
|
.where(
|
||||||
(c) =>
|
(c) =>
|
||||||
c.hasLocation &&
|
c.hasLocation &&
|
||||||
@@ -335,34 +339,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
|
peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
|
||||||
peers.length;
|
peers.length;
|
||||||
const offsetDeg = 0.003;
|
const offsetDeg = 0.003;
|
||||||
final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
|
final angle = (target.publicKey[1] / 255.0) * 2 * pi;
|
||||||
targetPos = LatLng(
|
targetPos = LatLng(
|
||||||
lat + offsetDeg * cos(angle),
|
lat + offsetDeg * cos(angle),
|
||||||
lon + offsetDeg * sin(angle),
|
lon + offsetDeg * sin(angle),
|
||||||
);
|
);
|
||||||
targetGuessed = true;
|
targetGuessed = true;
|
||||||
} else if (inferredPositions.containsKey(lastHop)) {
|
|
||||||
final lat = inferredPositions[lastHop]!.latitude;
|
|
||||||
final lon = inferredPositions[lastHop]!.longitude;
|
|
||||||
const offsetDeg = 0.003;
|
|
||||||
final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
|
|
||||||
targetPos = LatLng(
|
|
||||||
lat + offsetDeg * cos(angle),
|
|
||||||
lon + offsetDeg * sin(angle),
|
|
||||||
);
|
|
||||||
targetGuessed = true;
|
|
||||||
} else {
|
|
||||||
// As a last resort, just place it at the same position as the last hop.
|
|
||||||
final contact = pathContacts[lastHop];
|
|
||||||
if (contact != null && contact.hasLocation) {
|
|
||||||
const offsetDeg = 0.003;
|
|
||||||
final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
|
|
||||||
targetPos = LatLng(
|
|
||||||
contact.latitude! + offsetDeg * cos(angle),
|
|
||||||
contact.longitude! + offsetDeg * sin(angle),
|
|
||||||
);
|
|
||||||
targetGuessed = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -371,12 +353,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
|
|
||||||
_points = <LatLng>[];
|
_points = <LatLng>[];
|
||||||
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||||
int hopLast = 0;
|
|
||||||
int hopLastLast = 0;
|
|
||||||
for (final hop in _traceData!.pathData) {
|
for (final hop in _traceData!.pathData) {
|
||||||
if (hop == hopLastLast && widget.flipPathAround) {
|
|
||||||
break; //skip duplicate hops in round-trip paths
|
|
||||||
}
|
|
||||||
final contact = _traceData!.pathContacts[hop];
|
final contact = _traceData!.pathContacts[hop];
|
||||||
if (contact != null && contact.hasLocation) {
|
if (contact != null && contact.hasLocation) {
|
||||||
_points.add(LatLng(contact.latitude!, contact.longitude!));
|
_points.add(LatLng(contact.latitude!, contact.longitude!));
|
||||||
@@ -384,14 +361,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
final inferred = inferredPositions[hop];
|
final inferred = inferredPositions[hop];
|
||||||
if (inferred != null) _points.add(inferred);
|
if (inferred != null) _points.add(inferred);
|
||||||
}
|
}
|
||||||
hopLastLast = hopLast;
|
|
||||||
hopLast = hop;
|
|
||||||
}
|
|
||||||
if (targetPos != null) {
|
|
||||||
if (_targetContact != null && _targetContact!.type == advTypeChat) {
|
|
||||||
_points.add(targetPos);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (targetPos != null) _points.add(targetPos);
|
||||||
_polylines = _points.length > 1
|
_polylines = _points.length > 1
|
||||||
? [
|
? [
|
||||||
Polyline(
|
Polyline(
|
||||||
@@ -480,8 +451,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_hasData)
|
if (_hasData) _buildMapPathTrace(context, tileCache),
|
||||||
_buildMapPathTrace(context, tileCache, _targetContact),
|
|
||||||
if (_points.isEmpty &&
|
if (_points.isEmpty &&
|
||||||
!_hasData &&
|
!_hasData &&
|
||||||
!_isLoading &&
|
!_isLoading &&
|
||||||
@@ -510,28 +480,17 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
List<Marker> _buildHopMarkers(
|
List<Marker> _buildHopMarkers(
|
||||||
List<int> pathData, {
|
List<int> pathData, {
|
||||||
required bool showLabels,
|
required bool showLabels,
|
||||||
required Contact? target,
|
|
||||||
}) {
|
}) {
|
||||||
final markers = <Marker>[];
|
final markers = <Marker>[];
|
||||||
int hopLast = 0;
|
|
||||||
int hopLastLast = 0;
|
|
||||||
for (final hop in pathData) {
|
for (final hop in pathData) {
|
||||||
final contact = _traceData!.pathContacts[hop];
|
final contact = _traceData!.pathContacts[hop];
|
||||||
final inferred = _inferredHopPositions[hop];
|
final inferred = _inferredHopPositions[hop];
|
||||||
final hasGps = contact != null && contact.hasLocation;
|
final hasGps = contact != null && contact.hasLocation;
|
||||||
if (hop == hopLastLast && widget.flipPathAround) {
|
if (!hasGps && inferred == null) continue;
|
||||||
continue; //skip duplicate hops in round-trip paths
|
|
||||||
}
|
|
||||||
if (!hasGps && inferred == null) {
|
|
||||||
hopLastLast = hopLast;
|
|
||||||
hopLast = hop;
|
|
||||||
continue; //skip hops with no GPS and no inferred position
|
|
||||||
}
|
|
||||||
final point = hasGps
|
final point = hasGps
|
||||||
? LatLng(contact.latitude!, contact.longitude!)
|
? LatLng(contact.latitude!, contact.longitude!)
|
||||||
: inferred!;
|
: inferred!;
|
||||||
final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
|
final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||||
|
|
||||||
markers.add(
|
markers.add(
|
||||||
Marker(
|
Marker(
|
||||||
point: point,
|
point: point,
|
||||||
@@ -573,8 +532,6 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
hopLastLast = hopLast;
|
|
||||||
hopLast = hop;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final selfLat = context.read<MeshCoreConnector>().selfLatitude;
|
final selfLat = context.read<MeshCoreConnector>().selfLatitude;
|
||||||
@@ -624,9 +581,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
|
|
||||||
// Add target contact endpoint marker.
|
// Add target contact endpoint marker.
|
||||||
final targetPos = _targetContactPosition;
|
final targetPos = _targetContactPosition;
|
||||||
if (targetPos != null && target != null && target.type == advTypeChat) {
|
if (targetPos != null) {
|
||||||
final isGuessed = _targetContactIsGuessed;
|
final isGuessed = _targetContactIsGuessed;
|
||||||
final targetName = target.name;
|
final targetName = widget.targetContact?.name ?? '?';
|
||||||
markers.add(
|
markers.add(
|
||||||
Marker(
|
Marker(
|
||||||
point: targetPos,
|
point: targetPos,
|
||||||
@@ -762,7 +719,6 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
Widget _buildMapPathTrace(
|
Widget _buildMapPathTrace(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
MapTileCacheService tileCache,
|
MapTileCacheService tileCache,
|
||||||
Contact? target,
|
|
||||||
) {
|
) {
|
||||||
return FlutterMap(
|
return FlutterMap(
|
||||||
key: _mapKey,
|
key: _mapKey,
|
||||||
@@ -801,7 +757,6 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
markers: _buildHopMarkers(
|
markers: _buildHopMarkers(
|
||||||
_traceData!.pathData,
|
_traceData!.pathData,
|
||||||
showLabels: _showNodeLabels,
|
showLabels: _showNodeLabels,
|
||||||
target: target,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../services/app_settings_service.dart';
|
|
||||||
import '../utils/platform_info.dart';
|
import '../utils/platform_info.dart';
|
||||||
import '../widgets/adaptive_app_bar_title.dart';
|
import '../widgets/adaptive_app_bar_title.dart';
|
||||||
import 'contacts_screen.dart';
|
import 'contacts_screen.dart';
|
||||||
@@ -28,14 +27,8 @@ class _TcpScreenState extends State<TcpScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_hostController = TextEditingController(
|
_hostController = TextEditingController();
|
||||||
text: context.read<AppSettingsService>().settings.tcpServerAddress,
|
_portController = TextEditingController(text: '5000');
|
||||||
);
|
|
||||||
_portController = TextEditingController(
|
|
||||||
text: context.read<AppSettingsService>().settings.tcpServerPort > 0
|
|
||||||
? context.read<AppSettingsService>().settings.tcpServerPort.toString()
|
|
||||||
: '',
|
|
||||||
);
|
|
||||||
_connector = context.read<MeshCoreConnector>();
|
_connector = context.read<MeshCoreConnector>();
|
||||||
|
|
||||||
_connectionListener = () {
|
_connectionListener = () {
|
||||||
@@ -46,12 +39,6 @@ class _TcpScreenState extends State<TcpScreen> {
|
|||||||
if (_connector.state == MeshCoreConnectionState.connected &&
|
if (_connector.state == MeshCoreConnectionState.connected &&
|
||||||
_connector.isTcpTransportConnected &&
|
_connector.isTcpTransportConnected &&
|
||||||
!_navigatedToContacts) {
|
!_navigatedToContacts) {
|
||||||
context.read<AppSettingsService>().setTcpServerAddress(
|
|
||||||
_hostController.text,
|
|
||||||
);
|
|
||||||
context.read<AppSettingsService>().setTcpServerPort(
|
|
||||||
int.tryParse(_portController.text) ?? 0,
|
|
||||||
);
|
|
||||||
_navigatedToContacts = true;
|
_navigatedToContacts = true;
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ class AppDebugLogService extends ChangeNotifier {
|
|||||||
String message, {
|
String message, {
|
||||||
String tag = 'App',
|
String tag = 'App',
|
||||||
AppDebugLogLevel level = AppDebugLogLevel.info,
|
AppDebugLogLevel level = AppDebugLogLevel.info,
|
||||||
bool noNotify = false,
|
|
||||||
}) {
|
}) {
|
||||||
if (!_enabled && !kDebugMode) return;
|
if (!_enabled && !kDebugMode) return;
|
||||||
if (!_enabled) {
|
if (!_enabled) {
|
||||||
@@ -73,24 +72,22 @@ class AppDebugLogService extends ChangeNotifier {
|
|||||||
_entries.removeRange(0, _entries.length - maxEntries);
|
_entries.removeRange(0, _entries.length - maxEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!noNotify) {
|
notifyListeners();
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also print to console for development
|
// Also print to console for development
|
||||||
debugPrint('[$tag] $message');
|
debugPrint('[$tag] $message');
|
||||||
}
|
}
|
||||||
|
|
||||||
void info(String message, {String tag = 'App', bool noNotify = false}) {
|
void info(String message, {String tag = 'App'}) {
|
||||||
log(message, tag: tag, level: AppDebugLogLevel.info, noNotify: noNotify);
|
log(message, tag: tag, level: AppDebugLogLevel.info);
|
||||||
}
|
}
|
||||||
|
|
||||||
void warn(String message, {String tag = 'App', bool noNotify = false}) {
|
void warn(String message, {String tag = 'App'}) {
|
||||||
log(message, tag: tag, level: AppDebugLogLevel.warning, noNotify: noNotify);
|
log(message, tag: tag, level: AppDebugLogLevel.warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
void error(String message, {String tag = 'App', bool noNotify = false}) {
|
void error(String message, {String tag = 'App'}) {
|
||||||
log(message, tag: tag, level: AppDebugLogLevel.error, noNotify: noNotify);
|
log(message, tag: tag, level: AppDebugLogLevel.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
|
|||||||
@@ -182,12 +182,4 @@ class AppSettingsService extends ChangeNotifier {
|
|||||||
..remove(channelName);
|
..remove(channelName);
|
||||||
await updateSettings(_settings.copyWith(mutedChannels: updated));
|
await updateSettings(_settings.copyWith(mutedChannels: updated));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setTcpServerAddress(String value) async {
|
|
||||||
await updateSettings(_settings.copyWith(tcpServerAddress: value));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setTcpServerPort(int value) async {
|
|
||||||
await updateSettings(_settings.copyWith(tcpServerPort: value));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class ChatTextScaleService extends ChangeNotifier {
|
|||||||
|
|
||||||
void _commitScale() {
|
void _commitScale() {
|
||||||
_saveTimer?.cancel();
|
_saveTimer?.cancel();
|
||||||
PrefsManager.instance.setDouble(_prefKey, _scale);
|
unawaited(PrefsManager.instance.setDouble(_prefKey, _scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
double _clamp(double value) => value.clamp(_minScale, _maxScale).toDouble();
|
double _clamp(double value) => value.clamp(_minScale, _maxScale).toDouble();
|
||||||
|
|||||||
@@ -44,12 +44,6 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
[]; // Rolling buffer of recent ACK hashes
|
[]; // Rolling buffer of recent ACK hashes
|
||||||
final Map<String, List<String>> _pendingMessageQueuePerContact =
|
final Map<String, List<String>> _pendingMessageQueuePerContact =
|
||||||
{}; // contactPubKeyHex → FIFO queue of messageIds (DEPRECATED - will be removed)
|
{}; // contactPubKeyHex → FIFO queue of messageIds (DEPRECATED - will be removed)
|
||||||
final Map<String, List<String>> _sendQueue =
|
|
||||||
{}; // contactPubKeyHex → ordered list of messageIds awaiting send
|
|
||||||
final Set<String> _activeMessages =
|
|
||||||
{}; // messageIds currently in-flight (sent/retrying)
|
|
||||||
final Set<String> _resolvedMessages =
|
|
||||||
{}; // messageIds already resolved (prevents double _onMessageResolved)
|
|
||||||
final Map<String, String> _expectedHashToMessageId =
|
final Map<String, String> _expectedHashToMessageId =
|
||||||
{}; // expectedAckHashHex → messageId (for matching RESP_CODE_SENT by hash)
|
{}; // expectedAckHashHex → messageId (for matching RESP_CODE_SENT by hash)
|
||||||
|
|
||||||
@@ -162,49 +156,7 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
_addMessageCallback!(contact.publicKeyHex, message);
|
_addMessageCallback!(contact.publicKeyHex, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue per contact — only one message in-flight at a time to avoid
|
await _attemptSend(messageId);
|
||||||
// overflowing the firmware's 8-entry expected_ack_table.
|
|
||||||
final contactKey = contact.publicKeyHex;
|
|
||||||
_sendQueue[contactKey] ??= [];
|
|
||||||
_sendQueue[contactKey]!.add(messageId);
|
|
||||||
|
|
||||||
if (!_activeMessages.any(
|
|
||||||
(id) => _pendingContacts[id]?.publicKeyHex == contactKey,
|
|
||||||
)) {
|
|
||||||
_sendNextForContact(contactKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _sendNextForContact(String contactKey) {
|
|
||||||
final queue = _sendQueue[contactKey];
|
|
||||||
if (queue == null) return;
|
|
||||||
|
|
||||||
// Drain stale entries iteratively instead of recursing.
|
|
||||||
while (queue.isNotEmpty) {
|
|
||||||
final messageId = queue.removeAt(0);
|
|
||||||
if (_pendingMessages.containsKey(messageId)) {
|
|
||||||
_activeMessages.add(messageId);
|
|
||||||
_attemptSend(messageId).catchError((e) {
|
|
||||||
debugPrint('_attemptSend threw for $messageId: $e');
|
|
||||||
final msg = _pendingMessages[messageId];
|
|
||||||
if (msg != null) {
|
|
||||||
final failed = msg.copyWith(status: MessageStatus.failed);
|
|
||||||
_pendingMessages[messageId] = failed;
|
|
||||||
_updateMessageCallback?.call(failed);
|
|
||||||
}
|
|
||||||
_onMessageResolved(messageId, contactKey);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Message was cancelled/cleaned up while queued — try next
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onMessageResolved(String messageId, String contactKey) {
|
|
||||||
if (_resolvedMessages.contains(messageId)) return;
|
|
||||||
_resolvedMessages.add(messageId);
|
|
||||||
_activeMessages.remove(messageId);
|
|
||||||
_sendNextForContact(contactKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _attemptSend(String messageId) async {
|
Future<void> _attemptSend(String messageId) async {
|
||||||
@@ -217,11 +169,13 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
// Use the path that was captured when the message was first sent
|
// Use the path that was captured when the message was first sent
|
||||||
if (_setContactPathCallback != null && _clearContactPathCallback != null) {
|
if (_setContactPathCallback != null && _clearContactPathCallback != null) {
|
||||||
if (message.pathLength != null && message.pathLength! < 0) {
|
if (message.pathLength != null && message.pathLength! < 0) {
|
||||||
|
// Flood mode - clear the path
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'Setting flood mode for retry attempt ${message.retryCount}',
|
'Setting flood mode for retry attempt ${message.retryCount}',
|
||||||
);
|
);
|
||||||
await _clearContactPathCallback!(contact);
|
_clearContactPathCallback!(contact);
|
||||||
} else if (message.pathLength != null && message.pathLength! >= 0) {
|
} else if (message.pathLength != null && message.pathLength! >= 0) {
|
||||||
|
// Specific path (including direct neighbor with pathLength=0)
|
||||||
final pathStr = message.pathBytes.isEmpty
|
final pathStr = message.pathBytes.isEmpty
|
||||||
? 'direct'
|
? 'direct'
|
||||||
: message.pathBytes
|
: message.pathBytes
|
||||||
@@ -238,24 +192,6 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-validate after async gap — a timer or ACK could have resolved/retried
|
|
||||||
// this message while we were awaiting the path callback.
|
|
||||||
final currentMessage = _pendingMessages[messageId];
|
|
||||||
if (currentMessage == null || _resolvedMessages.contains(messageId)) {
|
|
||||||
debugPrint(
|
|
||||||
'_attemptSend: message $messageId resolved during path sync, aborting',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// If the message was retried by a timer during our await, the retryCount
|
|
||||||
// will have advanced. Only proceed if it still matches the attempt we started.
|
|
||||||
if (currentMessage.retryCount != message.retryCount) {
|
|
||||||
debugPrint(
|
|
||||||
'_attemptSend: message $messageId retryCount changed during path sync, aborting',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final attempt = message.retryCount.clamp(0, 3);
|
final attempt = message.retryCount.clamp(0, 3);
|
||||||
final timestampSeconds = message.timestamp.millisecondsSinceEpoch ~/ 1000;
|
final timestampSeconds = message.timestamp.millisecondsSinceEpoch ~/ 1000;
|
||||||
|
|
||||||
@@ -295,15 +231,6 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
|
|
||||||
if (_sendMessageCallback != null) {
|
if (_sendMessageCallback != null) {
|
||||||
_sendMessageCallback!(contact, message.text, attempt, timestampSeconds);
|
_sendMessageCallback!(contact, message.text, attempt, timestampSeconds);
|
||||||
} else {
|
|
||||||
// No send callback — message would be stuck forever. Fail it immediately.
|
|
||||||
debugPrint(
|
|
||||||
'_attemptSend: no sendMessageCallback, failing message $messageId',
|
|
||||||
);
|
|
||||||
final failedMessage = message.copyWith(status: MessageStatus.failed);
|
|
||||||
_pendingMessages[messageId] = failedMessage;
|
|
||||||
_updateMessageCallback?.call(failedMessage);
|
|
||||||
_onMessageResolved(messageId, contact.publicKeyHex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +281,6 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FALLBACK: Old queue-based matching (for messages sent before hash computation was added)
|
// FALLBACK: Old queue-based matching (for messages sent before hash computation was added)
|
||||||
// Only match within a single contact's queue to avoid cross-contact mismatches.
|
|
||||||
if (messageId == null && allowQueueFallback) {
|
if (messageId == null && allowQueueFallback) {
|
||||||
_debugLogService?.warn(
|
_debugLogService?.warn(
|
||||||
'RESP_CODE_SENT: ACK hash $ackHashHex not found in hash table, falling back to queue',
|
'RESP_CODE_SENT: ACK hash $ackHashHex not found in hash table, falling back to queue',
|
||||||
@@ -364,16 +290,13 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
'Hash-based match failed for $ackHashHex, falling back to queue-based matching',
|
'Hash-based match failed for $ackHashHex, falling back to queue-based matching',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Search all contact queues so concurrent chats don't miss matches.
|
for (var entry in _pendingMessageQueuePerContact.entries) {
|
||||||
final queuesToSearch = _pendingMessageQueuePerContact;
|
|
||||||
|
|
||||||
for (var entry in queuesToSearch.entries) {
|
|
||||||
final contactKey = entry.key;
|
final contactKey = entry.key;
|
||||||
final queue = entry.value;
|
final queue = entry.value;
|
||||||
|
|
||||||
// Drain stale entries until we find a valid one or exhaust the queue.
|
if (queue.isNotEmpty) {
|
||||||
while (queue.isNotEmpty) {
|
|
||||||
final candidateMessageId = queue.removeAt(0);
|
final candidateMessageId = queue.removeAt(0);
|
||||||
|
|
||||||
if (_pendingMessages.containsKey(candidateMessageId)) {
|
if (_pendingMessages.containsKey(candidateMessageId)) {
|
||||||
messageId = candidateMessageId;
|
messageId = candidateMessageId;
|
||||||
contact = _pendingContacts[candidateMessageId];
|
contact = _pendingContacts[candidateMessageId];
|
||||||
@@ -381,10 +304,21 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
'Queue-based match (fallback): $ackHashHex → message $messageId for $contactKey',
|
'Queue-based match (fallback): $ackHashHex → message $messageId for $contactKey',
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
} else {
|
||||||
|
debugPrint('Dequeued stale message $candidateMessageId - skipping');
|
||||||
|
if (queue.isNotEmpty) {
|
||||||
|
final nextMessageId = queue.removeAt(0);
|
||||||
|
if (_pendingMessages.containsKey(nextMessageId)) {
|
||||||
|
messageId = nextMessageId;
|
||||||
|
contact = _pendingContacts[nextMessageId];
|
||||||
|
debugPrint(
|
||||||
|
'Queue-based match (fallback): $ackHashHex → message $messageId',
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
debugPrint('Dequeued stale message $candidateMessageId - skipping');
|
|
||||||
}
|
}
|
||||||
if (messageId != null) break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,7 +463,22 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
} else {
|
} else {
|
||||||
// Max retries reached - mark as failed
|
// Max retries reached - mark as failed
|
||||||
final failedMessage = message.copyWith(status: MessageStatus.failed);
|
final failedMessage = message.copyWith(status: MessageStatus.failed);
|
||||||
_pendingMessages[messageId] = failedMessage;
|
|
||||||
|
// Move ACK hashes to history before removing
|
||||||
|
_moveAckHashesToHistory(messageId);
|
||||||
|
|
||||||
|
_pendingMessages.remove(messageId);
|
||||||
|
_pendingContacts.remove(messageId);
|
||||||
|
_pendingPathSelections.remove(messageId);
|
||||||
|
_timeoutTimers[messageId]?.cancel();
|
||||||
|
_timeoutTimers.remove(messageId);
|
||||||
|
|
||||||
|
// Clean up the queue entry for this contact
|
||||||
|
_pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(messageId);
|
||||||
|
if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ??
|
||||||
|
false) {
|
||||||
|
_pendingMessageQueuePerContact.remove(contact.publicKeyHex);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we should clear the path on max retry
|
// Check if we should clear the path on max retry
|
||||||
if (_appSettingsService?.settings.clearPathOnMaxRetry == true &&
|
if (_appSettingsService?.settings.clearPathOnMaxRetry == true &&
|
||||||
@@ -550,30 +499,6 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
// Message is done retrying — send next queued message for this contact
|
|
||||||
_onMessageResolved(messageId, contact.publicKeyHex);
|
|
||||||
|
|
||||||
// Keep message in pending maps for 30s grace period so late ACKs
|
|
||||||
// can still match and update the message to delivered.
|
|
||||||
_timeoutTimers[messageId] = Timer(const Duration(seconds: 30), () {
|
|
||||||
_moveAckHashesToHistory(messageId);
|
|
||||||
// Clean up ALL hash mappings for this message
|
|
||||||
_ackHashToMessageId.removeWhere(
|
|
||||||
(_, mapping) => mapping.messageId == messageId,
|
|
||||||
);
|
|
||||||
_expectedHashToMessageId.removeWhere((_, msgId) => msgId == messageId);
|
|
||||||
_pendingMessages.remove(messageId);
|
|
||||||
_pendingContacts.remove(messageId);
|
|
||||||
_pendingPathSelections.remove(messageId);
|
|
||||||
_timeoutTimers.remove(messageId);
|
|
||||||
_resolvedMessages.remove(messageId);
|
|
||||||
final contactKey = contact.publicKeyHex;
|
|
||||||
_pendingMessageQueuePerContact[contactKey]?.remove(messageId);
|
|
||||||
if (_pendingMessageQueuePerContact[contactKey]?.isEmpty ?? false) {
|
|
||||||
_pendingMessageQueuePerContact.remove(contactKey);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,15 +594,7 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (matchedMessageId != null) {
|
if (matchedMessageId != null) {
|
||||||
final message = _pendingMessages[matchedMessageId];
|
final message = _pendingMessages[matchedMessageId]!;
|
||||||
if (message == null) {
|
|
||||||
// Message was already cleaned up (e.g. grace period expired)
|
|
||||||
_ackHashToMessageId.remove(ackHashHex);
|
|
||||||
debugPrint(
|
|
||||||
'ACK matched $matchedMessageId but message already cleaned up',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final contact = _pendingContacts[matchedMessageId];
|
final contact = _pendingContacts[matchedMessageId];
|
||||||
final selection = _pendingPathSelections[matchedMessageId];
|
final selection = _pendingPathSelections[matchedMessageId];
|
||||||
|
|
||||||
@@ -699,21 +616,12 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
tripTimeMs: tripTimeMs,
|
tripTimeMs: tripTimeMs,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Clean up ALL hash mappings for this message (from all retry attempts)
|
|
||||||
_ackHashToMessageId.removeWhere(
|
|
||||||
(_, mapping) => mapping.messageId == matchedMessageId,
|
|
||||||
);
|
|
||||||
_expectedHashToMessageId.removeWhere(
|
|
||||||
(_, msgId) => msgId == matchedMessageId,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Move ACK hashes to history before removing
|
// Move ACK hashes to history before removing
|
||||||
_moveAckHashesToHistory(matchedMessageId);
|
_moveAckHashesToHistory(matchedMessageId);
|
||||||
|
|
||||||
_pendingMessages.remove(matchedMessageId);
|
_pendingMessages.remove(matchedMessageId);
|
||||||
_pendingContacts.remove(matchedMessageId);
|
_pendingContacts.remove(matchedMessageId);
|
||||||
_pendingPathSelections.remove(matchedMessageId);
|
_pendingPathSelections.remove(matchedMessageId);
|
||||||
_resolvedMessages.remove(matchedMessageId);
|
|
||||||
|
|
||||||
// Clean up the queue entry for this contact (remove any remaining references to this message)
|
// Clean up the queue entry for this contact (remove any remaining references to this message)
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
@@ -738,7 +646,6 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
true,
|
true,
|
||||||
tripTimeMs,
|
tripTimeMs,
|
||||||
);
|
);
|
||||||
_onMessageResolved(matchedMessageId, contact.publicKeyHex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -876,9 +783,6 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
_ackHistory.clear();
|
_ackHistory.clear();
|
||||||
_ackHashToMessageId.clear();
|
_ackHashToMessageId.clear();
|
||||||
_pendingMessageQueuePerContact.clear();
|
_pendingMessageQueuePerContact.clear();
|
||||||
_sendQueue.clear();
|
|
||||||
_activeMessages.clear();
|
|
||||||
_resolvedMessages.clear();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,9 +232,7 @@ class NotificationService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await _notifications.show(
|
await _notifications.show(
|
||||||
id: contactId != null
|
id: contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
|
||||||
? 'advert:$contactId'.hashCode
|
|
||||||
: DateTime.now().millisecondsSinceEpoch,
|
|
||||||
title: _l10n.notification_newTypeDiscovered(contactType),
|
title: _l10n.notification_newTypeDiscovered(contactType),
|
||||||
body: contactName,
|
body: contactName,
|
||||||
notificationDetails: notificationDetails,
|
notificationDetails: notificationDetails,
|
||||||
@@ -333,61 +331,6 @@ class NotificationService {
|
|||||||
await _notifications.cancel(id: id);
|
await _notifications.cancel(id: id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cancel the notification for a specific contact and update the app badge.
|
|
||||||
Future<void> clearContactNotification(
|
|
||||||
String contactId,
|
|
||||||
int totalUnreadCount,
|
|
||||||
) async {
|
|
||||||
if (!await _ensureInitialized()) return;
|
|
||||||
await _notifications.cancel(id: contactId.hashCode);
|
|
||||||
await _updateBadge(totalUnreadCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Cancel the notification for a specific channel and update the app badge.
|
|
||||||
Future<void> clearChannelNotification(
|
|
||||||
int channelIndex,
|
|
||||||
int totalUnreadCount,
|
|
||||||
) async {
|
|
||||||
if (!await _ensureInitialized()) return;
|
|
||||||
await _notifications.cancel(id: channelIndex.hashCode);
|
|
||||||
await _updateBadge(totalUnreadCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Cancel advert notifications for the given contact public key hexes.
|
|
||||||
Future<void> clearAdvertNotifications(List<String> contactIds) async {
|
|
||||||
if (!await _ensureInitialized()) return;
|
|
||||||
for (final id in contactIds) {
|
|
||||||
await _notifications.cancel(id: 'advert:$id'.hashCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _updateBadge(int count) async {
|
|
||||||
if (PlatformInfo.isIOS || PlatformInfo.isMacOS) {
|
|
||||||
// On Apple platforms, set the badge number directly via a silent update.
|
|
||||||
final darwinDetails = DarwinNotificationDetails(
|
|
||||||
presentAlert: false,
|
|
||||||
presentSound: false,
|
|
||||||
presentBadge: true,
|
|
||||||
badgeNumber: count,
|
|
||||||
);
|
|
||||||
final details = NotificationDetails(
|
|
||||||
iOS: darwinDetails,
|
|
||||||
macOS: darwinDetails,
|
|
||||||
);
|
|
||||||
// Use a fixed ID so each update replaces the previous one.
|
|
||||||
await _notifications.show(
|
|
||||||
id: 'badge_update'.hashCode,
|
|
||||||
title: null,
|
|
||||||
body: null,
|
|
||||||
notificationDetails: details,
|
|
||||||
);
|
|
||||||
// Immediately cancel the silent notification so it doesn't appear in tray.
|
|
||||||
await _notifications.cancel(id: 'badge_update'.hashCode);
|
|
||||||
}
|
|
||||||
// On Android, badge count is derived from active notifications,
|
|
||||||
// so cancelling the specific notification above is sufficient.
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────
|
||||||
// Public notification methods (rate limiting is enforced automatically)
|
// Public notification methods (rate limiting is enforced automatically)
|
||||||
// ─────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import '../storage/prefs_manager.dart';
|
||||||
|
import '../utils/contact_search.dart';
|
||||||
|
|
||||||
|
const String contactsAllGroupsValue = '__all__';
|
||||||
|
|
||||||
|
enum ChannelSortOption { manual, name, latestMessages, unread }
|
||||||
|
|
||||||
|
class UiViewStateService extends ChangeNotifier {
|
||||||
|
static const _keyContactsSelectedGroupName = 'ui_contacts_selected_group';
|
||||||
|
static const _keyContactsSortOption = 'ui_contacts_sort_option';
|
||||||
|
static const _keyContactsShowUnreadOnly = 'ui_contacts_show_unread_only';
|
||||||
|
static const _keyContactsTypeFilter = 'ui_contacts_type_filter';
|
||||||
|
static const _keyChannelsSortOption = 'ui_channels_sort_option';
|
||||||
|
static const _keyChannelsSortIndexLegacy = 'ui_channels_sort_index';
|
||||||
|
|
||||||
|
String _contactsSelectedGroupName = contactsAllGroupsValue;
|
||||||
|
String _contactsSearchText = '';
|
||||||
|
bool _contactsSearchExpanded = false;
|
||||||
|
ContactSortOption _contactsSortOption = ContactSortOption.lastSeen;
|
||||||
|
bool _contactsShowUnreadOnly = false;
|
||||||
|
ContactTypeFilter _contactsTypeFilter = ContactTypeFilter.all;
|
||||||
|
|
||||||
|
String _channelsSearchText = '';
|
||||||
|
ChannelSortOption _channelsSortOption = ChannelSortOption.manual;
|
||||||
|
|
||||||
|
String get contactsSelectedGroupName => _contactsSelectedGroupName;
|
||||||
|
String get contactsSearchText => _contactsSearchText;
|
||||||
|
bool get contactsSearchExpanded => _contactsSearchExpanded;
|
||||||
|
ContactSortOption get contactsSortOption => _contactsSortOption;
|
||||||
|
bool get contactsShowUnreadOnly => _contactsShowUnreadOnly;
|
||||||
|
ContactTypeFilter get contactsTypeFilter => _contactsTypeFilter;
|
||||||
|
String get channelsSearchText => _channelsSearchText;
|
||||||
|
ChannelSortOption get channelsSortOption => _channelsSortOption;
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
final prefs = PrefsManager.instance;
|
||||||
|
|
||||||
|
final selectedGroupName = prefs.getString(_keyContactsSelectedGroupName);
|
||||||
|
if (selectedGroupName != null && selectedGroupName.isNotEmpty) {
|
||||||
|
_contactsSelectedGroupName = selectedGroupName;
|
||||||
|
}
|
||||||
|
|
||||||
|
final sortStr = prefs.getString(_keyContactsSortOption);
|
||||||
|
if (sortStr != null) {
|
||||||
|
_contactsSortOption = ContactSortOption.values.firstWhere(
|
||||||
|
(e) => e.name == sortStr,
|
||||||
|
orElse: () => ContactSortOption.lastSeen,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_contactsShowUnreadOnly =
|
||||||
|
prefs.getBool(_keyContactsShowUnreadOnly) ?? false;
|
||||||
|
|
||||||
|
final typeStr = prefs.getString(_keyContactsTypeFilter);
|
||||||
|
if (typeStr != null) {
|
||||||
|
_contactsTypeFilter = ContactTypeFilter.values.firstWhere(
|
||||||
|
(e) => e.name == typeStr,
|
||||||
|
orElse: () => ContactTypeFilter.all,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final channelSortStr = prefs.getString(_keyChannelsSortOption);
|
||||||
|
if (channelSortStr != null) {
|
||||||
|
_channelsSortOption = ChannelSortOption.values.firstWhere(
|
||||||
|
(e) => e.name == channelSortStr,
|
||||||
|
orElse: () => ChannelSortOption.manual,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backward compatibility for old persisted index format.
|
||||||
|
switch (prefs.getInt(_keyChannelsSortIndexLegacy) ?? 0) {
|
||||||
|
case 0:
|
||||||
|
_channelsSortOption = ChannelSortOption.manual;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
_channelsSortOption = ChannelSortOption.name;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
_channelsSortOption = ChannelSortOption.latestMessages;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
_channelsSortOption = ChannelSortOption.unread;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
_channelsSortOption = ChannelSortOption.manual;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setContactsSelectedGroupName(String value) {
|
||||||
|
if (_contactsSelectedGroupName == value) return;
|
||||||
|
_contactsSelectedGroupName = value;
|
||||||
|
notifyListeners();
|
||||||
|
unawaited(
|
||||||
|
PrefsManager.instance.setString(_keyContactsSelectedGroupName, value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setContactsSearchText(String value) {
|
||||||
|
if (_contactsSearchText == value) return;
|
||||||
|
_contactsSearchText = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setContactsSearchExpanded(bool value) {
|
||||||
|
if (_contactsSearchExpanded == value) return;
|
||||||
|
_contactsSearchExpanded = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setContactsSortOption(ContactSortOption value) {
|
||||||
|
if (_contactsSortOption == value) return;
|
||||||
|
_contactsSortOption = value;
|
||||||
|
notifyListeners();
|
||||||
|
unawaited(
|
||||||
|
PrefsManager.instance.setString(_keyContactsSortOption, value.name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setContactsShowUnreadOnly(bool value) {
|
||||||
|
if (_contactsShowUnreadOnly == value) return;
|
||||||
|
_contactsShowUnreadOnly = value;
|
||||||
|
notifyListeners();
|
||||||
|
unawaited(PrefsManager.instance.setBool(_keyContactsShowUnreadOnly, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setContactsTypeFilter(ContactTypeFilter value) {
|
||||||
|
if (_contactsTypeFilter == value) return;
|
||||||
|
_contactsTypeFilter = value;
|
||||||
|
notifyListeners();
|
||||||
|
unawaited(
|
||||||
|
PrefsManager.instance.setString(_keyContactsTypeFilter, value.name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setChannelsSearchText(String value) {
|
||||||
|
if (_channelsSearchText == value) return;
|
||||||
|
_channelsSearchText = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setChannelsSortOption(ChannelSortOption value) {
|
||||||
|
if (_channelsSortOption == value) return;
|
||||||
|
_channelsSortOption = value;
|
||||||
|
notifyListeners();
|
||||||
|
unawaited(
|
||||||
|
PrefsManager.instance.setString(_keyChannelsSortOption, value.name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -189,10 +189,6 @@ class UsbSerialService {
|
|||||||
serial.setStopBits1();
|
serial.setStopBits1();
|
||||||
serial.setFlowControlNone();
|
serial.setFlowControlNone();
|
||||||
serial.setRTS(false);
|
serial.setRTS(false);
|
||||||
// Toggle DTR low→high so the device sees a fresh connection even
|
|
||||||
// if the previous disconnect didn't cleanly signal DTR drop.
|
|
||||||
serial.setDTR(false);
|
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
|
||||||
serial.setDTR(true);
|
serial.setDTR(true);
|
||||||
_serial = serial;
|
_serial = serial;
|
||||||
// Update the normalized port name to whichever candidate succeeded.
|
// Update the normalized port name to whichever candidate succeeded.
|
||||||
@@ -253,21 +249,6 @@ class UsbSerialService {
|
|||||||
_status = UsbSerialStatus.connected;
|
_status = UsbSerialStatus.connected;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> writeRaw(Uint8List data) async {
|
|
||||||
if (!isConnected) {
|
|
||||||
throw StateError('USB serial port is not open');
|
|
||||||
}
|
|
||||||
if (_useAndroidUsbHost) {
|
|
||||||
try {
|
|
||||||
await _androidMethodChannel.invokeMethod<void>('write', {'data': data});
|
|
||||||
} on PlatformException catch (error) {
|
|
||||||
throw StateError(error.message ?? error.code);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_serial!.write(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> write(Uint8List data) async {
|
Future<void> write(Uint8List data) async {
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
throw StateError('USB serial port is not open');
|
throw StateError('USB serial port is not open');
|
||||||
@@ -319,7 +300,6 @@ class UsbSerialService {
|
|||||||
_serial = null;
|
_serial = null;
|
||||||
try {
|
try {
|
||||||
if (serial?.isOpen() == FlOpenStatus.open) {
|
if (serial?.isOpen() == FlOpenStatus.open) {
|
||||||
serial?.setDTR(false);
|
|
||||||
serial?.closePort();
|
serial?.closePort();
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -370,7 +350,6 @@ class UsbSerialService {
|
|||||||
final serial = _serial;
|
final serial = _serial;
|
||||||
try {
|
try {
|
||||||
if (serial?.isOpen() == FlOpenStatus.open) {
|
if (serial?.isOpen() == FlOpenStatus.open) {
|
||||||
serial?.setDTR(false);
|
|
||||||
serial?.closePort(); // synchronous C call — kills the SerialThread
|
serial?.closePort(); // synchronous C call — kills the SerialThread
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|||||||
@@ -127,17 +127,6 @@ class UsbSerialService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> writeRaw(Uint8List data) async {
|
|
||||||
if (!isConnected || _writer == null) {
|
|
||||||
throw StateError('USB serial port is not open');
|
|
||||||
}
|
|
||||||
final promise = _writer!.callMethod<JSPromise<JSAny?>>(
|
|
||||||
'write'.toJS,
|
|
||||||
data.toJS,
|
|
||||||
);
|
|
||||||
await promise.toDart;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> write(Uint8List data) async {
|
Future<void> write(Uint8List data) async {
|
||||||
if (!isConnected || _writer == null) {
|
if (!isConnected || _writer == null) {
|
||||||
throw StateError('USB serial port is not open');
|
throw StateError('USB serial port is not open');
|
||||||
|
|||||||
@@ -23,23 +23,23 @@ class AppLogger {
|
|||||||
bool get isEnabled => _enabled;
|
bool get isEnabled => _enabled;
|
||||||
|
|
||||||
/// Log an info message
|
/// Log an info message
|
||||||
void info(String message, {String tag = 'App', bool noNotify = false}) {
|
void info(String message, {String tag = 'App'}) {
|
||||||
if (_enabled && _service != null) {
|
if (_enabled && _service != null) {
|
||||||
_service!.info(message, tag: tag, noNotify: noNotify);
|
_service!.info(message, tag: tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Log a warning message
|
/// Log a warning message
|
||||||
void warn(String message, {String tag = 'App', bool noNotify = false}) {
|
void warn(String message, {String tag = 'App'}) {
|
||||||
if (_enabled && _service != null) {
|
if (_enabled && _service != null) {
|
||||||
_service!.warn(message, tag: tag, noNotify: noNotify);
|
_service!.warn(message, tag: tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Log an error message
|
/// Log an error message
|
||||||
void error(String message, {String tag = 'App', bool noNotify = false}) {
|
void error(String message, {String tag = 'App'}) {
|
||||||
if (_enabled && _service != null) {
|
if (_enabled && _service != null) {
|
||||||
_service!.error(message, tag: tag, noNotify: noNotify);
|
_service!.error(message, tag: tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,10 +48,9 @@ class AppLogger {
|
|||||||
String message, {
|
String message, {
|
||||||
String tag = 'App',
|
String tag = 'App',
|
||||||
AppDebugLogLevel level = AppDebugLogLevel.info,
|
AppDebugLogLevel level = AppDebugLogLevel.info,
|
||||||
bool noNotify = false,
|
|
||||||
}) {
|
}) {
|
||||||
if (_enabled && _service != null) {
|
if (_enabled && _service != null) {
|
||||||
_service!.log(message, tag: tag, level: level, noNotify: noNotify);
|
_service!.log(message, tag: tag, level: level);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
enum ContactSortOption { lastSeen, recentMessages, name }
|
||||||
|
|
||||||
|
enum ContactTypeFilter { all, favorites, users, repeaters, rooms }
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
|
|
||||||
|
export 'contact_filter_types.dart';
|
||||||
|
|
||||||
bool matchesContactQuery(Contact contact, String query) {
|
bool matchesContactQuery(Contact contact, String query) {
|
||||||
final normalizedQuery = query.trim().toLowerCase();
|
final normalizedQuery = query.trim().toLowerCase();
|
||||||
if (normalizedQuery.isEmpty) return true;
|
if (normalizedQuery.isEmpty) return true;
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
import '../utils/contact_search.dart';
|
||||||
|
|
||||||
enum ContactSortOption { lastSeen, recentMessages, name }
|
class SortFilterMenuOption<T> {
|
||||||
|
final T value;
|
||||||
enum ContactTypeFilter { all, favorites, users, repeaters, rooms }
|
|
||||||
|
|
||||||
class SortFilterMenuOption {
|
|
||||||
final int value;
|
|
||||||
final String label;
|
final String label;
|
||||||
final bool? checked;
|
final bool? checked;
|
||||||
|
|
||||||
@@ -17,16 +14,16 @@ class SortFilterMenuOption {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class SortFilterMenuSection {
|
class SortFilterMenuSection<T> {
|
||||||
final String title;
|
final String title;
|
||||||
final List<SortFilterMenuOption> options;
|
final List<SortFilterMenuOption<T>> options;
|
||||||
|
|
||||||
const SortFilterMenuSection({required this.title, required this.options});
|
const SortFilterMenuSection({required this.title, required this.options});
|
||||||
}
|
}
|
||||||
|
|
||||||
class SortFilterMenu extends StatelessWidget {
|
class SortFilterMenu<T> extends StatelessWidget {
|
||||||
final List<SortFilterMenuSection> sections;
|
final List<SortFilterMenuSection<T>> sections;
|
||||||
final ValueChanged<int> onSelected;
|
final ValueChanged<T> onSelected;
|
||||||
final String tooltip;
|
final String tooltip;
|
||||||
final Widget icon;
|
final Widget icon;
|
||||||
|
|
||||||
@@ -40,7 +37,7 @@ class SortFilterMenu extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PopupMenuButton<int>(
|
return PopupMenuButton<T>(
|
||||||
icon: icon,
|
icon: icon,
|
||||||
tooltip: tooltip,
|
tooltip: tooltip,
|
||||||
onSelected: onSelected,
|
onSelected: onSelected,
|
||||||
@@ -53,11 +50,11 @@ class SortFilterMenu extends StatelessWidget {
|
|||||||
final visibleSections = sections
|
final visibleSections = sections
|
||||||
.where((section) => section.options.isNotEmpty)
|
.where((section) => section.options.isNotEmpty)
|
||||||
.toList();
|
.toList();
|
||||||
final entries = <PopupMenuEntry<int>>[];
|
final entries = <PopupMenuEntry<T>>[];
|
||||||
for (int i = 0; i < visibleSections.length; i++) {
|
for (int i = 0; i < visibleSections.length; i++) {
|
||||||
final section = visibleSections[i];
|
final section = visibleSections[i];
|
||||||
entries.add(
|
entries.add(
|
||||||
PopupMenuItem<int>(
|
PopupMenuItem<T>(
|
||||||
enabled: false,
|
enabled: false,
|
||||||
child: Text(section.title, style: labelStyle),
|
child: Text(section.title, style: labelStyle),
|
||||||
),
|
),
|
||||||
@@ -65,14 +62,14 @@ class SortFilterMenu extends StatelessWidget {
|
|||||||
for (final option in section.options) {
|
for (final option in section.options) {
|
||||||
if (option.checked == null) {
|
if (option.checked == null) {
|
||||||
entries.add(
|
entries.add(
|
||||||
PopupMenuItem<int>(
|
PopupMenuItem<T>(
|
||||||
value: option.value,
|
value: option.value,
|
||||||
child: Text(option.label),
|
child: Text(option.label),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
entries.add(
|
entries.add(
|
||||||
CheckedPopupMenuItem<int>(
|
CheckedPopupMenuItem<T>(
|
||||||
value: option.value,
|
value: option.value,
|
||||||
checked: option.checked ?? false,
|
checked: option.checked ?? false,
|
||||||
child: Text(option.label),
|
child: Text(option.label),
|
||||||
@@ -90,16 +87,23 @@ class SortFilterMenu extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const int _actionSortRecentMessages = 1;
|
sealed class _ContactsFilterAction {
|
||||||
const int _actionSortName = 2;
|
const _ContactsFilterAction();
|
||||||
const int _actionSortLastSeen = 3;
|
}
|
||||||
const int _actionFilterAll = 4;
|
|
||||||
const int _actionFilterFavorites = 5;
|
class _SortAction extends _ContactsFilterAction {
|
||||||
const int _actionFilterUsers = 6;
|
final ContactSortOption option;
|
||||||
const int _actionFilterRepeaters = 7;
|
const _SortAction(this.option);
|
||||||
const int _actionFilterRooms = 8;
|
}
|
||||||
const int _actionToggleUnreadOnly = 9;
|
|
||||||
const int _actionNewGroup = 10;
|
class _TypeFilterAction extends _ContactsFilterAction {
|
||||||
|
final ContactTypeFilter filter;
|
||||||
|
const _TypeFilterAction(this.filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ToggleUnreadAction extends _ContactsFilterAction {
|
||||||
|
const _ToggleUnreadAction();
|
||||||
|
}
|
||||||
|
|
||||||
class ContactsFilterMenu extends StatelessWidget {
|
class ContactsFilterMenu extends StatelessWidget {
|
||||||
final ContactSortOption sortOption;
|
final ContactSortOption sortOption;
|
||||||
@@ -108,7 +112,6 @@ class ContactsFilterMenu extends StatelessWidget {
|
|||||||
final ValueChanged<ContactSortOption> onSortChanged;
|
final ValueChanged<ContactSortOption> onSortChanged;
|
||||||
final ValueChanged<ContactTypeFilter> onTypeFilterChanged;
|
final ValueChanged<ContactTypeFilter> onTypeFilterChanged;
|
||||||
final ValueChanged<bool> onUnreadOnlyChanged;
|
final ValueChanged<bool> onUnreadOnlyChanged;
|
||||||
final VoidCallback onNewGroup;
|
|
||||||
|
|
||||||
const ContactsFilterMenu({
|
const ContactsFilterMenu({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -118,30 +121,29 @@ class ContactsFilterMenu extends StatelessWidget {
|
|||||||
required this.onSortChanged,
|
required this.onSortChanged,
|
||||||
required this.onTypeFilterChanged,
|
required this.onTypeFilterChanged,
|
||||||
required this.onUnreadOnlyChanged,
|
required this.onUnreadOnlyChanged,
|
||||||
required this.onNewGroup,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
return SortFilterMenu(
|
return SortFilterMenu<_ContactsFilterAction>(
|
||||||
tooltip: l10n.listFilter_tooltip,
|
tooltip: l10n.listFilter_tooltip,
|
||||||
sections: [
|
sections: [
|
||||||
SortFilterMenuSection(
|
SortFilterMenuSection(
|
||||||
title: l10n.listFilter_sortBy,
|
title: l10n.listFilter_sortBy,
|
||||||
options: [
|
options: [
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption(
|
||||||
value: _actionSortRecentMessages,
|
value: _SortAction(ContactSortOption.recentMessages),
|
||||||
label: l10n.listFilter_latestMessages,
|
label: l10n.listFilter_latestMessages,
|
||||||
checked: sortOption == ContactSortOption.recentMessages,
|
checked: sortOption == ContactSortOption.recentMessages,
|
||||||
),
|
),
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption(
|
||||||
value: _actionSortLastSeen,
|
value: _SortAction(ContactSortOption.lastSeen),
|
||||||
label: l10n.listFilter_heardRecently,
|
label: l10n.listFilter_heardRecently,
|
||||||
checked: sortOption == ContactSortOption.lastSeen,
|
checked: sortOption == ContactSortOption.lastSeen,
|
||||||
),
|
),
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption(
|
||||||
value: _actionSortName,
|
value: _SortAction(ContactSortOption.name),
|
||||||
label: l10n.listFilter_az,
|
label: l10n.listFilter_az,
|
||||||
checked: sortOption == ContactSortOption.name,
|
checked: sortOption == ContactSortOption.name,
|
||||||
),
|
),
|
||||||
@@ -151,80 +153,66 @@ class ContactsFilterMenu extends StatelessWidget {
|
|||||||
title: l10n.listFilter_filters,
|
title: l10n.listFilter_filters,
|
||||||
options: [
|
options: [
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption(
|
||||||
value: _actionFilterAll,
|
value: _TypeFilterAction(ContactTypeFilter.all),
|
||||||
label: l10n.listFilter_all,
|
label: l10n.listFilter_all,
|
||||||
checked: typeFilter == ContactTypeFilter.all,
|
checked: typeFilter == ContactTypeFilter.all,
|
||||||
),
|
),
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption(
|
||||||
value: _actionFilterFavorites,
|
value: _TypeFilterAction(ContactTypeFilter.favorites),
|
||||||
label: l10n.listFilter_favorites,
|
label: l10n.listFilter_favorites,
|
||||||
checked: typeFilter == ContactTypeFilter.favorites,
|
checked: typeFilter == ContactTypeFilter.favorites,
|
||||||
),
|
),
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption(
|
||||||
value: _actionFilterUsers,
|
value: _TypeFilterAction(ContactTypeFilter.users),
|
||||||
label: l10n.listFilter_users,
|
label: l10n.listFilter_users,
|
||||||
checked: typeFilter == ContactTypeFilter.users,
|
checked: typeFilter == ContactTypeFilter.users,
|
||||||
),
|
),
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption(
|
||||||
value: _actionFilterRepeaters,
|
value: _TypeFilterAction(ContactTypeFilter.repeaters),
|
||||||
label: l10n.listFilter_repeaters,
|
label: l10n.listFilter_repeaters,
|
||||||
checked: typeFilter == ContactTypeFilter.repeaters,
|
checked: typeFilter == ContactTypeFilter.repeaters,
|
||||||
),
|
),
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption(
|
||||||
value: _actionFilterRooms,
|
value: _TypeFilterAction(ContactTypeFilter.rooms),
|
||||||
label: l10n.listFilter_roomServers,
|
label: l10n.listFilter_roomServers,
|
||||||
checked: typeFilter == ContactTypeFilter.rooms,
|
checked: typeFilter == ContactTypeFilter.rooms,
|
||||||
),
|
),
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption(
|
||||||
value: _actionToggleUnreadOnly,
|
value: const _ToggleUnreadAction(),
|
||||||
label: l10n.listFilter_unreadOnly,
|
label: l10n.listFilter_unreadOnly,
|
||||||
checked: showUnreadOnly,
|
checked: showUnreadOnly,
|
||||||
),
|
),
|
||||||
SortFilterMenuOption(
|
|
||||||
value: _actionNewGroup,
|
|
||||||
label: l10n.listFilter_newGroup,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onSelected: (action) {
|
onSelected: (action) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case _actionSortRecentMessages:
|
case _SortAction(:final option):
|
||||||
onSortChanged(ContactSortOption.recentMessages);
|
onSortChanged(option);
|
||||||
break;
|
case _TypeFilterAction(:final filter):
|
||||||
case _actionSortName:
|
onTypeFilterChanged(filter);
|
||||||
onSortChanged(ContactSortOption.name);
|
case _ToggleUnreadAction():
|
||||||
break;
|
|
||||||
case _actionSortLastSeen:
|
|
||||||
onSortChanged(ContactSortOption.lastSeen);
|
|
||||||
break;
|
|
||||||
case _actionFilterAll:
|
|
||||||
onTypeFilterChanged(ContactTypeFilter.all);
|
|
||||||
break;
|
|
||||||
case _actionFilterUsers:
|
|
||||||
onTypeFilterChanged(ContactTypeFilter.users);
|
|
||||||
break;
|
|
||||||
case _actionFilterFavorites:
|
|
||||||
onTypeFilterChanged(ContactTypeFilter.favorites);
|
|
||||||
break;
|
|
||||||
case _actionFilterRepeaters:
|
|
||||||
onTypeFilterChanged(ContactTypeFilter.repeaters);
|
|
||||||
break;
|
|
||||||
case _actionFilterRooms:
|
|
||||||
onTypeFilterChanged(ContactTypeFilter.rooms);
|
|
||||||
break;
|
|
||||||
case _actionToggleUnreadOnly:
|
|
||||||
onUnreadOnlyChanged(!showUnreadOnly);
|
onUnreadOnlyChanged(!showUnreadOnly);
|
||||||
break;
|
|
||||||
case _actionNewGroup:
|
|
||||||
onNewGroup();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class _DiscoveryFilterAction {
|
||||||
|
const _DiscoveryFilterAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DiscoverySortAction extends _DiscoveryFilterAction {
|
||||||
|
final ContactSortOption option;
|
||||||
|
const _DiscoverySortAction(this.option);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DiscoveryTypeFilterAction extends _DiscoveryFilterAction {
|
||||||
|
final ContactTypeFilter filter;
|
||||||
|
const _DiscoveryTypeFilterAction(this.filter);
|
||||||
|
}
|
||||||
|
|
||||||
class DiscoveryContactsFilterMenu extends StatelessWidget {
|
class DiscoveryContactsFilterMenu extends StatelessWidget {
|
||||||
final ContactSortOption sortOption;
|
final ContactSortOption sortOption;
|
||||||
final ContactTypeFilter typeFilter;
|
final ContactTypeFilter typeFilter;
|
||||||
@@ -242,19 +230,19 @@ class DiscoveryContactsFilterMenu extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
return SortFilterMenu(
|
return SortFilterMenu<_DiscoveryFilterAction>(
|
||||||
tooltip: l10n.listFilter_tooltip,
|
tooltip: l10n.listFilter_tooltip,
|
||||||
sections: [
|
sections: [
|
||||||
SortFilterMenuSection(
|
SortFilterMenuSection(
|
||||||
title: l10n.listFilter_sortBy,
|
title: l10n.listFilter_sortBy,
|
||||||
options: [
|
options: [
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption(
|
||||||
value: _actionSortLastSeen,
|
value: _DiscoverySortAction(ContactSortOption.lastSeen),
|
||||||
label: l10n.listFilter_heardRecently,
|
label: l10n.listFilter_heardRecently,
|
||||||
checked: sortOption == ContactSortOption.lastSeen,
|
checked: sortOption == ContactSortOption.lastSeen,
|
||||||
),
|
),
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption(
|
||||||
value: _actionSortName,
|
value: _DiscoverySortAction(ContactSortOption.name),
|
||||||
label: l10n.listFilter_az,
|
label: l10n.listFilter_az,
|
||||||
checked: sortOption == ContactSortOption.name,
|
checked: sortOption == ContactSortOption.name,
|
||||||
),
|
),
|
||||||
@@ -264,22 +252,22 @@ class DiscoveryContactsFilterMenu extends StatelessWidget {
|
|||||||
title: l10n.listFilter_filters,
|
title: l10n.listFilter_filters,
|
||||||
options: [
|
options: [
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption(
|
||||||
value: _actionFilterAll,
|
value: _DiscoveryTypeFilterAction(ContactTypeFilter.all),
|
||||||
label: l10n.listFilter_all,
|
label: l10n.listFilter_all,
|
||||||
checked: typeFilter == ContactTypeFilter.all,
|
checked: typeFilter == ContactTypeFilter.all,
|
||||||
),
|
),
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption(
|
||||||
value: _actionFilterUsers,
|
value: _DiscoveryTypeFilterAction(ContactTypeFilter.users),
|
||||||
label: l10n.listFilter_users,
|
label: l10n.listFilter_users,
|
||||||
checked: typeFilter == ContactTypeFilter.users,
|
checked: typeFilter == ContactTypeFilter.users,
|
||||||
),
|
),
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption(
|
||||||
value: _actionFilterRepeaters,
|
value: _DiscoveryTypeFilterAction(ContactTypeFilter.repeaters),
|
||||||
label: l10n.listFilter_repeaters,
|
label: l10n.listFilter_repeaters,
|
||||||
checked: typeFilter == ContactTypeFilter.repeaters,
|
checked: typeFilter == ContactTypeFilter.repeaters,
|
||||||
),
|
),
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption(
|
||||||
value: _actionFilterRooms,
|
value: _DiscoveryTypeFilterAction(ContactTypeFilter.rooms),
|
||||||
label: l10n.listFilter_roomServers,
|
label: l10n.listFilter_roomServers,
|
||||||
checked: typeFilter == ContactTypeFilter.rooms,
|
checked: typeFilter == ContactTypeFilter.rooms,
|
||||||
),
|
),
|
||||||
@@ -288,27 +276,10 @@ class DiscoveryContactsFilterMenu extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
onSelected: (action) {
|
onSelected: (action) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case _actionSortName:
|
case _DiscoverySortAction(:final option):
|
||||||
onSortChanged(ContactSortOption.name);
|
onSortChanged(option);
|
||||||
break;
|
case _DiscoveryTypeFilterAction(:final filter):
|
||||||
case _actionSortLastSeen:
|
onTypeFilterChanged(filter);
|
||||||
onSortChanged(ContactSortOption.lastSeen);
|
|
||||||
break;
|
|
||||||
case _actionFilterAll:
|
|
||||||
onTypeFilterChanged(ContactTypeFilter.all);
|
|
||||||
break;
|
|
||||||
case _actionFilterUsers:
|
|
||||||
onTypeFilterChanged(ContactTypeFilter.users);
|
|
||||||
break;
|
|
||||||
case _actionFilterFavorites:
|
|
||||||
onTypeFilterChanged(ContactTypeFilter.favorites);
|
|
||||||
break;
|
|
||||||
case _actionFilterRepeaters:
|
|
||||||
onTypeFilterChanged(ContactTypeFilter.repeaters);
|
|
||||||
break;
|
|
||||||
case _actionFilterRooms:
|
|
||||||
onTypeFilterChanged(ContactTypeFilter.rooms);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
|
|||||||
builder: (context) => PathTraceMapScreen(
|
builder: (context) => PathTraceMapScreen(
|
||||||
title: context.l10n.contacts_repeaterPathTrace,
|
title: context.l10n.contacts_repeaterPathTrace,
|
||||||
path: Uint8List.fromList(pathBytes),
|
path: Uint8List.fromList(pathBytes),
|
||||||
flipPathAround: true,
|
flipPathRound: true,
|
||||||
targetContact: widget.contact,
|
targetContact: widget.contact,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -107,7 +107,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final pathForInput = currentContact.pathIdList;
|
final pathForInput = currentContact.pathIdList;
|
||||||
final availableContacts = connector.allContacts
|
final availableContacts = connector.contacts
|
||||||
.where((c) => c.publicKeyHex != currentContact.publicKeyHex)
|
.where((c) => c.publicKeyHex != currentContact.publicKeyHex)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
|
|
||||||
@@ -66,7 +65,7 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
|
|||||||
|
|
||||||
void _filterValidContacts() {
|
void _filterValidContacts() {
|
||||||
_validContacts = widget.availableContacts
|
_validContacts = widget.availableContacts
|
||||||
.where((c) => c.type == advTypeRepeater || c.type == advTypeRoom)
|
.where((c) => c.type == 2 || c.type == 3)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,7 +157,10 @@ class _SNRIndicatorState extends State<SNRIndicator> {
|
|||||||
repeater.snr,
|
repeater.snr,
|
||||||
widget.connector.currentSf,
|
widget.connector.currentSf,
|
||||||
);
|
);
|
||||||
final allContacts = widget.connector.allContacts;
|
final allContacts = [
|
||||||
|
...widget.connector.contacts,
|
||||||
|
...widget.connector.discoveredContacts,
|
||||||
|
];
|
||||||
final name = allContacts
|
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)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import flutter_blue_plus_darwin
|
|||||||
import flutter_local_notifications
|
import flutter_local_notifications
|
||||||
import mobile_scanner
|
import mobile_scanner
|
||||||
import package_info_plus
|
import package_info_plus
|
||||||
|
import path_provider_foundation
|
||||||
import share_plus
|
import share_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
@@ -20,6 +21,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||||
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
|
|||||||
Reference in New Issue
Block a user