mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-21 00:01:06 +10:00
Refactor code structure and remove redundant sections for improved readability and maintainability
This commit is contained in:
@@ -607,7 +607,7 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
AppSettingsService settingsService,
|
||||
MeshCoreConnector connector,
|
||||
) {
|
||||
final deviceId = connector.deviceId;
|
||||
final deviceId = connector.batteryDeviceKey;
|
||||
final isConnected = connector.isConnected && deviceId != null;
|
||||
final selection = isConnected
|
||||
? settingsService.batteryChemistryForDevice(deviceId)
|
||||
|
||||
@@ -381,12 +381,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
final messageIndex = index;
|
||||
Contact contact = _resolveContact(connector);
|
||||
final bool isRoom = contact.type == advTypeRoom;
|
||||
final message = reversedMessages[messageIndex];
|
||||
String fourByteHex = '';
|
||||
if (contact.type == advTypeRoom) {
|
||||
Contact? roomAuthor;
|
||||
if (isRoom) {
|
||||
// Room-server messages carry the original author's 4-byte prefix
|
||||
// separately from message.text; use it only for resolving the name.
|
||||
contact = _resolveContactFrom4Bytes(
|
||||
roomAuthor = _resolveContactFrom4Bytes(
|
||||
connector,
|
||||
message.fourByteRoomContactKey.isEmpty
|
||||
? Uint8List.fromList([0, 0, 0, 0])
|
||||
@@ -396,6 +398,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join()
|
||||
.toUpperCase();
|
||||
// Only adopt the author identity when we actually know them; never
|
||||
// fall back to the room server's own name as the sender.
|
||||
if (roomAuthor != null) contact = roomAuthor;
|
||||
}
|
||||
|
||||
return Builder(
|
||||
@@ -403,11 +408,12 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final textScale = context.select<ChatTextScaleService, double>(
|
||||
(service) => service.scale,
|
||||
);
|
||||
final resolvedContact = _resolveContact(connector);
|
||||
final bubble = _MessageBubble(
|
||||
message: message,
|
||||
senderName: resolvedContact.type == advTypeRoom
|
||||
? "${contact.name} [$fourByteHex]"
|
||||
senderName: isRoom
|
||||
? (roomAuthor != null
|
||||
? "${roomAuthor.name} [$fourByteHex]"
|
||||
: "[$fourByteHex]")
|
||||
: contact.name,
|
||||
sourceId: widget.contact.publicKeyHex,
|
||||
textScale: textScale,
|
||||
@@ -755,13 +761,17 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
return connector.contacts[_resolveContactIndex];
|
||||
}
|
||||
|
||||
Contact _resolveContactFrom4Bytes(
|
||||
Contact? _resolveContactFrom4Bytes(
|
||||
MeshCoreConnector connector,
|
||||
Uint8List key4Bytes,
|
||||
) {
|
||||
return connector.contacts.firstWhere(
|
||||
(c) => listEquals(c.publicKey.sublist(0, 4), key4Bytes.sublist(0, 4)),
|
||||
orElse: () => widget.contact,
|
||||
// Match against saved contacts first, then nodes only seen via discovery —
|
||||
// a room poster you haven't saved may still be in the discovered list.
|
||||
return connector.allContactsUnfiltered.cast<Contact?>().firstWhere(
|
||||
(c) =>
|
||||
c != null &&
|
||||
listEquals(c.publicKey.sublist(0, 4), key4Bytes.sublist(0, 4)),
|
||||
orElse: () => null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1049,7 +1059,11 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
if (message.isOutgoing) {
|
||||
senderName = connector.selfName ?? context.l10n.chat_me;
|
||||
} else if (_resolveContact(connector).type == advTypeRoom) {
|
||||
senderName = "${contact.name} [$fourByteHex]";
|
||||
// An unresolved author leaves `contact` as the room server itself; show
|
||||
// only the prefix rather than mislabeling the post with the room's name.
|
||||
senderName = contact.type == advTypeRoom
|
||||
? "[$fourByteHex]"
|
||||
: "${contact.name} [$fourByteHex]";
|
||||
} else {
|
||||
senderName = _resolveContact(connector).name;
|
||||
}
|
||||
|
||||
+358
-40
@@ -1,4 +1,3 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -94,6 +93,11 @@ class _MapScreenState extends State<MapScreen> {
|
||||
String _searchQuery = '';
|
||||
List<_GuessedLocation> _cachedGuessedLocations = [];
|
||||
String _guessedLocationsCacheKey = '';
|
||||
int? _sharedMarkersCacheSignature;
|
||||
Locale? _sharedMarkersCacheLocale;
|
||||
List<_SharedMarker> _cachedSharedMarkers = const [];
|
||||
_NodeMarkersCacheKey? _nodeMarkersCacheKey;
|
||||
List<Marker> _cachedNodeMarkers = const [];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -283,11 +287,23 @@ class _MapScreenState extends State<MapScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer3<MeshCoreConnector, AppSettingsService, PathHistoryService>(
|
||||
builder: (context, connector, settingsService, pathHistory, child) {
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
final connectorSnapshot = context
|
||||
.select<MeshCoreConnector, _MapConnectorSnapshot>(
|
||||
_MapConnectorSnapshot.fromConnector,
|
||||
);
|
||||
final connector = connectorSnapshot.connector;
|
||||
final settings = context.select<AppSettingsService, AppSettings>(
|
||||
(service) => service.settings,
|
||||
);
|
||||
final pathHistoryVersion = context.select<PathHistoryService, int>(
|
||||
(service) => service.version,
|
||||
);
|
||||
final settingsService = context.read<AppSettingsService>();
|
||||
final pathHistory = context.read<PathHistoryService>();
|
||||
final tileCache = context.read<MapTileCacheService>();
|
||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||
final settings = settingsService.settings;
|
||||
final allContacts = connector.allContacts;
|
||||
|
||||
final contacts = settings.mapShowDiscoveryContacts
|
||||
@@ -296,7 +312,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
|
||||
final highlightPosition = widget.highlightPosition;
|
||||
final sharedMarkers = settings.mapShowMarkers
|
||||
? _collectSharedMarkers(connector)
|
||||
? _collectSharedMarkers(
|
||||
connector,
|
||||
connectorSnapshot.markerSignature,
|
||||
)
|
||||
.where(
|
||||
(marker) =>
|
||||
!_hiddenMarkerIds.contains(marker.id) &&
|
||||
@@ -347,9 +366,17 @@ class _MapScreenState extends State<MapScreen> {
|
||||
.where((c) => c.hasLocation)
|
||||
.toList();
|
||||
|
||||
// Guessed markers represent the same node types as known-location
|
||||
// markers, so apply the node-type filters before estimating positions.
|
||||
final guessCandidates = _filterContactsBySettings(
|
||||
filteredByKeyPrefix,
|
||||
settings,
|
||||
noLocations: true,
|
||||
);
|
||||
|
||||
// Compute guessed locations with caching
|
||||
final maxRangeKm = _estimateLoRaRangeKm(connector);
|
||||
final filteredKeys = filteredByKeyPrefix
|
||||
final filteredKeys = guessCandidates
|
||||
.map((c) => '${c.publicKeyHex}:${c.path.join("-")}')
|
||||
.join(',');
|
||||
final anchorKeys = allContactsWithLocation
|
||||
@@ -359,12 +386,12 @@ class _MapScreenState extends State<MapScreen> {
|
||||
)
|
||||
.join(',');
|
||||
final cacheKey =
|
||||
'$filteredKeys|$anchorKeys|${pathHistory.version}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}';
|
||||
'$filteredKeys|$anchorKeys|$pathHistoryVersion:${connector.currentFreqHz}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}';
|
||||
if (cacheKey != _guessedLocationsCacheKey) {
|
||||
_guessedLocationsCacheKey = cacheKey;
|
||||
_cachedGuessedLocations = settings.mapShowGuessedLocations
|
||||
? _computeGuessedLocations(
|
||||
filteredByKeyPrefix,
|
||||
guessCandidates,
|
||||
allContactsWithLocation,
|
||||
pathHistory,
|
||||
maxRangeKm,
|
||||
@@ -759,9 +786,21 @@ class _MapScreenState extends State<MapScreen> {
|
||||
guessedLocations,
|
||||
showLabels: _showNodeLabels,
|
||||
),
|
||||
..._buildNodeMarkers(
|
||||
..._buildNodeMarkersCached(
|
||||
visibleContacts,
|
||||
settings,
|
||||
connectorSnapshot.contactsSignature,
|
||||
connectorSnapshot.batterySignature,
|
||||
_freshness,
|
||||
settings.mapTimeFilterHours,
|
||||
settings.mapKeyPrefixEnabled,
|
||||
settings.mapKeyPrefix,
|
||||
settings.mapShowDiscoveryContacts,
|
||||
Object.hashAllUnordered(
|
||||
settings.batteryChemistryByRepeaterId.entries.map(
|
||||
(entry) => Object.hash(entry.key, entry.value),
|
||||
),
|
||||
),
|
||||
showLabels: _showNodeLabels,
|
||||
selectedContact: selectedContact,
|
||||
),
|
||||
@@ -873,6 +912,59 @@ class _MapScreenState extends State<MapScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
List<Marker> _buildNodeMarkersCached(
|
||||
List<Contact> contacts,
|
||||
AppSettings settings,
|
||||
int contactsSignature,
|
||||
int batterySignature,
|
||||
_Freshness freshness,
|
||||
double timeFilterHours,
|
||||
bool keyPrefixEnabled,
|
||||
String keyPrefix,
|
||||
bool showDiscoveryContacts,
|
||||
int batteryChemistrySignature, {
|
||||
required bool showLabels,
|
||||
Contact? selectedContact,
|
||||
}) {
|
||||
final visibleContactsSignature = Object.hashAll(
|
||||
contacts.map(
|
||||
(contact) =>
|
||||
Object.hash(_mapContactSignature(contact), _ageOf(contact)),
|
||||
),
|
||||
);
|
||||
final key = _NodeMarkersCacheKey(
|
||||
contactsSignature: contactsSignature,
|
||||
visibleContactsSignature: visibleContactsSignature,
|
||||
batterySignature: batterySignature,
|
||||
freshness: freshness,
|
||||
timeFilterHours: timeFilterHours,
|
||||
keyPrefixEnabled: keyPrefixEnabled,
|
||||
keyPrefix: keyPrefix,
|
||||
showDiscoveryContacts: showDiscoveryContacts,
|
||||
batteryChemistrySignature: batteryChemistrySignature,
|
||||
showLabels: showLabels,
|
||||
selectedKey: selectedContact?.publicKeyHex,
|
||||
zoom: _zoom,
|
||||
overlapsMode: settings.mapShowOverlaps,
|
||||
showRepeaters: settings.mapShowRepeaters,
|
||||
showChatNodes: settings.mapShowChatNodes,
|
||||
showOtherNodes: settings.mapShowOtherNodes,
|
||||
isBuildingPathTrace: _isBuildingPathTrace,
|
||||
);
|
||||
if (key != _nodeMarkersCacheKey) {
|
||||
_nodeMarkersCacheKey = key;
|
||||
_cachedNodeMarkers = List.unmodifiable(
|
||||
_buildNodeMarkers(
|
||||
contacts,
|
||||
settings,
|
||||
showLabels: showLabels,
|
||||
selectedContact: selectedContact,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _cachedNodeMarkers;
|
||||
}
|
||||
|
||||
List<_GuessedLocation> _computeGuessedLocations(
|
||||
List<Contact> allContacts,
|
||||
List<Contact> withLocation,
|
||||
@@ -1146,17 +1238,18 @@ class _MapScreenState extends State<MapScreen> {
|
||||
}) {
|
||||
List<Contact> filtered = [];
|
||||
bool addContact = false;
|
||||
|
||||
for (final contact in contacts) {
|
||||
addContact = false;
|
||||
if (!contact.hasLocation && !noLocations) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply node type filters
|
||||
// Apply node type filters. The overlaps toggle is purely a visual
|
||||
// highlight (applied in _buildNodeMarkers) and no longer affects which
|
||||
// nodes are shown.
|
||||
if (contact.type == advTypeRepeater &&
|
||||
(settings.mapShowRepeaters ||
|
||||
_isBuildingPathTrace ||
|
||||
settings.mapShowOverlaps)) {
|
||||
(settings.mapShowRepeaters || _isBuildingPathTrace)) {
|
||||
addContact = true;
|
||||
}
|
||||
if (contact.type == advTypeChat &&
|
||||
@@ -1165,9 +1258,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
}
|
||||
if (contact.type != advTypeChat &&
|
||||
contact.type != advTypeRepeater &&
|
||||
(settings.mapShowOtherNodes ||
|
||||
_isBuildingPathTrace ||
|
||||
settings.mapShowOverlaps)) {
|
||||
(settings.mapShowOtherNodes || _isBuildingPathTrace)) {
|
||||
addContact = true;
|
||||
}
|
||||
|
||||
@@ -1175,25 +1266,6 @@ class _MapScreenState extends State<MapScreen> {
|
||||
addContact = false;
|
||||
}
|
||||
|
||||
if (settings.mapShowOverlaps) {
|
||||
final hasOverlap = contacts
|
||||
.where(
|
||||
(c) =>
|
||||
c.publicKeyHex != contact.publicKeyHex &&
|
||||
c.publicKey.first == contact.publicKey.first &&
|
||||
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
|
||||
(contact.type == advTypeRepeater ||
|
||||
contact.type == advTypeRoom),
|
||||
)
|
||||
.firstOrNull;
|
||||
|
||||
if (hasOverlap == null &&
|
||||
settings.mapShowOverlaps &&
|
||||
!_isBuildingPathTrace) {
|
||||
addContact = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (addContact) {
|
||||
filtered.add(contact);
|
||||
}
|
||||
@@ -1212,13 +1284,34 @@ class _MapScreenState extends State<MapScreen> {
|
||||
final selectedKey = selectedContact?.publicKeyHex;
|
||||
final items = contacts.where((c) => c.publicKeyHex != selectedKey).toList();
|
||||
|
||||
// Key-prefix overlaps are a visual highlight only: flag the repeaters/rooms
|
||||
// whose first key byte collides with another repeater/room on the map.
|
||||
final overlapPrefixes = <int>{};
|
||||
if (overlapsMode) {
|
||||
final counts = <int, int>{};
|
||||
for (final contact in contacts) {
|
||||
if (contact.type == advTypeRepeater || contact.type == advTypeRoom) {
|
||||
final prefix = contact.publicKey.first;
|
||||
counts[prefix] = (counts[prefix] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
counts.forEach((prefix, count) {
|
||||
if (count > 1) overlapPrefixes.add(prefix);
|
||||
});
|
||||
}
|
||||
bool isOverlap(Contact contact) =>
|
||||
overlapsMode &&
|
||||
(contact.type == advTypeRepeater || contact.type == advTypeRoom) &&
|
||||
overlapPrefixes.contains(contact.publicKey.first);
|
||||
|
||||
void addNode(Contact contact, {bool dot = false}) {
|
||||
markers.add(_nodeMarker(contact, overlapsMode: overlapsMode, dot: dot));
|
||||
final overlap = isOverlap(contact);
|
||||
markers.add(_nodeMarker(contact, overlapsMode: overlap, dot: dot));
|
||||
if (showLabels) {
|
||||
markers.add(
|
||||
_buildNodeLabelMarker(
|
||||
point: LatLng(contact.latitude!, contact.longitude!),
|
||||
label: overlapsMode
|
||||
label: overlap
|
||||
? "${contact.publicKeyHex.substring(0, 2)}:${contact.name}"
|
||||
: contact.name,
|
||||
),
|
||||
@@ -1255,7 +1348,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
markers.add(
|
||||
_nodeMarker(
|
||||
selectedContact,
|
||||
overlapsMode: overlapsMode,
|
||||
overlapsMode: isOverlap(selectedContact),
|
||||
selected: true,
|
||||
),
|
||||
);
|
||||
@@ -2371,7 +2464,16 @@ class _MapScreenState extends State<MapScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
List<_SharedMarker> _collectSharedMarkers(MeshCoreConnector connector) {
|
||||
List<_SharedMarker> _collectSharedMarkers(
|
||||
MeshCoreConnector connector,
|
||||
int markerSignature,
|
||||
) {
|
||||
final locale = Localizations.localeOf(context);
|
||||
if (_sharedMarkersCacheSignature == markerSignature &&
|
||||
_sharedMarkersCacheLocale == locale) {
|
||||
return _cachedSharedMarkers;
|
||||
}
|
||||
|
||||
// Build a _SharedMarker per message (history empty), grouped by dedupe key.
|
||||
// Afterwards pick the latest per key and fill its history from older ones.
|
||||
final updatesByKey = <String, List<_SharedMarker>>{};
|
||||
@@ -2463,7 +2565,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
});
|
||||
|
||||
markers.sort((a, b) => b.timestamp.compareTo(a.timestamp));
|
||||
return markers;
|
||||
_sharedMarkersCacheSignature = markerSignature;
|
||||
_sharedMarkersCacheLocale = locale;
|
||||
_cachedSharedMarkers = List.unmodifiable(markers);
|
||||
return _cachedSharedMarkers;
|
||||
}
|
||||
|
||||
Marker _buildSharedMarker(_SharedMarker marker) {
|
||||
@@ -3564,6 +3669,219 @@ enum _NodeAge { online, recent, stale }
|
||||
|
||||
enum _Freshness { all, online, recent, stale }
|
||||
|
||||
int _bytesSignature(Iterable<int>? bytes) {
|
||||
if (bytes == null) return 0;
|
||||
return Object.hashAll(bytes);
|
||||
}
|
||||
|
||||
int _mapContactSignature(Contact contact) {
|
||||
return Object.hash(
|
||||
contact.publicKeyHex,
|
||||
contact.name,
|
||||
contact.type,
|
||||
contact.flags,
|
||||
contact.pathLength,
|
||||
_bytesSignature(contact.path),
|
||||
contact.pathOverride,
|
||||
_bytesSignature(contact.pathOverrideBytes),
|
||||
contact.latitude,
|
||||
contact.longitude,
|
||||
contact.lastSeen.millisecondsSinceEpoch,
|
||||
contact.lastMessageAt.millisecondsSinceEpoch,
|
||||
contact.isActive,
|
||||
contact.wasPulled,
|
||||
);
|
||||
}
|
||||
|
||||
class _MapConnectorSnapshot {
|
||||
final MeshCoreConnector connector;
|
||||
final int contactsSignature;
|
||||
final int markerSignature;
|
||||
final int batterySignature;
|
||||
final int uiSignature;
|
||||
|
||||
const _MapConnectorSnapshot({
|
||||
required this.connector,
|
||||
required this.contactsSignature,
|
||||
required this.markerSignature,
|
||||
required this.batterySignature,
|
||||
required this.uiSignature,
|
||||
});
|
||||
|
||||
factory _MapConnectorSnapshot.fromConnector(MeshCoreConnector connector) {
|
||||
final allContacts = connector.allContacts;
|
||||
final contactsSignature = Object.hashAll(
|
||||
allContacts.map(_mapContactSignature),
|
||||
);
|
||||
final batterySignature = Object.hashAll(
|
||||
allContacts
|
||||
.where((contact) => contact.type == advTypeRepeater)
|
||||
.map(
|
||||
(contact) => Object.hash(
|
||||
contact.publicKeyHex,
|
||||
connector.getRepeaterBatteryMillivolts(contact.publicKeyHex),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final markerParts = <Object?>[connector.selfName];
|
||||
for (final contact in connector.contacts) {
|
||||
markerParts.add(contact.publicKeyHex);
|
||||
markerParts.add(contact.name);
|
||||
for (final message in connector.getMessages(contact)) {
|
||||
if (!message.text.trimLeft().startsWith('m:')) continue;
|
||||
markerParts.add(
|
||||
Object.hash(
|
||||
message.messageId,
|
||||
message.text,
|
||||
message.timestamp.millisecondsSinceEpoch,
|
||||
message.isOutgoing,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
for (final channel in connector.channels) {
|
||||
markerParts.add(
|
||||
Object.hash(
|
||||
channel.index,
|
||||
channel.name,
|
||||
channel.isPublicChannel,
|
||||
channel.isEmpty,
|
||||
),
|
||||
);
|
||||
for (final message in connector.getChannelMessages(channel)) {
|
||||
if (!message.text.trimLeft().startsWith('m:')) continue;
|
||||
markerParts.add(
|
||||
Object.hash(
|
||||
message.messageId,
|
||||
message.text,
|
||||
message.senderName,
|
||||
message.timestamp.millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return _MapConnectorSnapshot(
|
||||
connector: connector,
|
||||
contactsSignature: contactsSignature,
|
||||
markerSignature: Object.hashAll(markerParts),
|
||||
batterySignature: batterySignature,
|
||||
uiSignature: Object.hash(
|
||||
connector.isConnected,
|
||||
connector.selfLatitude,
|
||||
connector.selfLongitude,
|
||||
connector.currentFreqHz,
|
||||
connector.currentBwHz,
|
||||
connector.currentSf,
|
||||
connector.currentTxPower,
|
||||
connector.getTotalContactsUnreadCount(),
|
||||
connector.getTotalChannelsUnreadCount(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _MapConnectorSnapshot &&
|
||||
contactsSignature == other.contactsSignature &&
|
||||
markerSignature == other.markerSignature &&
|
||||
batterySignature == other.batterySignature &&
|
||||
uiSignature == other.uiSignature;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
contactsSignature,
|
||||
markerSignature,
|
||||
batterySignature,
|
||||
uiSignature,
|
||||
);
|
||||
}
|
||||
|
||||
class _NodeMarkersCacheKey {
|
||||
final int contactsSignature;
|
||||
final int visibleContactsSignature;
|
||||
final int batterySignature;
|
||||
final _Freshness freshness;
|
||||
final double timeFilterHours;
|
||||
final bool keyPrefixEnabled;
|
||||
final String keyPrefix;
|
||||
final bool showDiscoveryContacts;
|
||||
final int batteryChemistrySignature;
|
||||
final bool showLabels;
|
||||
final String? selectedKey;
|
||||
final double zoom;
|
||||
final bool overlapsMode;
|
||||
final bool showRepeaters;
|
||||
final bool showChatNodes;
|
||||
final bool showOtherNodes;
|
||||
final bool isBuildingPathTrace;
|
||||
|
||||
const _NodeMarkersCacheKey({
|
||||
required this.contactsSignature,
|
||||
required this.visibleContactsSignature,
|
||||
required this.batterySignature,
|
||||
required this.freshness,
|
||||
required this.timeFilterHours,
|
||||
required this.keyPrefixEnabled,
|
||||
required this.keyPrefix,
|
||||
required this.showDiscoveryContacts,
|
||||
required this.batteryChemistrySignature,
|
||||
required this.showLabels,
|
||||
required this.selectedKey,
|
||||
required this.zoom,
|
||||
required this.overlapsMode,
|
||||
required this.showRepeaters,
|
||||
required this.showChatNodes,
|
||||
required this.showOtherNodes,
|
||||
required this.isBuildingPathTrace,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _NodeMarkersCacheKey &&
|
||||
contactsSignature == other.contactsSignature &&
|
||||
visibleContactsSignature == other.visibleContactsSignature &&
|
||||
batterySignature == other.batterySignature &&
|
||||
freshness == other.freshness &&
|
||||
timeFilterHours == other.timeFilterHours &&
|
||||
keyPrefixEnabled == other.keyPrefixEnabled &&
|
||||
keyPrefix == other.keyPrefix &&
|
||||
showDiscoveryContacts == other.showDiscoveryContacts &&
|
||||
batteryChemistrySignature == other.batteryChemistrySignature &&
|
||||
showLabels == other.showLabels &&
|
||||
selectedKey == other.selectedKey &&
|
||||
zoom == other.zoom &&
|
||||
overlapsMode == other.overlapsMode &&
|
||||
showRepeaters == other.showRepeaters &&
|
||||
showChatNodes == other.showChatNodes &&
|
||||
showOtherNodes == other.showOtherNodes &&
|
||||
isBuildingPathTrace == other.isBuildingPathTrace;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
contactsSignature,
|
||||
visibleContactsSignature,
|
||||
batterySignature,
|
||||
freshness,
|
||||
timeFilterHours,
|
||||
keyPrefixEnabled,
|
||||
keyPrefix,
|
||||
showDiscoveryContacts,
|
||||
batteryChemistrySignature,
|
||||
showLabels,
|
||||
selectedKey,
|
||||
zoom,
|
||||
overlapsMode,
|
||||
showRepeaters,
|
||||
showChatNodes,
|
||||
showOtherNodes,
|
||||
isBuildingPathTrace,
|
||||
);
|
||||
}
|
||||
|
||||
class _GuessedLocation {
|
||||
final Contact contact;
|
||||
final LatLng position;
|
||||
|
||||
@@ -207,6 +207,15 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
}
|
||||
|
||||
void _toggleScan(MeshCoreConnector connector) {
|
||||
if (PlatformInfo.isWeb) {
|
||||
// flutter_blue_plus has no web backend, so a BLE scan silently no-ops in
|
||||
// the browser. Tell the user instead of leaving them staring at a button.
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.scanner_bluetoothWebUnsupported),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (connector.state == MeshCoreConnectionState.scanning) {
|
||||
connector.stopScan();
|
||||
} else {
|
||||
|
||||
@@ -725,18 +725,26 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
await connector.setNodeName(controller.text);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.settings_nodeNameUpdated),
|
||||
ListenableBuilder(
|
||||
listenable: controller,
|
||||
builder: (context, _) {
|
||||
final name = controller.text.trim();
|
||||
return TextButton(
|
||||
onPressed: name.isEmpty
|
||||
? null
|
||||
: () async {
|
||||
Navigator.pop(context);
|
||||
await connector.setNodeName(name);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.settings_nodeNameUpdated),
|
||||
);
|
||||
},
|
||||
child: Text(l10n.common_save),
|
||||
);
|
||||
},
|
||||
child: Text(l10n.common_save),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -386,6 +386,10 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
|
||||
void _showError(Object error) {
|
||||
if (!mounted) return;
|
||||
// Cancelling the browser's serial port picker is a normal user action, not
|
||||
// an error — don't show a scary red toast (and never leak the raw
|
||||
// DOMException text).
|
||||
if (_isUserCancelledPortPicker(error)) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(_friendlyErrorMessage(error)),
|
||||
@@ -393,6 +397,16 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
bool _isUserCancelledPortPicker(Object error) {
|
||||
if (error is StateError &&
|
||||
error.message.contains('No USB serial device selected')) {
|
||||
return true;
|
||||
}
|
||||
final text = error.toString();
|
||||
return text.contains('No port selected by the user') ||
|
||||
text.contains("Failed to execute 'requestPort'");
|
||||
}
|
||||
|
||||
String _friendlyErrorMessage(Object error) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user