Refactor code structure and remove redundant sections for improved readability and maintainability

This commit is contained in:
zjs81
2026-06-12 22:55:41 -07:00
parent 51d6210920
commit 3707acb124
34 changed files with 1008 additions and 84 deletions
+1 -1
View File
@@ -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)
+24 -10
View File
@@ -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
View File
@@ -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;
+9
View File
@@ -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 {
+18 -10
View File
@@ -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),
),
],
),
+14
View File
@@ -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;