mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-14 22:55:12 +10:00
Merge pull request #462 from HDDen/sync-progressbar
Onstart syncronization progressbar
This commit is contained in:
@@ -199,6 +199,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
double? _selfLongitude;
|
||||
final List<DirectRepeater> _directRepeaters = List.empty(growable: true);
|
||||
bool _isLoadingContacts = false;
|
||||
bool _hasLoadedContacts = false;
|
||||
bool _isLoadingChannels = false;
|
||||
bool _hasLoadedChannels = false;
|
||||
TimeoutPredictionService? _timeoutPredictionService;
|
||||
@@ -220,10 +221,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
bool _batteryRequested = false;
|
||||
bool _awaitingSelfInfo = false;
|
||||
bool _hasReceivedDeviceInfo = false;
|
||||
// Initial sync is serialized for predictable progress. Firmware exposes one
|
||||
// FIFO queued-message stream, so direct/room frames are buffered until after
|
||||
// contacts are known.
|
||||
bool _pendingInitialChannelSync = false;
|
||||
bool _pendingInitialContactsSync = false;
|
||||
bool _pendingInitialQueuedMessageSync = false;
|
||||
bool _bleInitialSyncStarted = false;
|
||||
bool _pendingDeferredChannelSyncAfterContacts = false;
|
||||
bool _webInitialHandshakeRequestSent = false;
|
||||
bool _preserveContactsOnRefresh = false;
|
||||
bool _autoAddUsers = false;
|
||||
@@ -238,13 +242,18 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
int _advertLocPolicy = 0;
|
||||
int _multiAcks = 0;
|
||||
|
||||
static const int _defaultMaxContacts = 32;
|
||||
static const int _defaultMaxChannels = 8;
|
||||
static const int _defaultMaxContacts = 350;
|
||||
static const int _defaultMaxChannels = 40;
|
||||
int _maxContacts = _defaultMaxContacts;
|
||||
int _maxChannels = _defaultMaxChannels;
|
||||
int? _contactSyncTotal;
|
||||
int _contactSyncReceived = 0;
|
||||
bool _contactSyncUsesSinceFilter = false;
|
||||
bool _isSyncingQueuedMessages = false;
|
||||
bool _deferQueuedContactMessagesUntilContacts = false;
|
||||
bool _isProcessingDeferredQueuedContactMessages = false;
|
||||
bool _queuedMessageSyncInFlight = false;
|
||||
bool _didInitialQueueSync = false;
|
||||
final List<Uint8List> _deferredQueuedContactMessageFrames = [];
|
||||
bool _pendingQueueSync = false;
|
||||
Timer? _queueSyncTimeout;
|
||||
int _queueSyncRetries = 0;
|
||||
@@ -373,7 +382,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
List<Channel> get channels => List.unmodifiable(_channels);
|
||||
bool get isConnected => _state == MeshCoreConnectionState.connected;
|
||||
bool get isLoadingContacts => _isLoadingContacts;
|
||||
bool get hasLoadedContacts => _hasLoadedContacts;
|
||||
bool get isLoadingChannels => _isLoadingChannels;
|
||||
bool get hasLoadedChannels => _hasLoadedChannels;
|
||||
Stream<Uint8List> get receivedFrames => _receivedFramesController.stream;
|
||||
Uint8List? get selfPublicKey => _selfPublicKey;
|
||||
String get selfPublicKeyHex => pubKeyToHex(_selfPublicKey ?? Uint8List(0));
|
||||
@@ -436,7 +447,16 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
int get maxContacts => _maxContacts;
|
||||
int get maxChannels => _maxChannels;
|
||||
Set<String> get knownContactKeys => Set.unmodifiable(_knownContactKeys);
|
||||
bool get isSyncingQueuedMessages => _isSyncingQueuedMessages;
|
||||
double? get contactSyncProgress {
|
||||
final total = _contactSyncTotal;
|
||||
if (!_isLoadingContacts || total == null || total <= 0) return null;
|
||||
return (_contactSyncReceived / total).clamp(0.0, 1.0).toDouble();
|
||||
}
|
||||
|
||||
bool get isSyncingQueuedMessages =>
|
||||
_isSyncingQueuedMessages || _isProcessingDeferredQueuedContactMessages;
|
||||
bool get isShowingQueuedMessageSyncProgress =>
|
||||
_deferQueuedContactMessagesUntilContacts && isSyncingQueuedMessages;
|
||||
bool get isSyncingChannels => _isSyncingChannels;
|
||||
int get channelSyncProgress =>
|
||||
_isSyncingChannels && _totalChannelsToRequest > 0
|
||||
@@ -1548,6 +1568,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
_setState(MeshCoreConnectionState.connected);
|
||||
_pendingInitialChannelSync = true;
|
||||
_pendingInitialQueuedMessageSync = true;
|
||||
_pendingInitialContactsSync = true;
|
||||
_appDebugLogService?.info(
|
||||
'connectUsb: requesting device info…',
|
||||
tag: 'USB',
|
||||
@@ -1658,6 +1680,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
_setState(MeshCoreConnectionState.connected);
|
||||
_pendingInitialChannelSync = true;
|
||||
_pendingInitialQueuedMessageSync = true;
|
||||
_pendingInitialContactsSync = true;
|
||||
await _requestDeviceInfo();
|
||||
_startBatteryPolling();
|
||||
if (_radioStatsPollRefCount > 0) _startRadioStatsPolling();
|
||||
@@ -2292,7 +2316,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
_bleInitialSyncStarted = true;
|
||||
_pendingInitialChannelSync = true;
|
||||
_pendingInitialContactsSync = true;
|
||||
_pendingInitialQueuedMessageSync = true;
|
||||
|
||||
await _requestDeviceInfo();
|
||||
_startBatteryPolling();
|
||||
@@ -2307,7 +2333,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
|
||||
await syncTime();
|
||||
unawaited(getChannels());
|
||||
_maybeStartInitialChannelSync();
|
||||
}
|
||||
|
||||
void _resetConnectionHandshakeState() {
|
||||
@@ -2320,11 +2346,39 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_selfInfoRetryTimer?.cancel();
|
||||
_selfInfoRetryTimer = null;
|
||||
_hasReceivedDeviceInfo = false;
|
||||
_resetSyncProgressState();
|
||||
_bleInitialSyncStarted = false;
|
||||
_pathHashByteWidth = 1;
|
||||
}
|
||||
|
||||
void _resetSyncProgressState() {
|
||||
_pendingInitialChannelSync = false;
|
||||
_pendingInitialContactsSync = false;
|
||||
_bleInitialSyncStarted = false;
|
||||
_pendingDeferredChannelSyncAfterContacts = false;
|
||||
_pathHashByteWidth = 1;
|
||||
_pendingInitialQueuedMessageSync = false;
|
||||
_contactSyncTotal = null;
|
||||
_contactSyncReceived = 0;
|
||||
_contactSyncUsesSinceFilter = false;
|
||||
_isLoadingContacts = false;
|
||||
_hasLoadedContacts = false;
|
||||
_isLoadingChannels = false;
|
||||
_hasLoadedChannels = false;
|
||||
_isSyncingQueuedMessages = false;
|
||||
_deferQueuedContactMessagesUntilContacts = false;
|
||||
_isProcessingDeferredQueuedContactMessages = false;
|
||||
_queuedMessageSyncInFlight = false;
|
||||
_deferredQueuedContactMessageFrames.clear();
|
||||
_pendingQueueSync = false;
|
||||
_queueSyncTimeout?.cancel();
|
||||
_queueSyncTimeout = null;
|
||||
_queueSyncRetries = 0;
|
||||
_isSyncingChannels = false;
|
||||
_channelSyncInFlight = false;
|
||||
_channelSyncTimeout?.cancel();
|
||||
_channelSyncTimeout = null;
|
||||
_channelSyncRetries = 0;
|
||||
_nextChannelIndexToRequest = 0;
|
||||
_totalChannelsToRequest = 0;
|
||||
_previousChannelsCache.clear();
|
||||
}
|
||||
|
||||
bool get _shouldAutoReconnect =>
|
||||
@@ -2461,17 +2515,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_batteryRequested = false;
|
||||
_awaitingSelfInfo = false;
|
||||
_hasReceivedDeviceInfo = false;
|
||||
_pendingInitialChannelSync = false;
|
||||
_pendingInitialContactsSync = false;
|
||||
_maxContacts = _defaultMaxContacts;
|
||||
_maxChannels = _defaultMaxChannels;
|
||||
_isSyncingQueuedMessages = false;
|
||||
_queuedMessageSyncInFlight = false;
|
||||
_didInitialQueueSync = false;
|
||||
_pendingQueueSync = false;
|
||||
_isSyncingChannels = false;
|
||||
_channelSyncInFlight = false;
|
||||
_hasLoadedChannels = false;
|
||||
_resetSyncProgressState();
|
||||
_pendingChannelSentQueue.clear();
|
||||
_pendingGenericAckQueue.clear();
|
||||
_reactionSendQueueSequence = 0;
|
||||
@@ -2724,10 +2770,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
_isLoadingContacts = true;
|
||||
_preserveContactsOnRefresh = preserveExisting;
|
||||
_contactSyncTotal = null;
|
||||
_contactSyncReceived = 0;
|
||||
_contactSyncUsesSinceFilter = since != null;
|
||||
if (!preserveExisting) {
|
||||
_hasLoadedContacts = false;
|
||||
_contacts.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
notifyListeners();
|
||||
|
||||
await sendFrame(buildGetContactsFrame(since: since));
|
||||
}
|
||||
@@ -3325,11 +3375,20 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
Future<void> syncQueuedMessages({bool force = false}) async {
|
||||
if (!isConnected) return;
|
||||
if (!force && _isSyncingQueuedMessages) return;
|
||||
if (_isProcessingDeferredQueuedContactMessages) {
|
||||
_pendingQueueSync = true;
|
||||
return;
|
||||
}
|
||||
if (_awaitingSelfInfo || _isLoadingContacts) {
|
||||
_pendingQueueSync = true;
|
||||
return;
|
||||
}
|
||||
if (_isSyncingChannels || _channelSyncInFlight) {
|
||||
_pendingQueueSync = true;
|
||||
return;
|
||||
}
|
||||
_isSyncingQueuedMessages = true;
|
||||
notifyListeners();
|
||||
await _requestNextQueuedMessage();
|
||||
}
|
||||
|
||||
@@ -3363,6 +3422,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_isSyncingQueuedMessages = false;
|
||||
_queueSyncTimeout?.cancel();
|
||||
_queueSyncRetries = 0;
|
||||
notifyListeners();
|
||||
_continueAfterQueuedMessageSync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3382,6 +3443,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_queuedMessageSyncInFlight = false;
|
||||
_isSyncingQueuedMessages = false;
|
||||
_queueSyncRetries = 0;
|
||||
notifyListeners();
|
||||
_continueAfterQueuedMessageSync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3481,6 +3544,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
_isLoadingChannels = true;
|
||||
_isSyncingChannels = true;
|
||||
_hasLoadedChannels = false;
|
||||
_previousChannelsCache = List<Channel>.from(_channels);
|
||||
_channels.clear();
|
||||
_nextChannelIndexToRequest = 0;
|
||||
@@ -3570,6 +3634,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_nextChannelIndexToRequest++;
|
||||
_channelSyncRetries = 0;
|
||||
_channelSyncInFlight = false;
|
||||
notifyListeners();
|
||||
unawaited(_requestNextChannel());
|
||||
}
|
||||
}
|
||||
@@ -3605,16 +3670,31 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (completed) {
|
||||
_hasLoadedChannels = true;
|
||||
_previousChannelsCache.clear();
|
||||
} else if (_channels.isEmpty && _previousChannelsCache.isNotEmpty) {
|
||||
// A failed initial sync should not leave the UI empty/spinning forever.
|
||||
// Restore the pre-sync list so cached channels remain usable.
|
||||
_channels.addAll(_previousChannelsCache);
|
||||
_applyChannelOrder();
|
||||
_recalculateCachedChannelsUnreadTotal();
|
||||
}
|
||||
|
||||
// Fallback: if contact sync was deferred waiting for channel 0 but
|
||||
// channel sync finished without triggering it, start contacts now.
|
||||
if (_pendingInitialContactsSync && isConnected) {
|
||||
_pendingInitialContactsSync = false;
|
||||
unawaited(getContacts());
|
||||
if (isConnected) {
|
||||
_startPostChannelInitialQueuedMessageSync();
|
||||
}
|
||||
|
||||
// Keep cache on failure/disconnection for future attempts
|
||||
if (!completed) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void _startPostChannelInitialQueuedMessageSync() {
|
||||
if (_pendingInitialQueuedMessageSync || _pendingQueueSync) {
|
||||
_deferQueuedContactMessagesUntilContacts = _pendingInitialContactsSync;
|
||||
_pendingInitialQueuedMessageSync = false;
|
||||
_pendingQueueSync = false;
|
||||
unawaited(syncQueuedMessages(force: true));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setChannel(int index, String name, Uint8List psk) async {
|
||||
@@ -3666,6 +3746,20 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_contacts.clear();
|
||||
}
|
||||
_isLoadingContacts = true;
|
||||
_contactSyncReceived = 0;
|
||||
// Firmware v3+ includes total contacts after CONTACTS_START.
|
||||
// Incremental sync reports total contacts, not filtered result count.
|
||||
if (frame.length >= 5 && !_contactSyncUsesSinceFilter) {
|
||||
final reader = BufferReader(frame);
|
||||
reader.skipBytes(1);
|
||||
_contactSyncTotal = reader.readUInt32LE();
|
||||
} else if (!_contactSyncUsesSinceFilter) {
|
||||
// Older firmwares may omit the count; use the nRF node capacity as
|
||||
// a conservative progress fallback instead of hiding the progress.
|
||||
_contactSyncTotal = _defaultMaxContacts;
|
||||
} else {
|
||||
_contactSyncTotal = null;
|
||||
}
|
||||
notifyListeners();
|
||||
break;
|
||||
case pushCodeAdvert:
|
||||
@@ -3683,7 +3777,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
case respCodeEndOfContacts:
|
||||
debugPrint('Got END_OF_CONTACTS');
|
||||
_isLoadingContacts = false;
|
||||
_hasLoadedContacts = true;
|
||||
_preserveContactsOnRefresh = false;
|
||||
_contactSyncUsesSinceFilter = false;
|
||||
unawaited(updateKnownDiscovered());
|
||||
notifyListeners();
|
||||
unawaited(_persistContacts());
|
||||
@@ -3693,23 +3789,21 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
!_channelSyncInFlight) {
|
||||
unawaited(_requestNextChannel());
|
||||
}
|
||||
if (!_didInitialQueueSync || _pendingQueueSync) {
|
||||
_didInitialQueueSync = true;
|
||||
if (_deferQueuedContactMessagesUntilContacts) {
|
||||
unawaited(_processDeferredQueuedContactMessages());
|
||||
} else if (_pendingQueueSync) {
|
||||
_pendingQueueSync = false;
|
||||
unawaited(syncQueuedMessages(force: true));
|
||||
}
|
||||
if (_pendingDeferredChannelSyncAfterContacts &&
|
||||
(_activeTransport == MeshCoreTransportType.bluetooth ||
|
||||
_activeTransport == MeshCoreTransportType.usb ||
|
||||
_activeTransport == MeshCoreTransportType.tcp)) {
|
||||
_pendingDeferredChannelSyncAfterContacts = false;
|
||||
_pendingInitialChannelSync = false;
|
||||
unawaited(getChannels());
|
||||
}
|
||||
break;
|
||||
case respCodeContactMsgRecv:
|
||||
case respCodeContactMsgRecvV3:
|
||||
_handleIncomingMessage(frame);
|
||||
if (_shouldDeferQueuedContactMessage(frame)) {
|
||||
_deferredQueuedContactMessageFrames.add(Uint8List.fromList(frame));
|
||||
_handleQueuedMessageReceived();
|
||||
} else {
|
||||
unawaited(_handleIncomingMessage(frame));
|
||||
}
|
||||
break;
|
||||
case respCodeChannelMsgRecv:
|
||||
case respCodeChannelMsgRecvV3:
|
||||
@@ -3893,23 +3987,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_selfInfoRetryTimer = null;
|
||||
notifyListeners();
|
||||
|
||||
// Auto-fetch contacts after getting self info. On web BLE, defer this
|
||||
// until after channel 0 so startup writes stay serialized.
|
||||
if (PlatformInfo.isWeb &&
|
||||
_activeTransport == MeshCoreTransportType.bluetooth) {
|
||||
_pendingInitialContactsSync = true;
|
||||
} else if (_activeTransport == MeshCoreTransportType.usb ||
|
||||
_activeTransport == MeshCoreTransportType.tcp) {
|
||||
_pendingDeferredChannelSyncAfterContacts = true;
|
||||
getContacts();
|
||||
} else {
|
||||
getContacts();
|
||||
}
|
||||
if (_shouldGateInitialChannelSync &&
|
||||
_activeTransport != MeshCoreTransportType.usb &&
|
||||
_activeTransport != MeshCoreTransportType.tcp) {
|
||||
_maybeStartInitialChannelSync();
|
||||
}
|
||||
// Start the serialized initial sync pipeline after SELF_INFO.
|
||||
_maybeStartInitialChannelSync();
|
||||
}
|
||||
|
||||
void _handleDeviceInfo(Uint8List frame) {
|
||||
@@ -3949,7 +4028,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
unawaited(loadAllChannelMessages(maxChannels: nextMaxChannels));
|
||||
if (isConnected &&
|
||||
_selfPublicKey != null &&
|
||||
(!_shouldGateInitialChannelSync || !_pendingInitialChannelSync)) {
|
||||
!_pendingInitialChannelSync) {
|
||||
unawaited(getChannels(maxChannels: nextMaxChannels));
|
||||
}
|
||||
}
|
||||
@@ -3964,12 +4043,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (!_pendingInitialChannelSync || !isConnected) {
|
||||
return;
|
||||
}
|
||||
if (_selfPublicKey == null || !_hasReceivedDeviceInfo) {
|
||||
if (_selfPublicKey == null ||
|
||||
(_shouldGateInitialChannelSync && !_hasReceivedDeviceInfo)) {
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingInitialChannelSync = false;
|
||||
unawaited(getChannels(maxChannels: _maxChannels));
|
||||
unawaited(getChannels(maxChannels: _maxChannels, force: true));
|
||||
}
|
||||
|
||||
void _handleNoMoreMessages() {
|
||||
@@ -3978,6 +4058,64 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_isSyncingQueuedMessages = false;
|
||||
_queuedMessageSyncInFlight = false;
|
||||
_queueSyncRetries = 0; // Reset retry counter on successful completion
|
||||
notifyListeners();
|
||||
_continueAfterQueuedMessageSync();
|
||||
}
|
||||
|
||||
bool _shouldDeferQueuedContactMessage(Uint8List frame) {
|
||||
if (!_deferQueuedContactMessagesUntilContacts ||
|
||||
!_isSyncingQueuedMessages) {
|
||||
return false;
|
||||
}
|
||||
if (frame.isEmpty) return false;
|
||||
return frame[0] == respCodeContactMsgRecv ||
|
||||
frame[0] == respCodeContactMsgRecvV3;
|
||||
}
|
||||
|
||||
void _continueAfterQueuedMessageSync() {
|
||||
if (!_deferQueuedContactMessagesUntilContacts) return;
|
||||
if (_pendingInitialContactsSync && isConnected) {
|
||||
_pendingInitialContactsSync = false;
|
||||
unawaited(getContacts());
|
||||
return;
|
||||
}
|
||||
unawaited(_processDeferredQueuedContactMessages());
|
||||
}
|
||||
|
||||
Future<void> _processDeferredQueuedContactMessages() async {
|
||||
if (!_deferQueuedContactMessagesUntilContacts ||
|
||||
_isProcessingDeferredQueuedContactMessages) {
|
||||
return;
|
||||
}
|
||||
if (_deferredQueuedContactMessageFrames.isEmpty) {
|
||||
_deferQueuedContactMessagesUntilContacts = false;
|
||||
notifyListeners();
|
||||
if (_pendingQueueSync && isConnected) {
|
||||
_pendingQueueSync = false;
|
||||
unawaited(syncQueuedMessages(force: true));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_isProcessingDeferredQueuedContactMessages = true;
|
||||
notifyListeners();
|
||||
try {
|
||||
// Replay direct/room queued messages only after contacts are loaded, so
|
||||
// sender prefixes can be resolved against the current contact list.
|
||||
while (_deferredQueuedContactMessageFrames.isNotEmpty) {
|
||||
final frame = _deferredQueuedContactMessageFrames.removeAt(0);
|
||||
await _handleIncomingMessage(frame);
|
||||
}
|
||||
} finally {
|
||||
_deferQueuedContactMessagesUntilContacts = false;
|
||||
_isProcessingDeferredQueuedContactMessages = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
if (_pendingQueueSync && isConnected) {
|
||||
_pendingQueueSync = false;
|
||||
unawaited(syncQueuedMessages(force: true));
|
||||
}
|
||||
}
|
||||
|
||||
void _handleQueuedMessageReceived() {
|
||||
@@ -3986,6 +4124,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_queueSyncTimeout?.cancel(); // Cancel timeout - message arrived
|
||||
_queuedMessageSyncInFlight = false;
|
||||
_queueSyncRetries = 0; // Reset retry counter on successful message
|
||||
notifyListeners();
|
||||
unawaited(_requestNextQueuedMessage());
|
||||
}
|
||||
|
||||
@@ -4127,11 +4266,15 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
void _handleContact(Uint8List frame, {bool isContact = true}) {
|
||||
final contactTmp = Contact.fromFrame(frame);
|
||||
if (contactTmp != null) {
|
||||
if (isContact && _isLoadingContacts) {
|
||||
_contactSyncReceived++;
|
||||
}
|
||||
if (listEquals(contactTmp.publicKey, _selfPublicKey)) {
|
||||
appLogger.info(
|
||||
'Ignoring contact with self public key: ${contactTmp.name}',
|
||||
tag: 'Connector',
|
||||
);
|
||||
notifyListeners();
|
||||
removeContact(contactTmp);
|
||||
return;
|
||||
}
|
||||
@@ -4195,6 +4338,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
"Discovered contact ${contact.name} (type ${contact.typeLabelRaw}) not added due to auto-add settings",
|
||||
tag: 'Connector',
|
||||
);
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -4403,7 +4547,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return false;
|
||||
}
|
||||
|
||||
void _handleIncomingMessage(Uint8List frame) async {
|
||||
Future<void> _handleIncomingMessage(Uint8List frame) async {
|
||||
if (_selfPublicKey == null) return;
|
||||
|
||||
// If we're syncing the queued messages, advance the queue immediately
|
||||
@@ -5173,14 +5317,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
// Move to next channel
|
||||
_nextChannelIndexToRequest++;
|
||||
if (PlatformInfo.isWeb &&
|
||||
_activeTransport == MeshCoreTransportType.bluetooth &&
|
||||
channel.index == 0 &&
|
||||
_pendingInitialContactsSync) {
|
||||
_pendingInitialContactsSync = false;
|
||||
unawaited(getContacts());
|
||||
return;
|
||||
}
|
||||
notifyListeners();
|
||||
unawaited(_requestNextChannel());
|
||||
return;
|
||||
} else {
|
||||
@@ -5880,14 +6017,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
// Preserve deviceId and displayName for UI display during reconnection
|
||||
// They're only cleared on manual disconnect via disconnect() method
|
||||
_hasReceivedDeviceInfo = false;
|
||||
_pendingInitialChannelSync = false;
|
||||
_pendingInitialContactsSync = false;
|
||||
_maxContacts = _defaultMaxContacts;
|
||||
_maxChannels = _defaultMaxChannels;
|
||||
_isSyncingQueuedMessages = false;
|
||||
_queuedMessageSyncInFlight = false;
|
||||
_isSyncingChannels = false;
|
||||
_channelSyncInFlight = false;
|
||||
_resetSyncProgressState();
|
||||
_pendingChannelSentQueue.clear();
|
||||
_pendingGenericAckQueue.clear();
|
||||
_reactionSendQueueSequence = 0;
|
||||
|
||||
+23
-1
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -214,7 +215,10 @@ class MeshCoreApp extends StatelessWidget {
|
||||
// Update notification service with resolved locale
|
||||
final locale = Localizations.localeOf(context);
|
||||
NotificationService().setLocale(locale);
|
||||
return child ?? const SizedBox.shrink();
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: _systemUiOverlayStyle(context),
|
||||
child: child ?? const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
|
||||
? const ChromeRequiredScreen()
|
||||
@@ -236,6 +240,24 @@ class MeshCoreApp extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
SystemUiOverlayStyle _systemUiOverlayStyle(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
final iconBrightness = isDark ? Brightness.light : Brightness.dark;
|
||||
|
||||
// Keep Android system bars aligned with the resolved Flutter theme.
|
||||
return SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: iconBrightness,
|
||||
statusBarBrightness: isDark ? Brightness.dark : Brightness.light,
|
||||
systemNavigationBarColor: colorScheme.surface,
|
||||
systemNavigationBarIconBrightness: iconBrightness,
|
||||
systemNavigationBarDividerColor: colorScheme.surface,
|
||||
systemNavigationBarContrastEnforced: false,
|
||||
);
|
||||
}
|
||||
|
||||
Locale? _localeFromSetting(String? languageCode) {
|
||||
if (languageCode == null) return null;
|
||||
return Locale(languageCode);
|
||||
|
||||
@@ -11,6 +11,7 @@ import '../services/app_settings_service.dart';
|
||||
import '../services/notification_service.dart';
|
||||
import '../services/translation_service.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'map_cache_screen.dart';
|
||||
|
||||
@@ -23,6 +24,7 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
appBar: AppBar(
|
||||
title: AdaptiveAppBarTitle(context.l10n.appSettings_title),
|
||||
centerTitle: true,
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
|
||||
@@ -35,6 +35,7 @@ import '../widgets/gif_picker.dart';
|
||||
import '../widgets/message_translation_button.dart';
|
||||
import '../widgets/message_status_icon.dart';
|
||||
import '../widgets/radio_stats_entry.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import '../widgets/translated_message_content.dart';
|
||||
import '../widgets/unread_divider.dart';
|
||||
import 'channel_message_path_screen.dart';
|
||||
@@ -303,6 +304,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
],
|
||||
),
|
||||
centerTitle: false,
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
actions: [
|
||||
const RadioStatsIconButton(),
|
||||
PopupMenuButton<String>(
|
||||
|
||||
@@ -23,6 +23,7 @@ import '../widgets/list_filter_widget.dart';
|
||||
import '../widgets/empty_state.dart';
|
||||
import '../widgets/qr_code_display.dart';
|
||||
import '../widgets/quick_switch_bar.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import '../widgets/unread_badge.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'channel_chat_screen.dart';
|
||||
@@ -103,6 +104,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
title: AppBarTitle(context.l10n.channels_title),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
@@ -152,12 +154,17 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
await context.read<MeshCoreConnector>().getChannels(force: true);
|
||||
},
|
||||
child: () {
|
||||
if (connector.isLoadingChannels) {
|
||||
final channels = connector.channels;
|
||||
final waitingForFirstChannel =
|
||||
connector.isLoadingChannels && channels.isEmpty;
|
||||
|
||||
// Only block the list while the first channel is actively loading.
|
||||
// If the initial sync aborts, show cached/partial channels instead
|
||||
// of trapping the user behind an idle spinner.
|
||||
if (waitingForFirstChannel) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final channels = connector.channels;
|
||||
|
||||
if (channels.isEmpty) {
|
||||
return ListView(
|
||||
children: [
|
||||
@@ -274,7 +281,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
buildDefaultDragHandles: false,
|
||||
itemCount: filteredChannels.length,
|
||||
onReorderItem: (oldIndex, newIndex) {
|
||||
if (newIndex > oldIndex) newIndex -= 1;
|
||||
// onReorderItem already adjusts newIndex after the
|
||||
// removed item, unlike the deprecated onReorder.
|
||||
final reordered = List<Channel>.from(
|
||||
filteredChannels,
|
||||
);
|
||||
|
||||
@@ -41,6 +41,7 @@ import '../widgets/gif_picker.dart';
|
||||
import '../widgets/message_translation_button.dart';
|
||||
import '../widgets/path_selection_dialog.dart';
|
||||
import '../widgets/radio_stats_entry.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import '../widgets/translated_message_content.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
@@ -216,6 +217,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
},
|
||||
),
|
||||
centerTitle: false,
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
actions: [
|
||||
Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import '../widgets/empty_state.dart';
|
||||
import '../widgets/quick_switch_bar.dart';
|
||||
import '../widgets/repeater_login_dialog.dart';
|
||||
import '../widgets/room_login_dialog.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import '../widgets/unread_badge.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'channels_screen.dart';
|
||||
@@ -318,6 +319,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
appBar: AppBar(
|
||||
title: AppBarTitle(context.l10n.contacts_title),
|
||||
automaticallyImplyLeading: false,
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
@@ -606,15 +608,14 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
Widget _buildContactsBody(BuildContext context, MeshCoreConnector connector) {
|
||||
final viewState = context.watch<UiViewStateService>();
|
||||
final contacts = connector.contacts;
|
||||
final shouldShowStartupSpinner =
|
||||
contacts.isEmpty &&
|
||||
_groups.isEmpty &&
|
||||
final waitingForInitialContacts =
|
||||
connector.isConnected &&
|
||||
(connector.isLoadingContacts ||
|
||||
connector.isLoadingChannels ||
|
||||
connector.selfPublicKey == null);
|
||||
!connector.hasLoadedContacts &&
|
||||
!connector.isLoadingContacts;
|
||||
final waitingForFirstContact =
|
||||
connector.isLoadingContacts && contacts.isEmpty;
|
||||
|
||||
if (shouldShowStartupSpinner) {
|
||||
if (waitingForInitialContacts || waitingForFirstContact) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import '../services/map_tile_cache_service.dart';
|
||||
import '../utils/contact_search.dart';
|
||||
import '../utils/route_transitions.dart';
|
||||
import '../widgets/quick_switch_bar.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import '../icons/los_icon.dart';
|
||||
import 'channels_screen.dart';
|
||||
import 'chat_screen.dart';
|
||||
@@ -414,6 +415,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
title: AppBarTitle(context.l10n.map_title),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
actions: [
|
||||
if (!_isBuildingPathTrace)
|
||||
IconButton(
|
||||
|
||||
@@ -11,7 +11,7 @@ import '../utils/app_logger.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../widgets/device_tile.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'contacts_screen.dart';
|
||||
import 'channels_screen.dart';
|
||||
import 'tcp_screen.dart';
|
||||
import 'usb_screen.dart';
|
||||
|
||||
@@ -46,7 +46,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
_changedNavigation = true;
|
||||
if (mounted) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const ContactsScreen()),
|
||||
MaterialPageRoute(builder: (context) => const ChannelsScreen()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'app_settings_screen.dart';
|
||||
import 'app_debug_log_screen.dart';
|
||||
import 'ble_debug_log_screen.dart';
|
||||
import '../widgets/radio_stats_entry.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
|
||||
/// Convert device coding-rate value (1-4 on some firmware, 5-8 on others)
|
||||
/// to the UI enum range (always 5-8).
|
||||
@@ -67,6 +68,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
indicators: false,
|
||||
subtitle: false,
|
||||
),
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
|
||||
@@ -9,7 +9,7 @@ import '../services/app_settings_service.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'contacts_screen.dart';
|
||||
import 'channels_screen.dart';
|
||||
import 'usb_screen.dart';
|
||||
|
||||
class TcpScreen extends StatefulWidget {
|
||||
@@ -24,7 +24,7 @@ class _TcpScreenState extends State<TcpScreen> {
|
||||
late final TextEditingController _portController;
|
||||
late final MeshCoreConnector _connector;
|
||||
late final VoidCallback _connectionListener;
|
||||
bool _navigatedToContacts = false;
|
||||
bool _navigatedToChannels = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -42,20 +42,20 @@ class _TcpScreenState extends State<TcpScreen> {
|
||||
_connectionListener = () {
|
||||
if (!mounted) return;
|
||||
if (_connector.state == MeshCoreConnectionState.disconnected) {
|
||||
_navigatedToContacts = false;
|
||||
_navigatedToChannels = false;
|
||||
}
|
||||
if (_connector.state == MeshCoreConnectionState.connected &&
|
||||
_connector.isTcpTransportConnected &&
|
||||
!_navigatedToContacts) {
|
||||
!_navigatedToChannels) {
|
||||
context.read<AppSettingsService>().setTcpServerAddress(
|
||||
_hostController.text,
|
||||
);
|
||||
context.read<AppSettingsService>().setTcpServerPort(
|
||||
int.tryParse(_portController.text) ?? 0,
|
||||
);
|
||||
_navigatedToContacts = true;
|
||||
_navigatedToChannels = true;
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
||||
MaterialPageRoute(builder: (_) => const ChannelsScreen()),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -67,7 +67,7 @@ class _TcpScreenState extends State<TcpScreen> {
|
||||
_hostController.dispose();
|
||||
_portController.dispose();
|
||||
_connector.removeListener(_connectionListener);
|
||||
if (!_navigatedToContacts &&
|
||||
if (!_navigatedToChannels &&
|
||||
_connector.activeTransport == MeshCoreTransportType.tcp &&
|
||||
_connector.state != MeshCoreConnectionState.disconnected) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import '../widgets/path_management_dialog.dart';
|
||||
import '../helpers/cayenne_lpp.dart';
|
||||
import '../utils/battery_utils.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
|
||||
class TelemetryScreen extends StatefulWidget {
|
||||
final Contact contact;
|
||||
@@ -239,6 +240,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
],
|
||||
),
|
||||
centerTitle: false,
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
actions: [
|
||||
PopupMenuButton<String>(
|
||||
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
|
||||
|
||||
@@ -11,7 +11,7 @@ import '../utils/platform_info.dart';
|
||||
import '../utils/usb_port_labels.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'contacts_screen.dart';
|
||||
import 'channels_screen.dart';
|
||||
import 'scanner_screen.dart';
|
||||
import 'tcp_screen.dart';
|
||||
|
||||
@@ -25,7 +25,7 @@ class UsbScreen extends StatefulWidget {
|
||||
class _UsbScreenState extends State<UsbScreen> {
|
||||
final List<String> _ports = <String>[];
|
||||
bool _isLoadingPorts = true;
|
||||
bool _navigatedToContacts = false;
|
||||
bool _navigatedToChannels = false;
|
||||
bool _didScheduleInitialLoad = false;
|
||||
Timer? _hotPlugTimer;
|
||||
late final MeshCoreConnector _connector;
|
||||
@@ -41,14 +41,14 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
_connectionListener = () {
|
||||
if (!mounted) return;
|
||||
if (_connector.state == MeshCoreConnectionState.disconnected) {
|
||||
_navigatedToContacts = false;
|
||||
_navigatedToChannels = false;
|
||||
}
|
||||
if (_connector.state == MeshCoreConnectionState.connected &&
|
||||
_connector.isUsbTransportConnected &&
|
||||
!_navigatedToContacts) {
|
||||
_navigatedToContacts = true;
|
||||
!_navigatedToChannels) {
|
||||
_navigatedToChannels = true;
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
||||
MaterialPageRoute(builder: (_) => const ChannelsScreen()),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -72,7 +72,7 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
_hotPlugTimer?.cancel();
|
||||
_hotPlugTimer = null;
|
||||
_connector.removeListener(_connectionListener);
|
||||
if (!_navigatedToContacts &&
|
||||
if (!_navigatedToChannels &&
|
||||
_connector.activeTransport == MeshCoreTransportType.usb &&
|
||||
_connector.state != MeshCoreConnectionState.disconnected) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
|
||||
class SyncProgressAppBarBottom extends StatelessWidget
|
||||
implements PreferredSizeWidget {
|
||||
static const double height = 3;
|
||||
|
||||
const SyncProgressAppBarBottom({super.key});
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(height);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
final state = _SyncProgressState.fromConnector(connector);
|
||||
if (state == null) return const SizedBox(height: height);
|
||||
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: LinearProgressIndicator(
|
||||
value: state.value,
|
||||
minHeight: height,
|
||||
color: state.color,
|
||||
backgroundColor: state.color.withValues(alpha: 0.18),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SyncProgressState {
|
||||
final double? value;
|
||||
final Color color;
|
||||
|
||||
const _SyncProgressState({required this.value, required this.color});
|
||||
|
||||
static _SyncProgressState? fromConnector(MeshCoreConnector connector) {
|
||||
if (connector.isLoadingContacts) {
|
||||
return _SyncProgressState(
|
||||
value: connector.contactSyncProgress,
|
||||
color: Colors.red,
|
||||
);
|
||||
}
|
||||
if (connector.isSyncingChannels) {
|
||||
return _SyncProgressState(
|
||||
value: connector.channelSyncProgress / 100,
|
||||
color: Colors.blue,
|
||||
);
|
||||
}
|
||||
if (connector.isShowingQueuedMessageSyncProgress) {
|
||||
return const _SyncProgressState(value: null, color: Colors.green);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user