mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-04-20 22:13:48 +00:00
Enhance contact handling and UI updates across multiple screens
add unfiltered contact access and improve last seen resolution
This commit is contained in:
parent
9dbf374ac6
commit
882abf3879
9 changed files with 142 additions and 16 deletions
|
|
@ -328,6 +328,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
),
|
||||
]);
|
||||
|
||||
List<Contact> get allContactsUnfiltered =>
|
||||
List.unmodifiable([..._contacts, ..._discoveredContacts]);
|
||||
|
||||
List<Contact> get discoveredContacts {
|
||||
return List.unmodifiable(_discoveredContacts);
|
||||
}
|
||||
|
|
@ -2372,6 +2375,18 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
});
|
||||
}
|
||||
|
||||
Contact getFromDiscovered(Contact contact) {
|
||||
final tmp = _discoveredContacts.firstWhere(
|
||||
(c) => c.publicKeyHex == contact.publicKeyHex,
|
||||
orElse: () => contact,
|
||||
);
|
||||
return contact.copyWith(
|
||||
rawPacket: tmp.rawPacket,
|
||||
latitude: tmp.latitude,
|
||||
longitude: tmp.longitude,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> getContacts({int? since, bool preserveExisting = false}) async {
|
||||
if (!isConnected) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,12 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||
final primaryPath = !channelMessage && !message.isOutgoing
|
||||
? Uint8List.fromList(primaryPathTmp.reversed.toList())
|
||||
: primaryPathTmp;
|
||||
<<<<<<< HEAD
|
||||
final hops = _buildPathHops(primaryPath, connector, l10n);
|
||||
=======
|
||||
final contacts = connector.allContactsUnfiltered;
|
||||
final hops = _buildPathHops(primaryPath, contacts, l10n);
|
||||
>>>>>>> da74560 (Enhance contact handling and UI updates across multiple screens)
|
||||
final hasHopDetails = primaryPath.isNotEmpty;
|
||||
final observedLabel = _formatObservedHops(
|
||||
primaryPath.length,
|
||||
|
|
@ -385,7 +390,10 @@ class _ChannelMessagePathMapScreenState
|
|||
: selectedPathTmp;
|
||||
|
||||
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
||||
final hops = _buildPathHops(selectedPath, connector, context.l10n);
|
||||
final contacts = connector.allContactsUnfiltered
|
||||
.map((c) => connector.getFromDiscovered(c))
|
||||
.toList();
|
||||
final hops = _buildPathHops(selectedPath, contacts, context.l10n);
|
||||
|
||||
final points = <LatLng>[];
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,13 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
DateTime _resolveLastSeen(Contact contact) {
|
||||
if (contact.type != advTypeChat) return contact.lastSeen;
|
||||
return contact.lastMessageAt.isAfter(contact.lastSeen)
|
||||
? contact.lastMessageAt
|
||||
: contact.lastSeen;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
|
|
@ -108,11 +115,56 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
|||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Text(
|
||||
_formatLastSeen(context, contact.lastSeen),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
// Clamp text scaling in trailing section to prevent overflow while
|
||||
// maintaining accessibility. Primary content (title/subtitle) scales normally.
|
||||
trailing: MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
textScaler: TextScaler.linear(
|
||||
MediaQuery.textScalerOf(
|
||||
context,
|
||||
).scale(1.0).clamp(1.0, 1.3),
|
||||
),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: 120,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
_formatLastSeen(
|
||||
context,
|
||||
_resolveLastSeen(contact),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (contact.hasLocation)
|
||||
Icon(
|
||||
Icons.location_on,
|
||||
size: 14,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
if (contact.rawPacket != null)
|
||||
const SizedBox(width: 2),
|
||||
if (contact.rawPacket != null)
|
||||
Icon(
|
||||
Icons.cell_tower,
|
||||
size: 14,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
|
|
|
|||
|
|
@ -139,7 +139,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||
builder: (context, connector, settingsService, pathHistory, child) {
|
||||
final tileCache = context.read<MapTileCacheService>();
|
||||
final settings = settingsService.settings;
|
||||
final allContacts = connector.allContacts;
|
||||
final allContacts = connector.allContacts
|
||||
.map((c) => connector.getFromDiscovered(c))
|
||||
.toList();
|
||||
|
||||
final contacts = settings.mapShowDiscoveryContacts
|
||||
? allContacts
|
||||
|
|
@ -2158,10 +2160,10 @@ class _MapScreenState extends State<MapScreen> {
|
|||
|
||||
void _removePath() {
|
||||
setState(() {
|
||||
_pathTrace.removeLast(); // Remove last node from path trace
|
||||
_pathTraceContacts.remove(
|
||||
_pathTrace.last,
|
||||
); // Remove last contact from path trace
|
||||
_pathTrace.removeLast(); // Remove last node from path trace
|
||||
_points.removeLast(); // Remove last point from points list
|
||||
_polylines.clear(); // Clear polylines
|
||||
});
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ class PathTraceMapScreen extends StatefulWidget {
|
|||
|
||||
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
static const double _labelZoomThreshold = 8.5;
|
||||
//miles to meters conversion for filtering out repeaters that are too far from the last known GPS hop to be a likely match, to avoid false matches that throw off the inferred positions of other hops in the path
|
||||
static const double _maxRepeaterMatchDistanceMeters = 40 * 1609.344;
|
||||
|
||||
StreamSubscription<Uint8List>? _frameSubscription;
|
||||
Timer? _timeoutTimer;
|
||||
|
|
@ -268,17 +270,41 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||
.toList();
|
||||
|
||||
Map<int, Contact> pathContacts = {};
|
||||
Contact lastContact = Contact(
|
||||
path: Uint8List(0),
|
||||
pathLength: 0,
|
||||
publicKey: connector.selfPublicKey ?? Uint8List(0),
|
||||
name: context.l10n.pathTrace_you,
|
||||
type: advTypeChat,
|
||||
latitude: connector.selfLatitude,
|
||||
longitude: connector.selfLongitude,
|
||||
lastSeen: DateTime.now(),
|
||||
);
|
||||
if (widget.pathContacts != null) {
|
||||
pathContacts = {for (var c in widget.pathContacts!) c.publicKey[0]: c};
|
||||
} else {
|
||||
final contacts = connector.allContacts;
|
||||
final contacts = connector.allContactsUnfiltered
|
||||
.map((c) => connector.getFromDiscovered(c))
|
||||
.toList();
|
||||
contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
|
||||
if (lastContact.latitude != null &&
|
||||
lastContact.longitude != null &&
|
||||
repeater.hasLocation &&
|
||||
lastContact.hasLocation &&
|
||||
Distance().distance(
|
||||
LatLng(lastContact.latitude!, lastContact.longitude!),
|
||||
LatLng(repeater.latitude!, repeater.longitude!),
|
||||
) >
|
||||
_maxRepeaterMatchDistanceMeters) {
|
||||
return; //skip reapeaters that are far away from the last one with known GPS, to avoid false matches
|
||||
}
|
||||
for (var repeaterData in pathData) {
|
||||
if (listEquals(
|
||||
repeater.publicKey.sublist(0, 1),
|
||||
Uint8List.fromList([repeaterData]),
|
||||
)) {
|
||||
pathContacts[repeaterData] = repeater;
|
||||
lastContact = repeater;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,12 +14,13 @@ class ContactExport {
|
|||
final double lon;
|
||||
final String desc;
|
||||
final double? ele;
|
||||
|
||||
final String url;
|
||||
ContactExport({
|
||||
required this.name,
|
||||
required this.lat,
|
||||
required this.lon,
|
||||
required this.desc,
|
||||
required this.url,
|
||||
this.ele,
|
||||
});
|
||||
}
|
||||
|
|
@ -40,6 +41,7 @@ class GpxExport {
|
|||
String name,
|
||||
double lat,
|
||||
double lon,
|
||||
String url,
|
||||
String desc, [
|
||||
double? ele,
|
||||
]) {
|
||||
|
|
@ -50,55 +52,72 @@ class GpxExport {
|
|||
lon: lon,
|
||||
desc: desc.trim(),
|
||||
ele: ele,
|
||||
url: url,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void addRepeaters() {
|
||||
final contacts = _connector.contacts
|
||||
final contacts = _connector.allContacts
|
||||
.where((c) => c.type == advTypeRepeater || c.type == advTypeRoom)
|
||||
.map((c) => _connector.getFromDiscovered(c))
|
||||
.toList();
|
||||
for (var contact in contacts) {
|
||||
if (contact.latitude == null || contact.longitude == null) {
|
||||
continue;
|
||||
}
|
||||
final url = contact.rawPacket != null
|
||||
? "meshcore://${pubKeyToHex(contact.rawPacket!)}"
|
||||
: "";
|
||||
_addContact(
|
||||
contact.name,
|
||||
contact.latitude!,
|
||||
contact.longitude!,
|
||||
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
|
||||
url,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void addContacts() {
|
||||
final contacts = _connector.contacts
|
||||
final contacts = _connector.allContacts
|
||||
.where((c) => c.type == advTypeChat)
|
||||
.map((c) => _connector.getFromDiscovered(c))
|
||||
.toList();
|
||||
for (var contact in contacts) {
|
||||
if (contact.latitude == null || contact.longitude == null) {
|
||||
continue;
|
||||
}
|
||||
final url = contact.rawPacket != null
|
||||
? "meshcore://${pubKeyToHex(contact.rawPacket!)}"
|
||||
: "";
|
||||
_addContact(
|
||||
contact.name,
|
||||
contact.latitude!,
|
||||
contact.longitude!,
|
||||
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
|
||||
url,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void addAll() {
|
||||
final contacts = _connector.contacts;
|
||||
for (var contact in contacts.toList()) {
|
||||
final contacts = _connector.allContacts
|
||||
.map((c) => _connector.getFromDiscovered(c))
|
||||
.toList();
|
||||
for (var contact in contacts) {
|
||||
if (contact.latitude == null || contact.longitude == null) {
|
||||
continue;
|
||||
}
|
||||
final url = contact.rawPacket != null
|
||||
? "meshcore://${pubKeyToHex(contact.rawPacket!)}"
|
||||
: "";
|
||||
_addContact(
|
||||
contact.name,
|
||||
contact.latitude ?? 0.0,
|
||||
contact.longitude ?? 0.0,
|
||||
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
|
||||
url,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -138,6 +157,9 @@ class GpxExport {
|
|||
ele: c.ele,
|
||||
name: c.name,
|
||||
desc: c.desc,
|
||||
extensions: {
|
||||
"meshcore": {"url": c.url},
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||
import 'package:meshcore_open/models/path_history.dart';
|
||||
import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||
import 'package:meshcore_open/widgets/elements_ui.dart';
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
|
|||
messageBytes: responseBytes,
|
||||
);
|
||||
final timeoutSeconds = (timeoutMs / 1000).ceil();
|
||||
final timeout = Duration(milliseconds: timeoutMs);
|
||||
final timeout = Duration(milliseconds: timeoutMs + 2000);
|
||||
final selectionLabel = selection.useFlood
|
||||
? 'flood'
|
||||
: '${selection.hopCount} hops';
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
|
|||
messageBytes: responseBytes,
|
||||
);
|
||||
final timeoutSeconds = (timeoutMs / 1000).ceil();
|
||||
final timeout = Duration(milliseconds: timeoutMs);
|
||||
final timeout = Duration(milliseconds: timeoutMs + 2000);
|
||||
final selectionLabel = selection.useFlood
|
||||
? 'flood'
|
||||
: '${selection.hopCount} hops';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue